package kubernetes import ( "errors" "fmt" "net/http" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/internal/endpointutils" kcli "github.com/portainer/portainer/api/kubernetes/cli" 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" clientV1 "k8s.io/client-go/tools/clientcmd/api/v1" ) // @id GetKubernetesConfig // @summary Generate a kubeconfig file // @description Generate a kubeconfig file that allows a client to communicate with the Kubernetes API server // @description **Access policy**: Authenticated user. // @tags kubernetes // @security ApiKeyAuth || jwt // @produce application/json, application/yaml // @param ids query []int false "will include only these environments(endpoints)" // @param excludeIds query []int false "will exclude these environments(endpoints)" // @success 200 {object} interface{} "Success" // @failure 400 "Invalid request payload, such as missing required fields or fields not meeting validation criteria." // @failure 401 "Unauthorized access - the user is not authenticated or does not have the necessary permissions. Ensure that you have provided a valid API key or JWT token, and that you have the required permissions." // @failure 403 "Permission denied - the user is authenticated but does not have the necessary permissions to access the requested resource or perform the specified operation. Check your user roles and permissions." // @failure 404 "Unable to find an environment with the specified identifier." // @failure 500 "Server error occurred while attempting to generate the kubeconfig file." // @router /kubernetes/config [get] func (handler *Handler) getKubernetesConfig(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { tokenData, err := security.RetrieveTokenData(r) if err != nil { log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Permission denied to access environment") return httperror.Forbidden("Permission denied to access environment", err) } bearerToken, err := handler.JwtService.GenerateTokenForKubeconfig(tokenData) if err != nil { log.Error().Err(err).Str("context", "getKubernetesConfig").Msg("Unable to generate JWT token") return httperror.InternalServerError("Unable to generate JWT token", err) } endpoints, handlerErr := handler.filterUserKubeEndpoints(r) if handlerErr != nil { log.Error().Err(handlerErr).Str("context", "getKubernetesConfig").Msg("Unable to filter user kube endpoints") return handlerErr } if len(endpoints) == 0 { log.Error().Str("context", "getKubernetesConfig").Msg("Empty endpoints list") return httperror.BadRequest("empty endpoints list", errors.New("empty endpoints list")) } config := handler.buildConfig(r, tokenData, bearerToken, endpoints, false) return writeFileContent(w, r, endpoints, tokenData, config) } func (handler *Handler) filterUserKubeEndpoints(r *http.Request) ([]portainer.Endpoint, *httperror.HandlerError) { var endpointIDs []portainer.EndpointID _ = request.RetrieveJSONQueryParameter(r, "ids", &endpointIDs, true) var excludeEndpointIDs []portainer.EndpointID _ = request.RetrieveJSONQueryParameter(r, "excludeIds", &excludeEndpointIDs, true) if len(endpointIDs) > 0 && len(excludeEndpointIDs) > 0 { log.Error().Str("context", "filterUserKubeEndpoints").Msg("Can't provide both 'ids' and 'excludeIds' parameters") return nil, httperror.BadRequest("Can't provide both 'ids' and 'excludeIds' parameters", errors.New("invalid parameters")) } securityContext, err := security.RetrieveRestrictedRequestContext(r) if err != nil { log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve info from request context") return nil, httperror.InternalServerError("Unable to retrieve info from request context", err) } endpointGroups, err := handler.DataStore.EndpointGroup().ReadAll() if err != nil { log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment groups from the database") return nil, httperror.InternalServerError("Unable to retrieve environment groups from the database", err) } if len(endpointIDs) > 0 { var endpoints []portainer.Endpoint for _, endpointID := range endpointIDs { endpoint, err := handler.DataStore.Endpoint().Endpoint(endpointID) if err != nil { log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environment from the database") return nil, httperror.InternalServerError("Unable to retrieve environment from the database", err) } if !endpointutils.IsKubernetesEndpoint(endpoint) { continue } endpoints = append(endpoints, *endpoint) } filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext) return filteredEndpoints, nil } var kubeEndpoints []portainer.Endpoint endpoints, err := handler.DataStore.Endpoint().Endpoints() if err != nil { log.Error().Err(err).Str("context", "filterUserKubeEndpoints").Msg("Unable to retrieve environments from the database") return nil, httperror.InternalServerError("Unable to retrieve environments from the database", err) } for _, endpoint := range endpoints { if !endpointutils.IsKubernetesEndpoint(&endpoint) { continue } kubeEndpoints = append(kubeEndpoints, endpoint) } filteredEndpoints := security.FilterEndpoints(kubeEndpoints, endpointGroups, securityContext) if len(excludeEndpointIDs) > 0 { filteredEndpoints = endpointutils.FilterByExcludeIDs(filteredEndpoints, excludeEndpointIDs) } return filteredEndpoints, nil } func (handler *Handler) buildConfig(r *http.Request, tokenData *portainer.TokenData, bearerToken string, endpoints []portainer.Endpoint, isInternal bool) *clientV1.Config { var configAuthInfos []clientV1.NamedAuthInfo configClusters := make([]clientV1.NamedCluster, len(endpoints)) configContexts := make([]clientV1.NamedContext, len(endpoints)) authInfosSet := make(map[string]bool) for idx, endpoint := range endpoints { instanceID := handler.KubernetesClientFactory.GetInstanceID() serviceAccountName := kcli.UserServiceAccountName(int(tokenData.ID), instanceID) configClusters[idx] = handler.buildCluster(r, endpoint, isInternal) configContexts[idx] = buildContext(serviceAccountName, endpoint) if !authInfosSet[serviceAccountName] { configAuthInfos = append(configAuthInfos, buildAuthInfo(serviceAccountName, bearerToken)) authInfosSet[serviceAccountName] = true } } return &clientV1.Config{ APIVersion: "v1", Kind: "Config", Clusters: configClusters, Contexts: configContexts, CurrentContext: configContexts[0].Name, AuthInfos: configAuthInfos, } } // 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), Cluster: clientV1.Cluster{ Server: kubeConfigInternal.ClusterServerURL, InsecureSkipTLSVerify: true, }, } } func buildClusterName(endpointName string) string { return "portainer-cluster-" + endpointName } func buildContext(serviceAccountName string, endpoint portainer.Endpoint) clientV1.NamedContext { return clientV1.NamedContext{ Name: "portainer-ctx-" + endpoint.Name, Context: clientV1.Context{ AuthInfo: serviceAccountName, Cluster: buildClusterName(endpoint.Name), }, } } func buildAuthInfo(serviceAccountName string, bearerToken string) clientV1.NamedAuthInfo { return clientV1.NamedAuthInfo{ Name: serviceAccountName, AuthInfo: clientV1.AuthInfo{ Token: bearerToken, }, } } func writeFileContent(w http.ResponseWriter, r *http.Request, endpoints []portainer.Endpoint, tokenData *portainer.TokenData, config *clientV1.Config) *httperror.HandlerError { filenameSuffix := "kubeconfig" if len(endpoints) == 1 { filenameSuffix = endpoints[0].Name } filenameBase := fmt.Sprintf("%s-%s", tokenData.Username, filenameSuffix) if r.Header.Get("Accept") == "text/yaml" { yaml, err := kcli.GenerateYAML(config) if err != nil { log.Error().Err(err).Str("context", "writeFileContent").Msg("Failed to generate Kubeconfig") return httperror.InternalServerError("Failed to generate Kubeconfig", err) } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.yaml", filenameBase)) return response.YAML(w, yaml) } w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; %s.json", filenameBase)) return response.JSON(w, config) }