From bef452f3221cda5575eded16a60c25c3525530c3 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Tue, 5 Sep 2023 16:32:19 +1200 Subject: [PATCH 1/4] speed up namespace lookup --- http/handler/kubernetes/handler.go | 3 ++ .../kubernetes/kubernetes_namespaces.go | 33 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 http/handler/kubernetes/kubernetes_namespaces.go diff --git a/http/handler/kubernetes/handler.go b/http/handler/kubernetes/handler.go index 6b47dd0e7..304f9131f 100644 --- a/http/handler/kubernetes/handler.go +++ b/http/handler/kubernetes/handler.go @@ -25,5 +25,8 @@ func NewHandler(notaryService *security.NotaryService, kubernetesDeployer *exec. h.Handle("/kubernetes/stack", notaryService.DigitalSignatureVerification(httperror.LoggerHandler(h.kubernetesDeploy))).Methods(http.MethodPost) + h.Handle("/kubernetes/namespaces", + notaryService.DigitalSignatureVerification(httperror.LoggerHandler(h.kubernetesGetNamespaces))).Methods(http.MethodGet) + return h } diff --git a/http/handler/kubernetes/kubernetes_namespaces.go b/http/handler/kubernetes/kubernetes_namespaces.go new file mode 100644 index 000000000..a2175a957 --- /dev/null +++ b/http/handler/kubernetes/kubernetes_namespaces.go @@ -0,0 +1,33 @@ +package kubernetes + +import ( + "fmt" + "net/http" + + httperror "github.com/portainer/portainer/pkg/libhttp/error" + "github.com/portainer/portainer/pkg/libhttp/request" + "github.com/portainer/portainer/pkg/libhttp/response" +) + +type getNamespacePayload struct{} + +func (payload *getNamespacePayload) Validate(r *http.Request) error { + + return nil +} + +func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload getNamespacePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return httperror.BadRequest("Invalid request payload", err) + } + + for _, header := range r.Header { + for _, value := range header { + fmt.Println("Header:", value) + } + } + + return response.Empty(rw) +} From 0f6eaaa5c4265cff11d8ee0cab0564b046c22d2d Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Wed, 13 Sep 2023 10:48:02 +1200 Subject: [PATCH 2/4] remove the payload --- http/handler/handler.go | 3 +++ http/handler/handlerv2.go | 5 ++++ .../kubernetes/kubernetes_namespaces.go | 23 +++++++++++-------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/http/handler/handler.go b/http/handler/handler.go index ba07f44be..76724a2e0 100644 --- a/http/handler/handler.go +++ b/http/handler/handler.go @@ -23,6 +23,7 @@ import ( "github.com/portainer/agent/http/proxy" "github.com/portainer/agent/http/security" kubecli "github.com/portainer/agent/kubernetes" + "github.com/rs/zerolog/log" ) // Handler is the main handler of the application. @@ -100,6 +101,8 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, request *http.Request) { } rw.Header().Set(agent.HTTPResponseAgentPlatform, strconv.Itoa(int(agentPlatformIdentifier))) + log.Debug().Msgf("Handling request: %s %s", request.Method, request.URL.Path) + switch { case strings.HasPrefix(request.URL.Path, "/v1"): h.ServeHTTPV1(rw, request) diff --git a/http/handler/handlerv2.go b/http/handler/handlerv2.go index 52f2a5350..c4c284af1 100644 --- a/http/handler/handlerv2.go +++ b/http/handler/handlerv2.go @@ -3,10 +3,15 @@ package handler import ( "net/http" "strings" + + "github.com/rs/zerolog/log" ) // ServeHTTPV2 is the HTTP router for all v2 api requests. func (h *Handler) ServeHTTPV2(rw http.ResponseWriter, request *http.Request) { + + log.Debug().Msgf("Request: %s %s", request.Method, request.URL.Path) + switch { case strings.HasPrefix(request.URL.Path, "/v2/ping"): http.StripPrefix("/v2", h.pingHandler).ServeHTTP(rw, request) diff --git a/http/handler/kubernetes/kubernetes_namespaces.go b/http/handler/kubernetes/kubernetes_namespaces.go index a2175a957..af08a6245 100644 --- a/http/handler/kubernetes/kubernetes_namespaces.go +++ b/http/handler/kubernetes/kubernetes_namespaces.go @@ -5,23 +5,26 @@ import ( "net/http" httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/request" "github.com/portainer/portainer/pkg/libhttp/response" + "github.com/rs/zerolog/log" ) -type getNamespacePayload struct{} +// type getNamespacePayload struct{} -func (payload *getNamespacePayload) Validate(r *http.Request) error { +// func (payload *getNamespacePayload) Validate(r *http.Request) error { - return nil -} +// return nil +// } func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { - var payload getNamespacePayload - err := request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return httperror.BadRequest("Invalid request payload", err) - } + + log.Debug().Msgf("GetNamespaces Handler: Request: %s %s", r.Method, r.URL.Path) + + // var payload getNamespacePayload + // err := request.DecodeAndValidateJSONPayload(r, &payload) + // if err != nil { + // return httperror.BadRequest("Invalid request payload", err) + // } for _, header := range r.Header { for _, value := range header { From 1f5fbd66b64f1e9e443c005761b93b1ba567c18d Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Thu, 14 Sep 2023 11:23:05 +1200 Subject: [PATCH 3/4] pass through original request --- .../kubernetes/kubernetes_namespaces.go | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/http/handler/kubernetes/kubernetes_namespaces.go b/http/handler/kubernetes/kubernetes_namespaces.go index af08a6245..45c866433 100644 --- a/http/handler/kubernetes/kubernetes_namespaces.go +++ b/http/handler/kubernetes/kubernetes_namespaces.go @@ -1,36 +1,51 @@ package kubernetes import ( - "fmt" + "context" "net/http" + "path" + "strings" + "github.com/portainer/agent" httperror "github.com/portainer/portainer/pkg/libhttp/error" "github.com/portainer/portainer/pkg/libhttp/response" "github.com/rs/zerolog/log" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) -// type getNamespacePayload struct{} +func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { + log.Debug().Msgf("GetNamespaces Handler: Request: %s %s", r.Method, r.URL.Path) + + config, err := rest.InClusterConfig() + if err != nil { + return httperror.InternalServerError("Unable to read service account token file", err) + } -// func (payload *getNamespacePayload) Validate(r *http.Request) error { + token := r.Header.Get(agent.HTTPKubernetesSATokenHeaderName) + if len(token) == 0 { + config.BearerToken = token + } -// return nil -// } + // adjust the API path to match the Kubernetes API + api := path.Join("/api/v1/", strings.TrimPrefix(r.URL.Path, "/kubernetes")) -func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { + // Create an HTTP client from the Kubernetes configuration + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + return httperror.InternalServerError("Unable to create HTTP client", err) + } - log.Debug().Msgf("GetNamespaces Handler: Request: %s %s", r.Method, r.URL.Path) + restClient := clientSet.RESTClient() - // var payload getNamespacePayload - // err := request.DecodeAndValidateJSONPayload(r, &payload) - // if err != nil { - // return httperror.BadRequest("Invalid request payload", err) - // } + // Create an HTTP request using the client + req := restClient.Get().RequestURI(api) - for _, header := range r.Header { - for _, value := range header { - fmt.Println("Header:", value) - } + // Send the HTTP request + resp, err := req.DoRaw(context.Background()) + if err != nil { + panic(err) } - return response.Empty(rw) + return response.JSON(rw, string(resp)) } From 1a1a5b96b81bd6dd717c262d5edca8c43bf7c731 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Thu, 14 Sep 2023 14:22:08 +1200 Subject: [PATCH 4/4] a bit more stuff --- go.mod | 1 + go.sum | 6 + http/handler/handlerv2.go | 4 +- http/handler/kubernetes/handler.go | 5 + .../kubernetes/kubernetes_configmaps.go | 111 ++++++++++++++++++ .../kubernetes/kubernetes_namespaces.go | 64 +++++++++- 6 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 http/handler/kubernetes/kubernetes_configmaps.go diff --git a/go.mod b/go.mod index 87ccb2ed2..9c51823ba 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c github.com/mitchellh/mapstructure v1.5.0 github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b + github.com/perimeterx/marshmallow v1.1.5 github.com/pkg/errors v0.9.1 github.com/portainer/portainer v0.6.1-0.20230901222702-8cc5e0796c4a github.com/rs/zerolog v1.29.0 diff --git a/go.sum b/go.sum index 645fc7e23..e6672d81d 100644 --- a/go.sum +++ b/go.sum @@ -100,6 +100,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -269,6 +271,8 @@ github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -318,6 +322,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/wI2L/jsondiff v0.2.0 h1:dE00WemBa1uCjrzQUUTE/17I6m5qAaN0EMFOg2Ynr/k= github.com/wI2L/jsondiff v0.2.0/go.mod h1:axTcwtBkY4TsKuV+RgoMhHyHKKFRI6nnjRLi8LLYQnA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/http/handler/handlerv2.go b/http/handler/handlerv2.go index c4c284af1..2a113ac82 100644 --- a/http/handler/handlerv2.go +++ b/http/handler/handlerv2.go @@ -10,7 +10,7 @@ import ( // ServeHTTPV2 is the HTTP router for all v2 api requests. func (h *Handler) ServeHTTPV2(rw http.ResponseWriter, request *http.Request) { - log.Debug().Msgf("Request: %s %s", request.Method, request.URL.Path) + //log.Debug().Msgf("Request: %s %s", request.Method, request.URL.Path) switch { case strings.HasPrefix(request.URL.Path, "/v2/ping"): @@ -26,8 +26,10 @@ func (h *Handler) ServeHTTPV2(rw http.ResponseWriter, request *http.Request) { case strings.HasPrefix(request.URL.Path, "/v2/websocket"): http.StripPrefix("/v2", h.webSocketHandler).ServeHTTP(rw, request) case strings.HasPrefix(request.URL.Path, "/v2/kubernetes"): + log.Debug().Msgf("Got it: %s %s", request.Method, request.URL.Path) http.StripPrefix("/v2", h.kubernetesHandler).ServeHTTP(rw, request) case strings.HasPrefix(request.URL.Path, "/"): + log.Debug().Msgf("default: %s %s", request.Method, request.URL.Path) h.dockerProxyHandler.ServeHTTP(rw, request) } } diff --git a/http/handler/kubernetes/handler.go b/http/handler/kubernetes/handler.go index 304f9131f..cb33ebb2f 100644 --- a/http/handler/kubernetes/handler.go +++ b/http/handler/kubernetes/handler.go @@ -27,6 +27,11 @@ func NewHandler(notaryService *security.NotaryService, kubernetesDeployer *exec. h.Handle("/kubernetes/namespaces", notaryService.DigitalSignatureVerification(httperror.LoggerHandler(h.kubernetesGetNamespaces))).Methods(http.MethodGet) + h.Handle("/kubernetes/namespaces/{namespace}", + notaryService.DigitalSignatureVerification(httperror.LoggerHandler(h.kubernetesGetNamespaces))).Methods(http.MethodGet) + + h.Handle("/kubernetes/namespaces/{namespace}/{configmaps|secrets}", + notaryService.DigitalSignatureVerification(httperror.LoggerHandler(h.kubernetesGetConfigMaps))).Methods(http.MethodGet) return h } diff --git a/http/handler/kubernetes/kubernetes_configmaps.go b/http/handler/kubernetes/kubernetes_configmaps.go new file mode 100644 index 000000000..15cc27190 --- /dev/null +++ b/http/handler/kubernetes/kubernetes_configmaps.go @@ -0,0 +1,111 @@ +package kubernetes + +import ( + "context" + "net/http" + "path" + "strings" + + "github.com/portainer/agent" + httperror "github.com/portainer/portainer/pkg/libhttp/error" + "github.com/rs/zerolog/log" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// type ( +// FilteredNamespaceResponse struct { +// Kind string `json:"kind"` +// Name string `json:"name"` +// MetaData struct { +// Name string `json:"name"` +// CreationTimestamp string `json:"creationTimestamp"` +// } `json:"metadata"` +// } + +// FilteredNamespacesResponse struct { +// APIVersion string `json:"apiVersion"` +// Items []struct { +// Metadata struct { +// CreationTimestamp string `json:"creationTimestamp"` +// Labels struct { +// Kubernetes_io_metadata_name string `json:"kubernetes.io/metadata.name"` +// } `json:"labels"` +// Name string `json:"name"` +// ResourceVersion string `json:"resourceVersion"` +// UID string `json:"uid"` +// } `json:"metadata"` +// } `json:"items"` +// Kind string `json:"kind"` +// Metadata struct { +// ResourceVersion string `json:"resourceVersion"` +// } `json:"metadata"` +// } +// ) + +func (handler *Handler) kubernetesGetConfigMaps(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { + log.Debug().Msgf("GetNamespaces Handler: Request: %s %s", r.Method, r.URL.Path) + + config, err := rest.InClusterConfig() + if err != nil { + return httperror.InternalServerError("Unable to read service account token file", err) + } + + token := r.Header.Get(agent.HTTPKubernetesSATokenHeaderName) + if len(token) == 0 { + config.BearerToken = token + } + + // adjust the API path to match the Kubernetes API + api := path.Join("/api/v1/", strings.TrimPrefix(r.URL.Path, "/kubernetes")) + if len(r.URL.RawQuery) > 0 { + api = api + "?" + r.URL.RawQuery + } + + log.Debug().Msgf("New API path: %s", api) + + // Create an HTTP client from the Kubernetes configuration + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + return httperror.InternalServerError("Unable to create HTTP client", err) + } + + restClient := clientSet.RESTClient() + + // Create an HTTP request using the client + req := restClient.Get().RequestURI(api) + + // Send the HTTP request + resp, err := req.DoRaw(context.Background()) + if err != nil { + panic(err) + } + + return filteredConfigMaps(rw, resp) +} + +func filteredConfigMaps(rw http.ResponseWriter, data []byte) *httperror.HandlerError { + // var namespacesResponse []FilteredNamespaceResponse + // err := json.Unmarshal([]byte(data), &namespacesResponse) + // if err != nil { + // return httperror.InternalServerError("Unable to unmarshal response", err) + // } + + // v := struct { + // Kind string `json:"kind"` + // }{} + + // result, err := marshmallow.Unmarshal(data, &v) + // if err != nil { + // return httperror.InternalServerError("Unable to unmarshal response", err) + // } + + // if v.Kind == "NamespaceList" { + // result["items"] = []FilteredNamespaceResponse{} + // } + + //return response.JSON(rw, result) + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + return nil +} diff --git a/http/handler/kubernetes/kubernetes_namespaces.go b/http/handler/kubernetes/kubernetes_namespaces.go index 45c866433..5006d604f 100644 --- a/http/handler/kubernetes/kubernetes_namespaces.go +++ b/http/handler/kubernetes/kubernetes_namespaces.go @@ -8,12 +8,41 @@ import ( "github.com/portainer/agent" httperror "github.com/portainer/portainer/pkg/libhttp/error" - "github.com/portainer/portainer/pkg/libhttp/response" "github.com/rs/zerolog/log" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" ) +// type ( +// FilteredNamespaceResponse struct { +// Kind string `json:"kind"` +// Name string `json:"name"` +// MetaData struct { +// Name string `json:"name"` +// CreationTimestamp string `json:"creationTimestamp"` +// } `json:"metadata"` +// } + +// FilteredNamespacesResponse struct { +// APIVersion string `json:"apiVersion"` +// Items []struct { +// Metadata struct { +// CreationTimestamp string `json:"creationTimestamp"` +// Labels struct { +// Kubernetes_io_metadata_name string `json:"kubernetes.io/metadata.name"` +// } `json:"labels"` +// Name string `json:"name"` +// ResourceVersion string `json:"resourceVersion"` +// UID string `json:"uid"` +// } `json:"metadata"` +// } `json:"items"` +// Kind string `json:"kind"` +// Metadata struct { +// ResourceVersion string `json:"resourceVersion"` +// } `json:"metadata"` +// } +// ) + func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http.Request) *httperror.HandlerError { log.Debug().Msgf("GetNamespaces Handler: Request: %s %s", r.Method, r.URL.Path) @@ -29,6 +58,11 @@ func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http. // adjust the API path to match the Kubernetes API api := path.Join("/api/v1/", strings.TrimPrefix(r.URL.Path, "/kubernetes")) + if len(r.URL.RawQuery) > 0 { + api = api + "?" + r.URL.RawQuery + } + + log.Debug().Msgf("New API path: %s", api) // Create an HTTP client from the Kubernetes configuration clientSet, err := kubernetes.NewForConfig(config) @@ -47,5 +81,31 @@ func (handler *Handler) kubernetesGetNamespaces(rw http.ResponseWriter, r *http. panic(err) } - return response.JSON(rw, string(resp)) + return filteredNamespaces(rw, resp) +} + +func filteredNamespaces(rw http.ResponseWriter, data []byte) *httperror.HandlerError { + // var namespacesResponse []FilteredNamespaceResponse + // err := json.Unmarshal([]byte(data), &namespacesResponse) + // if err != nil { + // return httperror.InternalServerError("Unable to unmarshal response", err) + // } + + // v := struct { + // Kind string `json:"kind"` + // }{} + + // result, err := marshmallow.Unmarshal(data, &v) + // if err != nil { + // return httperror.InternalServerError("Unable to unmarshal response", err) + // } + + // if v.Kind == "NamespaceList" { + // result["items"] = []FilteredNamespaceResponse{} + // } + + //return response.JSON(rw, result) + rw.Header().Set("Content-Type", "application/json") + rw.Write(data) + return nil }