mirror of
https://github.com/portainer/portainer.git
synced 2025-08-05 05:45:22 +02:00
refactor(app): details widget migration [EE-5352] (#8886)
This commit is contained in:
parent
fdd79cece8
commit
af77e33993
57 changed files with 2046 additions and 1079 deletions
|
@ -1,76 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import _ from 'lodash-es';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_AnnotationsToSkip } from 'Kubernetes/models/history/models';
|
||||
|
||||
class KubernetesApplicationRollbackHelper {
|
||||
static getPatchPayload(application, targetRevision) {
|
||||
let result;
|
||||
|
||||
switch (application.ApplicationType) {
|
||||
case KubernetesApplicationTypes.DEPLOYMENT:
|
||||
result = KubernetesApplicationRollbackHelper._getDeploymentPayload(application, targetRevision);
|
||||
break;
|
||||
case KubernetesApplicationTypes.DAEMONSET:
|
||||
result = KubernetesApplicationRollbackHelper._getDaemonSetPayload(application, targetRevision);
|
||||
break;
|
||||
case KubernetesApplicationTypes.STATEFULSET:
|
||||
result = KubernetesApplicationRollbackHelper._getStatefulSetPayload(application, targetRevision);
|
||||
break;
|
||||
default:
|
||||
throw new PortainerError('Unable to determine which association to use to convert patch');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static _getDeploymentPayload(deploymentApp, targetRevision) {
|
||||
const target = angular.copy(targetRevision);
|
||||
const deployment = deploymentApp.Raw;
|
||||
|
||||
// remove hash label before patching back into the deployment
|
||||
delete target.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey];
|
||||
|
||||
// compute deployment annotations
|
||||
const annotations = {};
|
||||
_.forEach(KubernetesSystem_AnnotationsToSkip, (_, k) => {
|
||||
const v = deployment.metadata.annotations[k];
|
||||
if (v) {
|
||||
annotations[k] = v;
|
||||
}
|
||||
});
|
||||
_.forEach(target.metadata.annotations, (v, k) => {
|
||||
if (!KubernetesSystem_AnnotationsToSkip[k]) {
|
||||
annotations[k] = v;
|
||||
}
|
||||
});
|
||||
// Create a patch of the Deployment that replaces spec.template
|
||||
const patch = [
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/spec/template',
|
||||
value: target.spec.template,
|
||||
},
|
||||
{
|
||||
op: 'replace',
|
||||
path: '/metadata/annotations',
|
||||
value: annotations,
|
||||
},
|
||||
];
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
static _getDaemonSetPayload(daemonSet, targetRevision) {
|
||||
void daemonSet;
|
||||
return targetRevision.data;
|
||||
}
|
||||
|
||||
static _getStatefulSetPayload(statefulSet, targetRevision) {
|
||||
void statefulSet;
|
||||
return targetRevision.data;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesApplicationRollbackHelper;
|
|
@ -1,27 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
class KubernetesDaemonSetHistoryHelper {
|
||||
static _isControlledBy(daemonSet) {
|
||||
return (item) => _.find(item.metadata.ownerReferences, { uid: daemonSet.metadata.uid }) !== undefined;
|
||||
}
|
||||
|
||||
static filterOwnedRevisions(crList, daemonSet) {
|
||||
// filter ControllerRevisions that has the same selector as the DaemonSet
|
||||
// NOTE : this should be done in HTTP request based on daemonSet.spec.selector.matchLabels
|
||||
// instead of getting all CR and filtering them here
|
||||
const sameLabelsCR = _.filter(crList, ['metadata.labels', daemonSet.spec.selector.matchLabels]);
|
||||
// Only include the RS whose ControllerRef matches the DaemonSet.
|
||||
const controlledCR = _.filter(sameLabelsCR, KubernetesDaemonSetHistoryHelper._isControlledBy(daemonSet));
|
||||
// sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new)
|
||||
const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']);
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
// getCurrentRS returns the newest CR the given daemonSet targets (latest version)
|
||||
static getCurrentRevision(crList) {
|
||||
const current = _.last(crList);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDaemonSetHistoryHelper;
|
|
@ -1,56 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
import { KubernetesSystem_DefaultDeploymentUniqueLabelKey, KubernetesSystem_RevisionAnnotation } from 'Kubernetes/models/history/models';
|
||||
|
||||
class KubernetesDeploymentHistoryHelper {
|
||||
static _isControlledBy(deployment) {
|
||||
return (item) => _.find(item.metadata.ownerReferences, { uid: deployment.metadata.uid }) !== undefined;
|
||||
}
|
||||
|
||||
static filterOwnedRevisions(rsList, deployment) {
|
||||
// filter RS that has the same selector as the Deployment
|
||||
// NOTE : this should be done in HTTP request based on deployment.spec.selector
|
||||
// instead of getting all RS and filtering them here
|
||||
const sameLabelsRS = _.filter(rsList, ['spec.selector', deployment.spec.selector]);
|
||||
// Only include the RS whose ControllerRef matches the Deployment.
|
||||
const controlledRS = _.filter(sameLabelsRS, KubernetesDeploymentHistoryHelper._isControlledBy(deployment));
|
||||
// sorts the list of ReplicaSet by creation timestamp, using the names as a tie breaker (old to new)
|
||||
const sortedList = _.sortBy(controlledRS, ['metadata.creationTimestamp', 'metadata.name']);
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
// getCurrentRS returns the new RS the given deployment targets (the one with the same pod template).
|
||||
static getCurrentRevision(rsListOriginal, deployment) {
|
||||
const rsList = angular.copy(rsListOriginal);
|
||||
|
||||
// In rare cases, such as after cluster upgrades, Deployment may end up with
|
||||
// having more than one new ReplicaSets that have the same template as its template,
|
||||
// see https://github.com/kubernetes/kubernetes/issues/40415
|
||||
// We deterministically choose the oldest new ReplicaSet (first match)
|
||||
const current = _.find(rsList, (item) => {
|
||||
// returns true if two given template.spec are equal, ignoring the diff in value of Labels[pod-template-hash]
|
||||
// We ignore pod-template-hash because:
|
||||
// 1. The hash result would be different upon podTemplateSpec API changes
|
||||
// (e.g. the addition of a new field will cause the hash code to change)
|
||||
// 2. The deployment template won't have hash labels
|
||||
delete item.spec.template.metadata.labels[KubernetesSystem_DefaultDeploymentUniqueLabelKey];
|
||||
return _.isEqual(deployment.spec.template, item.spec.template);
|
||||
});
|
||||
current.revision = current.metadata.annotations[KubernetesSystem_RevisionAnnotation];
|
||||
return current;
|
||||
}
|
||||
|
||||
// filters the RSList to drop all RS that have never been a version of the Deployment
|
||||
// also add the revision as a field inside the RS
|
||||
// Note: this should not impact rollback process as we only patch
|
||||
// metadata.annotations and spec.template
|
||||
static filterVersionedRevisions(rsList) {
|
||||
const filteredRS = _.filter(rsList, (item) => item.metadata.annotations[KubernetesSystem_RevisionAnnotation] !== undefined);
|
||||
return _.map(filteredRS, (item) => {
|
||||
item.revision = item.metadata.annotations[KubernetesSystem_RevisionAnnotation];
|
||||
return item;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDeploymentHistoryHelper;
|
|
@ -1,50 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
import KubernetesDeploymentHistoryHelper from 'Kubernetes/helpers/history/deployment';
|
||||
import KubernetesDaemonSetHistoryHelper from 'Kubernetes/helpers/history/daemonset';
|
||||
import KubernetesStatefulSetHistoryHelper from 'Kubernetes/helpers/history/statefulset';
|
||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
|
||||
class KubernetesHistoryHelper {
|
||||
static getRevisions(rawRevisions, application) {
|
||||
let currentRevision, revisionsList;
|
||||
|
||||
switch (application.ApplicationType) {
|
||||
case KubernetesApplicationTypes.DEPLOYMENT:
|
||||
[currentRevision, revisionsList] = KubernetesHistoryHelper._getDeploymentRevisions(rawRevisions, application.Raw);
|
||||
break;
|
||||
case KubernetesApplicationTypes.DAEMONSET:
|
||||
[currentRevision, revisionsList] = KubernetesHistoryHelper._getDaemonSetRevisions(rawRevisions, application.Raw);
|
||||
break;
|
||||
case KubernetesApplicationTypes.STATEFULSET:
|
||||
[currentRevision, revisionsList] = KubernetesHistoryHelper._getStatefulSetRevisions(rawRevisions, application.Raw);
|
||||
break;
|
||||
default:
|
||||
throw new PortainerError('Unable to determine which association to use to get revisions');
|
||||
}
|
||||
revisionsList = _.sortBy(revisionsList, 'revision');
|
||||
return [currentRevision, revisionsList];
|
||||
}
|
||||
|
||||
static _getDeploymentRevisions(rsList, deployment) {
|
||||
const appRS = KubernetesDeploymentHistoryHelper.filterOwnedRevisions(rsList, deployment);
|
||||
const currentRS = KubernetesDeploymentHistoryHelper.getCurrentRevision(appRS, deployment);
|
||||
const versionedRS = KubernetesDeploymentHistoryHelper.filterVersionedRevisions(appRS);
|
||||
return [currentRS, versionedRS];
|
||||
}
|
||||
|
||||
static _getDaemonSetRevisions(crList, daemonSet) {
|
||||
const appCR = KubernetesDaemonSetHistoryHelper.filterOwnedRevisions(crList, daemonSet);
|
||||
const currentCR = KubernetesDaemonSetHistoryHelper.getCurrentRevision(appCR, daemonSet);
|
||||
return [currentCR, appCR];
|
||||
}
|
||||
|
||||
static _getStatefulSetRevisions(crList, statefulSet) {
|
||||
const appCR = KubernetesStatefulSetHistoryHelper.filterOwnedRevisions(crList, statefulSet);
|
||||
const currentCR = KubernetesStatefulSetHistoryHelper.getCurrentRevision(appCR, statefulSet);
|
||||
return [currentCR, appCR];
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesHistoryHelper;
|
|
@ -1,27 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
class KubernetesStatefulSetHistoryHelper {
|
||||
static _isControlledBy(statefulSet) {
|
||||
return (item) => _.find(item.metadata.ownerReferences, { uid: statefulSet.metadata.uid }) !== undefined;
|
||||
}
|
||||
|
||||
static filterOwnedRevisions(crList, statefulSet) {
|
||||
// filter ControllerRevisions that has the same selector as the StatefulSet
|
||||
// NOTE : this should be done in HTTP request based on statefulSet.spec.selector.matchLabels
|
||||
// instead of getting all CR and filtering them here
|
||||
const sameLabelsCR = _.filter(crList, ['metadata.labels', statefulSet.spec.selector.matchLabels]);
|
||||
// Only include the RS whose ControllerRef matches the StatefulSet.
|
||||
const controlledCR = _.filter(sameLabelsCR, KubernetesStatefulSetHistoryHelper._isControlledBy(statefulSet));
|
||||
// sorts the list of ControllerRevisions by revision, using the creationTimestamp as a tie breaker (old to new)
|
||||
const sortedList = _.sortBy(controlledCR, ['revision', 'metadata.creationTimestamp']);
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
// getCurrentRS returns the newest CR the given statefulSet targets (latest version)
|
||||
static getCurrentRevision(crList) {
|
||||
const current = _.last(crList);
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesStatefulSetHistoryHelper;
|
|
@ -112,23 +112,6 @@ class KubernetesHorizontalPodAutoScalerService {
|
|||
delete(horizontalPodAutoScaler) {
|
||||
return this.$async(this.deleteAsync, horizontalPodAutoScaler);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * ROLLBACK
|
||||
// */
|
||||
// async rollbackAsync(namespace, name, payload) {
|
||||
// try {
|
||||
// const params = new KubernetesCommonParams();
|
||||
// params.id = name;
|
||||
// await this.KubernetesHorizontalPodAutoScalers(namespace).rollback(params, payload).$promise;
|
||||
// } catch (err) {
|
||||
// throw new PortainerError('Unable to rollback horizontalPodAutoScaler', err);
|
||||
// }
|
||||
// }
|
||||
|
||||
// rollback(namespace, name, payload) {
|
||||
// return this.$async(this.rollbackAsync, namespace, name, payload);
|
||||
// }
|
||||
}
|
||||
|
||||
export default KubernetesHorizontalPodAutoScalerService;
|
||||
|
|
|
@ -35,8 +35,6 @@ const _KubernetesApplication = Object.freeze({
|
|||
TotalPodsCount: 0,
|
||||
Yaml: '',
|
||||
Note: '',
|
||||
Revisions: undefined,
|
||||
CurrentRevision: undefined,
|
||||
Raw: undefined, // only filled when inspecting app details / create / edit view (never filled in multiple-apps views)
|
||||
AutoScaler: undefined, // only filled if the application has an HorizontalPodAutoScaler bound to it
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export const KubernetesSystem_DefaultDeploymentUniqueLabelKey = 'pod-template-hash';
|
||||
export const KubernetesSystem_RevisionAnnotation = 'deployment.kubernetes.io/revision';
|
||||
export const KubernetesSystem_RevisionHistoryAnnotation = 'deployment.kubernetes.io/revision-history';
|
||||
export const KubernetesSystem_ChangeCauseAnnotation = 'kubernetes.io/change-cause';
|
||||
export const KubernetesSystem_DesiredReplicasAnnotation = 'deployment.kubernetes.io/desired-replicas';
|
||||
export const KubernetesSystem_MaxReplicasAnnotation = 'deployment.kubernetes.io/max-replicas';
|
||||
|
||||
|
|
|
@ -8,9 +8,12 @@ import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/Acce
|
|||
import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector';
|
||||
import { KubeApplicationAccessPolicySelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationAccessPolicySelector';
|
||||
import { KubeApplicationDeploymentTypeSelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationDeploymentTypeSelector';
|
||||
import { ApplicationSummaryWidget } from '@/react/kubernetes/applications/DetailsView';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import {
|
||||
ApplicationSummaryWidget,
|
||||
ApplicationDetailsWidget,
|
||||
} from '@/react/kubernetes/applications/DetailsView';
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
|
||||
export const componentsModule = angular
|
||||
|
@ -93,4 +96,11 @@ export const componentsModule = angular
|
|||
withUIRouter(withReactQuery(withUserProvider(ApplicationSummaryWidget))),
|
||||
[]
|
||||
)
|
||||
)
|
||||
.component(
|
||||
'applicationDetailsWidget',
|
||||
r2a(
|
||||
withUIRouter(withReactQuery(withUserProvider(ApplicationDetailsWidget))),
|
||||
[]
|
||||
)
|
||||
).name;
|
||||
|
|
|
@ -4,7 +4,6 @@ import PortainerError from 'Portainer/error';
|
|||
|
||||
import { KubernetesApplication, KubernetesApplicationDeploymentTypes, KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
import KubernetesApplicationHelper from 'Kubernetes/helpers/application';
|
||||
import KubernetesApplicationRollbackHelper from 'Kubernetes/helpers/application/rollback';
|
||||
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
||||
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
||||
|
@ -29,7 +28,6 @@ class KubernetesApplicationService {
|
|||
KubernetesPersistentVolumeClaimService,
|
||||
KubernetesNamespaceService,
|
||||
KubernetesPodService,
|
||||
KubernetesHistoryService,
|
||||
KubernetesHorizontalPodAutoScalerService,
|
||||
KubernetesIngressService
|
||||
) {
|
||||
|
@ -43,7 +41,6 @@ class KubernetesApplicationService {
|
|||
this.KubernetesPersistentVolumeClaimService = KubernetesPersistentVolumeClaimService;
|
||||
this.KubernetesNamespaceService = KubernetesNamespaceService;
|
||||
this.KubernetesPodService = KubernetesPodService;
|
||||
this.KubernetesHistoryService = KubernetesHistoryService;
|
||||
this.KubernetesHorizontalPodAutoScalerService = KubernetesHorizontalPodAutoScalerService;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
|
||||
|
@ -52,7 +49,6 @@ class KubernetesApplicationService {
|
|||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.patchPartialAsync = this.patchPartialAsync.bind(this);
|
||||
this.rollbackAsync = this.rollbackAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
/* #endregion */
|
||||
|
@ -123,8 +119,6 @@ class KubernetesApplicationService {
|
|||
application.AutoScaler = scaler;
|
||||
application.Ingresses = ingresses;
|
||||
|
||||
await this.KubernetesHistoryService.get(application);
|
||||
|
||||
if (service.Yaml) {
|
||||
application.Yaml += '---\n' + service.Yaml;
|
||||
}
|
||||
|
@ -428,18 +422,6 @@ class KubernetesApplicationService {
|
|||
return this.$async(this.deleteAsync, application);
|
||||
}
|
||||
/* #endregion */
|
||||
|
||||
/* #region ROLLBACK */
|
||||
async rollbackAsync(application, targetRevision) {
|
||||
const payload = KubernetesApplicationRollbackHelper.getPatchPayload(application, targetRevision);
|
||||
const apiService = this._getApplicationApiService(application);
|
||||
await apiService.rollback(application.ResourcePool, application.Name, payload);
|
||||
}
|
||||
|
||||
rollback(application, targetRevision) {
|
||||
return this.$async(this.rollbackAsync, application, targetRevision);
|
||||
}
|
||||
/* #endregion */
|
||||
}
|
||||
|
||||
export default KubernetesApplicationService;
|
||||
|
|
|
@ -13,7 +13,6 @@ class KubernetesDaemonSetService {
|
|||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.rollbackAsync = this.rollbackAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
|
@ -110,23 +109,6 @@ class KubernetesDaemonSetService {
|
|||
delete(daemonSet) {
|
||||
return this.$async(this.deleteAsync, daemonSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
async rollbackAsync(namespace, name, payload) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
await this.KubernetesDaemonSets(namespace).rollback(params, payload).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to rollback daemonset', err);
|
||||
}
|
||||
}
|
||||
|
||||
rollback(namespace, name, payload) {
|
||||
return this.$async(this.rollbackAsync, namespace, name, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDaemonSetService;
|
||||
|
|
|
@ -13,7 +13,6 @@ class KubernetesDeploymentService {
|
|||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.rollbackAsync = this.rollbackAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
|
@ -110,23 +109,6 @@ class KubernetesDeploymentService {
|
|||
delete(deployment) {
|
||||
return this.$async(this.deleteAsync, deployment);
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
async rollbackAsync(namespace, name, payload) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
await this.KubernetesDeployments(namespace).rollback(params, payload).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to rollback deployment', err);
|
||||
}
|
||||
}
|
||||
|
||||
rollback(namespace, name, payload) {
|
||||
return this.$async(this.rollbackAsync, namespace, name, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesDeploymentService;
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
import KubernetesHistoryHelper from 'Kubernetes/helpers/history';
|
||||
import { KubernetesApplicationTypes } from 'Kubernetes/models/application/models';
|
||||
|
||||
class KubernetesHistoryService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesReplicaSetService, KubernetesControllerRevisionService) {
|
||||
this.$async = $async;
|
||||
this.KubernetesReplicaSetService = KubernetesReplicaSetService;
|
||||
this.KubernetesControllerRevisionService = KubernetesControllerRevisionService;
|
||||
|
||||
this.getAsync = this.getAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async getAsync(application) {
|
||||
try {
|
||||
const namespace = application.ResourcePool;
|
||||
let rawRevisions;
|
||||
|
||||
switch (application.ApplicationType) {
|
||||
case KubernetesApplicationTypes.DEPLOYMENT:
|
||||
rawRevisions = await this.KubernetesReplicaSetService.get(namespace);
|
||||
break;
|
||||
case KubernetesApplicationTypes.DAEMONSET:
|
||||
rawRevisions = await this.KubernetesControllerRevisionService.get(namespace);
|
||||
break;
|
||||
case KubernetesApplicationTypes.STATEFULSET:
|
||||
rawRevisions = await this.KubernetesControllerRevisionService.get(namespace);
|
||||
break;
|
||||
case KubernetesApplicationTypes.POD:
|
||||
rawRevisions = [];
|
||||
break;
|
||||
default:
|
||||
throw new PortainerError('Unable to determine which association to use for history');
|
||||
}
|
||||
if (rawRevisions.length) {
|
||||
const [currentRevision, revisionsList] = KubernetesHistoryHelper.getRevisions(rawRevisions, application);
|
||||
application.CurrentRevision = currentRevision;
|
||||
application.Revisions = revisionsList;
|
||||
}
|
||||
return application;
|
||||
} catch (err) {
|
||||
throw new PortainerError('', err);
|
||||
}
|
||||
}
|
||||
|
||||
get(application) {
|
||||
return this.$async(this.getAsync, application);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesHistoryService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesHistoryService', KubernetesHistoryService);
|
|
@ -1,31 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import PortainerError from 'Portainer/error';
|
||||
|
||||
class KubernetesReplicaSetService {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesReplicaSets) {
|
||||
this.$async = $async;
|
||||
this.KubernetesReplicaSets = KubernetesReplicaSets;
|
||||
|
||||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET
|
||||
*/
|
||||
async getAllAsync(namespace) {
|
||||
try {
|
||||
const data = await this.KubernetesReplicaSets(namespace).get().$promise;
|
||||
return data.items;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to retrieve ReplicaSets', err);
|
||||
}
|
||||
}
|
||||
|
||||
get(namespace) {
|
||||
return this.$async(this.getAllAsync, namespace);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesReplicaSetService;
|
||||
angular.module('portainer.kubernetes').service('KubernetesReplicaSetService', KubernetesReplicaSetService);
|
|
@ -14,7 +14,6 @@ class KubernetesStatefulSetService {
|
|||
this.getAllAsync = this.getAllAsync.bind(this);
|
||||
this.createAsync = this.createAsync.bind(this);
|
||||
this.patchAsync = this.patchAsync.bind(this);
|
||||
this.rollbackAsync = this.rollbackAsync.bind(this);
|
||||
this.deleteAsync = this.deleteAsync.bind(this);
|
||||
}
|
||||
|
||||
|
@ -122,23 +121,6 @@ class KubernetesStatefulSetService {
|
|||
delete(statefulSet) {
|
||||
return this.$async(this.deleteAsync, statefulSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
async rollbackAsync(namespace, name, payload) {
|
||||
try {
|
||||
const params = new KubernetesCommonParams();
|
||||
params.id = name;
|
||||
await this.KubernetesStatefulSets(namespace).rollback(params, payload).$promise;
|
||||
} catch (err) {
|
||||
throw new PortainerError('Unable to rollback statefulSet', err);
|
||||
}
|
||||
}
|
||||
|
||||
rollback(namespace, name, payload) {
|
||||
return this.$async(this.rollbackAsync, namespace, name, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default KubernetesStatefulSetService;
|
||||
|
|
|
@ -86,308 +86,7 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div ng-if="!ctrl.isSystemNamespace()" class="mb-4 flex items-center gap-1">
|
||||
<button
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-light vertical-center ml-2"
|
||||
ui-sref="kubernetes.applications.application.edit"
|
||||
style="margin-left: 0"
|
||||
data-cy="k8sAppDetail-editAppButton"
|
||||
>
|
||||
<pr-icon icon="'pencil'" class="mr-1"></pr-icon>{{ ctrl.stack.IsComposeFormat ? 'View this application' : 'Edit this application' }}
|
||||
</button>
|
||||
<button
|
||||
authorization="K8sApplicationDetailsW"
|
||||
ng-if="ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-light ml-2"
|
||||
ui-sref="kubernetes.applications.application.edit"
|
||||
data-cy="k8sAppDetail-editAppButton"
|
||||
>
|
||||
<pr-icon icon="'pencil'" class-name="'mr-1'"></pr-icon>Edit external application
|
||||
</button>
|
||||
<be-teaser-button
|
||||
icon="'refresh-cw'"
|
||||
feature-id="ctrl.limitedFeature"
|
||||
message="'A rolling restart of the application is performed.'"
|
||||
heading="'Rolling restart'"
|
||||
button-text="'Rolling restart'"
|
||||
class-name="'be-tooltip-teaser'"
|
||||
className="'be-tooltip-teaser'"
|
||||
></be-teaser-button>
|
||||
<button
|
||||
ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD"
|
||||
type="button"
|
||||
class="btn btn-sm btn-light ml-2"
|
||||
ng-click="ctrl.redeployApplication()"
|
||||
data-cy="k8sAppDetail-redeployButton"
|
||||
>
|
||||
<pr-icon icon="'rotate-cw'" class="'mr-1'"></pr-icon>Redeploy
|
||||
</button>
|
||||
<button
|
||||
ng-if="!ctrl.isExternalApplication()"
|
||||
type="button"
|
||||
class="btn btn-sm btn-light"
|
||||
ng-click="ctrl.rollbackApplication()"
|
||||
ng-disabled="ctrl.application.Revisions.length < 2 || ctrl.state.appType !== ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
|
||||
data-cy="k8sAppDetail-rollbackButton"
|
||||
>
|
||||
<pr-icon icon="'rotate-ccw'" class="mr-1"></pr-icon>Rollback to previous configuration
|
||||
</button>
|
||||
<a
|
||||
ng-if="ctrl.isStack() && ctrl.stackFileContent"
|
||||
class="btn btn-sm btn-primary space-left"
|
||||
ui-sref="kubernetes.templates.custom.new({fileContent: ctrl.stackFileContent})"
|
||||
>
|
||||
<pr-icon icon="'plus'" class="mr-1"></pr-icon>Create template from application
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- ACCESSING APPLICATION -->
|
||||
<div class="text-muted" style="margin-bottom: 15px"> <pr-icon icon="'external-link'" class="mr-1"></pr-icon>Accessing the application </div>
|
||||
|
||||
<div class="small text-muted" ng-if="ctrl.application.PublishedPorts.length === 0" style="margin-bottom: 15px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>This application is not exposing any port.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.Services.length !== 0">
|
||||
<!-- Services notice -->
|
||||
<div>
|
||||
<div class="small text-muted">
|
||||
<p> <pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>This application is exposed through service(s) as below: </p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- table -->
|
||||
<kubernetes-application-services-table
|
||||
services="ctrl.application.Services"
|
||||
application="ctrl.application"
|
||||
public-url="ctrl.state.publicUrl"
|
||||
></kubernetes-application-services-table>
|
||||
<!-- table -->
|
||||
|
||||
<!-- table -->
|
||||
<kubernetes-application-ingress-table application="ctrl.application" public-url="ctrl.state.publicUrl"></kubernetes-application-ingress-table>
|
||||
<!-- table -->
|
||||
</div>
|
||||
<!-- !ACCESSING APPLICATION -->
|
||||
<!-- AUTO SCALING -->
|
||||
<div class="text-muted" style="margin-bottom: 15px"> <pr-icon icon="'move'" class="mr-1"></pr-icon>Auto-scaling </div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.application.AutoScaler" style="margin-bottom: 15px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
|
||||
This application does not have an autoscaling policy defined.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.application.AutoScaler">
|
||||
<div style="margin-top: 15px; width: 50%">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%">Minimum instances</td>
|
||||
<td style="width: 33%">Maximum instances</td>
|
||||
<td style="width: 33%">
|
||||
Target CPU usage
|
||||
<portainer-tooltip message="'The autoscaler will ensure enough instances are running to maintain an average CPU usage across all instances.'">
|
||||
</portainer-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td data-cy="k8sAppDetail-minReplicas">{{ ctrl.application.AutoScaler.MinReplicas }}</td>
|
||||
<td data-cy="k8sAppDetail-maxReplicas">{{ ctrl.application.AutoScaler.MaxReplicas }}</td>
|
||||
<td data-cy="k8sAppDetail-targetCPU">{{ ctrl.application.AutoScaler.TargetCPUUtilization }}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !AUTO SCALING -->
|
||||
|
||||
<!-- CONFIGURATIONS -->
|
||||
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px">
|
||||
<pr-icon icon="'file'" class="mr-1"></pr-icon>
|
||||
ConfigMap or Secret
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.application.Env.length > 0 && !ctrl.hasVolumeConfiguration()" style="margin-bottom: 15px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
|
||||
This application is not using any environment variable, ConfigMap or Secret.
|
||||
</div>
|
||||
|
||||
<table class="table" ng-if="ctrl.application.Env.length > 0">
|
||||
<tr class="text-muted">
|
||||
<td style="width: 25%">Container</td>
|
||||
<td style="width: 25%">Environment variable</td>
|
||||
<td style="width: 25%">Value</td>
|
||||
<td style="width: 25%">ConfigMap or Secret</td>
|
||||
</tr>
|
||||
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
|
||||
<tr ng-repeat="envvar in container.Env | orderBy: 'name'">
|
||||
<td data-cy="k8sAppDetail-containerName">
|
||||
{{ container.Name }}
|
||||
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT">
|
||||
<pr-icon icon="'asterisk'"></pr-icon>
|
||||
{{ envvar.valueFrom.fieldRef.fieldPath }} (<a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" target="_blank">init container</a
|
||||
>)</span
|
||||
>
|
||||
</td>
|
||||
<td data-cy="k8sAppDetail-envVarName">{{ envvar.name }}</td>
|
||||
<td>
|
||||
<span ng-if="envvar.value" data-cy="k8sAppDetail-envVarValue">{{ envvar.value }}</span>
|
||||
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-envVarValue"
|
||||
><pr-icon icon="'key'" class="mr-1"></pr-icon>{{ envvar.valueFrom.configMapKeyRef.key }}</span
|
||||
>
|
||||
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-envVarValue"
|
||||
><pr-icon icon="'key'" class="mr-1"></pr-icon>{{ envvar.valueFrom.secretKeyRef.key }}</span
|
||||
>
|
||||
<span ng-if="envvar.valueFrom.fieldRef" data-cy="k8sAppDetail-envVarValue"
|
||||
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
|
||||
href="https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/#capabilities-of-the-downward-api"
|
||||
target="_blank"
|
||||
>downward API</a
|
||||
>)</span
|
||||
>
|
||||
<span ng-if="!envvar.value && !envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef && !envvar.valueFrom.fieldRef">-</span>
|
||||
</td>
|
||||
<td>
|
||||
<span ng-if="envvar.value || envvar.valueFrom.fieldRef || (!envvar.valueFrom.secretKeyRef && !envvar.valueFrom.configMapKeyRef)">-</span>
|
||||
<span ng-if="envvar.valueFrom.configMapKeyRef" data-cy="k8sAppDetail-configName"
|
||||
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.configMapKeyRef.name, namespace: ctrl.application.ResourcePool })"
|
||||
><pr-icon icon="'file'" class="mr-1"></pr-icon>{{ envvar.valueFrom.configMapKeyRef.name }}</a
|
||||
></span
|
||||
>
|
||||
<span ng-if="envvar.valueFrom.secretKeyRef" data-cy="k8sAppDetail-configName"
|
||||
><a ui-sref="kubernetes.configurations.configuration({ name: envvar.valueFrom.secretKeyRef.name, namespace: ctrl.application.ResourcePool })"
|
||||
><pr-icon icon="'file'" class="mr-1"></pr-icon>{{ envvar.valueFrom.secretKeyRef.name }}</a
|
||||
></span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table" ng-if="ctrl.hasVolumeConfiguration()">
|
||||
<tr class="text-muted">
|
||||
<td style="width: 25%">Container</td>
|
||||
<td style="width: 25%">Configuration path</td>
|
||||
<td style="width: 25%">Value</td>
|
||||
<td style="width: 25%">Configuration</td>
|
||||
</tr>
|
||||
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
|
||||
<tr ng-repeat="volume in container.ConfigurationVolumes track by $index" style="border-top: 0">
|
||||
<td>
|
||||
{{ container.Name }}
|
||||
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT"
|
||||
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
|
||||
href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
|
||||
target="_blank"
|
||||
>init container</a
|
||||
>)</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
{{ volume.fileMountPath }}
|
||||
</td>
|
||||
<td>
|
||||
<pr-icon icon="'plus'" class="mr-1" ng-if="volume.configurationKey"></pr-icon>
|
||||
{{ volume.configurationKey ? volume.configurationKey : '-' }}
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="kubernetes.configurations.configuration({ name: volume.configurationName, namespace: ctrl.application.ResourcePool })"
|
||||
><pr-icon icon="'plus'" class="mr-1"></pr-icon>{{ volume.configurationName }}</a
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- !CONFIGURATIONS -->
|
||||
|
||||
<!-- DATA PERSISTENCE -->
|
||||
<div class="text-muted" style="margin-bottom: 15px; margin-top: 25px">
|
||||
<pr-icon icon="'database'" class="mr-1"></pr-icon>
|
||||
Data persistence
|
||||
</div>
|
||||
|
||||
<div class="small text-muted" ng-if="!ctrl.hasPersistedFolders()">
|
||||
<pr-icon icon="'info'" mode="'primary'" class="mr-1"></pr-icon>
|
||||
This application has no persisted folders.
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.hasPersistedFolders()">
|
||||
<div class="small text-muted vertical-center" style="margin-bottom: 15px">
|
||||
Data access policy:
|
||||
<pr-icon icon="ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyIcon"></pr-icon>
|
||||
{{ ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyText }}
|
||||
<portainer-tooltip position="'right'" message="ctrl.application.DataAccessPolicy | kubernetesApplicationDataAccessPolicyTooltip"> </portainer-tooltip>
|
||||
</div>
|
||||
|
||||
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.SHARED">
|
||||
<tr class="text-muted">
|
||||
<td style="width: 33%">Persisted folder</td>
|
||||
<td style="width: 66%">Persistence</td>
|
||||
</tr>
|
||||
<tbody ng-repeat="container in ctrl.application.Containers" style="border-top: 0">
|
||||
<tr ng-repeat="volume in container.PersistedFolders track by $index">
|
||||
<td data-cy="k8sAppDetail-volMountPath">
|
||||
{{ volume.MountPath }}
|
||||
</td>
|
||||
<td ng-if="volume.PersistentVolumeClaimName">
|
||||
<a
|
||||
class="hyperlink"
|
||||
ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName, namespace: ctrl.application.ResourcePool })"
|
||||
data-cy="k8sAppDetail-volClaimName"
|
||||
><pr-icon icon="'database'" class="mr-1"></pr-icon>{{ volume.PersistentVolumeClaimName }}</a
|
||||
>
|
||||
</td>
|
||||
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table" ng-if="ctrl.application.DataAccessPolicy === ctrl.ApplicationDataAccessPolicies.ISOLATED">
|
||||
<thead>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 25%">Container name</td>
|
||||
<td style="width: 25%">Pod name</td>
|
||||
<td style="width: 25%">Persisted folder</td>
|
||||
<td style="width: 25%">Persistence</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody ng-repeat="container in ctrl.allContainers track by $index" style="border-top: none">
|
||||
<tr ng-repeat="volume in container.PersistedFolders track by $index">
|
||||
<td>
|
||||
{{ container.Name }}
|
||||
<span ng-if="container.Type === ctrl.KubernetesPodContainerTypes.INIT"
|
||||
><pr-icon icon="'asterisk'"></pr-icon> {{ envvar.valueFrom.fieldRef.fieldPath }} (<a
|
||||
href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/"
|
||||
target="_blank"
|
||||
>init container</a
|
||||
>)</span
|
||||
>
|
||||
</td>
|
||||
<td>{{ container.PodName }}</td>
|
||||
<td>
|
||||
{{ volume.MountPath }}
|
||||
</td>
|
||||
<td ng-if="volume.PersistentVolumeClaimName">
|
||||
<a
|
||||
class="hyperlink"
|
||||
ui-sref="kubernetes.volumes.volume({ name: volume.PersistentVolumeClaimName + '-' + container.PodName, namespace: ctrl.application.ResourcePool })"
|
||||
>
|
||||
<pr-icon icon="'database'" class="mr-1"></pr-icon>{{ volume.PersistentVolumeClaimName + '-' + container.PodName }}</a
|
||||
>
|
||||
</td>
|
||||
<td ng-if="volume.HostPath"> {{ volume.HostPath }} on host filesystem </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- !DATA PERSISTENCE -->
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
<application-details-widget></application-details-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -15,9 +15,6 @@ import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
|||
import { KubernetesPodNodeAffinityNodeSelectorRequirementOperators } from 'Kubernetes/pod/models';
|
||||
import { KubernetesPodContainerTypes } from 'Kubernetes/pod/models/index';
|
||||
import KubernetesNamespaceHelper from 'Kubernetes/helpers/namespaceHelper';
|
||||
import { confirmUpdate, confirm } from '@@/modals/confirm';
|
||||
import { buildConfirmButton } from '@@/modals/utils';
|
||||
import { ModalType } from '@@/modals';
|
||||
|
||||
function computeTolerations(nodes, application) {
|
||||
const pod = application.Pods[0];
|
||||
|
@ -146,11 +143,6 @@ 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);
|
||||
this.copyLoadBalancerIP = this.copyLoadBalancerIP.bind(this);
|
||||
}
|
||||
|
||||
selectTab(index) {
|
||||
|
@ -166,128 +158,10 @@ class KubernetesApplicationController {
|
|||
return KubernetesNamespaceHelper.isSystemNamespace(this.application.ResourcePool);
|
||||
}
|
||||
|
||||
isExternalApplication() {
|
||||
return KubernetesApplicationHelper.isExternalApplication(this.application);
|
||||
}
|
||||
|
||||
copyLoadBalancerIP() {
|
||||
this.clipboard.copyText(this.application.LoadBalancerIPAddress);
|
||||
$('#copyNotificationLB').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
copyApplicationName() {
|
||||
this.clipboard.copyText(this.application.Name);
|
||||
$('#copyNotificationApplicationName').show().fadeOut(2500);
|
||||
}
|
||||
|
||||
hasPersistedFolders() {
|
||||
return this.application && this.application.PersistedFolders.length;
|
||||
}
|
||||
|
||||
hasVolumeConfiguration() {
|
||||
return this.application && this.application.ConfigurationVolumes.length;
|
||||
}
|
||||
|
||||
hasEventWarnings() {
|
||||
return this.state.eventWarningCount;
|
||||
}
|
||||
|
||||
buildIngressRuleURL(rule) {
|
||||
const hostname = rule.Host ? rule.Host : rule.IP;
|
||||
return 'http://' + hostname + rule.Path;
|
||||
}
|
||||
|
||||
portHasIngressRules(port) {
|
||||
return port.IngressRules.length > 0;
|
||||
}
|
||||
|
||||
ruleCanBeDisplayed(rule) {
|
||||
return !rule.Host && !rule.IP ? false : true;
|
||||
}
|
||||
|
||||
isStack() {
|
||||
return this.application.StackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* ROLLBACK
|
||||
*/
|
||||
async rollbackApplicationAsync() {
|
||||
try {
|
||||
// await this.KubernetesApplicationService.rollback(this.application, this.formValues.SelectedRevision);
|
||||
const revision = _.nth(this.application.Revisions, -2);
|
||||
await this.KubernetesApplicationService.rollback(this.application, revision);
|
||||
this.Notifications.success('Success', 'Application successfully rolled back');
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to rollback the application');
|
||||
}
|
||||
}
|
||||
|
||||
rollbackApplication() {
|
||||
confirmUpdate('Rolling back the application to a previous configuration may cause service interruption. Do you wish to continue?', (confirmed) => {
|
||||
if (confirmed) {
|
||||
return this.$async(this.rollbackApplicationAsync);
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* REDEPLOY
|
||||
*/
|
||||
async redeployApplicationAsync() {
|
||||
const confirmed = await confirm({
|
||||
modalType: ModalType.Warn,
|
||||
title: 'Are you sure?',
|
||||
message: 'Redeploying the application may cause a service interruption. Do you wish to continue?',
|
||||
confirmButton: buildConfirmButton('Redeploy'),
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const promises = _.map(this.application.Pods, (item) => this.KubernetesPodService.delete(item));
|
||||
await Promise.all(promises);
|
||||
this.Notifications.success('Success', 'Application successfully redeployed');
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to redeploy the application');
|
||||
}
|
||||
}
|
||||
|
||||
redeployApplication() {
|
||||
return this.$async(this.redeployApplicationAsync);
|
||||
}
|
||||
|
||||
/**
|
||||
* UPDATE
|
||||
*/
|
||||
async updateApplicationAsync() {
|
||||
try {
|
||||
const application = angular.copy(this.application);
|
||||
application.Note = this.formValues.Note;
|
||||
await this.KubernetesApplicationService.patch(this.application, application, true);
|
||||
this.Notifications.success('Success', 'Application successfully updated');
|
||||
this.$state.reload(this.$state.current);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to update application');
|
||||
}
|
||||
}
|
||||
|
||||
updateApplication() {
|
||||
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 = `manifest`;
|
||||
} else if (this.application.ApplicationKind === this.KubernetesDeploymentTypes.URL) {
|
||||
this.state.appType = `manifest`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* EVENTS
|
||||
*/
|
||||
|
@ -325,22 +199,7 @@ class KubernetesApplicationController {
|
|||
this.KubernetesNodeService.get(),
|
||||
]);
|
||||
this.application = application;
|
||||
if (this.application.StackId) {
|
||||
this.stack = await this.StackService.stack(application.StackId);
|
||||
}
|
||||
this.allContainers = KubernetesApplicationHelper.associateAllContainersAndApplication(application);
|
||||
this.formValues.Note = this.application.Note;
|
||||
this.formValues.Services = this.application.Services;
|
||||
if (this.application.Note) {
|
||||
this.state.expandedNote = true;
|
||||
}
|
||||
if (this.application.CurrentRevision) {
|
||||
this.formValues.SelectedRevision = _.find(this.application.Revisions, { revision: this.application.CurrentRevision.revision });
|
||||
}
|
||||
|
||||
this.state.useIngress = _.find(application.PublishedPorts, (p) => {
|
||||
return this.portHasIngressRules(p);
|
||||
});
|
||||
|
||||
this.placements = computePlacements(nodes, this.application);
|
||||
this.state.placementWarning = _.find(this.placements, { AcceptsApplication: true }) ? false : true;
|
||||
|
@ -379,7 +238,6 @@ class KubernetesApplicationController {
|
|||
eventWarningCount: 0,
|
||||
placementWarning: false,
|
||||
expandedNote: false,
|
||||
useIngress: false,
|
||||
useServerMetrics: this.endpoint.Kubernetes.Configuration.UseServerMetrics,
|
||||
publicUrl: this.endpoint.PublicURL,
|
||||
};
|
||||
|
@ -391,12 +249,8 @@ class KubernetesApplicationController {
|
|||
SelectedRevision: undefined,
|
||||
};
|
||||
|
||||
const resourcePools = await this.KubernetesResourcePoolService.get();
|
||||
this.allNamespaces = resourcePools.map(({ Namespace }) => Namespace.Name);
|
||||
|
||||
await this.getApplication();
|
||||
await this.getEvents();
|
||||
this.updateApplicationKindText();
|
||||
this.state.viewReady = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
export default class KubernetesApplicationIngressController {
|
||||
/* @ngInject */
|
||||
constructor($async, KubernetesIngressService) {
|
||||
this.$async = $async;
|
||||
this.KubernetesIngressService = KubernetesIngressService;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.hasIngress;
|
||||
this.applicationIngress = [];
|
||||
const ingresses = await this.KubernetesIngressService.get(this.application.ResourcePool);
|
||||
const services = this.application.Services;
|
||||
|
||||
_.forEach(services, (service) => {
|
||||
_.forEach(ingresses, (ingress) => {
|
||||
_.forEach(ingress.Paths, (path) => {
|
||||
if (path.ServiceName === service.metadata.name) {
|
||||
path.Secure = ingress.TLS && ingress.TLS.filter((tls) => tls.hosts && tls.hosts.includes(path.Host)).length > 0;
|
||||
this.applicationIngress.push(path);
|
||||
this.hasIngress = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
<div style="margin-top: 15px" ng-if="$ctrl.hasIngress">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 15%">Ingress name</td>
|
||||
<td style="width: 10%">Service name</td>
|
||||
<td style="width: 10%">Host</td>
|
||||
<td style="width: 10%">Port</td>
|
||||
<td style="width: 10%">Path</td>
|
||||
<td style="width: 15%">HTTP Route</td>
|
||||
</tr>
|
||||
<tr ng-repeat="ingress in $ctrl.applicationIngress">
|
||||
<td
|
||||
><a authorization="K8sIngressesW" ui-sref="kubernetes.ingresses.edit({ name: ingress.IngressName, namespace: $ctrl.application.ResourcePool })">{{
|
||||
ingress.IngressName
|
||||
}}</a></td
|
||||
>
|
||||
<td>{{ ingress.ServiceName }}</td>
|
||||
<td>{{ ingress.Host }}</td>
|
||||
<td>{{ ingress.Port }}</td>
|
||||
<td>{{ ingress.Path }}</td>
|
||||
<td
|
||||
><a target="_blank" href="{{ ingress.Secure ? 'https' : 'http' }}://{{ ingress.Host }}{{ ingress.Path }}">{{ ingress.Host }}{{ ingress.Path }}</a></td
|
||||
>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,11 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import controller from './ingress-table.controller';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationIngressTable', {
|
||||
templateUrl: './ingress-table.html',
|
||||
controller,
|
||||
bindings: {
|
||||
application: '<',
|
||||
publicUrl: '<',
|
||||
},
|
||||
});
|
|
@ -1,62 +0,0 @@
|
|||
<!-- table -->
|
||||
<div style="margin-top: 15px">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr class="text-muted">
|
||||
<td style="width: 15%">Service name</td>
|
||||
<td style="width: 10%">Type</td>
|
||||
<td style="width: 10%">Cluster IP</td>
|
||||
<td style="width: 10%">External IP</td>
|
||||
<td style="width: 10%">Container port</td>
|
||||
<td style="width: 15%">Service port(s)</td>
|
||||
</tr>
|
||||
<tr ng-repeat="service in $ctrl.services">
|
||||
<td>{{ service.metadata.name }}</td>
|
||||
<td>{{ service.spec.type }}</td>
|
||||
<td>{{ service.spec.clusterIP }}</td>
|
||||
<td ng-show="service.spec.type === 'LoadBalancer'">
|
||||
<div ng-show="service.status.loadBalancer.ingress">
|
||||
<a class="vertical-center hyperlink" target="_blank" ng-href="http://{{ service.status.loadBalancer.ingress[0].ip }}:{{ service.spec.ports[0].port }}">
|
||||
<pr-icon icon="'external-link'"></pr-icon>
|
||||
<span data-cy="k8sAppDetail-containerPort"> Access </span>
|
||||
</a>
|
||||
</div>
|
||||
<div ng-show="!service.status.loadBalancer.ingress">
|
||||
{{ service.spec.externalIP ? service.spec.externalIP : 'pending...' }}
|
||||
</div>
|
||||
</td>
|
||||
<td ng-show="service.spec.type !== 'LoadBalancer'">{{ service.spec.externalIP ? service.spec.externalIP : '-' }}</td>
|
||||
|
||||
<td data-cy="k8sAppDetail-containerPort">
|
||||
<div ng-repeat="port in service.spec.ports">{{ port.targetPort }}</div>
|
||||
</td>
|
||||
<td ng-if="!ctrl.portHasIngressRules(port)">
|
||||
<div ng-repeat="port in service.spec.ports">
|
||||
<a
|
||||
class="vertical-center hyperlink"
|
||||
ng-if="$ctrl.publicUrl && port.nodePort"
|
||||
ng-href="http://{{ $ctrl.publicUrl }}:{{ port.nodePort }}"
|
||||
target="_blank"
|
||||
style="margin-left: 5px"
|
||||
>
|
||||
<pr-icon icon="'external-link'"></pr-icon>
|
||||
<span data-cy="k8sAppDetail-containerPort">
|
||||
{{ port.port }}
|
||||
</span>
|
||||
<span>{{ port.nodePort ? ':' : '' }}</span>
|
||||
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
|
||||
</a>
|
||||
|
||||
<div ng-if="!$ctrl.publicUrl">
|
||||
<span data-cy="k8sAppDetail-servicePort">
|
||||
{{ port.port }}
|
||||
</span>
|
||||
<span>{{ port.nodePort ? ':' : '' }}</span>
|
||||
<span data-cy="k8sAppDetail-nodePort"> {{ port.nodePort }}/{{ port.protocol }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
angular.module('portainer.kubernetes').component('kubernetesApplicationServicesTable', {
|
||||
templateUrl: './services-table.html',
|
||||
bindings: {
|
||||
services: '<',
|
||||
application: '<',
|
||||
publicUrl: '<',
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue