import { Ingress } from '@/react/kubernetes/ingresses/types'; import { ServiceFormValues } from '../../CreateView/application-services/types'; import { ApplicationFormValues } from '../../types'; import { generateNewIngressesFromFormPaths, getServicePatchPayload, } from '../../CreateView/application-services/utils'; import { KubernetesResourceType, KubernetesResourceAction, Summary, } from './types'; export function getArticle( resourceType: KubernetesResourceType, resourceAction: KubernetesResourceAction ) { if (resourceAction === 'Delete' || resourceAction === 'Update') { return 'the'; } if (resourceAction === 'Create' && resourceType === 'Ingress') { return 'an'; } return 'a'; } /** * generateResourceSummaryList maps formValues to create and update summaries */ export function getAppResourceSummaries( newFormValues: ApplicationFormValues, oldFormValues?: ApplicationFormValues ): Array { if (!oldFormValues) { return getCreatedApplicationResourcesNew(newFormValues); } return getUpdatedApplicationResources(newFormValues, oldFormValues); } function getCreatedApplicationResourcesNew( formValues: ApplicationFormValues ): Array { // app summary const appSummary: Summary = { action: 'Create', kind: formValues.ApplicationType, name: formValues.Name, }; // service summaries const serviceFormSummaries: Array = formValues.Services?.map((service) => ({ action: 'Create', kind: 'Service', name: service.Name || '', type: service.Type, })) || []; // statefulsets require a headless service (https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#limitations) // create a headless service summary if the application is a statefulset const headlessSummary: Array = formValues.ApplicationType === 'StatefulSet' ? [ { action: 'Create', kind: 'Service', name: `headless-${formValues.Name}`, type: 'ClusterIP', }, ] : []; const serviceSummaries = [...serviceFormSummaries, ...headlessSummary]; // ingress summaries const ingressesSummaries: Array = formValues.Services?.flatMap((service) => { // a single service port can have multiple ingress paths (and even use different ingresses) const servicePathsIngressNames = service.Ports.flatMap( (port) => port.ingressPaths?.map((path) => path.IngressName) || [] ); const uniqueIngressNames = [...new Set(servicePathsIngressNames)]; return uniqueIngressNames.map((ingressName) => ({ action: 'Update', kind: 'Ingress', name: ingressName || '', })); }) || []; // persistent volume claim (pvc) summaries const pvcSummaries: Array = // apps with a isolated data access policy are statefulsets. // statefulset pvcs are defined in spec.volumeClaimTemplates. // https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-storage formValues.DataAccessPolicy === 'Shared' ? formValues.PersistedFolders?.map((volume) => ({ action: 'Create', kind: 'PersistentVolumeClaim', name: volume.existingVolume?.PersistentVolumeClaim.Name || volume.persistentVolumeClaimName || '', })) || [] : []; // horizontal pod autoscaler summaries const hpaSummary: Array = formValues.AutoScaler?.isUsed === true && formValues.DeploymentType !== 'Global' ? [ { action: 'Create', kind: 'HorizontalPodAutoscaler', name: formValues.Name, }, ] : []; return [ appSummary, ...serviceSummaries, ...ingressesSummaries, ...pvcSummaries, ...hpaSummary, ]; } function getUpdatedApplicationResources( newFormValues: ApplicationFormValues, oldFormValues: ApplicationFormValues ) { // app summaries const updateAppSummaries: Array = oldFormValues.ApplicationType !== newFormValues.ApplicationType ? [ { action: 'Delete', kind: oldFormValues.ApplicationType, name: oldFormValues.Name, }, { action: 'Create', kind: newFormValues.ApplicationType, name: newFormValues.Name, }, ] : [ { action: 'Update', kind: newFormValues.ApplicationType, name: newFormValues.Name, }, ]; // service summaries const serviceSummaries: Array = getServiceUpdateResourceSummary( oldFormValues.Services, newFormValues.Services ); // ingress summaries const oldServicePorts = oldFormValues.Services?.flatMap( (service) => service.Ports ); const oldIngresses = generateNewIngressesFromFormPaths( oldFormValues.OriginalIngresses, oldServicePorts, oldServicePorts ); const newServicePorts = newFormValues.Services?.flatMap( (service) => service.Ports ); const newIngresses = generateNewIngressesFromFormPaths( newFormValues.OriginalIngresses, newServicePorts, oldServicePorts ); const ingressSummaries = getIngressUpdateSummary(oldIngresses, newIngresses); // persistent volume claim (pvc) summaries const pvcSummaries: Array = // apps with a isolated data access policy are statefulsets. // statefulset pvcs are defined in spec.volumeClaimTemplates. // https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#stable-storage newFormValues.DataAccessPolicy === 'Shared' ? newFormValues.PersistedFolders?.flatMap((newVolume) => { const oldVolume = oldFormValues.PersistedFolders?.find( (oldVolume) => oldVolume.persistentVolumeClaimName === newVolume.persistentVolumeClaimName ); if (!oldVolume) { return [ { action: 'Create', kind: 'PersistentVolumeClaim', name: newVolume.existingVolume?.PersistentVolumeClaim.Name || newVolume.persistentVolumeClaimName || '', }, ]; } // updating a pvc is not supported return []; }) || [] : []; // TODO: horizontal pod autoscaler summaries const createHPASummary: Array = newFormValues.AutoScaler?.isUsed && !oldFormValues.AutoScaler?.isUsed ? [ { action: 'Create', kind: 'HorizontalPodAutoscaler', name: newFormValues.Name, }, ] : []; const deleteHPASummary: Array = !newFormValues.AutoScaler?.isUsed && oldFormValues.AutoScaler?.isUsed ? [ { action: 'Delete', kind: 'HorizontalPodAutoscaler', name: oldFormValues.Name, }, ] : []; const isHPAUpdated = newFormValues.AutoScaler?.isUsed && oldFormValues.AutoScaler?.isUsed && (newFormValues.AutoScaler?.minReplicas !== oldFormValues.AutoScaler?.minReplicas || newFormValues.AutoScaler?.maxReplicas !== oldFormValues.AutoScaler?.maxReplicas || newFormValues.AutoScaler?.targetCpuUtilizationPercentage !== oldFormValues.AutoScaler?.targetCpuUtilizationPercentage); const updateHPASummary: Array = isHPAUpdated ? [ { action: 'Update', kind: 'HorizontalPodAutoscaler', name: newFormValues.Name, }, ] : []; const hpaSummaries = [ ...createHPASummary, ...deleteHPASummary, ...updateHPASummary, ]; return [ ...updateAppSummaries, ...serviceSummaries, ...ingressSummaries, ...pvcSummaries, ...hpaSummaries, ]; } // getServiceUpdateResourceSummary replicates KubernetesServiceService.patch function getServiceUpdateResourceSummary( oldServices?: Array, newServices?: Array ): Array { const updateAndCreateSummaries = newServices?.flatMap((newService) => { const oldServiceMatched = oldServices?.find( (oldService) => oldService.Name === newService.Name ); if (oldServiceMatched) { return getServiceUpdateSummary(oldServiceMatched, newService); } return [ { action: 'Create', kind: 'Service', name: newService.Name || '', type: newService.Type || 'ClusterIP', }, ]; }) || []; const deleteSummaries = oldServices?.flatMap((oldService) => { const newServiceMatched = newServices?.find( (newService) => newService.Name === oldService.Name ); if (newServiceMatched) { return []; } return [ { action: 'Delete', kind: 'Service', name: oldService.Name || '', type: oldService.Type || 'ClusterIP', }, ]; }) || []; return [...updateAndCreateSummaries, ...deleteSummaries]; } function getServiceUpdateSummary( oldService: ServiceFormValues, newService: ServiceFormValues ): Array { const payload = getServicePatchPayload(oldService, newService); if (payload.length) { return [ { action: 'Update', kind: 'Service', name: oldService.Name || '', type: oldService.Type || 'ClusterIP', }, ]; } return []; } export function getIngressUpdateSummary( oldIngresses: Array, newIngresses: Array ): Array { const ingressesSummaries = newIngresses.flatMap((newIng) => { const oldIng = oldIngresses.find((oldIng) => oldIng.Name === newIng.Name); if (oldIng) { return getIngressUpdateResourceSummary(oldIng, newIng); } return []; }); return ingressesSummaries; } // getIngressUpdateResourceSummary checks if any ingress paths have been changed function getIngressUpdateResourceSummary( oldIngress: Ingress, newIngress: Ingress ): Array { const newIngressPaths = newIngress.Paths?.flatMap((path) => path.Path) || []; const oldIngressPaths = oldIngress.Paths?.flatMap((path) => path.Path) || []; const isAnyNewPathMissingOldPath = newIngressPaths.some( (path) => !oldIngressPaths.includes(path) ); const isAnyOldPathMissingNewPath = oldIngressPaths.some( (path) => !newIngressPaths.includes(path) ); if (isAnyNewPathMissingOldPath || isAnyOldPathMissingNewPath) { return [ { action: 'Update', kind: 'Ingress', name: oldIngress.Name, }, ]; } return []; }