mirror of https://github.com/portainer/portainer
feat(kubernetes/summary): summary of k8s actions upon deploying/updating resources EE-436 (#5137)
* feat EE-440/EE-436 kubernetes-resources-summary-panel * bugfix: returning created resources after update * fixed patch based bugs - displaying accurate updates for k8s resources Co-authored-by: Simon Meng <simon.meng@portainer.io>pull/5162/head
parent
267968e099
commit
eae2f5c9fc
|
@ -1,11 +1,9 @@
|
||||||
export function KubernetesResourcePoolFormValues(defaults) {
|
export function KubernetesResourcePoolFormValues(defaults) {
|
||||||
return {
|
this.Name = '';
|
||||||
Name: '',
|
this.MemoryLimit = defaults.MemoryLimit;
|
||||||
MemoryLimit: defaults.MemoryLimit,
|
this.CpuLimit = defaults.CpuLimit;
|
||||||
CpuLimit: defaults.CpuLimit,
|
this.HasQuota = false;
|
||||||
HasQuota: false,
|
this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue
|
||||||
IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
export const KubernetesResourceTypes = Object.freeze({
|
||||||
|
NAMESPACE: 'Namespace',
|
||||||
|
RESOURCEQUOTA: 'ResourceQuota',
|
||||||
|
CONFIGMAP: 'ConfigMap',
|
||||||
|
SECRET: 'Secret',
|
||||||
|
DEPLOYMENT: 'Deployment',
|
||||||
|
STATEFULSET: 'StatefulSet',
|
||||||
|
DAEMONSET: 'Daemonset',
|
||||||
|
PERSISTENT_VOLUME_CLAIM: 'PersistentVolumeClaim',
|
||||||
|
SERVICE: 'Service',
|
||||||
|
INGRESS: 'Ingress',
|
||||||
|
HORIZONTAL_POD_AUTOSCALER: 'HorizontalPodAutoscaler',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const KubernetesResourceActions = Object.freeze({
|
||||||
|
CREATE: 'Create',
|
||||||
|
UPDATE: 'Update',
|
||||||
|
DELETE: 'Delete',
|
||||||
|
});
|
|
@ -220,6 +220,11 @@ class KubernetesApplicationService {
|
||||||
// resource creation flow
|
// resource creation flow
|
||||||
// should we keep formValues > Resource_1 || Resource_2
|
// should we keep formValues > Resource_1 || Resource_2
|
||||||
// or should we switch to formValues > Composite > Resource_1 || Resource_2
|
// or should we switch to formValues > Composite > Resource_1 || Resource_2
|
||||||
|
/**
|
||||||
|
* NOTE: Keep this method flow in sync with `getCreatedApplicationResources` method in the `applicationService` file
|
||||||
|
* To synchronise with kubernetes resource creation summary output, any new resources created in this method should
|
||||||
|
* also be displayed in the summary output (getCreatedApplicationResources)
|
||||||
|
*/
|
||||||
async createAsync(formValues) {
|
async createAsync(formValues) {
|
||||||
try {
|
try {
|
||||||
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||||
|
@ -266,6 +271,11 @@ class KubernetesApplicationService {
|
||||||
|
|
||||||
/* #region PATCH */
|
/* #region PATCH */
|
||||||
// this function accepts KubernetesApplicationFormValues as parameters
|
// this function accepts KubernetesApplicationFormValues as parameters
|
||||||
|
/**
|
||||||
|
* NOTE: Keep this method flow in sync with `getUpdatedApplicationResources` method in the `applicationService` file
|
||||||
|
* To synchronise with kubernetes resource creation, update and delete summary output, any new resources created
|
||||||
|
* in this method should also be displayed in the summary output (getUpdatedApplicationResources)
|
||||||
|
*/
|
||||||
async patchAsync(oldFormValues, newFormValues) {
|
async patchAsync(oldFormValues, newFormValues) {
|
||||||
try {
|
try {
|
||||||
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||||
|
|
|
@ -1543,6 +1543,13 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- summary -->
|
||||||
|
<kubernetes-summary-view
|
||||||
|
ng-if="!(!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.state.pullImageValidity)"
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
old-form-values="ctrl.savedFormValues"
|
||||||
|
></kubernetes-summary-view>
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -123,6 +123,12 @@
|
||||||
is-editor-dirty="ctrl.state.isEditorDirty"
|
is-editor-dirty="ctrl.state.isEditorDirty"
|
||||||
></kubernetes-configuration-data>
|
></kubernetes-configuration-data>
|
||||||
|
|
||||||
|
<!-- summary -->
|
||||||
|
<kubernetes-summary-view
|
||||||
|
ng-if="!(!kubernetesConfigurationCreationForm.$valid || !ctrl.isFormValid() || ctrl.state.actionInProgress)"
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
></kubernetes-summary-view>
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||||
Actions
|
Actions
|
||||||
|
|
|
@ -85,6 +85,12 @@
|
||||||
is-editor-dirty="ctrl.state.isEditorDirty"
|
is-editor-dirty="ctrl.state.isEditorDirty"
|
||||||
></kubernetes-configuration-data>
|
></kubernetes-configuration-data>
|
||||||
|
|
||||||
|
<!-- summary -->
|
||||||
|
<kubernetes-summary-view
|
||||||
|
ng-if="!(!ctrl.isFormValid() || !kubernetesConfigurationCreationForm.$valid || ctrl.state.actionInProgress)"
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
></kubernetes-summary-view>
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
<div class="col-sm-12 form-section-title" style="margin-top: 10px;">
|
||||||
Actions
|
Actions
|
||||||
|
|
|
@ -338,6 +338,10 @@
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- summary -->
|
||||||
|
<kubernetes-summary-view ng-if="resourcePoolCreationForm.$valid && !ctrl.isCreateButtonDisabled()" form-values="ctrl.formValues"></kubernetes-summary-view>
|
||||||
|
<!-- !summary -->
|
||||||
|
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -329,6 +329,14 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- #endregion -->
|
<!-- #endregion -->
|
||||||
|
|
||||||
|
<!-- summary -->
|
||||||
|
<kubernetes-summary-view
|
||||||
|
ng-if="resourcePoolEditForm.$valid && !ctrl.isUpdateButtonDisabled()"
|
||||||
|
form-values="ctrl.formValues"
|
||||||
|
old-form-values="ctrl.savedFormValues"
|
||||||
|
></kubernetes-summary-view>
|
||||||
|
<!-- !summary -->
|
||||||
|
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
<div ng-if="ctrl.isAdmin && ctrl.isEditable" class="col-sm-12 form-section-title">
|
||||||
Actions
|
Actions
|
||||||
|
|
|
@ -0,0 +1,242 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
|
||||||
|
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
|
||||||
|
import { KubernetesDeployment } from 'Kubernetes/models/deployment/models';
|
||||||
|
import { KubernetesStatefulSet } from 'Kubernetes/models/stateful-set/models';
|
||||||
|
import { KubernetesDaemonSet } from 'Kubernetes/models/daemon-set/models';
|
||||||
|
import { KubernetesServiceTypes } from 'Kubernetes/models/service/models';
|
||||||
|
import {
|
||||||
|
KubernetesApplication,
|
||||||
|
KubernetesApplicationDeploymentTypes,
|
||||||
|
KubernetesApplicationPublishingTypes,
|
||||||
|
KubernetesApplicationTypes,
|
||||||
|
} from 'Kubernetes/models/application/models';
|
||||||
|
import { KubernetesHorizontalPodAutoScalerHelper } from 'Kubernetes/horizontal-pod-auto-scaler/helper';
|
||||||
|
import { KubernetesHorizontalPodAutoScalerConverter } from 'Kubernetes/horizontal-pod-auto-scaler/converter';
|
||||||
|
import KubernetesApplicationConverter from 'Kubernetes/converters/application';
|
||||||
|
import KubernetesServiceConverter from 'Kubernetes/converters/service';
|
||||||
|
import { KubernetesIngressConverter } from 'Kubernetes/ingress/converter';
|
||||||
|
import KubernetesPersistentVolumeClaimConverter from 'Kubernetes/converters/persistentVolumeClaim';
|
||||||
|
|
||||||
|
const { CREATE, UPDATE, DELETE } = KubernetesResourceActions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get summary of Kubernetes resources to be created, updated or deleted
|
||||||
|
* @param {KubernetesApplicationFormValues} formValues
|
||||||
|
*/
|
||||||
|
export default function (formValues, oldFormValues = {}) {
|
||||||
|
if (oldFormValues instanceof KubernetesApplicationFormValues) {
|
||||||
|
const resourceSummary = getUpdatedApplicationResources(oldFormValues, formValues);
|
||||||
|
return resourceSummary;
|
||||||
|
}
|
||||||
|
const resourceSummary = getCreatedApplicationResources(formValues);
|
||||||
|
return resourceSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get summary of Kubernetes resources to be created
|
||||||
|
* @param {KubernetesApplicationFormValues} formValues
|
||||||
|
*/
|
||||||
|
function getCreatedApplicationResources(formValues) {
|
||||||
|
const resources = [];
|
||||||
|
|
||||||
|
let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues);
|
||||||
|
|
||||||
|
if (service) {
|
||||||
|
// Service
|
||||||
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: service.Name, type: service.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
|
if (formValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
// Ingress
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(formValues, service.Name);
|
||||||
|
resources.push(...getIngressUpdateSummary(formValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app instanceof KubernetesStatefulSet) {
|
||||||
|
// Service
|
||||||
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: headlessService.Name, type: headlessService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
|
} else {
|
||||||
|
// Persistent volume claims
|
||||||
|
const persistentVolumeClaimResources = claims
|
||||||
|
.filter((pvc) => !pvc.PreviousName && !pvc.Id)
|
||||||
|
.map((pvc) => ({ action: CREATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: pvc.Name }));
|
||||||
|
resources.push(...persistentVolumeClaimResources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal pod autoscalers
|
||||||
|
if (formValues.AutoScaler.IsUsed && formValues.DeploymentType !== KubernetesApplicationDeploymentTypes.GLOBAL) {
|
||||||
|
const kind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(app);
|
||||||
|
const autoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(formValues, kind);
|
||||||
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: autoScaler.Name });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deployment
|
||||||
|
const appResourceType = getApplicationResourceType(app);
|
||||||
|
if (appResourceType !== null) {
|
||||||
|
resources.push({ action: CREATE, kind: appResourceType, name: app.Name });
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get summary of Kubernetes resources to be created, updated and/or deleted
|
||||||
|
* @param {KubernetesApplicationFormValues} oldFormValues
|
||||||
|
* @param {KubernetesApplicationFormValues} newFormValues
|
||||||
|
*/
|
||||||
|
function getUpdatedApplicationResources(oldFormValues, newFormValues) {
|
||||||
|
const resources = [];
|
||||||
|
|
||||||
|
const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues);
|
||||||
|
const [newApp, newHeadlessService, newService, newClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(newFormValues);
|
||||||
|
|
||||||
|
const oldAppResourceType = getApplicationResourceType(oldApp);
|
||||||
|
const newAppResourceType = getApplicationResourceType(newApp);
|
||||||
|
|
||||||
|
if (oldAppResourceType !== newAppResourceType) {
|
||||||
|
// Deployment
|
||||||
|
resources.push({ action: DELETE, kind: oldAppResourceType, name: oldApp.Name });
|
||||||
|
if (oldService) {
|
||||||
|
// Service
|
||||||
|
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
|
}
|
||||||
|
// re-creation of resources
|
||||||
|
const createdApplicationResourceSummary = getCreatedApplicationResources(newFormValues);
|
||||||
|
resources.push(...createdApplicationResourceSummary);
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newApp instanceof KubernetesStatefulSet) {
|
||||||
|
const headlessServiceUpdateResourceSummary = getServiceUpdateResourceSummary(oldHeadlessService, newHeadlessService);
|
||||||
|
if (headlessServiceUpdateResourceSummary) {
|
||||||
|
resources.push(headlessServiceUpdateResourceSummary);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Persistent volume claims
|
||||||
|
const claimSummaries = newClaims
|
||||||
|
.map((pvc) => {
|
||||||
|
if (!pvc.PreviousName && !pvc.Id) {
|
||||||
|
return { action: CREATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: pvc.Name };
|
||||||
|
} else if (!pvc.Id) {
|
||||||
|
const oldClaim = _.find(oldClaims, { Name: pvc.PreviousName });
|
||||||
|
return getVolumeClaimUpdateResourceSummary(oldClaim, pvc);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((pvc) => pvc); // remove nulls
|
||||||
|
resources.push(...claimSummaries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deployment
|
||||||
|
resources.push({ action: UPDATE, kind: oldAppResourceType, name: oldApp.Name });
|
||||||
|
|
||||||
|
if (oldService && newService) {
|
||||||
|
// Service
|
||||||
|
const serviceUpdateResourceSummary = getServiceUpdateResourceSummary(oldService, newService);
|
||||||
|
if (serviceUpdateResourceSummary) {
|
||||||
|
resources.push(serviceUpdateResourceSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS || oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
// Ingress
|
||||||
|
const oldIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(oldFormValues, oldService.Name);
|
||||||
|
const newIngresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||||
|
resources.push(...getIngressUpdateSummary(oldIngresses, newIngresses));
|
||||||
|
}
|
||||||
|
} else if (!oldService && newService) {
|
||||||
|
// Service
|
||||||
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.SERVICE, name: newService.Name, type: newService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
|
if (newFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
// Ingress
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, newService.Name);
|
||||||
|
resources.push(...getIngressUpdateSummary(newFormValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
|
} else if (oldService && !newService) {
|
||||||
|
// Service
|
||||||
|
resources.push({ action: DELETE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP });
|
||||||
|
if (oldFormValues.PublishingType === KubernetesApplicationPublishingTypes.INGRESS) {
|
||||||
|
// Ingress
|
||||||
|
const ingresses = KubernetesIngressConverter.applicationFormValuesToIngresses(newFormValues, oldService.Name);
|
||||||
|
resources.push(...getIngressUpdateSummary(oldFormValues.OriginalIngresses, ingresses));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(newApp);
|
||||||
|
const newAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(newFormValues, newKind);
|
||||||
|
if (!oldFormValues.AutoScaler.IsUsed) {
|
||||||
|
if (newFormValues.AutoScaler.IsUsed) {
|
||||||
|
// Horizontal pod autoscalers
|
||||||
|
resources.push({ action: CREATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: newAutoScaler.Name });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Horizontal pod autoscalers
|
||||||
|
const oldKind = KubernetesHorizontalPodAutoScalerHelper.getApplicationTypeString(oldApp);
|
||||||
|
const oldAutoScaler = KubernetesHorizontalPodAutoScalerConverter.applicationFormValuesToModel(oldFormValues, oldKind);
|
||||||
|
if (newFormValues.AutoScaler.IsUsed) {
|
||||||
|
const hpaUpdateSummary = getHorizontalPodAutoScalerUpdateResourceSummary(oldAutoScaler, newAutoScaler);
|
||||||
|
if (hpaUpdateSummary) {
|
||||||
|
resources.push(hpaUpdateSummary);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resources.push({ action: DELETE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: oldAutoScaler.Name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApplicationResourceType(app) {
|
||||||
|
if (app instanceof KubernetesDeployment || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DEPLOYMENT)) {
|
||||||
|
return KubernetesResourceTypes.DEPLOYMENT;
|
||||||
|
} else if (app instanceof KubernetesDaemonSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.DAEMONSET)) {
|
||||||
|
return KubernetesResourceTypes.DAEMONSET;
|
||||||
|
} else if (app instanceof KubernetesStatefulSet || (app instanceof KubernetesApplication && app.ApplicationType === KubernetesApplicationTypes.STATEFULSET)) {
|
||||||
|
return KubernetesResourceTypes.STATEFULSET;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIngressUpdateSummary(oldIngresses, newIngresses) {
|
||||||
|
const ingressesSummaries = newIngresses
|
||||||
|
.map((newIng) => {
|
||||||
|
const oldIng = _.find(oldIngresses, { Name: newIng.Name });
|
||||||
|
return getIngressUpdateResourceSummary(oldIng, newIng);
|
||||||
|
})
|
||||||
|
.filter((s) => s); // remove nulls
|
||||||
|
return ingressesSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getIngressUpdateResourceSummary replicates KubernetesIngressService.patch
|
||||||
|
function getIngressUpdateResourceSummary(oldIngress, newIngress) {
|
||||||
|
const payload = KubernetesIngressConverter.patchPayload(oldIngress, newIngress);
|
||||||
|
if (payload.length) {
|
||||||
|
return { action: UPDATE, kind: KubernetesResourceTypes.INGRESS, name: oldIngress.Name };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getVolumeClaimUpdateResourceSummary replicates KubernetesPersistentVolumeClaimService.patch
|
||||||
|
function getVolumeClaimUpdateResourceSummary(oldPVC, newPVC) {
|
||||||
|
const payload = KubernetesPersistentVolumeClaimConverter.patchPayload(oldPVC, newPVC);
|
||||||
|
if (payload.length) {
|
||||||
|
return { action: UPDATE, kind: KubernetesResourceTypes.PERSISTENT_VOLUME_CLAIM, name: oldPVC.Name };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceUpdateResourceSummary replicates KubernetesServiceService.patch
|
||||||
|
function getServiceUpdateResourceSummary(oldService, newService) {
|
||||||
|
const payload = KubernetesServiceConverter.patchPayload(oldService, newService);
|
||||||
|
if (payload.length) {
|
||||||
|
return { action: UPDATE, kind: KubernetesResourceTypes.SERVICE, name: oldService.Name, type: oldService.Type || KubernetesServiceTypes.CLUSTER_IP };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHorizontalPodAutoScalerUpdateResourceSummary replicates KubernetesHorizontalPodAutoScalerService.patch
|
||||||
|
function getHorizontalPodAutoScalerUpdateResourceSummary(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler) {
|
||||||
|
const payload = KubernetesHorizontalPodAutoScalerConverter.patchPayload(oldHorizontalPodAutoScaler, newHorizontalPodAutoScaler);
|
||||||
|
if (payload.length) {
|
||||||
|
return { action: UPDATE, kind: KubernetesResourceTypes.HORIZONTAL_POD_AUTOSCALER, name: oldHorizontalPodAutoScaler.Name };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { KubernetesResourceTypes, KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
|
||||||
|
import { KubernetesConfigurationTypes } from 'Kubernetes/models/configuration/models';
|
||||||
|
|
||||||
|
const { CREATE, UPDATE } = KubernetesResourceActions;
|
||||||
|
|
||||||
|
export default function (formValues) {
|
||||||
|
const action = formValues.Id ? UPDATE : CREATE;
|
||||||
|
if (formValues.Type === KubernetesConfigurationTypes.CONFIGMAP) {
|
||||||
|
return [{ action, kind: KubernetesResourceTypes.CONFIGMAP, name: formValues.Name }];
|
||||||
|
} else if (formValues.Type === KubernetesConfigurationTypes.SECRET) {
|
||||||
|
return [{ action, kind: KubernetesResourceTypes.SECRET, name: formValues.Name }];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import _ from 'lodash-es';
|
||||||
|
import * as JsonPatch from 'fast-json-patch';
|
||||||
|
import { KubernetesResourceActions } from 'Kubernetes/models/resource-types/models';
|
||||||
|
|
||||||
|
function findCreateResources(newResources, oldResources) {
|
||||||
|
return _.differenceBy(newResources, oldResources, 'Name');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findDeleteResources(newResources, oldResources) {
|
||||||
|
return _.differenceBy(oldResources, newResources, 'Name');
|
||||||
|
}
|
||||||
|
|
||||||
|
function findUpdateResources(newResources, oldResources) {
|
||||||
|
const updateResources = _.intersectionWith(newResources, oldResources, (newResource, oldResource) => {
|
||||||
|
// find out resources with same name but content changed
|
||||||
|
if (newResource.Name != oldResource.Name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !isEqual(newResource, oldResource);
|
||||||
|
});
|
||||||
|
|
||||||
|
return updateResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEqual(newResource, oldResource) {
|
||||||
|
let patches = JsonPatch.compare(newResource, oldResource);
|
||||||
|
patches = _.filter(patches, (change) => {
|
||||||
|
return !_.includes(change.path, '$$hashKey') && !_.includes(change.path, 'Duplicate');
|
||||||
|
});
|
||||||
|
|
||||||
|
return !patches.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doGetResourcesSummary(newResources, oldResources, kind, action, actionFilter) {
|
||||||
|
const filteredResources = actionFilter(newResources, oldResources);
|
||||||
|
const summary = filteredResources.map((resource) => ({ name: resource.Name, action, kind }));
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getResourcesSummary(newResources, oldResources, kind) {
|
||||||
|
if (!Array.isArray(newResources)) {
|
||||||
|
newResources = newResources ? [newResources] : [];
|
||||||
|
oldResources = oldResources ? [oldResources] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = [
|
||||||
|
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.CREATE, findCreateResources),
|
||||||
|
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.UPDATE, findUpdateResources),
|
||||||
|
...doGetResourcesSummary(newResources, oldResources, kind, KubernetesResourceActions.DELETE, findDeleteResources),
|
||||||
|
];
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import KubernetesResourcePoolConverter from 'Kubernetes/converters/resourcePool';
|
||||||
|
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
|
import { KubernetesResourceQuotaDefaults } from 'Kubernetes/models/resource-quota/models';
|
||||||
|
import { KubernetesResourceTypes } from 'Kubernetes/models/resource-types/models';
|
||||||
|
import { getResourcesSummary } from 'Kubernetes/views/summary/resources/helpers';
|
||||||
|
|
||||||
|
export default function (newFormValues, oldFormValues) {
|
||||||
|
const [newNamespace, newQuota, newIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(newFormValues);
|
||||||
|
|
||||||
|
if (!(oldFormValues instanceof KubernetesResourcePoolFormValues)) {
|
||||||
|
oldFormValues = new KubernetesResourcePoolFormValues(KubernetesResourceQuotaDefaults);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [oldNamespace, oldQuota, oldIngresses] = KubernetesResourcePoolConverter.formValuesToResourcePool(oldFormValues);
|
||||||
|
|
||||||
|
const resources = [
|
||||||
|
...getResourcesSummary(newNamespace, oldNamespace, KubernetesResourceTypes.NAMESPACE),
|
||||||
|
...getResourcesSummary(newQuota, oldQuota, KubernetesResourceTypes.RESOURCEQUOTA),
|
||||||
|
...getResourcesSummary(newIngresses, oldIngresses, KubernetesResourceTypes.INGRESS),
|
||||||
|
];
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
<div
|
||||||
|
class="col-xs-12 form-section-title interactive"
|
||||||
|
ng-click="$ctrl.toggleSummary()"
|
||||||
|
style="display: flex; justify-content: space-between;"
|
||||||
|
ng-if="$ctrl.state.resources.length > 0"
|
||||||
|
>
|
||||||
|
Summary
|
||||||
|
<span class="small space-left">
|
||||||
|
<a ng-if="!$ctrl.state.expandedTemplate"><i class="fa fa-angle-down" aria-hidden="true"></i> expand</a>
|
||||||
|
<a ng-if="$ctrl.state.expandedTemplate"><i class="fa fa-angle-up" aria-hidden="true"></i> collapse</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" ng-if="$ctrl.state.expandedTemplate">
|
||||||
|
<div class="col-sm-12 small text-muted">
|
||||||
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Portainer will execute the following Kubernetes actions.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 small text-muted" style="padding-top: 1em;" ng-if="$ctrl.state.resources.length > 0">
|
||||||
|
<ul>
|
||||||
|
<li ng-repeat="summary in $ctrl.state.resources" ng-if="summary.action && summary.kind && summary.name">
|
||||||
|
{{ summary.action }}
|
||||||
|
{{ $ctrl.getArticle(summary.kind, summary.action) }}
|
||||||
|
<span style="color: black; font-weight: 700;">{{ summary.kind }}</span> named <code>{{ summary.name }}</code>
|
||||||
|
<span ng-if="summary.type">
|
||||||
|
of type <code>{{ summary.type }}</code></span
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li ng-if="$ctrl.state.limits.memory">
|
||||||
|
Set the memory resources limits and requests to <code>{{ $ctrl.state.limits.memory }}M</code>
|
||||||
|
</li>
|
||||||
|
<li ng-if="$ctrl.state.limits.cpu">
|
||||||
|
Set the CPU resources limits and requests to <code>{{ $ctrl.state.limits.cpu }}</code>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,9 @@
|
||||||
|
angular.module('portainer.kubernetes').component('kubernetesSummaryView', {
|
||||||
|
templateUrl: './summary.html',
|
||||||
|
controller: 'KubernetesSummaryController',
|
||||||
|
controllerAs: '$ctrl',
|
||||||
|
bindings: {
|
||||||
|
formValues: '<',
|
||||||
|
oldFormValues: '<',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,89 @@
|
||||||
|
import angular from 'angular';
|
||||||
|
import { KubernetesConfigurationFormValues } from 'Kubernetes/models/configuration/formvalues';
|
||||||
|
import { KubernetesResourcePoolFormValues } from 'Kubernetes/models/resource-pool/formValues';
|
||||||
|
import { KubernetesApplicationFormValues } from 'Kubernetes/models/application/formValues';
|
||||||
|
import { KubernetesResourceActions, KubernetesResourceTypes } from 'Kubernetes/models/resource-types/models';
|
||||||
|
import getApplicationResources from './resources/applicationResources';
|
||||||
|
import getNamespaceResources from './resources/namespaceResources';
|
||||||
|
import getConfigurationResources from './resources/configurationResources';
|
||||||
|
|
||||||
|
class KubernetesSummaryController {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($scope, LocalStorage, KubernetesResourcePoolService) {
|
||||||
|
this.LocalStorage = LocalStorage;
|
||||||
|
this.KubernetesResourcePoolService = KubernetesResourcePoolService;
|
||||||
|
|
||||||
|
this.toggleSummary = this.toggleSummary.bind(this);
|
||||||
|
this.generateResourceSummaryList = this.generateResourceSummaryList.bind(this);
|
||||||
|
|
||||||
|
// Deep-watch changes on formValues property
|
||||||
|
$scope.$watch(
|
||||||
|
'$ctrl.formValues',
|
||||||
|
(formValues) => {
|
||||||
|
this.state.resources = this.generateResourceSummaryList(formValues);
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getArticle(resourceType, resourceAction) {
|
||||||
|
let article = 'a';
|
||||||
|
if (resourceAction === KubernetesResourceActions.CREATE) {
|
||||||
|
if (resourceType === KubernetesResourceTypes.INGRESS) {
|
||||||
|
article = 'an';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
article = 'the';
|
||||||
|
}
|
||||||
|
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toggleSummary toggles the summary panel state and persists it to browser localstorage
|
||||||
|
*/
|
||||||
|
toggleSummary() {
|
||||||
|
this.state.expandedTemplate = !this.state.expandedTemplate;
|
||||||
|
this.LocalStorage.storeKubernetesSummaryToggle(this.state.expandedTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* generateResourceSummaryList maps formValues to custom object
|
||||||
|
* @param {object} formValues
|
||||||
|
* @returns {object} => { action: "string", kind: "string", name: "string" }
|
||||||
|
*/
|
||||||
|
generateResourceSummaryList(formValues) {
|
||||||
|
const oldFormValues = this.oldFormValues;
|
||||||
|
|
||||||
|
if (formValues instanceof KubernetesConfigurationFormValues) {
|
||||||
|
// Configuration
|
||||||
|
return getConfigurationResources(formValues);
|
||||||
|
} else if (formValues instanceof KubernetesResourcePoolFormValues) {
|
||||||
|
// Namespaces
|
||||||
|
return getNamespaceResources(formValues, oldFormValues);
|
||||||
|
} else if (formValues instanceof KubernetesApplicationFormValues) {
|
||||||
|
// Applications
|
||||||
|
|
||||||
|
// extract cpu and memory requests & limits for pod
|
||||||
|
this.state.limits = { cpu: formValues.CpuLimit, memory: formValues.MemoryLimit };
|
||||||
|
|
||||||
|
return getApplicationResources(formValues, oldFormValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
const toggleValue = this.LocalStorage.getKubernetesSummaryToggle();
|
||||||
|
const expanded = typeof toggleValue === 'boolean' ? toggleValue : true;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
expandedTemplate: expanded,
|
||||||
|
resources: [],
|
||||||
|
limits: { cpu: null, memory: null },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KubernetesSummaryController;
|
||||||
|
angular.module('portainer.kubernetes').controller('KubernetesSummaryController', KubernetesSummaryController);
|
|
@ -144,6 +144,12 @@ angular.module('portainer.app').factory('LocalStorage', [
|
||||||
cleanEndpointData() {
|
cleanEndpointData() {
|
||||||
localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE');
|
localStorageService.remove('ENDPOINT_ID', 'ENDPOINT_PUBLIC_URL', 'ENDPOINT_OFFLINE_MODE', 'ENDPOINTS_DATA', 'ENDPOINT_STATE');
|
||||||
},
|
},
|
||||||
|
storeKubernetesSummaryToggle(value) {
|
||||||
|
localStorageService.set('kubernetes_summary_expanded', value);
|
||||||
|
},
|
||||||
|
getKubernetesSummaryToggle() {
|
||||||
|
return localStorageService.get('kubernetes_summary_expanded');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
Loading…
Reference in New Issue