From 6d87c77ab0f659f5e17d1ee6160bafedf9579817 Mon Sep 17 00:00:00 2001 From: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Date: Wed, 25 Aug 2021 14:04:12 +1200 Subject: [PATCH] feat(stack): front end backport changes to CE EE-1199 (#5455) * feat(stack): front end backport changes to CE EE-1199 * fix k8s deploy logic * fixed web editor confirmation message typo. EE-1501 * fix(stack): fixed issue auth detail not remembered EE-1502 (#5459) * show status in buttons * removed onChangeRef function. * moved buttons in git form to its own component * removed unused variable. Co-authored-by: ArrisLee --- .../handler/stacks/create_kubernetes_stack.go | 2 +- app/kubernetes/converters/application.js | 4 + .../models/application/models/constants.js | 4 + .../models/application/models/index.js | 8 + .../create/createApplication.html | 2848 +++++++++-------- .../create/createApplicationController.js | 66 +- .../views/applications/edit/application.html | 3 +- .../edit/applicationController.js | 19 +- .../form-components/web-editor-form/index.js | 1 - .../git-form/git-form-auth-fieldset/index.js | 1 + .../git-form-info-panel.html | 15 + .../git-form/git-form-info-panel/index.js | 9 + .../components/forms/git-form/index.js | 2 + .../kubernetes-app-git-form.controller.js | 87 + .../kubernetes-app-git-form.html | 58 + .../kubernetes-app-git-form.js | 15 + .../stack-redeploy-git-form.html | 21 +- app/portainer/services/api/stackService.js | 33 + .../stacks/create/createStackController.js | 2 +- 19 files changed, 1783 insertions(+), 1415 deletions(-) create mode 100644 app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html create mode 100644 app/portainer/components/forms/git-form/git-form-info-panel/index.js create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html create mode 100644 app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 852969cd9..83cb77f9d 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -217,7 +217,7 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p return "", errors.Wrap(err, "failed to add application labels") } - return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace) + return handler.KubernetesDeployer.Deploy(request, endpoint, string(manifest), namespace) } diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 881e98ece..dca8c5522 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -14,6 +14,8 @@ import { KubernetesPortainerApplicationNote, KubernetesPortainerApplicationOwnerLabel, KubernetesPortainerApplicationStackNameLabel, + KubernetesPortainerApplicationStackIdLabel, + KubernetesPortainerApplicationKindLabel, } from 'Kubernetes/models/application/models'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; @@ -55,6 +57,8 @@ class KubernetesApplicationConverter { res.Id = data.metadata.uid; res.Name = data.metadata.name; res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; + res.StackId = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackIdLabel] || '' : ''; + res.ApplicationKind = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationKindLabel] || '' : ''; res.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : ''; res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : ''; res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name; diff --git a/app/kubernetes/models/application/models/constants.js b/app/kubernetes/models/application/models/constants.js index 970cb3557..b33300754 100644 --- a/app/kubernetes/models/application/models/constants.js +++ b/app/kubernetes/models/application/models/constants.js @@ -41,6 +41,10 @@ export const KubernetesApplicationQuotaDefaults = { export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; +export const KubernetesPortainerApplicationStackIdLabel = 'io.portainer.kubernetes.application.stackid'; + +export const KubernetesPortainerApplicationKindLabel = 'io.portainer.kubernetes.application.kind'; + export const KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name'; export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; diff --git a/app/kubernetes/models/application/models/index.js b/app/kubernetes/models/application/models/index.js index 35520acfd..b6148e752 100644 --- a/app/kubernetes/models/application/models/index.js +++ b/app/kubernetes/models/application/models/index.js @@ -7,6 +7,8 @@ const _KubernetesApplication = Object.freeze({ Id: '', Name: '', StackName: '', + StackId: '', + ApplicationKind: '', ApplicationOwner: '', ApplicationName: '', ResourcePool: '', @@ -91,3 +93,9 @@ export class KubernetesApplicationPort { Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort))); } } + +export const KubernetesDeploymentTypes = Object.freeze({ + GIT: 'git', + CONTENT: 'content', + APPLICATION_FORM: 'application form', +}); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index c256ec3e0..3d87c1308 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -16,7 +16,13 @@
-
+ +
Namespace
@@ -47,1503 +53,1544 @@
-
- Application -
- -
- -
- + + + + + + + + +

+ + This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...). +

+

+ You can get more information about Kubernetes file format in the + official documentation. +

+
+
+ + +
+
+ Application
-
-
-
-
-

This field is required.

-

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic - character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+ +
+ +
+
+
+
+
+

This field is required.

+

This field must consist of lower case alphanumeric characters or '-', start with an alphabetic + character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123').

+
+

An application with the same name already exists inside the selected namespace.

-

An application with the same name already exists inside the selected namespace.

-
- + - + -
-
- -
-
- -
- Stack -
- -
-
- - Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use the - application name. -
-
- -
- -
- -
-
- - -
- Environment -
- -
-
- - - add environment variable - -
- -
-
-
-
-
- name - -
-
- -
- value - -
- -
- - -
-
-
-
-
- -

Environment variable name is required.

-

This field must consist of alphabetic characters, digits, '_', '-', or '.', and must not - start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

-
-

This environment variable is already defined.

-
-
-
-
-
-
-
-
- - -
- Configurations -
- -
-
- - - add configuration - -
-
- - Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via - the override button. -
-
- - -
- -
- -
-
- - - -
- -
-
-
- The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: - - {{ key }}{{ $last ? '' : ', ' }} - -
-
- - - -
-
-
-
-
- configuration key - -
- -
-
- path on disk - -
-
- -
- - -
-
- -
-
-
-
-
- -

Path is required.

-
-

This path is already used.

-
-
-
-
-
-
- -
- - - -
- Persisting data -
- -
-
- - No storage option is available to persist data, contact your administrator to enable a storage option. -
-
- -
-
- - - add persisted folder - -
- -
-
-
- path in container - -
- -
- - - - -
- -
- requested size - - - - -
- -
- storage - - -
- -
- volume - -
- -
-
- - -
-
-
- -
-
-
- -

Path is required.

-
-

This path is already defined.

-
-
- -
- -
-
- -

Size is required.

-

This value must be greater than zero.

-
-
-
- -

Volume is required.

-
-

This volume is already used.

-
-
- -
-
-
-
- - - -
- +
+
+ Stack +
+
- Specify how the data will be used across instances. + + Portainer can automatically bundle multiple applications inside a stack. Enter a name of a new stack or select an existing stack in the list. Leave empty to use + the application name.
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- -
- - -
- Resource reservations -
- -
-
- - Resource reservations are applied per instance of the application. -
-
- -
-
- - A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are - inherited from the namespace quota. -
-
- -
-
- - This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the - namespace. -
-
- - -
- -
- -
-
- -
-
-

- Maximum memory usage (MB) -

-
-
-
-
-
-

Value must be between {{ ctrl.state.sliders.memory.min }} and - {{ ctrl.state.sliders.memory.max }} -

-
-
-
- - -
- -
- -
-
-

- Maximum CPU usage -

-
-
- - - -
- Deployment -
- -
-
- Select how you want to deploy your application inside the cluster. -
-
- - -
-
-
- - -
-
- - -
-
+
+ +
-
-
- + - -
-
- - -
-
-
-
- -

Instance count is required.

-

Instance count must be greater than 0.

-
-
-
- - -
-
- - This application will reserve the following resources: - {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and - {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. -
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the instance count. -
-
- -
-
- - The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. -
-
- - - -
- Auto-scaling -
- -
-
- - -
-
- -
-
-

- This feature is currently disabled and must be enabled by an administrator user. -

-

- Server metrics features must be enabled in the - endpoint configuration view. -

-
-
- -
- - - - - - - - - - - - - -
Minimum instancesMaximum instances - Target CPU usage (%) - - -
-
- -
-
-
- -

Minimum instances is required.

-

Minimum instances must be greater than 0.

-

Minimum instances must be smaller than maximum instances.

-
-
-
-
-
- -
-
-
- -

Maximum instances is required.

-

Maximum instances must be greater than minimum instances.

-
-
-
-
-
- -
-
-
- -

Target CPU usage is required.

-

Target CPU usage must be greater than 0.

-

Target CPU usage must be smaller than 100.

-
-
-
-
- -
-
- - This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy. -
-
-
- - -
- Placement preferences and constraints + Environment
- - +
- - - add rule + + + add environment variable
-
- - Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. -
-
-
-
- -
-
- -
- -
- - -
-
-
-
-
-

- This label is already defined. -

+
+
+
+
+ name + +
+ +
+ value + +
+ +
+ + +
+
+
+
+
+ +

Environment variable name is required.

+

This field must consist of alphabetic characters, digits, '_', '-', or '.', and must + not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.

+
+

This environment variable is already defined.

+
+
+
+
-
+ + +
+ Configurations +
+ +
+
+ + + add configuration + +
+
+ + Portainer will automatically expose all the keys of a configuration as environment variables. This behavior can be overriden to filesystem mounts for each key via + the override button. +
+
+ + +
+ +
+ +
+
+ + + +
+ +
+
+
+ The following keys will be loaded from the {{ config.SelectedConfiguration.Name }} configuration as environment variables: + + {{ key }}{{ $last ? '' : ', ' }} + +
+
+ + + +
+
+
+
+
+ configuration key + +
+ +
+
+ path on disk + +
+
+ +
+ + +
+
+ +
+
+
+
+
+ +

Path is required.

+
+

This path is already used.

+
+
+
+
+
+
+ +
+ + + +
+ Persisting data +
+ +
+
+ + No storage option is available to persist data, contact your administrator to enable a storage option. +
+
+ +
+
+ + + add persisted folder + +
+ +
+
+
+ path in container + +
+ +
+ + + + +
+ +
+ requested size + + + + +
+ +
+ storage + + +
+ +
+ volume + +
+ +
+
+ + +
+
+
+ +
+
+
+ +

Path is required.

+
+

This path is already defined.

+
+
+ +
+ +
+
+ +

Size is required.

+

This value must be greater than zero.

+
+
+
+ +

Volume is required.

+
+

This volume is already used.

+
+
+ +
+
+
+
+ + + +
- +
- Specify the policy associated to the placement rules. + Specify how the data will be used across instances.
- -
+ +
-
- -
- +
-
-
- Publishing the application -
- -
-
- Select how you want to publish your application. +
+ Resource reservations
-
- - -
-
-
- - - -
- -
- - - -
-
- - - -
-
- - - + +
+
+ + Resource reservations are applied per instance of the application.
-
- - -
-
- - - publish a new port - +
+
+ + A resource quota is set on this namespace, you must specify resource reservations. Resource reservations are applied per instance of the application. Maximums are + inherited from the namespace quota. +
+
+
+ + This namespace has exhausted its resource capacity and you will not be able to deploy the application. Contact your administrator to expand the capacity of the + namespace. +
+
+ + +
+ +
+ +
+
+ +
+
+

+ Maximum memory usage (MB) +

+
+
+
+
+
+

Value must be between {{ ctrl.state.sliders.memory.min }} and + {{ ctrl.state.sliders.memory.max }} +

+
+
+
+ + +
+ +
+ +
+
+

+ Maximum CPU usage +

+
+
+ + + +
+ Deployment +
+ +
+
+ Select how you want to deploy your application inside the cluster. +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+
+
+
+ +

Instance count is required.

+

Instance count must be greater than 0.

+
+
+
+ +
- - When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a port - number inside the default range 30000-32767. -
-
- At least one published port must be defined. +
+ + This application will reserve the following resources: + {{ ctrl.formValues.CpuLimit * ctrl.formValues.ReplicaCount | kubernetesApplicationCPUValue }} CPU and + {{ ctrl.formValues.MemoryLimit * ctrl.formValues.ReplicaCount }} MB of memory. +
-
- -
-
- container port - -
+
+
+ + This application would exceed available resources. Please review resource reservations or the instance count. +
+
-
- node port - -
+
+
+ + The following storage option(s) do not support concurrent access from multiples instances: {{ ctrl.getNonScalableStorage() }}. You will not be able to scale that application. +
+
+ -
- load balancer port - -
+ +
+ Auto-scaling +
-
- ingress - -
+
+
+ + +
+
-
- hostname - -
+
+
+

+ This feature is currently disabled and must be enabled by an administrator user. +

+

+ Server metrics features must be enabled in the + endpoint configuration view. +

+
+
-
- route - -
+
+ + + + + + + + + + + + + +
Minimum instancesMaximum instances + Target CPU usage (%) + + +
+
+ +
+
+
+ +

Minimum instances is required.

+

Minimum instances must be greater than 0.

+

Minimum instances must be smaller than maximum instances.

+
+
+
+
+
+ +
+
+
+ +

Maximum instances is required.

+

Maximum instances must be greater than minimum instances.

+
+
+
+
+
+ +
+
+
+ +

Target CPU usage is required.

+

Target CPU usage must be greater than 0.

+

Target CPU usage must be smaller than 100.

+
+
+
+
-
-
- - -
- - +
+
+ + This application would exceed available resources. Please review resource reservations or the maximum instance count of the auto-scaling policy.
+
+ + +
+
+ Placement preferences and constraints +
+ + +
+
+ + + add rule + +
+ +
+ + Deploy this application on nodes that respect ALL of the following placement rules. Placement rules are based on node labels. +
+ +
+
+
+ +
+
+ +
+ +
+ + +
+
+
+
+
+

+ This label is already defined. +

+
+
+
+
+
+
+
+
+ +
+
+ +
+
+ Specify the policy associated to the placement rules. +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+ Publishing the application +
+ +
+
+ Select how you want to publish your application. +
+
+ + +
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+
+ + + +
+
+
+ + + +
+
+ + + publish a new port + +
-
-
+ + When publishing a port in cluster mode, the node port is optional. If left empty Kubernetes will use a random port number. If you wish to specify a port, use a + port number inside the default range 30000-32767. +
+
+ At least one published port must be defined. +
+ +
+ +
+ container port + +
+ +
-
-

Container port number is required.

-

Container port number must be inside the range 1-65535.

-

Container port number must be inside the range 1-65535.

-
-

- This port is already used. -

+ node port +
-
-
-
-

Node port number must be inside the range 30000-32767.

-

Node port number must be inside the range 30000-32767.

-
-

- This port is already used. -

+ load balancer port +
-
-
-
-
-

Ingress selection is required.

-
-
-
-
-
-

Route is required.

-

This field must consist of alphanumeric characters or the special characters: '-', '_' or - '/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').

ingress + +
+ +
+ hostname + +
+ +
+ route + +
+ +
+
+ +
-

- This route is already used. -

+ +
+ -
-
-
-

Load balancer port number is required.

-

Load balancer port number must be inside the range 1-65535.

-

Load balancer port number must be inside the range 1-65535.

+ +
+
+
+
+

Container port number is required.

+

Container port number must be inside the range 1-65535.

+

Container port number must be inside the range 1-65535.

+
+

+ This port is already used. +

-

- - This port is already used. -

+ +
+
+
+

Node port number must be inside the range 30000-32767.

+

Node port number must be inside the range 30000-32767.

+
+

+ This port is already used. +

+
+
+ +
+
+
+

Ingress selection is required.

+
+
+
+
+
+
+

Route is required.

+

This field must consist of alphanumeric characters or the special characters: '-', '_' or + '/'. It must start and end with an alphanumeric character (e.g. 'my-route', or 'route-123').

+
+

+ This route is already used. +

+
+
+ +
+
+
+

Load balancer port number is required.

+

Load balancer port number must be inside the range 1-65535.

+

Load balancer port number must be inside the range 1-65535.

+
+

+ + This port is already used. +

+
+
+ +
- -
+
-
+ + + +
- - - - - -
+
Actions
@@ -1551,6 +1598,7 @@
+ +
diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index aca7d8dcc..4cad583b0 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -10,6 +10,7 @@ import { KubernetesApplicationQuotaDefaults, KubernetesApplicationTypes, KubernetesApplicationPlacementTypes, + KubernetesDeploymentTypes, } from 'Kubernetes/models/application/models'; import { KubernetesApplicationConfigurationFormValue, @@ -49,7 +50,8 @@ class KubernetesCreateApplicationController { KubernetesPersistentVolumeClaimService, KubernetesNamespaceHelper, KubernetesVolumeService, - RegistryService + RegistryService, + StackService ) { this.$async = $async; this.$state = $state; @@ -66,6 +68,7 @@ class KubernetesCreateApplicationController { this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.RegistryService = RegistryService; + this.StackService = StackService; this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; @@ -74,8 +77,11 @@ class KubernetesCreateApplicationController { this.ApplicationTypes = KubernetesApplicationTypes; this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes; this.ServiceTypes = KubernetesServiceTypes; + this.KubernetesDeploymentTypes = KubernetesDeploymentTypes; this.state = { + appType: this.KubernetesDeploymentTypes.APPLICATION_FORM, + updateWebEditorInProgress: false, actionInProgress: false, useLoadBalancer: false, useServerMetrics: false, @@ -124,13 +130,54 @@ class KubernetesCreateApplicationController { this.state.useServerMetrics = false; this.formValues = new KubernetesApplicationFormValues(); + this.gitFormValues = { + RefName: '', + RepositoryAuthentication: false, + RepositoryUsername: '', + RepositoryPassword: '', + }; this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.deployApplicationAsync = this.deployApplicationAsync.bind(this); this.setPullImageValidity = this.setPullImageValidity.bind(this); + this.onChangeFileContent = this.onChangeFileContent.bind(this); } /* #endregion */ + onChangeFileContent(value) { + if (this.stackFileContent.replace(/(\r\n|\n|\r)/gm, '') !== value.replace(/(\r\n|\n|\r)/gm, '')) { + this.state.isEditorDirty = true; + this.stackFileContent = value; + } + } + + async updateApplicationViaWebEditor() { + return this.$async(async () => { + try { + const confirmed = await this.ModalService.confirmAsync({ + title: 'Are you sure?', + message: 'Any changes to this application will be overriden and may cause a service interruption. Do you wish to continue', + buttons: { + confirm: { + label: 'Update', + className: 'btn-warning', + }, + }, + }); + if (!confirmed) { + return; + } + this.state.updateWebEditorInProgress = true; + await this.StackService.updateKubeStack({ EndpointId: this.endpoint.Id, Id: this.application.StackId }, this.stackFileContent, null); + await this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Failed redeploying application'); + } finally { + this.state.updateWebEditorInProgress = false; + } + }); + } + setPullImageValidity(validity) { this.state.pullImageValidity = validity; } @@ -980,6 +1027,23 @@ class KubernetesCreateApplicationController { this.nodesLabels, this.filteredIngresses ); + + if (this.application.ApplicationKind) { + this.state.appType = this.KubernetesDeploymentTypes[this.application.ApplicationKind.toUpperCase()]; + if (this.application.StackId) { + if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.GIT) { + this.stack = await this.StackService.stack(this.application.StackId); + this.gitFormValues.RefName = this.stack.GitConfig.ReferenceName; + if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { + this.gitFormValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; + this.gitFormValues.RepositoryAuthentication = true; + } + } else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.CONTENT) { + this.stackFileContent = await this.StackService.getStackFile(this.application.StackId); + } + } + } + this.formValues.OriginalIngresses = this.filteredIngresses; this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel); this.savedFormValues = angular.copy(this.formValues); diff --git a/app/kubernetes/views/applications/edit/application.html b/app/kubernetes/views/applications/edit/application.html index 49ae11105..7e8895ca5 100644 --- a/app/kubernetes/views/applications/edit/application.html +++ b/app/kubernetes/views/applications/edit/application.html @@ -66,7 +66,8 @@ Creation {{ ctrl.application.ApplicationOwner }} - {{ ctrl.application.CreationDate | getisodate }} + {{ ctrl.application.CreationDate | getisodate }} + Deployed from {{ ctrl.state.appType }} diff --git a/app/kubernetes/views/applications/edit/applicationController.js b/app/kubernetes/views/applications/edit/applicationController.js index 1c07e0b44..43554a280 100644 --- a/app/kubernetes/views/applications/edit/applicationController.js +++ b/app/kubernetes/views/applications/edit/applicationController.js @@ -1,7 +1,12 @@ import angular from 'angular'; import _ from 'lodash-es'; import * as JsonPatch from 'fast-json-patch'; -import { KubernetesApplicationDataAccessPolicies, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models'; +import { + KubernetesApplicationDataAccessPolicies, + KubernetesApplicationDeploymentTypes, + KubernetesApplicationTypes, + KubernetesDeploymentTypes, +} from 'Kubernetes/models/application/models'; import KubernetesEventHelper from 'Kubernetes/helpers/eventHelper'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; @@ -127,6 +132,7 @@ class KubernetesApplicationController { this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.KubernetesApplicationTypes = KubernetesApplicationTypes; this.EndpointProvider = EndpointProvider; + this.KubernetesDeploymentTypes = KubernetesDeploymentTypes; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; this.KubernetesServiceTypes = KubernetesServiceTypes; @@ -137,6 +143,7 @@ class KubernetesApplicationController { this.getApplicationAsync = this.getApplicationAsync.bind(this); this.getEvents = this.getEvents.bind(this); this.getEventsAsync = this.getEventsAsync.bind(this); + this.updateApplicationKindText = this.updateApplicationKindText.bind(this); this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this); this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this); @@ -258,6 +265,14 @@ class KubernetesApplicationController { return this.$async(this.updateApplicationAsync); } + updateApplicationKindText() { + if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.GIT) { + this.state.appType = `git repository`; + } else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.CONTENT) { + this.state.appType = `web editor`; + } + } + /** * EVENTS */ @@ -334,6 +349,7 @@ class KubernetesApplicationController { namespace: this.$transition$.params().namespace, name: this.$transition$.params().name, }, + appType: this.KubernetesDeploymentTypes.APPLICATION_FORM, eventWarningCount: 0, placementWarning: false, expandedNote: false, @@ -350,6 +366,7 @@ class KubernetesApplicationController { await this.getApplication(); await this.getEvents(); + this.updateApplicationKindText(); this.state.viewReady = true; } diff --git a/app/portainer/components/form-components/web-editor-form/index.js b/app/portainer/components/form-components/web-editor-form/index.js index bc1c80a07..ab3c06417 100644 --- a/app/portainer/components/form-components/web-editor-form/index.js +++ b/app/portainer/components/form-components/web-editor-form/index.js @@ -9,7 +9,6 @@ export const webEditorForm = { placeholder: '@', yml: '<', value: '<', - onChange: '<', }, diff --git a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js index 3000869c3..a5fe96be2 100644 --- a/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js +++ b/app/portainer/components/forms/git-form/git-form-auth-fieldset/index.js @@ -6,6 +6,7 @@ export const gitFormAuthFieldset = { bindings: { model: '<', onChange: '<', + showAuthExplanation: '<', isEdit: '<', }, }; diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html b/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html new file mode 100644 index 000000000..ae8c95252 --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-info-panel/git-form-info-panel.html @@ -0,0 +1,15 @@ +
+
+

+ This stack was deployed from the git repository {{ $ctrl.url }} + . +

+

+ Update + {{ $ctrl.configFilePath }},{{ $ctrl.additionalFiles.join(',') }} + in git and pull from here to update the stack. +

+
+
diff --git a/app/portainer/components/forms/git-form/git-form-info-panel/index.js b/app/portainer/components/forms/git-form/git-form-info-panel/index.js new file mode 100644 index 000000000..c2529969b --- /dev/null +++ b/app/portainer/components/forms/git-form/git-form-info-panel/index.js @@ -0,0 +1,9 @@ +export const gitFormInfoPanel = { + templateUrl: './git-form-info-panel.html', + bindings: { + url: '<', + configFilePath: '<', + additionalFiles: '<', + className: '@', + }, +}; diff --git a/app/portainer/components/forms/git-form/index.js b/app/portainer/components/forms/git-form/index.js index 60eff71e6..e73966464 100644 --- a/app/portainer/components/forms/git-form/index.js +++ b/app/portainer/components/forms/git-form/index.js @@ -8,6 +8,7 @@ import { gitFormAutoUpdateFieldset } from './git-form-auto-update-fieldset'; import { gitFormComposePathField } from './git-form-compose-path-field'; import { gitFormRefField } from './git-form-ref-field'; import { gitFormUrlField } from './git-form-url-field'; +import { gitFormInfoPanel } from './git-form-info-panel'; export default angular .module('portainer.app.components.forms.git', []) @@ -15,6 +16,7 @@ export default angular .component('gitFormRefField', gitFormRefField) .component('gitForm', gitForm) .component('gitFormUrlField', gitFormUrlField) + .component('gitFormInfoPanel', gitFormInfoPanel) .component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel) .component('gitFormAdditionalFileItem', gitFormAdditionalFileItem) .component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset) diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js new file mode 100644 index 000000000..1ada91fcf --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.controller.js @@ -0,0 +1,87 @@ +class KubernetesAppGitFormController { + /* @ngInject */ + constructor($async, $state, StackService, ModalService, Notifications) { + this.$async = $async; + this.$state = $state; + this.StackService = StackService; + this.ModalService = ModalService; + this.Notifications = Notifications; + + this.state = { + saveGitSettingsInProgress: false, + redeployInProgress: false, + showConfig: true, + isEdit: false, + }; + + this.onChange = this.onChange.bind(this); + this.onChangeRef = this.onChangeRef.bind(this); + } + + onChangeRef(value) { + this.onChange({ RefName: value }); + } + + onChange(values) { + this.gitFormValues = { + ...this.gitFormValues, + ...values, + }; + } + + async pullAndRedeployApplication() { + return this.$async(async () => { + try { + const confirmed = await this.ModalService.confirmAsync({ + title: 'Are you sure?', + message: 'Any changes to this application will be overriden by the definition in git and may cause a service interruption. Do you wish to continue', + buttons: { + confirm: { + label: 'Update', + className: 'btn-warning', + }, + }, + }); + if (!confirmed) { + return; + } + this.state.redeployInProgress = true; + this.Notifications.success('Pulled and redeployed stack successfully'); + await this.StackService.updateKubeGit(this.stack.Id, this.stack.EndpointId, this.namespace, this.gitFormValues); + await this.$state.reload(); + } catch (err) { + this.Notifications.error('Failure', err, 'Failed redeploying application'); + } finally { + this.state.redeployInProgress = false; + } + }); + } + + async saveGitSettings() { + return this.$async(async () => { + try { + this.state.saveGitSettingsInProgress = true; + await this.StackService.updateKubeStack({ EndpointId: this.stack.EndpointId, Id: this.stack.Id }, null, this.gitFormValues); + this.Notifications.success('Save stack settings successfully'); + } catch (err) { + this.Notifications.error('Failure', err, 'Unable to save application settings'); + } finally { + this.state.saveGitSettingsInProgress = false; + } + }); + } + + isSubmitButtonDisabled() { + return this.state.saveGitSettingsInProgress || this.state.redeployInProgress; + } + + $onInit() { + if (this.stack.GitConfig && this.stack.GitConfig.Authentication) { + this.formValues.RepositoryUsername = this.stack.GitConfig.Authentication.Username; + this.formValues.RepositoryAuthentication = true; + this.state.isEdit = true; + } + } +} + +export default KubernetesAppGitFormController; diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html new file mode 100644 index 000000000..aefa5bed9 --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.html @@ -0,0 +1,58 @@ + +
+ Redeploy from git repository +
+
+
+

+ Pull the latest manifest from git and redeploy the application. +

+
+
+ + + + + +
+ Actions +
+ + + + diff --git a/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js new file mode 100644 index 000000000..7cdf6914f --- /dev/null +++ b/app/portainer/components/forms/kubernetes-app-git-form/kubernetes-app-git-form.js @@ -0,0 +1,15 @@ +import angular from 'angular'; +import controller from './kubernetes-app-git-form.controller'; + +const kubernetesAppGitForm = { + templateUrl: './kubernetes-app-git-form.html', + controller, + bindings: { + gitFormValues: '<', + namespace: '<', + stack: '<', + isEdit: '<', + }, +}; + +angular.module('portainer.app').component('kubernetesAppGitForm', kubernetesAppGitForm); diff --git a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html index 340400573..1cb08d54f 100644 --- a/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html +++ b/app/portainer/components/forms/stack-redeploy-git-form/stack-redeploy-git-form.html @@ -2,21 +2,12 @@
Redeploy from git repository
-
-
-

- This stack was deployed from the git repository {{ $ctrl.model.URL }} - . -

-

- Update - {{ $ctrl.model.ConfigFilePath }},{{ $ctrl.stack.AdditionalFiles.join(',') }} - in git and pull from here to update the stack. -

-
-
+
diff --git a/app/portainer/services/api/stackService.js b/app/portainer/services/api/stackService.js index 86ad1905a..da3a1a545 100644 --- a/app/portainer/services/api/stackService.js +++ b/app/portainer/services/api/stackService.js @@ -15,6 +15,7 @@ angular.module('portainer.app').factory('StackService', [ 'use strict'; var service = { updateGit, + updateKubeGit, }; service.stack = function (id) { @@ -268,6 +269,25 @@ angular.module('portainer.app').factory('StackService', [ return Stack.update({ endpointId: stack.EndpointId }, { id: stack.Id, StackFileContent: stackFile, Env: env, Prune: prune }).$promise; }; + service.updateKubeStack = function (stack, stackFile, gitConfig) { + let payload = {}; + + if (stackFile) { + payload = { + StackFileContent: stackFile, + }; + } else { + payload = { + RepositoryReferenceName: gitConfig.RefName, + RepositoryAuthentication: gitConfig.RepositoryAuthentication, + RepositoryUsername: gitConfig.RepositoryUsername, + RepositoryPassword: gitConfig.RepositoryPassword, + }; + } + + return Stack.update({ id: stack.Id, endpointId: stack.EndpointId }, payload).$promise; + }; + service.createComposeStackFromFileUpload = function (name, stackFile, env, endpointId) { return FileUploadService.createComposeStack(name, stackFile, env, endpointId); }; @@ -417,6 +437,19 @@ angular.module('portainer.app').factory('StackService', [ ).$promise; } + function updateKubeGit(id, endpointId, namespace, gitConfig) { + return Stack.updateGit( + { endpointId, id }, + { + Namespace: namespace, + RepositoryReferenceName: gitConfig.RefName, + RepositoryAuthentication: gitConfig.RepositoryAuthentication, + RepositoryUsername: gitConfig.RepositoryUsername, + RepositoryPassword: gitConfig.RepositoryPassword, + } + ).$promise; + } + service.updateGitStackSettings = function (id, endpointId, env, gitConfig) { // prepare auto update const autoUpdate = {}; diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index fd4d66bb1..a60b29327 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -33,7 +33,7 @@ angular StackFile: null, RepositoryURL: '', RepositoryReferenceName: '', - RepositoryAuthentication: false, + RepositoryAuthentication: true, RepositoryUsername: '', RepositoryPassword: '', Env: [],