diff --git a/go.mod b/go.mod index 87ccb2ed..9c51823b 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 645fc7e2..e6672d81 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/handler.go b/http/handler/handler.go index ba07f44b..76724a2e 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 52f2a535..2a113ac8 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) @@ -21,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 6b47dd0e..cb33ebb2 100644 --- a/http/handler/kubernetes/handler.go +++ b/http/handler/kubernetes/handler.go @@ -25,5 +25,13 @@ 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) + 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 00000000..15cc2719 --- /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 new file mode 100644 index 00000000..5006d604 --- /dev/null +++ b/http/handler/kubernetes/kubernetes_namespaces.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) 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) + } + + 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 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 +}