diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 56d5e0278..f6129d81d 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -16,6 +16,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" k "github.com/portainer/portainer/api/kubernetes" + "github.com/portainer/portainer/api/http/client" ) const defaultReferenceName = "refs/heads/master" @@ -37,6 +38,12 @@ type kubernetesGitDeploymentPayload struct { FilePathInRepository string } +type kubernetesManifestURLDeploymentPayload struct { + Namespace string + ComposeFormat bool + ManifestURL string +} + func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") @@ -66,6 +73,13 @@ func (payload *kubernetesGitDeploymentPayload) Validate(r *http.Request) error { return nil } +func (payload *kubernetesManifestURLDeploymentPayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.ManifestURL) || !govalidator.IsURL(payload.ManifestURL) { + return errors.New("Invalid manifest URL") + } + return nil +} + type createKubernetesStackResponse struct { Output string `json:"Output"` } @@ -171,6 +185,61 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr return response.JSON(w, resp) } + +func (handler *Handler) createKubernetesStackFromManifestURL(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError { + var payload kubernetesManifestURLDeploymentPayload + if err := request.DecodeAndValidateJSONPayload(r, &payload); err != nil { + return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid request payload", Err: err} + } + + stackID := handler.DataStore.Stack().GetNextIdentifier() + stack := &portainer.Stack{ + ID: portainer.StackID(stackID), + Type: portainer.KubernetesStack, + EndpointID: endpoint.ID, + EntryPoint: filesystem.ManifestFileDefaultName, + Status: portainer.StackStatusActive, + CreationDate: time.Now().Unix(), + } + + var manifestContent []byte + manifestContent, err := client.Get(payload.ManifestURL, 30) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve manifest from URL", err} + } + + stackFolder := strconv.Itoa(int(stack.ID)) + projectPath, err := handler.FileService.StoreStackFileFromBytes(stackFolder, stack.EntryPoint, manifestContent) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist Kubernetes manifest file on disk", Err: err} + } + stack.ProjectPath = projectPath + + doCleanUp := true + defer handler.cleanUp(stack, &doCleanUp) + + output, err := handler.deployKubernetesStack(r, endpoint, string(manifestContent), payload.ComposeFormat, payload.Namespace, k.KubeAppLabels{ + StackID: stackID, + Name: stack.Name, + Owner: stack.CreatedBy, + Kind: "url", + }) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err} + } + + err = handler.DataStore.Stack().CreateStack(stack) + if err != nil { + return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to persist the Kubernetes stack inside the database", Err: err} + } + + resp := &createKubernetesStackResponse{ + Output: output, + } + + return response.JSON(w, resp) +} + func (handler *Handler) deployKubernetesStack(r *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string, appLabels k.KubeAppLabels) (string, error) { handler.stackCreationMutex.Lock() defer handler.stackCreationMutex.Unlock() diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index d21bf3a1c..2ffb455e8 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -149,6 +149,8 @@ func (handler *Handler) createKubernetesStack(w http.ResponseWriter, r *http.Req return handler.createKubernetesStackFromFileContent(w, r, endpoint) case "repository": return handler.createKubernetesStackFromGitRepository(w, r, endpoint) + case "url": + return handler.createKubernetesStackFromManifestURL(w, r, endpoint) } return &httperror.HandlerError{StatusCode: http.StatusBadRequest, Message: "Invalid value for query parameter: method. Value must be one of: string or repository", Err: errors.New(request.ErrInvalidQueryParameter)} } diff --git a/app/kubernetes/models/deploy.js b/app/kubernetes/models/deploy.js index 797dee0ea..6d162ce5d 100644 --- a/app/kubernetes/models/deploy.js +++ b/app/kubernetes/models/deploy.js @@ -7,9 +7,11 @@ export const KubernetesDeployBuildMethods = Object.freeze({ GIT: 1, WEB_EDITOR: 2, CUSTOM_TEMPLATE: 3, + URL: 4 }); export const KubernetesDeployRequestMethods = Object.freeze({ REPOSITORY: 'repository', STRING: 'string', + URL: 'url' }); diff --git a/app/kubernetes/views/deploy/deploy.html b/app/kubernetes/views/deploy/deploy.html index 9f5d71240..dd7b61228 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -111,6 +111,33 @@ + + +
+
+ URL +
+
+ + Indicate the URL to the manifest. + +
+
+ +
+ +
+
+
+ +
Actions diff --git a/app/kubernetes/views/deploy/deployController.js b/app/kubernetes/views/deploy/deployController.js index 6eac3855f..fff228bbe 100644 --- a/app/kubernetes/views/deploy/deployController.js +++ b/app/kubernetes/views/deploy/deployController.js @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash-es'; import stripAnsi from 'strip-ansi'; +import PortainerError from 'Portainer/error'; import { KubernetesDeployManifestTypes, KubernetesDeployBuildMethods, KubernetesDeployRequestMethods } from 'Kubernetes/models/deploy'; import { buildOption } from '@/portainer/components/box-selector'; @@ -25,6 +26,7 @@ class KubernetesDeployController { this.methodOptions = [ buildOption('method_repo', 'fab fa-github', 'Git Repository', 'Use a git repository', KubernetesDeployBuildMethods.GIT), buildOption('method_editor', 'fa fa-edit', 'Web editor', 'Use our Web editor', KubernetesDeployBuildMethods.WEB_EDITOR), + buildOption('method_url', 'fa fa-globe', 'URL', 'Specify a URL to a file', KubernetesDeployBuildMethods.URL), buildOption('method_template', 'fa fa-rocket', 'Custom Template', 'Use a custom template', KubernetesDeployBuildMethods.CUSTOM_TEMPLATE), ]; @@ -57,10 +59,11 @@ class KubernetesDeployController { this.state.BuildMethod === KubernetesDeployBuildMethods.GIT && (!this.formValues.RepositoryURL || !this.formValues.FilePathInRepository || - (this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))); - const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent); + (this.formValues.RepositoryAuthentication && (!this.formValues.RepositoryUsername || !this.formValues.RepositoryPassword))) && _.isEmpty(this.formValues.Namespace); + const isWebEditorInvalid = this.state.BuildMethod === KubernetesDeployBuildMethods.WEB_EDITOR && _.isEmpty(this.formValues.EditorContent) && _.isEmpty(this.formValues.Namespace); + const isURLFormInvalid = this.state.BuildMethod == KubernetesDeployBuildMethods.WEB_EDITOR.URL && _.isEmpty(this.formValues.ManifestURL); - return isGitFormInvalid || isWebEditorInvalid || _.isEmpty(this.formValues.Namespace) || this.state.actionInProgress; + return isGitFormInvalid || isWebEditorInvalid || isURLFormInvalid || this.state.actionInProgress; } onChangeFormValues(values) { @@ -111,16 +114,25 @@ class KubernetesDeployController { this.state.actionInProgress = true; try { - let method = KubernetesDeployRequestMethods.STRING; + let method; let composeFormat = this.state.DeployType === this.ManifestDeployTypes.COMPOSE; switch (this.state.BuildMethod) { - case KubernetesDeployBuildMethods.GIT: + case this.BuildMethods.GIT: method = KubernetesDeployRequestMethods.REPOSITORY; break; - case KubernetesDeployBuildMethods.CUSTOM_TEMPLATE: - composeFormat = false; + case this.BuildMethods.WEB_EDITOR: + method = KubernetesDeployRequestMethods.STRING; break; + case KubernetesDeployBuildMethods.CUSTOM_TEMPLATE: + method = KubernetesDeployRequestMethods.STRING; + composeFormat = false; + break; + case this.BuildMethods.URL: + method = KubernetesDeployRequestMethods.URL; + break; + default: + throw new PortainerError('Unable to determine build method'); } const payload = { @@ -137,8 +149,10 @@ class KubernetesDeployController { payload.RepositoryPassword = this.formValues.RepositoryPassword; } payload.FilePathInRepository = this.formValues.FilePathInRepository; - } else { + } else if (method === KubernetesDeployRequestMethods.STRING) { payload.StackFileContent = this.formValues.EditorContent; + } else { + payload.ManifestURL = this.formValues.ManifestURL; } await this.StackService.kubernetesDeploy(this.endpointId, method, payload);