diff --git a/app/kubernetes/models/resource-pool/formValues.js b/app/kubernetes/models/resource-pool/formValues.js index bb3f598c0..55efbfbba 100644 --- a/app/kubernetes/models/resource-pool/formValues.js +++ b/app/kubernetes/models/resource-pool/formValues.js @@ -1,11 +1,9 @@ export function KubernetesResourcePoolFormValues(defaults) { - return { - Name: '', - MemoryLimit: defaults.MemoryLimit, - CpuLimit: defaults.CpuLimit, - HasQuota: false, - IngressClasses: [], // KubernetesResourcePoolIngressClassFormValue - }; + this.Name = ''; + this.MemoryLimit = defaults.MemoryLimit; + this.CpuLimit = defaults.CpuLimit; + this.HasQuota = false; + this.IngressClasses = []; // KubernetesResourcePoolIngressClassFormValue } /** diff --git a/app/kubernetes/models/resource-types/models.js b/app/kubernetes/models/resource-types/models.js new file mode 100644 index 000000000..4e739642e --- /dev/null +++ b/app/kubernetes/models/resource-types/models.js @@ -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', +}); diff --git a/app/kubernetes/services/applicationService.js b/app/kubernetes/services/applicationService.js index 0452d829d..0f13b5dd2 100644 --- a/app/kubernetes/services/applicationService.js +++ b/app/kubernetes/services/applicationService.js @@ -220,6 +220,11 @@ class KubernetesApplicationService { // resource creation flow // should we keep formValues > 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) { try { let [app, headlessService, service, claims] = KubernetesApplicationConverter.applicationFormValuesToApplication(formValues); @@ -266,6 +271,11 @@ class KubernetesApplicationService { /* #region PATCH */ // 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) { try { const [oldApp, oldHeadlessService, oldService, oldClaims] = KubernetesApplicationConverter.applicationFormValuesToApplication(oldFormValues); diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index 1e0b85111..58537b1e4 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1543,6 +1543,13 @@ + + +
Actions
diff --git a/app/kubernetes/views/configurations/create/createConfiguration.html b/app/kubernetes/views/configurations/create/createConfiguration.html index d598ebc52..314c50af9 100644 --- a/app/kubernetes/views/configurations/create/createConfiguration.html +++ b/app/kubernetes/views/configurations/create/createConfiguration.html @@ -123,6 +123,12 @@ is-editor-dirty="ctrl.state.isEditorDirty" > + + +
Actions diff --git a/app/kubernetes/views/configurations/edit/configuration.html b/app/kubernetes/views/configurations/edit/configuration.html index d278a663f..09b07e13e 100644 --- a/app/kubernetes/views/configurations/edit/configuration.html +++ b/app/kubernetes/views/configurations/edit/configuration.html @@ -85,6 +85,12 @@ is-editor-dirty="ctrl.state.isEditorDirty" > + + +
Actions diff --git a/app/kubernetes/views/resource-pools/create/createResourcePool.html b/app/kubernetes/views/resource-pools/create/createResourcePool.html index 19075ff1e..1b2bb4555 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePool.html +++ b/app/kubernetes/views/resource-pools/create/createResourcePool.html @@ -338,6 +338,10 @@
+ + + +
Actions
diff --git a/app/kubernetes/views/resource-pools/edit/resourcePool.html b/app/kubernetes/views/resource-pools/edit/resourcePool.html index 1dc64330f..78c180980 100644 --- a/app/kubernetes/views/resource-pools/edit/resourcePool.html +++ b/app/kubernetes/views/resource-pools/edit/resourcePool.html @@ -329,6 +329,14 @@
+ + + +
Actions diff --git a/app/kubernetes/views/summary/resources/applicationResources.js b/app/kubernetes/views/summary/resources/applicationResources.js new file mode 100644 index 000000000..4ea6cbf9b --- /dev/null +++ b/app/kubernetes/views/summary/resources/applicationResources.js @@ -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; +} diff --git a/app/kubernetes/views/summary/resources/configurationResources.js b/app/kubernetes/views/summary/resources/configurationResources.js new file mode 100644 index 000000000..a2e04e1b0 --- /dev/null +++ b/app/kubernetes/views/summary/resources/configurationResources.js @@ -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 }]; + } +} diff --git a/app/kubernetes/views/summary/resources/helpers.js b/app/kubernetes/views/summary/resources/helpers.js new file mode 100644 index 000000000..53d32cb8b --- /dev/null +++ b/app/kubernetes/views/summary/resources/helpers.js @@ -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; +} diff --git a/app/kubernetes/views/summary/resources/namespaceResources.js b/app/kubernetes/views/summary/resources/namespaceResources.js new file mode 100644 index 000000000..1b9ec1bd2 --- /dev/null +++ b/app/kubernetes/views/summary/resources/namespaceResources.js @@ -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; +} diff --git a/app/kubernetes/views/summary/summary.html b/app/kubernetes/views/summary/summary.html new file mode 100644 index 000000000..8ab3e4f16 --- /dev/null +++ b/app/kubernetes/views/summary/summary.html @@ -0,0 +1,38 @@ +
+ Summary + + expand + collapse + +
+ +
+
+ + Portainer will execute the following Kubernetes actions. +
+ +
+
    +
  • + {{ summary.action }} + {{ $ctrl.getArticle(summary.kind, summary.action) }} + {{ summary.kind }} named {{ summary.name }} + + of type {{ summary.type }} +
  • +
  • + Set the memory resources limits and requests to {{ $ctrl.state.limits.memory }}M +
  • +
  • + Set the CPU resources limits and requests to {{ $ctrl.state.limits.cpu }} +
  • +
+
+
diff --git a/app/kubernetes/views/summary/summary.js b/app/kubernetes/views/summary/summary.js new file mode 100644 index 000000000..841a9f46a --- /dev/null +++ b/app/kubernetes/views/summary/summary.js @@ -0,0 +1,9 @@ +angular.module('portainer.kubernetes').component('kubernetesSummaryView', { + templateUrl: './summary.html', + controller: 'KubernetesSummaryController', + controllerAs: '$ctrl', + bindings: { + formValues: '<', + oldFormValues: '<', + }, +}); diff --git a/app/kubernetes/views/summary/summaryController.js b/app/kubernetes/views/summary/summaryController.js new file mode 100644 index 000000000..0579569e3 --- /dev/null +++ b/app/kubernetes/views/summary/summaryController.js @@ -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); diff --git a/app/portainer/services/localStorage.js b/app/portainer/services/localStorage.js index 5026331bc..558201942 100644 --- a/app/portainer/services/localStorage.js +++ b/app/portainer/services/localStorage.js @@ -144,6 +144,12 @@ angular.module('portainer.app').factory('LocalStorage', [ cleanEndpointData() { 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'); + }, }; }, ]);