diff --git a/api/http/handler/helm/handler.go b/api/http/handler/helm/handler.go index dcf598412..a55345cc2 100644 --- a/api/http/handler/helm/handler.go +++ b/api/http/handler/helm/handler.go @@ -110,7 +110,7 @@ func (handler *Handler) getHelmClusterAccess(r *http.Request) (*options.Kubernet hostURL = r.Host } - kubeConfigInternal := handler.kubeClusterAccessService.GetData(hostURL, endpoint.ID) + kubeConfigInternal := handler.kubeClusterAccessService.GetClusterDetails(hostURL, endpoint.ID, true) return &options.KubernetesClusterAccess{ ClusterServerURL: kubeConfigInternal.ClusterServerURL, CertificateAuthorityFile: kubeConfigInternal.CertificateAuthorityFile, diff --git a/api/http/handler/kubernetes/config.go b/api/http/handler/kubernetes/config.go index e0b596054..32a1aa8eb 100644 --- a/api/http/handler/kubernetes/config.go +++ b/api/http/handler/kubernetes/config.go @@ -51,7 +51,7 @@ func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Reque return httperror.BadRequest("empty endpoints list", errors.New("empty endpoints list")) } - config, handlerErr := handler.buildConfig(r, tokenData, bearerToken, endpoints) + config, handlerErr := handler.buildConfig(r, tokenData, bearerToken, endpoints, false) if handlerErr != nil { return handlerErr } @@ -115,7 +115,7 @@ func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.En return filteredEndpoints, nil } -func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint) (*clientV1.Config, *httperror.HandlerError) { +func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint, isInternal bool) (*clientV1.Config, *httperror.HandlerError) { configClusters := make([]clientV1.NamedCluster, len(endpoints)) configContexts := make([]clientV1.NamedContext, len(endpoints)) var configAuthInfos []clientV1.NamedAuthInfo @@ -125,7 +125,7 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD instanceID := handler.KubernetesClientFactory.GetInstanceID() serviceAccountName := kcli.UserServiceAccountName(int(tokenData.ID), instanceID) - configClusters[idx] = handler.buildCluster(r, endpoint) + configClusters[idx] = handler.buildCluster(r, endpoint, isInternal) configContexts[idx] = buildContext(serviceAccountName, endpoint) if !authInfosSet[serviceAccountName] { configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccountName, bearerToken)) @@ -143,8 +143,9 @@ func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenD }, nil } -func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint) clientV1.NamedCluster { - kubeConfigInternal := handler.kubeClusterAccessService.GetData(r.Host, endpoint.ID) +// buildCluster builds a Kubernetes cluster configuration based on the endpoint and if it's used internally or externally. +func (handler *Handler) buildCluster(r *http.Request, endpoint portainer.Endpoint, isInternal bool) clientV1.NamedCluster { + kubeConfigInternal := handler.kubeClusterAccessService.GetClusterDetails(r.Host, endpoint.ID, isInternal) return clientV1.NamedCluster{ Name: buildClusterName(endpoint.Name), diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index c6bda07d3..d634da743 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -171,6 +171,7 @@ func (handler *Handler) kubeClient(next http.Handler) http.Handler { tokenData, bearerToken, singleEndpointList, + true, ) if err != nil { httperror.WriteError( diff --git a/api/kubernetes/kubeclusteraccess_service.go b/api/kubernetes/kubeclusteraccess_service.go index a5145d585..60f0cc503 100644 --- a/api/kubernetes/kubeclusteraccess_service.go +++ b/api/kubernetes/kubeclusteraccess_service.go @@ -4,9 +4,9 @@ import ( "crypto/x509" "encoding/base64" "encoding/pem" - "fmt" + "net/url" "os" - "strings" + "strconv" portainer "github.com/portainer/portainer/api" @@ -17,7 +17,7 @@ import ( // KubeClusterAccessService represents a service that is responsible for centralizing kube cluster access data type KubeClusterAccessService interface { IsSecure() bool - GetData(hostURL string, endpointId portainer.EndpointID) kubernetesClusterAccessData + GetClusterDetails(hostURL string, endpointId portainer.EndpointID, isInternal bool) kubernetesClusterAccessData } // KubernetesClusterAccess represents core details which can be used to generate KubeConfig file/data @@ -89,21 +89,20 @@ func (service *kubeClusterAccessService) IsSecure() bool { return service.certificateAuthorityData != "" } -// GetData returns K8s cluster access details for the specified environment(endpoint). +// GetClusterDetails returns K8s cluster access details for the specified environment(endpoint). // The struct can be used to: // - generate a kubeconfig file // - pass down params to binaries -func (service *kubeClusterAccessService) GetData(hostURL string, endpointID portainer.EndpointID) kubernetesClusterAccessData { - baseURL := service.baseURL - - // When the api call is internal, the baseURL should not be used. +// - isInternal is used to determine whether the kubeclient is accessed internally (for example using the kube client for backend calls) or externally (for example downloading the kubeconfig file) +func (service *kubeClusterAccessService) GetClusterDetails(hostURL string, endpointID portainer.EndpointID, isInternal bool) kubernetesClusterAccessData { if hostURL == "localhost" { hostURL += service.httpsBindAddr - baseURL = "/" } - if baseURL != "/" { - baseURL = fmt.Sprintf("/%s/", strings.Trim(baseURL, "/")) + baseURL := service.baseURL + // When the kubeclient call is internal, the baseURL should not be used. + if isInternal { + baseURL = "" } log.Debug(). @@ -112,9 +111,10 @@ func (service *kubeClusterAccessService) GetData(hostURL string, endpointID port Str("base_URL", baseURL). Msg("kubeconfig") - clusterURL := hostURL + baseURL - - clusterServerURL := fmt.Sprintf("https://%sapi/endpoints/%d/kubernetes", clusterURL, endpointID) + clusterServerURL, err := url.JoinPath("https://", hostURL, baseURL, "/api/endpoints/", strconv.Itoa(int(endpointID)), "/kubernetes") + if err != nil { + log.Error().Err(err).Msg("Failed to create Kubeconfig cluster URL") + } return kubernetesClusterAccessData{ ClusterServerURL: clusterServerURL, diff --git a/api/kubernetes/kubeclusteraccess_service_test.go b/api/kubernetes/kubeclusteraccess_service_test.go index 71ce50f30..706b13849 100644 --- a/api/kubernetes/kubeclusteraccess_service_test.go +++ b/api/kubernetes/kubeclusteraccess_service_test.go @@ -91,21 +91,21 @@ func TestKubeClusterAccessService_IsSecure(t *testing.T) { func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) { is := assert.New(t) - t.Run("GetData contains host address", func(t *testing.T) { + t.Run("GetClusterDetails contains host address", func(t *testing.T) { kcs := NewKubeClusterAccessService("/", "", "") - clusterAccessDetails := kcs.GetData("mysite.com", 1) + clusterAccessDetails := kcs.GetClusterDetails("mysite.com", 1, true) is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "https://mysite.com"), "should contain host address") }) - t.Run("GetData contains environment proxy url", func(t *testing.T) { + t.Run("GetClusterDetails contains environment proxy url", func(t *testing.T) { kcs := NewKubeClusterAccessService("/", "", "") - clusterAccessDetails := kcs.GetData("mysite.com", 100) + clusterAccessDetails := kcs.GetClusterDetails("mysite.com", 100, true) is.True(strings.Contains(clusterAccessDetails.ClusterServerURL, "api/endpoints/100/kubernetes"), "should contain environment proxy url") }) - t.Run("GetData returns insecure cluster access config", func(t *testing.T) { + t.Run("GetClusterDetails returns insecure cluster access config", func(t *testing.T) { kcs := NewKubeClusterAccessService("/", ":9443", "") - clusterAccessDetails := kcs.GetData("mysite.com", 1) + clusterAccessDetails := kcs.GetClusterDetails("mysite.com", 1, true) wantClusterAccessDetails := kubernetesClusterAccessData{ ClusterServerURL: "https://mysite.com/api/endpoints/1/kubernetes", @@ -113,14 +113,14 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) { CertificateAuthorityData: "", } - is.Equal(clusterAccessDetails, wantClusterAccessDetails) + is.Equal(wantClusterAccessDetails, clusterAccessDetails) }) - t.Run("GetData returns secure cluster access config", func(t *testing.T) { + t.Run("GetClusterDetails returns secure cluster access config", func(t *testing.T) { filePath := createTempFile("valid-cert.crt", certData, t) kcs := NewKubeClusterAccessService("/", "", filePath) - clusterAccessDetails := kcs.GetData("localhost", 1) + clusterAccessDetails := kcs.GetClusterDetails("localhost", 1, true) wantClusterAccessDetails := kubernetesClusterAccessData{ ClusterServerURL: "https://localhost/api/endpoints/1/kubernetes", @@ -128,6 +128,6 @@ func TestKubeClusterAccessService_GetKubeConfigInternal(t *testing.T) { CertificateAuthorityData: certDataString, } - is.Equal(clusterAccessDetails, wantClusterAccessDetails) + is.Equal(wantClusterAccessDetails, clusterAccessDetails) }) }