diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index fd5fa1b72..360e3b663 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -239,8 +239,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService, return docker.NewClientFactory(signatureService, reverseTunnelService) } -func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *kubecli.ClientFactory { - return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID, dataStore) +func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*kubecli.ClientFactory, error) { + return kubecli.NewClientFactory(signatureService, reverseTunnelService, dataStore, instanceID, addrHTTPS, userSessionTimeout) } func initSnapshotService( @@ -612,7 +612,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { reverseTunnelService := chisel.NewService(dataStore, shutdownCtx) dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService) - kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID, dataStore) + kubernetesClientFactory, err := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, dataStore, instanceID, *flags.AddrHTTPS, settings.UserSessionTimeout) snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory, shutdownCtx) if err != nil { diff --git a/api/go.mod b/api/go.mod index 60d1da09d..e7f2ae2c5 100644 --- a/api/go.mod +++ b/api/go.mod @@ -31,6 +31,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/koding/websocketproxy v0.0.0-20181220232114-7ed82d81a28c github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 + github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 github.com/portainer/docker-compose-wrapper v0.0.0-20220708023447-a69a4ebaa021 github.com/portainer/libcrypto v0.0.0-20220506221303-1f4fb3b30f9a diff --git a/api/go.sum b/api/go.sum index 650360013..1a60e9a02 100644 --- a/api/go.sum +++ b/api/go.sum @@ -352,6 +352,8 @@ github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrB github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6 h1:lNCW6THrCKBiJBpz8kbVGjC7MgdCGKwuvBgc7LoD6sw= github.com/orcaman/concurrent-map v0.0.0-20190826125027-8c72a8bb44f6/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/api/http/handler/kubernetes/configmaps_and_secrets.go b/api/http/handler/kubernetes/configmaps_and_secrets.go index b38b16640..d1c7af22f 100644 --- a/api/http/handler/kubernetes/configmaps_and_secrets.go +++ b/api/http/handler/kubernetes/configmaps_and_secrets.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -9,7 +10,23 @@ import ( ) func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespace, err := request.RetrieveRouteVariableValue(r, "namespace") if err != nil { @@ -22,7 +39,7 @@ func (handler *Handler) getKubernetesConfigMaps(w http.ResponseWriter, r *http.R configmaps, err := cli.GetConfigMapsAndSecrets(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve configmaps and secrets", err, ) } diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index 680e4ea46..7d46c00d3 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -3,6 +3,8 @@ package kubernetes import ( "errors" "net/http" + "net/url" + "strconv" portainer "github.com/portainer/portainer/api" portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors" @@ -24,7 +26,6 @@ type Handler struct { *mux.Router authorizationService *authorization.Service DataStore dataservices.DataStore - KubernetesClient portainer.KubeClient KubernetesClientFactory *cli.ClientFactory JwtService dataservices.JWTService kubeClusterAccessService kubernetes.KubeClusterAccessService @@ -39,7 +40,6 @@ func NewHandler(bouncer *security.RequestBouncer, authorizationService *authoriz JwtService: jwtService, kubeClusterAccessService: kubeClusterAccessService, KubernetesClientFactory: kubernetesClientFactory, - KubernetesClient: kubernetesClient, } kubeRouter := h.PathPrefix("/kubernetes").Subrouter() @@ -85,13 +85,19 @@ func kubeOnlyMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, request *http.Request) { endpoint, err := middlewares.FetchEndpoint(request) if err != nil { - httperror.WriteError(rw, http.StatusInternalServerError, "Unable to find an environment on request context", err) + httperror.InternalServerError( + "Unable to find an environment on request context", + err, + ) return } if !endpointutils.IsKubernetesEndpoint(endpoint) { errMessage := "environment is not a Kubernetes environment" - httperror.WriteError(rw, http.StatusBadRequest, errMessage, errors.New(errMessage)) + httperror.BadRequest( + errMessage, + errors.New(errMessage), + ) return } @@ -109,6 +115,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Invalid environment identifier route variable", err, ) + return } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) @@ -119,6 +126,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Unable to find an environment with the specified identifier inside the database", err, ) + return } else if err != nil { httperror.WriteError( w, @@ -126,23 +134,101 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { "Unable to find an environment with the specified identifier inside the database", err, ) + return } if handler.KubernetesClientFactory == nil { next.ServeHTTP(w, r) return } - kubeCli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + // Generate a proxied kubeconfig, then create a kubeclient using it. + tokenData, err := security.RetrieveTokenData(r) + if err != nil { + httperror.WriteError( + w, + http.StatusForbidden, + "Permission denied to access environment", + err, + ) + return + } + bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData) if err != nil { httperror.WriteError( w, http.StatusInternalServerError, - "Unable to create Kubernetes client", + "Unable to create JWT token", err, ) - + return } - handler.KubernetesClient = kubeCli + singleEndpointList := []portainer.Endpoint{ + *endpoint, + } + config, handlerErr := handler.buildConfig( + r, + tokenData, + bearerToken, + singleEndpointList, + ) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable to build endpoint kubeconfig", + handlerErr.Err, + ) + return + } + + if len(config.Clusters) == 0 { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable build cluster kubeconfig", + errors.New("Unable build cluster kubeconfig"), + ) + return + } + + // Manually setting the localhost to route + // the request to proxy server + serverURL, err := url.Parse(config.Clusters[0].Cluster.Server) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable parse cluster's kubeconfig server URL", + nil, + ) + return + } + serverURL.Scheme = "https" + serverURL.Host = "localhost" + handler.KubernetesClientFactory.AddrHTTPS + config.Clusters[0].Cluster.Server = serverURL.String() + + yaml, err := cli.GenerateYAML(config) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Unable to generate yaml from endpoint kubeconfig", + err, + ) + return + } + kubeCli, err := handler.KubernetesClientFactory.CreateKubeClientFromKubeConfig(endpoint.Name, []byte(yaml)) + if err != nil { + httperror.WriteError( + w, + http.StatusInternalServerError, + "Failed to create client from kubeconfig", + err, + ) + return + } + + handler.KubernetesClientFactory.SetProxyKubeClient(strconv.Itoa(int(endpoint.ID)), r.Header.Get("Authorization"), kubeCli) next.ServeHTTP(w, r) }) } diff --git a/api/http/handler/kubernetes/ingresses.go b/api/http/handler/kubernetes/ingresses.go index 741436f82..abc9842ed 100644 --- a/api/http/handler/kubernetes/ingresses.go +++ b/api/http/handler/kubernetes/ingresses.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -49,7 +50,13 @@ func (handler *Handler) getKubernetesIngressControllers(w http.ResponseWriter, r ) } - controllers := cli.GetIngressControllers() + controllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Failed to fetch ingressclasses", + err, + ) + } existingClasses := endpoint.Kubernetes.Configuration.IngressClasses var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { @@ -129,8 +136,23 @@ func (handler *Handler) getKubernetesIngressControllersByNamespace(w http.Respon ) } - cli := handler.KubernetesClient - currentControllers := cli.GetIngressControllers() + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + + currentControllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Failed to fetch ingressclasses", + err, + ) + } kubernetesConfig := endpoint.Kubernetes.Configuration existingClasses := kubernetesConfig.IngressClasses ingressAvailabilityPerNamespace := kubernetesConfig.IngressAvailabilityPerNamespace @@ -229,7 +251,13 @@ func (handler *Handler) updateKubernetesIngressControllers(w http.ResponseWriter } existingClasses := endpoint.Kubernetes.Configuration.IngressClasses - controllers := cli.GetIngressControllers() + controllers, err := cli.GetIngressControllers() + if err != nil { + return httperror.InternalServerError( + "Unable to get ingress controllers", + err, + ) + } var updatedClasses []portainer.KubernetesIngressClassConfig for i := range controllers { controllers[i].Availability = true @@ -401,11 +429,28 @@ func (handler *Handler) getKubernetesIngresses(w http.ResponseWriter, r *http.Re ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + ingresses, err := cli.GetIngresses(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve ingresses", err, ) } @@ -431,11 +476,28 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.CreateIngress(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve the ingress", err, ) } @@ -443,10 +505,26 @@ func (handler *Handler) createKubernetesIngress(w http.ResponseWriter, r *http.R } func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sIngressDeleteRequests - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest("Invalid request payload", err) } @@ -454,7 +532,7 @@ func (handler *Handler) deleteKubernetesIngresses(w http.ResponseWriter, r *http err = cli.DeleteIngresses(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete ingresses", err, ) } @@ -479,11 +557,28 @@ func (handler *Handler) updateKubernetesIngress(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.UpdateIngress(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to update the ingress", err, ) } diff --git a/api/http/handler/kubernetes/namespaces.go b/api/http/handler/kubernetes/namespaces.go index 884ae7038..68ba70261 100644 --- a/api/http/handler/kubernetes/namespaces.go +++ b/api/http/handler/kubernetes/namespaces.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -10,12 +11,28 @@ import ( ) func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespaces, err := cli.GetNamespaces() if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to retrieve namespaces", err, ) } @@ -24,10 +41,26 @@ func (handler *Handler) getKubernetesNamespaces(w http.ResponseWriter, r *http.R } func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sNamespaceDetails - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest( "Invalid request payload", @@ -38,7 +71,7 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http err = cli.CreateNamespace(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to create namespace", err, ) } @@ -46,7 +79,23 @@ func (handler *Handler) createKubernetesNamespace(w http.ResponseWriter, r *http } func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } namespace, err := request.RetrieveRouteVariableValue(r, "namespace") if err != nil { @@ -59,7 +108,7 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt err = cli.DeleteNamespace(namespace) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete namespace", err, ) } @@ -68,17 +117,33 @@ func (handler *Handler) deleteKubernetesNamespaces(w http.ResponseWriter, r *htt } func (handler *Handler) updateKubernetesNamespace(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sNamespaceDetails - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest("Invalid request payload", err) } err = cli.UpdateNamespace(payload) if err != nil { - return httperror.InternalServerError("Unable to retrieve nodes limits", err) + return httperror.InternalServerError("Unable to update namespace", err) } return nil } diff --git a/api/http/handler/kubernetes/services.go b/api/http/handler/kubernetes/services.go index 2e60513ce..0cc223eb9 100644 --- a/api/http/handler/kubernetes/services.go +++ b/api/http/handler/kubernetes/services.go @@ -2,6 +2,7 @@ package kubernetes import ( "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -18,7 +19,24 @@ func (handler *Handler) getKubernetesServices(w http.ResponseWriter, r *http.Req ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + services, err := cli.GetServices(namespace) if err != nil { return httperror.InternalServerError( @@ -48,11 +66,28 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } + err = cli.CreateService(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to create sercice", err, ) } @@ -60,10 +95,26 @@ func (handler *Handler) createKubernetesService(w http.ResponseWriter, r *http.R } func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } var payload models.K8sServiceDeleteRequests - err := request.DecodeAndValidateJSONPayload(r, &payload) + err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return httperror.BadRequest( "Invalid request payload", @@ -74,7 +125,7 @@ func (handler *Handler) deleteKubernetesServices(w http.ResponseWriter, r *http. err = cli.DeleteServices(payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to delete service", err, ) } @@ -99,11 +150,27 @@ func (handler *Handler) updateKubernetesService(w http.ResponseWriter, r *http.R ) } - cli := handler.KubernetesClient + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return httperror.BadRequest( + "Invalid environment identifier route variable", + err, + ) + } + + cli, ok := handler.KubernetesClientFactory.GetProxyKubeClient( + strconv.Itoa(endpointID), r.Header.Get("Authorization"), + ) + if !ok { + return httperror.InternalServerError( + "Failed to lookup KubeClient", + nil, + ) + } err = cli.UpdateService(namespace, payload) if err != nil { return httperror.InternalServerError( - "Unable to retrieve nodes limits", + "Unable to update service", err, ) } diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index 23bd59993..821a17a0e 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -5,8 +5,10 @@ import ( "net/http" "strconv" "sync" + "time" cmap "github.com/orcaman/concurrent-map" + "github.com/patrickmn/go-cache" "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/dataservices" @@ -23,6 +25,8 @@ type ( signatureService portainer.DigitalSignatureService instanceID string endpointClients cmap.ConcurrentMap + endpointProxyClients *cache.Cache + AddrHTTPS string } // KubeClient represent a service used to execute Kubernetes operations @@ -34,14 +38,24 @@ type ( ) // NewClientFactory returns a new instance of a ClientFactory -func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string, dataStore dataservices.DataStore) *ClientFactory { +func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, dataStore dataservices.DataStore, instanceID, addrHTTPS, userSessionTimeout string) (*ClientFactory, error) { + if userSessionTimeout == "" { + userSessionTimeout = portainer.DefaultUserSessionTimeout + } + timeout, err := time.ParseDuration(userSessionTimeout) + if err != nil { + return nil, err + } + return &ClientFactory{ dataStore: dataStore, signatureService: signatureService, reverseTunnelService: reverseTunnelService, instanceID: instanceID, endpointClients: cmap.New(), - } + endpointProxyClients: cache.New(timeout, timeout), + AddrHTTPS: addrHTTPS, + }, nil } func (factory *ClientFactory) GetInstanceID() (instanceID string) { @@ -59,7 +73,7 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta key := strconv.Itoa(int(endpoint.ID)) client, ok := factory.endpointClients.Get(key) if !ok { - client, err := factory.createKubeClient(endpoint) + client, err := factory.createCachedAdminKubeClient(endpoint) if err != nil { return nil, err } @@ -71,7 +85,49 @@ func (factory *ClientFactory) GetKubeClient(endpoint *portainer.Endpoint) (porta return client.(portainer.KubeClient), nil } -func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { +// GetProxyKubeClient retrieves a KubeClient from the cache. You should be +// calling SetProxyKubeClient before first. It is normally, called the +// kubernetes middleware. +func (factory *ClientFactory) GetProxyKubeClient(endpointID, token string) (portainer.KubeClient, bool) { + client, ok := factory.endpointProxyClients.Get(endpointID + "." + token) + if !ok { + return nil, false + } + return client.(portainer.KubeClient), true +} + +// SetProxyKubeClient stores a kubeclient in the cache. +func (factory *ClientFactory) SetProxyKubeClient(endpointID, token string, cli portainer.KubeClient) { + factory.endpointProxyClients.Set(endpointID+"."+token, cli, 0) +} + +// CreateKubeClientFromKubeConfig creates a KubeClient from a clusterID, and +// Kubernetes config. +func (factory *ClientFactory) CreateKubeClientFromKubeConfig(clusterID string, kubeConfig []byte) (portainer.KubeClient, error) { + config, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig)) + if err != nil { + return nil, err + } + cliConfig, err := config.ClientConfig() + if err != nil { + return nil, err + } + + cli, err := kubernetes.NewForConfig(cliConfig) + if err != nil { + return nil, err + } + + kubecli := &KubeClient{ + cli: cli, + instanceID: factory.instanceID, + lock: &sync.Mutex{}, + } + + return kubecli, nil +} + +func (factory *ClientFactory) createCachedAdminKubeClient(endpoint *portainer.Endpoint) (portainer.KubeClient, error) { cli, err := factory.CreateClient(endpoint) if err != nil { return nil, err diff --git a/api/kubernetes/cli/ingress.go b/api/kubernetes/cli/ingress.go index ce7ffc442..7eaf3e593 100644 --- a/api/kubernetes/cli/ingress.go +++ b/api/kubernetes/cli/ingress.go @@ -5,11 +5,12 @@ import ( "strings" "github.com/portainer/portainer/api/database/models" + "github.com/rs/zerolog/log" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { +func (kcl *KubeClient) GetIngressControllers() (models.K8sIngressControllers, error) { var controllers []models.K8sIngressController // We know that each existing class points to a controller so we can start @@ -17,19 +18,22 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { classClient := kcl.cli.NetworkingV1().IngressClasses() classList, err := classClient.List(context.Background(), metav1.ListOptions{}) if err != nil { - return nil + return nil, err } // We want to know which of these controllers is in use. var ingresses []models.K8sIngressInfo namespaces, err := kcl.GetNamespaces() if err != nil { - return nil + return nil, err } for namespace := range namespaces { t, err := kcl.GetIngresses(namespace) if err != nil { - return nil + // User might not be able to list ingresses in system/not allowed + // namespaces. + log.Debug().Err(err).Msg("failed to list ingresses for the current user, skipped sending ingress") + continue } ingresses = append(ingresses, t...) } @@ -58,7 +62,7 @@ func (kcl *KubeClient) GetIngressControllers() models.K8sIngressControllers { } controllers = append(controllers, controller) } - return controllers + return controllers, nil } // GetIngresses gets all the ingresses for a given namespace in a k8s endpoint. diff --git a/api/kubernetes/cli/resource_test.go b/api/kubernetes/cli/resource_test.go index 902671103..d86044e98 100644 --- a/api/kubernetes/cli/resource_test.go +++ b/api/kubernetes/cli/resource_test.go @@ -85,7 +85,7 @@ func Test_GenerateYAML(t *testing.T) { t.Errorf("generateYamlConfig failed; err=%s", err) } - if compareYAMLStrings(yaml, ryt.wantYAML) != 0 { + if compareYAMLStrings(string(yaml), ryt.wantYAML) != 0 { t.Errorf("generateYamlConfig failed;\ngot=\n%s\nwant=\n%s", yaml, ryt.wantYAML) } }) diff --git a/api/kubernetes/cli/role.go b/api/kubernetes/cli/role.go index 4bf7f58fc..80fc825e2 100644 --- a/api/kubernetes/cli/role.go +++ b/api/kubernetes/cli/role.go @@ -25,6 +25,11 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule { Resources: []string{"namespaces", "pods", "nodes"}, APIGroups: []string{"metrics.k8s.io"}, }, + { + Verbs: []string{"list"}, + Resources: []string{"ingressclasses"}, + APIGroups: []string{"networking.k8s.io"}, + }, } } diff --git a/api/kubernetes/kubeclusteraccess_service.go b/api/kubernetes/kubeclusteraccess_service.go index d7da7bd2e..4a97885d4 100644 --- a/api/kubernetes/kubeclusteraccess_service.go +++ b/api/kubernetes/kubeclusteraccess_service.go @@ -106,7 +106,7 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/")) } - log.Info(). + log.Debug(). Str("host_URL", hostURL). Str("HTTPS_bind_address", service.httpsBindAddr). Str("base_URL", baseURL). diff --git a/api/portainer.go b/api/portainer.go index 11ae98781..0277de258 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1357,7 +1357,7 @@ type ( GetNamespaces() (map[string]K8sNamespaceInfo, error) DeleteNamespace(namespace string) error GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) - GetIngressControllers() models.K8sIngressControllers + GetIngressControllers() (models.K8sIngressControllers, error) CreateIngress(namespace string, info models.K8sIngressInfo) error UpdateIngress(namespace string, info models.K8sIngressInfo) error GetIngresses(namespace string) ([]models.K8sIngressInfo, error) diff --git a/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx b/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx index 738c432ef..f4f201873 100644 --- a/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx +++ b/app/kubernetes/react/views/networks/ingresses/IngressDatatable/IngressDataTable.tsx @@ -3,7 +3,7 @@ import { useRouter } from '@uirouter/react'; import { useEnvironmentId } from '@/portainer/hooks/useEnvironmentId'; import { useNamespaces } from '@/react/kubernetes/namespaces/queries'; -import { Authorized } from '@/portainer/hooks/useUser'; +import { useAuthorizations, Authorized } from '@/portainer/hooks/useUser'; import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm'; import { Datatable } from '@@/datatables'; @@ -55,6 +55,7 @@ export function IngressDataTable() { }} getRowId={(row) => row.Name + row.Type + row.Namespace} renderTableActions={tableActions} + disableSelect={useCheckboxes()} /> ); @@ -80,7 +81,7 @@ export function IngressDataTable() { - +