Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to proxy arbitrary K8s APIs #21

Merged
merged 1 commit into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions client/web/antrea-ui/src/api/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface AgentInfo {
export const controllerInfoAPI = {
fetch: async (): Promise<ControllerInfo> => {
return api.get(
`info/controller`,
`k8s/apis/crd.antrea.io/v1beta1/antreacontrollerinfos/antrea-controller`
).then((response) => response.data as ControllerInfo).catch((error) => {
console.error("Unable to fetch Controller Info");
handleError(error);
Expand All @@ -90,16 +90,16 @@ export const controllerInfoAPI = {
export const agentInfoAPI = {
fetchAll: async (): Promise<AgentInfo[]> => {
return api.get(
`info/agents`,
).then((response) => response.data as AgentInfo[]).catch((error) => {
`k8s/apis/crd.antrea.io/v1beta1/antreaagentinfos`,
).then((response) => response.data.items as AgentInfo[]).catch((error) => {
console.error("Unable to fetch Agent Infos");
handleError(error);
});
},

fetch: async (name: string): Promise<AgentInfo> => {
return api.get(
`info/agents/${name}`,
`k8s/apis/crd.antrea.io/v1beta1/antreaagentinfos/${name}`,
).then((response) => response.data as AgentInfo).catch((error) => {
console.error("Unable to fetch Agent Info");
handleError(error);
Expand Down
18 changes: 13 additions & 5 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"flag"
"fmt"
"net/http"
"net/url"
"os"
"time"

Expand All @@ -32,6 +33,7 @@ import (

"antrea.io/antrea-ui/pkg/auth"
"antrea.io/antrea-ui/pkg/env"
"antrea.io/antrea-ui/pkg/handlers/k8sproxy"
traceflowhandler "antrea.io/antrea-ui/pkg/handlers/traceflow"
"antrea.io/antrea-ui/pkg/k8s"
"antrea.io/antrea-ui/pkg/password"
Expand Down Expand Up @@ -98,13 +100,18 @@ func ginLogger(logger logr.Logger, level int) gin.HandlerFunc {
func run() error {
logger.Info("Starting Antrea UI backend", "version", version.GetFullVersionWithRuntimeInfo())

k8sClient, err := k8s.DynamicClient()
k8sRESTConfig, k8sHTTPClient, k8sDynamicClient, err := k8s.Client()
if err != nil {
return fmt.Errorf("failed to create K8s dynamic client: %w", err)
return fmt.Errorf("failed to create K8s client: %w", err)
}
k8sServerURL, err := url.Parse(k8sRESTConfig.Host)
if err != nil {
return fmt.Errorf("failed to parse K8s server URL '%s': %w", k8sRESTConfig.Host, err)
}

traceflowHandler := traceflowhandler.NewRequestsHandler(logger, k8sClient)
passwordStore := password.NewStore(passwordrw.NewK8sSecret(env.GetNamespace(), "antrea-ui-passwd", k8sClient), passwordhasher.NewArgon2id())
traceflowHandler := traceflowhandler.NewRequestsHandler(logger, k8sDynamicClient)
k8sProxyHandler := k8sproxy.NewK8sProxyHandler(logger, k8sServerURL, k8sHTTPClient.Transport)
passwordStore := password.NewStore(passwordrw.NewK8sSecret(env.GetNamespace(), "antrea-ui-passwd", k8sDynamicClient), passwordhasher.NewArgon2id())
if err := passwordStore.Init(context.Background()); err != nil {
return err
}
Expand All @@ -125,8 +132,9 @@ func run() error {

s := server.NewServer(
logger,
k8sClient,
k8sDynamicClient,
traceflowHandler,
k8sProxyHandler,
passwordStore,
tokenManager,
server.SetCookieSecure(cookieSecure),
Expand Down
44 changes: 44 additions & 0 deletions pkg/handlers/k8sproxy/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2023 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package k8sproxy

import (
"net/http"
"net/http/httputil"
"net/url"

"github.com/go-logr/logr"
)

type transportWrapper struct {
logger logr.Logger
t http.RoundTripper
}

func (w *transportWrapper) RoundTrip(r *http.Request) (*http.Response, error) {
w.logger.V(4).Info("Proxying request", "url", r.URL)
return w.t.RoundTrip(r)
}

func NewK8sProxyHandler(logger logr.Logger, k8sServerURL *url.URL, k8sHTTPTransport http.RoundTripper) http.Handler {
// TODO: the httputil.ReverseProxy is much improved in Go v1.20, but we currently use Go
// v1.19. When we upgrade, we should revisit this code.
k8sReverseProxy := httputil.NewSingleHostReverseProxy(k8sServerURL)
k8sReverseProxy.Transport = &transportWrapper{
logger: logger,
t: k8sHTTPTransport,
}
return k8sReverseProxy
}
53 changes: 53 additions & 0 deletions pkg/handlers/k8sproxy/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2023 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package k8sproxy

import (
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/go-logr/logr/testr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestK8sProxyHandler(t *testing.T) {
var capturedReq *http.Request
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedReq = r
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()

logger := testr.New(t)
serverURL, err := url.Parse(ts.URL)
require.NoError(t, err)
h := NewK8sProxyHandler(logger, serverURL, http.DefaultTransport)

req, err := http.NewRequest("GET", "/api/v1/k8s/api/v1/pods", nil)
req.RemoteAddr = "127.0.0.1:32167"
require.NoError(t, err)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.NotNil(t, capturedReq)
assert.Equal(t, "GET", capturedReq.Method)
assert.Equal(t, "/api/v1/k8s/api/v1/pods", capturedReq.URL.String())
// TODO: after we improve the reverse proxy, we need to do more validation
header := capturedReq.Header
assert.Equal(t, "127.0.0.1", header.Get("X-Forwarded-For"))
}
22 changes: 20 additions & 2 deletions pkg/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ package k8s

import (
"flag"
"net/http"
"os"

"k8s.io/client-go/dynamic"
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
Expand All @@ -32,7 +34,7 @@ func inCluster() bool {
return inCluster
}

func DynamicClient() (dynamic.Interface, error) {
func restConfig() (*rest.Config, error) {
var config *rest.Config
if inCluster() {
var err error
Expand All @@ -48,7 +50,23 @@ func DynamicClient() (dynamic.Interface, error) {
return nil, err
}
}
return dynamic.NewForConfig(config)
return config, nil
}

func Client() (*rest.Config, *http.Client, *dynamic.DynamicClient, error) {
config, err := restConfig()
if err != nil {
return nil, nil, nil, err
}
httpClient, err := rest.HTTPClientFor(config)
if err != nil {
return nil, nil, nil, err
}
client, err := dynamic.NewForConfigAndClient(config, httpClient)
if err != nil {
return nil, nil, nil, err
}
return config, httpClient, client, nil
}

func init() {
Expand Down
4 changes: 3 additions & 1 deletion pkg/server/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package server
import (
"fmt"
"net/http"
"time"

"github.com/gin-gonic/gin"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -101,7 +102,8 @@ func (s *server) GetAgentInfo(c *gin.Context) {

func (s *server) AddInfoRoutes(r *gin.RouterGroup) {
r = r.Group("/info")
r.Use(s.checkBearerToken)
removalDate := time.Date(2023, 7, 1, 0, 0, 0, 0, time.UTC)
r.Use(s.checkBearerToken, announceDeprecationMiddleware(removalDate, "use /k8s instead"))
r.GET("/controller", s.GetControllerInfo)
r.GET("/agents", s.GetAgentInfos)
r.GET("/agents/:name", s.GetAgentInfo)
Expand Down
9 changes: 9 additions & 0 deletions pkg/server/info_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ func createTestControllerInfo(ctx context.Context, k8sClient dynamic.Interface,
return err
}

func checkInfoDeprecationHeaders(t *testing.T, header http.Header) {
assert.Equal(t, `299 - "Deprecated API: use /k8s instead"`, header.Get("Warning"))
assert.Equal(t, "Sat, 01 Jul 2023 00:00:00 GMT", header.Get("Sunset"))
}

func TestGetControllerInfo(t *testing.T) {
ctx := context.Background()
ts := newTestServer(t)
Expand All @@ -56,6 +61,7 @@ func TestGetControllerInfo(t *testing.T) {
ts.router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "antrea-controller", gjson.GetBytes(rr.Body.Bytes(), "metadata.name").String())
checkInfoDeprecationHeaders(t, rr.Result().Header)
}

func createTestAgentInfo(ctx context.Context, k8sClient dynamic.Interface, name string) error {
Expand Down Expand Up @@ -87,6 +93,7 @@ func TestGetAgentInfo(t *testing.T) {
ts.router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "node-A", gjson.GetBytes(rr.Body.Bytes(), "metadata.name").String())
checkInfoDeprecationHeaders(t, rr.Result().Header)
})

t.Run("invalid name", func(t *testing.T) {
Expand All @@ -96,6 +103,7 @@ func TestGetAgentInfo(t *testing.T) {
rr := httptest.NewRecorder()
ts.router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusNotFound, rr.Code)
checkInfoDeprecationHeaders(t, rr.Result().Header)
})
}

Expand All @@ -112,4 +120,5 @@ func TestGetAgentInfos(t *testing.T) {
ts.router.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Len(t, gjson.ParseBytes(rr.Body.Bytes()).Array(), 2)
checkInfoDeprecationHeaders(t, rr.Result().Header)
}
64 changes: 64 additions & 0 deletions pkg/server/k8s.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2023 Antrea Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
"net/http"
"strings"

"github.com/gin-gonic/gin"
)

// allowedPaths contains the K8s api paths that we are proxying.
// Note the leading slash, since the Gin "catch-all" parameter ("/*path") will include it.
var allowedPaths = []string{
"/apis/crd.antrea.io/v1beta1/antreaagentinfos",
"/apis/crd.antrea.io/v1beta1/antreacontrollerinfos",
}

func (s *server) GetK8s(c *gin.Context) {
// we need to strip the beginning of the path (/api/v1/k8s) before proxying
path := c.Param("path")
request := c.Request
request.URL.Path = path
// we also ensure that the Bearer Token is removed
request.Header.Del("Authorization")
s.k8sProxyHandler.ServeHTTP(c.Writer, c.Request)
}

func (s *server) checkK8sPath(c *gin.Context) {
if sError := func() *serverError {
path := c.Param("path")
for _, allowedPath := range allowedPaths {
if strings.HasPrefix(path, allowedPath) {
return nil
}
}
return &serverError{
code: http.StatusNotFound,
message: "This K8s API path is not being proxied",
}
}(); sError != nil {
s.HandleError(c, sError)
c.Abort()
return
}
}

func (s *server) AddK8sRoutes(r *gin.RouterGroup) {
r = r.Group("/k8s")
r.Use(s.checkBearerToken)
r.GET("/*path", s.checkK8sPath, s.GetK8s)
}
Loading