package stacks import ( "errors" "net/http" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/useractivity" endpointutils "github.com/portainer/portainer/api/internal/endpoint" ) type kubernetesStackPayload struct { ComposeFormat bool Namespace string StackFileContent string } func (payload *kubernetesStackPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") } if govalidator.IsNull(payload.Namespace) { return errors.New("Invalid namespace") } return nil } type createKubernetesStackResponse struct { Output string `json:"Output"` } func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { if !endpointutils.IsKubernetesEndpoint(endpoint) { return &httperror.HandlerError{http.StatusBadRequest, "Endpoint type does not match", errors.New("Endpoint type does not match")} } permissionDeniedErr := "Permission denied to access endpoint" tokenData, err := security.RetrieveTokenData(r) if err != nil { return &httperror.HandlerError{http.StatusForbidden, permissionDeniedErr, err} } var payload kubernetesStackPayload err = request.DecodeAndValidateJSONPayload(r, &payload) if err != nil { return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} } namespace := payload.Namespace if tokenData.Role != portainer.AdministratorRole { // check if the user has OperationK8sApplicationsAdvancedDeploymentRW access in the endpoint endpointRole, err := handler.AuthorizationService.GetUserEndpointRole(int(tokenData.ID), int(endpoint.ID)) if err != nil { return &httperror.HandlerError{http.StatusForbidden, permissionDeniedErr, err} } else if !endpointRole.Authorizations[portainer.OperationK8sApplicationsAdvancedDeploymentRW] { err = errors.New(permissionDeniedErr) return &httperror.HandlerError{http.StatusForbidden, permissionDeniedErr, err} } // will skip if user can access all namespaces if !endpointRole.Authorizations[portainer.OperationK8sAccessAllNamespaces] { cli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} } // check if the user has RW access to the namespace namespaceAuthorizations, err := handler.AuthorizationService.GetNamespaceAuthorizations(int(tokenData.ID), *endpoint, cli) if err != nil { return &httperror.HandlerError{http.StatusForbidden, permissionDeniedErr, err} } else if auth, ok := namespaceAuthorizations[namespace]; !ok || !auth[portainer.OperationK8sAccessNamespaceWrite] { err = errors.New(permissionDeniedErr) return &httperror.HandlerError{http.StatusForbidden, permissionDeniedErr, err} } } } output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, namespace) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to deploy Kubernetes stack", err} } resp := &createKubernetesStackResponse{ Output: output, } useractivity.LogHttpActivity(handler.UserActivityStore, endpoint.Name, r, payload) return response.JSON(w, resp) } func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) { handler.stackCreationMutex.Lock() defer handler.stackCreationMutex.Unlock() if composeFormat { convertedConfig, err := handler.KubernetesDeployer.ConvertCompose(stackConfig) if err != nil { return "", err } stackConfig = string(convertedConfig) } return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace) }