1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-05 05:45:22 +02:00

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>
This commit is contained in:
zees-dev 2021-06-10 10:38:23 +12:00 committed by GitHub
parent 267968e099
commit eae2f5c9fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 539 additions and 7 deletions

View file

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

View file

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

View file

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

View file

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