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 <arris_li@hotmail.com>
pull/5505/head
fhanportainer 2021-08-25 14:04:12 +12:00 committed by GitHub
parent 9fae031390
commit 6d87c77ab0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1783 additions and 1415 deletions

View File

@ -217,7 +217,7 @@ func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *p
return "", errors.Wrap(err, "failed to add application labels") 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)
} }

View File

@ -14,6 +14,8 @@ import {
KubernetesPortainerApplicationNote, KubernetesPortainerApplicationNote,
KubernetesPortainerApplicationOwnerLabel, KubernetesPortainerApplicationOwnerLabel,
KubernetesPortainerApplicationStackNameLabel, KubernetesPortainerApplicationStackNameLabel,
KubernetesPortainerApplicationStackIdLabel,
KubernetesPortainerApplicationKindLabel,
} from 'Kubernetes/models/application/models'; } from 'Kubernetes/models/application/models';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper'; import KubernetesResourceReservationHelper from 'Kubernetes/helpers/resourceReservationHelper';
@ -55,6 +57,8 @@ class KubernetesApplicationConverter {
res.Id = data.metadata.uid; res.Id = data.metadata.uid;
res.Name = data.metadata.name; res.Name = data.metadata.name;
res.StackName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationStackNameLabel] || '-' : '-'; 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.ApplicationOwner = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationOwnerLabel] || '' : '';
res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : ''; res.Note = data.metadata.annotations ? data.metadata.annotations[KubernetesPortainerApplicationNote] || '' : '';
res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name; res.ApplicationName = data.metadata.labels ? data.metadata.labels[KubernetesPortainerApplicationNameLabel] || res.Name : res.Name;

View File

@ -41,6 +41,10 @@ export const KubernetesApplicationQuotaDefaults = {
export const KubernetesPortainerApplicationStackNameLabel = 'io.portainer.kubernetes.application.stack'; 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 KubernetesPortainerApplicationNameLabel = 'io.portainer.kubernetes.application.name';
export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner'; export const KubernetesPortainerApplicationOwnerLabel = 'io.portainer.kubernetes.application.owner';

View File

@ -7,6 +7,8 @@ const _KubernetesApplication = Object.freeze({
Id: '', Id: '',
Name: '', Name: '',
StackName: '', StackName: '',
StackId: '',
ApplicationKind: '',
ApplicationOwner: '', ApplicationOwner: '',
ApplicationName: '', ApplicationName: '',
ResourcePool: '', ResourcePool: '',
@ -91,3 +93,9 @@ export class KubernetesApplicationPort {
Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort))); Object.assign(this, JSON.parse(JSON.stringify(_KubernetesApplicationPort)));
} }
} }
export const KubernetesDeploymentTypes = Object.freeze({
GIT: 'git',
CONTENT: 'content',
APPLICATION_FORM: 'application form',
});

View File

@ -16,7 +16,13 @@
<rd-widget> <rd-widget>
<rd-widget-body> <rd-widget-body>
<form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off"> <form class="form-horizontal" name="kubernetesApplicationCreationForm" autocomplete="off">
<div class="col-sm-12 form-section-title"> <git-form-info-panel
ng-if="ctrl.state.appType == ctrl.KubernetesDeploymentTypes.GIT"
class-name="text-muted"
url="ctrl.stack.GitConfig.URL"
config-file-path="ctrl.stack.GitConfig.ConfigFilePath"
></git-form-info-panel>
<div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
Namespace Namespace
</div> </div>
<!-- #region NAMESPACE --> <!-- #region NAMESPACE -->
@ -47,6 +53,40 @@
</div> </div>
</div> </div>
<!-- #endregion --> <!-- #endregion -->
<!-- #region Git repository -->
<kubernetes-app-git-form
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.GIT"
git-form-values="ctrl.gitFormValues"
stack="ctrl.stack"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
is-edit="ctrl.state.isEdit"
></kubernetes-app-git-form>
<!-- #endregion -->
<!-- #region web editor -->
<web-editor-form
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT"
value="ctrl.stackFileContent"
yml="true"
identifier="kubernetes-deploy-editor"
placeholder="# Define or paste the content of your manifest file here"
on-change="(ctrl.onChangeFileContent)"
>
<editor-description>
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This feature allows you to deploy any kind of Kubernetes resource in this environment (Deployment, Secret, ConfigMap...).
</p>
<p>
You can get more information about Kubernetes file format in the
<a href="https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/" target="_blank">official documentation</a>.
</p>
</editor-description>
</web-editor-form>
<!-- #endregion -->
<div ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM">
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Application Application
</div> </div>
@ -109,8 +149,8 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-12 small text-muted"> <div class="col-sm-12 small text-muted">
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
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 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
application name. the application name.
</div> </div>
</div> </div>
@ -194,14 +234,15 @@
class="small text-warning" class="small text-warning"
style="margin-top: 5px;" style="margin-top: 5px;"
ng-show=" ng-show="
kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid || ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined kubernetesApplicationCreationForm['environment_variable_name_' + $index].$invalid ||
ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined
" "
> >
<ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error"> <ng-messages for="kubernetesApplicationCreationForm['environment_variable_name_' + $index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p> <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Environment variable name is required.</p>
<p ng-message="pattern" <p ng-message="pattern"
><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must not ><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field must consist of alphabetic characters, digits, '_', '-', or '.', and must
start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p not start with a digit (e.g. 'my.env-name', or 'MY_ENV.NAME', or 'MyEnvName1'.</p
> >
</ng-messages> </ng-messages>
<p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined" <p ng-if="ctrl.state.duplicates.environmentVariables.refs[$index] !== undefined"
@ -880,7 +921,10 @@
<td style="width: 33%; border: none; padding: 2px 0 2px 0;">Maximum instances</td> <td style="width: 33%; border: none; padding: 2px 0 2px 0;">Maximum instances</td>
<td style="width: 33%; border: none; padding: 2px 0 2px 0;"> <td style="width: 33%; border: none; padding: 2px 0 2px 0;">
Target CPU usage (<b>%</b>) Target CPU usage (<b>%</b>)
<portainer-tooltip position="bottom" message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances."> <portainer-tooltip
position="bottom"
message="The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances."
>
</portainer-tooltip> </portainer-tooltip>
</td> </td>
</tr> </tr>
@ -1239,8 +1283,8 @@
ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.formValues.PublishedPorts.length > 0" ng-if="ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.formValues.PublishedPorts.length > 0"
> >
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
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 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
number inside the default range <code>30000-32767</code>. port number inside the default range <code>30000-32767</code>.
</div> </div>
<div ng-if="ctrl.isNotInternalAndHasNoPublishedPorts()" class="col-sm-12 small text-muted text-warning" style="margin-top: 12px;"> <div ng-if="ctrl.isNotInternalAndHasNoPublishedPorts()" class="col-sm-12 small text-muted text-warning" style="margin-top: 12px;">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> At least one published port must be defined. <i class="fa fa-exclamation-triangle" aria-hidden="true"></i> At least one published port must be defined.
@ -1440,7 +1484,8 @@
kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid || kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid ||
ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined || ctrl.state.duplicates.publishedPorts.containerPorts.refs[$index] !== undefined ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined) || (ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.CLUSTER && ctrl.state.duplicates.publishedPorts.nodePorts.refs[$index] !== undefined) ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS && ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined) || (ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.INGRESS &&
ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined) ||
(ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER && (ctrl.formValues.PublishingType === ctrl.ApplicationPublishingTypes.LOAD_BALANCER &&
ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined) ctrl.state.duplicates.publishedPorts.loadBalancerPorts.refs[$index] !== undefined)
" "
@ -1493,7 +1538,9 @@
<div <div
class="small text-warning" class="small text-warning"
style="margin-top: 5px;" style="margin-top: 5px;"
ng-if="kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined" ng-if="
kubernetesApplicationCreationForm['ingress_route_' + $index].$invalid || ctrl.state.duplicates.publishedPorts.ingressRoutes.refs[$index] !== undefined
"
> >
<div ng-messages="kubernetesApplicationCreationForm['ingress_route_'+$index].$error"> <div ng-messages="kubernetesApplicationCreationForm['ingress_route_'+$index].$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p> <p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Route is required.</p>
@ -1542,8 +1589,8 @@
form-values="ctrl.formValues" form-values="ctrl.formValues"
old-form-values="ctrl.savedFormValues" old-form-values="ctrl.savedFormValues"
></kubernetes-summary-view> ></kubernetes-summary-view>
</div>
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title" ng-if="ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.GIT">
Actions Actions
</div> </div>
@ -1551,6 +1598,7 @@
<div class="form-group"> <div class="form-group">
<div class="col-sm-12"> <div class="col-sm-12">
<button <button
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button" type="button"
class="btn btn-primary btn-sm" class="btn btn-primary btn-sm"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity" ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity"
@ -1563,13 +1611,25 @@
<span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span> <span ng-show="ctrl.state.isEdit && ctrl.state.actionInProgress">Update in progress...</span>
</button> </button>
<button <button
ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress" ng-if="ctrl.state.isEdit && !ctrl.state.actionInProgress && ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button" type="button"
class="btn btn-sm btn-default" class="btn btn-sm btn-default"
ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })" ui-sref="kubernetes.applications.application({ name: ctrl.application.Name, namespace: ctrl.application.ResourcePool })"
> >
Cancel Cancel
</button> </button>
<!-- #Web editor buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="ctrl.updateApplicationViaWebEditor()"
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.CONTENT || ctrl.state.updateWebEditorInProgress"
ng-disabled="!kubernetesApplicationCreationForm.$valid || !ctrl.state.isEditorDirty || ctrl.state.updateWebEditorInProgress"
style="margin-top: 7px; margin-left: 0;"
button-spinner="ctrl.state.updateWebEditorInProgress"
>
<span ng-show="!ctrl.state.updateWebEditorInProgress">Update the application</span>
<span ng-show="ctrl.state.updateWebEditorInProgress">Update in progress...</span>
</button>
</div> </div>
</div> </div>
<!-- #endregion --> <!-- #endregion -->

View File

@ -10,6 +10,7 @@ import {
KubernetesApplicationQuotaDefaults, KubernetesApplicationQuotaDefaults,
KubernetesApplicationTypes, KubernetesApplicationTypes,
KubernetesApplicationPlacementTypes, KubernetesApplicationPlacementTypes,
KubernetesDeploymentTypes,
} from 'Kubernetes/models/application/models'; } from 'Kubernetes/models/application/models';
import { import {
KubernetesApplicationConfigurationFormValue, KubernetesApplicationConfigurationFormValue,
@ -49,7 +50,8 @@ class KubernetesCreateApplicationController {
KubernetesPersistentVolumeClaimService, KubernetesPersistentVolumeClaimService,
KubernetesNamespaceHelper, KubernetesNamespaceHelper,
KubernetesVolumeService, KubernetesVolumeService,
RegistryService RegistryService,
StackService
) { ) {
this.$async = $async; this.$async = $async;
this.$state = $state; this.$state = $state;
@ -66,6 +68,7 @@ class KubernetesCreateApplicationController {
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService; this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
this.KubernetesNamespaceHelper = KubernetesNamespaceHelper; this.KubernetesNamespaceHelper = KubernetesNamespaceHelper;
this.RegistryService = RegistryService; this.RegistryService = RegistryService;
this.StackService = StackService;
this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.ApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
@ -74,8 +77,11 @@ class KubernetesCreateApplicationController {
this.ApplicationTypes = KubernetesApplicationTypes; this.ApplicationTypes = KubernetesApplicationTypes;
this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes; this.ApplicationConfigurationFormValueOverridenKeyTypes = KubernetesApplicationConfigurationFormValueOverridenKeyTypes;
this.ServiceTypes = KubernetesServiceTypes; this.ServiceTypes = KubernetesServiceTypes;
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
this.state = { this.state = {
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
updateWebEditorInProgress: false,
actionInProgress: false, actionInProgress: false,
useLoadBalancer: false, useLoadBalancer: false,
useServerMetrics: false, useServerMetrics: false,
@ -124,13 +130,54 @@ class KubernetesCreateApplicationController {
this.state.useServerMetrics = false; this.state.useServerMetrics = false;
this.formValues = new KubernetesApplicationFormValues(); this.formValues = new KubernetesApplicationFormValues();
this.gitFormValues = {
RefName: '',
RepositoryAuthentication: false,
RepositoryUsername: '',
RepositoryPassword: '',
};
this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.deployApplicationAsync = this.deployApplicationAsync.bind(this); this.deployApplicationAsync = this.deployApplicationAsync.bind(this);
this.setPullImageValidity = this.setPullImageValidity.bind(this); this.setPullImageValidity = this.setPullImageValidity.bind(this);
this.onChangeFileContent = this.onChangeFileContent.bind(this);
} }
/* #endregion */ /* #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) { setPullImageValidity(validity) {
this.state.pullImageValidity = validity; this.state.pullImageValidity = validity;
} }
@ -980,6 +1027,23 @@ class KubernetesCreateApplicationController {
this.nodesLabels, this.nodesLabels,
this.filteredIngresses 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.OriginalIngresses = this.filteredIngresses;
this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel); this.formValues.ImageModel = await this.parseImageConfiguration(this.formValues.ImageModel);
this.savedFormValues = angular.copy(this.formValues); this.savedFormValues = angular.copy(this.formValues);

View File

@ -67,6 +67,7 @@
<td> <td>
<span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px;"> <i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }} </span> <span ng-if="ctrl.application.ApplicationOwner" style="margin-right: 5px;"> <i class="fas fa-user"></i> {{ ctrl.application.ApplicationOwner }} </span>
<span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }}</span> <span> <i class="fas fa-clock"></i> {{ ctrl.application.CreationDate | getisodate }}</span>
<span> <i class="fa fa-file-code space-left space-right" aria-hidden="true"></i> Deployed from {{ ctrl.state.appType }}</span>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -1,7 +1,12 @@
import angular from 'angular'; import angular from 'angular';
import _ from 'lodash-es'; import _ from 'lodash-es';
import * as JsonPatch from 'fast-json-patch'; 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 KubernetesEventHelper from 'Kubernetes/helpers/eventHelper';
import KubernetesApplicationHelper from 'Kubernetes/helpers/application'; import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models'; import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
@ -127,6 +132,7 @@ class KubernetesApplicationController {
this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes; this.KubernetesApplicationDeploymentTypes = KubernetesApplicationDeploymentTypes;
this.KubernetesApplicationTypes = KubernetesApplicationTypes; this.KubernetesApplicationTypes = KubernetesApplicationTypes;
this.EndpointProvider = EndpointProvider; this.EndpointProvider = EndpointProvider;
this.KubernetesDeploymentTypes = KubernetesDeploymentTypes;
this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies; this.ApplicationDataAccessPolicies = KubernetesApplicationDataAccessPolicies;
this.KubernetesServiceTypes = KubernetesServiceTypes; this.KubernetesServiceTypes = KubernetesServiceTypes;
@ -137,6 +143,7 @@ class KubernetesApplicationController {
this.getApplicationAsync = this.getApplicationAsync.bind(this); this.getApplicationAsync = this.getApplicationAsync.bind(this);
this.getEvents = this.getEvents.bind(this); this.getEvents = this.getEvents.bind(this);
this.getEventsAsync = this.getEventsAsync.bind(this); this.getEventsAsync = this.getEventsAsync.bind(this);
this.updateApplicationKindText = this.updateApplicationKindText.bind(this);
this.updateApplicationAsync = this.updateApplicationAsync.bind(this); this.updateApplicationAsync = this.updateApplicationAsync.bind(this);
this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this); this.redeployApplicationAsync = this.redeployApplicationAsync.bind(this);
this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this); this.rollbackApplicationAsync = this.rollbackApplicationAsync.bind(this);
@ -258,6 +265,14 @@ class KubernetesApplicationController {
return this.$async(this.updateApplicationAsync); 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 * EVENTS
*/ */
@ -334,6 +349,7 @@ class KubernetesApplicationController {
namespace: this.$transition$.params().namespace, namespace: this.$transition$.params().namespace,
name: this.$transition$.params().name, name: this.$transition$.params().name,
}, },
appType: this.KubernetesDeploymentTypes.APPLICATION_FORM,
eventWarningCount: 0, eventWarningCount: 0,
placementWarning: false, placementWarning: false,
expandedNote: false, expandedNote: false,
@ -350,6 +366,7 @@ class KubernetesApplicationController {
await this.getApplication(); await this.getApplication();
await this.getEvents(); await this.getEvents();
this.updateApplicationKindText();
this.state.viewReady = true; this.state.viewReady = true;
} }

View File

@ -9,7 +9,6 @@ export const webEditorForm = {
placeholder: '@', placeholder: '@',
yml: '<', yml: '<',
value: '<', value: '<',
onChange: '<', onChange: '<',
}, },

View File

@ -6,6 +6,7 @@ export const gitFormAuthFieldset = {
bindings: { bindings: {
model: '<', model: '<',
onChange: '<', onChange: '<',
showAuthExplanation: '<',
isEdit: '<', isEdit: '<',
}, },
}; };

View File

@ -0,0 +1,15 @@
<div class="form-group" ng-class="$ctrl.className">
<div class="col-sm-12">
<p>
This stack was deployed from the git repository <code>{{ $ctrl.url }}</code>
.
</p>
<p>
Update
<code
>{{ $ctrl.configFilePath }}<span ng-if="$ctrl.additionalFiles.length > 0">,{{ $ctrl.additionalFiles.join(',') }}</span></code
>
in git and pull from here to update the stack.
</p>
</div>
</div>

View File

@ -0,0 +1,9 @@
export const gitFormInfoPanel = {
templateUrl: './git-form-info-panel.html',
bindings: {
url: '<',
configFilePath: '<',
additionalFiles: '<',
className: '@',
},
};

View File

@ -8,6 +8,7 @@ import { gitFormAutoUpdateFieldset } from './git-form-auto-update-fieldset';
import { gitFormComposePathField } from './git-form-compose-path-field'; import { gitFormComposePathField } from './git-form-compose-path-field';
import { gitFormRefField } from './git-form-ref-field'; import { gitFormRefField } from './git-form-ref-field';
import { gitFormUrlField } from './git-form-url-field'; import { gitFormUrlField } from './git-form-url-field';
import { gitFormInfoPanel } from './git-form-info-panel';
export default angular export default angular
.module('portainer.app.components.forms.git', []) .module('portainer.app.components.forms.git', [])
@ -15,6 +16,7 @@ export default angular
.component('gitFormRefField', gitFormRefField) .component('gitFormRefField', gitFormRefField)
.component('gitForm', gitForm) .component('gitForm', gitForm)
.component('gitFormUrlField', gitFormUrlField) .component('gitFormUrlField', gitFormUrlField)
.component('gitFormInfoPanel', gitFormInfoPanel)
.component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel) .component('gitFormAdditionalFilesPanel', gitFormAdditionalFilesPanel)
.component('gitFormAdditionalFileItem', gitFormAdditionalFileItem) .component('gitFormAdditionalFileItem', gitFormAdditionalFileItem)
.component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset) .component('gitFormAutoUpdateFieldset', gitFormAutoUpdateFieldset)

View File

@ -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;

View File

@ -0,0 +1,58 @@
<form name="$ctrl.redeployGitForm">
<div class="col-sm-12 form-section-title">
Redeploy from git repository
</div>
<div class="form-group text-muted">
<div class="col-sm-12">
<p>
Pull the latest manifest from git and redeploy the application.
</p>
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
<p>
<a class="small interactive" ng-click="$ctrl.state.showConfig = !$ctrl.state.showConfig">
<i ng-class="['fa space-right', { 'fa-minus': $ctrl.state.showConfig, 'fa-plus': !$ctrl.state.showConfig }]" aria-hidden="true"></i>
{{ $ctrl.state.showConfig ? 'Hide' : 'Advanced' }} configuration
</a>
</p>
</div>
</div>
<git-form-ref-field ng-if="$ctrl.state.showConfig" value="$ctrl.gitFormValues.RefName" on-change="($ctrl.onChangeRef)"></git-form-ref-field>
<git-form-auth-fieldset
ng-if="$ctrl.state.showConfig"
model="$ctrl.gitFormValues"
is-edit="$ctrl.isEdit"
on-change="($ctrl.onChange)"
show-auth-explanation="true"
></git-form-auth-fieldset>
<div class="col-sm-12 form-section-title">
Actions
</div>
<!-- #Git buttons -->
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.pullAndRedeployApplication()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="ctrl.state.redeployInProgress"
analytics-category="kubernetes"
analytics-event="kubernetes-application-edit-git-pull"
>
<span ng-show="!$ctrl.state.redeployInProgress"> <i class="fa fa-sync space-right" aria-hidden="true"></i> Pull and update application </span>
<span ng-show="$ctrl.state.redeployInProgress">In progress...</span>
</button>
<button
class="btn btn-sm btn-primary"
ng-click="$ctrl.saveGitSettings()"
ng-disabled="$ctrl.isSubmitButtonDisabled() || !$ctrl.redeployGitForm.$valid"
style="margin-top: 7px; margin-left: 0;"
button-spinner="$ctrl.state.saveGitSettingsInProgress"
>
<span ng-show="!$ctrl.state.saveGitSettingsInProgress"> Save settings </span>
<span ng-show="$ctrl.state.saveGitSettingsInProgress">In progress...</span>
</button>
</form>

View File

@ -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);

View File

@ -2,21 +2,12 @@
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Redeploy from git repository Redeploy from git repository
</div> </div>
<div class="text-muted small form-group"> <git-form-info-panel
<div class="col-sm-12"> class-name="text-muted small"
<p> url="$ctrl.model.URL"
This stack was deployed from the git repository <code>{{ $ctrl.model.URL }}</code> config-file-path="$ctrl.model.ConfigFilePath"
. additional-files="$ctrl.stack.AdditionalFiles"
</p> ></git-form-info-panel>
<p>
Update
<code
>{{ $ctrl.model.ConfigFilePath }}<span ng-if="$ctrl.stack.AdditionalFiles.length > 0">,{{ $ctrl.stack.AdditionalFiles.join(',') }}</span></code
>
in git and pull from here to update the stack.
</p>
</div>
</div>
<git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset> <git-form-auto-update-fieldset model="$ctrl.formValues.AutoUpdate" on-change="($ctrl.onChange)"></git-form-auto-update-fieldset>
<div class="form-group"> <div class="form-group">

View File

@ -15,6 +15,7 @@ angular.module('portainer.app').factory('StackService', [
'use strict'; 'use strict';
var service = { var service = {
updateGit, updateGit,
updateKubeGit,
}; };
service.stack = function (id) { 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; 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) { service.createComposeStackFromFileUpload = function (name, stackFile, env, endpointId) {
return FileUploadService.createComposeStack(name, stackFile, env, endpointId); return FileUploadService.createComposeStack(name, stackFile, env, endpointId);
}; };
@ -417,6 +437,19 @@ angular.module('portainer.app').factory('StackService', [
).$promise; ).$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) { service.updateGitStackSettings = function (id, endpointId, env, gitConfig) {
// prepare auto update // prepare auto update
const autoUpdate = {}; const autoUpdate = {};

View File

@ -33,7 +33,7 @@ angular
StackFile: null, StackFile: null,
RepositoryURL: '', RepositoryURL: '',
RepositoryReferenceName: '', RepositoryReferenceName: '',
RepositoryAuthentication: false, RepositoryAuthentication: true,
RepositoryUsername: '', RepositoryUsername: '',
RepositoryPassword: '', RepositoryPassword: '',
Env: [], Env: [],