From 1dd3dbfac53bddf759cb84abdcec1bbe86478da3 Mon Sep 17 00:00:00 2001 From: deviantony Date: Sat, 28 Aug 2021 20:58:46 +1200 Subject: [PATCH] quick poc to deploy applications from a manifest URL on k8s --- api/exec/kubernetes_deploy.go | 4 +- .../handler/stacks/create_kubernetes_stack.go | 63 +++++++++++++++++++ api/http/handler/stacks/stack_create.go | 2 + app/kubernetes/models/deploy.js | 2 + app/kubernetes/views/deploy/deploy.html | 29 ++++++++- .../views/deploy/deployController.js | 28 +++++++-- gruntfile.js | 2 +- 7 files changed, 122 insertions(+), 8 deletions(-) diff --git a/api/exec/kubernetes_deploy.go b/api/exec/kubernetes_deploy.go index 13ae7faab..e611c4b0a 100644 --- a/api/exec/kubernetes_deploy.go +++ b/api/exec/kubernetes_deploy.go @@ -94,7 +94,9 @@ func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *port args = append(args, "--server", endpoint.URL) args = append(args, "--insecure-skip-tls-verify") args = append(args, "--token", token) - args = append(args, "--namespace", namespace) + if (namespace != "") { + args = append(args, "--namespace", namespace) + } args = append(args, "apply", "-f", "-") var stderr bytes.Buffer diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 4de14d3a3..eca22b9b1 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -15,6 +15,7 @@ import ( "github.com/portainer/libhttp/response" portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/client" ) const defaultReferenceName = "refs/heads/master" @@ -36,6 +37,12 @@ type kubernetesGitDeploymentPayload struct { FilePathInRepository string } +type kubernetesManifestURLDeploymentPayload struct { + ComposeFormat bool + ManifestURL string +} + + func (payload *kubernetesStringDeploymentPayload) Validate(r *http.Request) error { if govalidator.IsNull(payload.StackFileContent) { return errors.New("Invalid stack file content") @@ -65,6 +72,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"` } @@ -155,6 +169,55 @@ 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, "") + 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(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (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..1c9c11b17 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 34fe609fe..fd545c6e4 100644 --- a/app/kubernetes/models/deploy.js +++ b/app/kubernetes/models/deploy.js @@ -6,9 +6,11 @@ export const KubernetesDeployManifestTypes = Object.freeze({ export const KubernetesDeployBuildMethods = Object.freeze({ GIT: 1, WEB_EDITOR: 2, + URL: 3 }); 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 237acec2c..6e1357a34 100644 --- a/app/kubernetes/views/deploy/deploy.html +++ b/app/kubernetes/views/deploy/deploy.html @@ -16,7 +16,7 @@ Deploy
-
+
@@ -101,6 +101,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 a22329aaa..16683235e 100644 --- a/app/kubernetes/views/deploy/deployController.js +++ b/app/kubernetes/views/deploy/deployController.js @@ -24,6 +24,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), ]; this.state = { @@ -54,10 +55,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) { @@ -91,7 +93,20 @@ class KubernetesDeployController { this.state.actionInProgress = true; try { - const method = this.state.BuildMethod === this.BuildMethods.GIT ? KubernetesDeployRequestMethods.REPOSITORY : KubernetesDeployRequestMethods.STRING; + let method; + switch (this.state.BuildMethod) { + case this.BuildMethods.GIT: + method = KubernetesDeployRequestMethods.REPOSITORY; + break; + case this.BuildMethods.WEB_EDITOR: + method = KubernetesDeployRequestMethods.STRING; + break; + case this.BuildMethods.URL: + method = KubernetesDeployRequestMethods.URL; + break; + default: + throw new PortainerError('Unable to determine build method'); + } const payload = { ComposeFormat: this.state.DeployType === this.ManifestDeployTypes.COMPOSE, @@ -107,8 +122,11 @@ class KubernetesDeployController { payload.RepositoryPassword = this.formValues.RepositoryPassword; } payload.FilePathInRepository = this.formValues.FilePathInRepository; - } else { + } else if (method === KubernetesDeployRequestMethods.WEB_EDITOR) { payload.StackFileContent = this.formValues.EditorContent; + } else { + payload.ManifestURL = this.formValues.ManifestURL; + delete payload.Namespace; } await this.StackService.kubernetesDeploy(this.endpointId, method, payload); diff --git a/gruntfile.js b/gruntfile.js index 6dbbc1c00..bf92abfe0 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -175,7 +175,7 @@ function shell_build_binary_azuredevops(p, a) { function shell_run_container() { return [ 'docker rm -f portainer', - 'docker run -d -p 8000:8000 -p 9000:9000 -v ' + portainer_root + '/dist:/app -v ' + + 'docker run -d --network host -p 8000:8000 -p 9000:9000 -v ' + portainer_root + '/dist:/app -v ' + portainer_data + ':/data -v /var/run/docker.sock:/var/run/docker.sock:z -v /var/run/docker.sock:/var/run/alternative.sock:z -v /tmp:/tmp --name portainer portainer/base /app/portainer', ].join(';');