1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-24 15:59:41 +02:00

fix(apps): update associated resources on deletion [r8s-124] (#75)

This commit is contained in:
Ali 2024-11-01 21:03:49 +13:00 committed by GitHub
parent d418784346
commit c1316532eb
15 changed files with 281 additions and 90 deletions

View file

@ -1,5 +1,8 @@
import { useQuery } from '@tanstack/react-query';
import { HorizontalPodAutoscalerList } from 'kubernetes-types/autoscaling/v1';
import {
HorizontalPodAutoscaler,
HorizontalPodAutoscalerList,
} from 'kubernetes-types/autoscaling/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
@ -27,23 +30,15 @@ export function useApplicationHorizontalPodAutoscaler(
if (!app) {
return null;
}
const horizontalPodAutoscalers =
await getNamespaceHorizontalPodAutoscalers(environmentId, namespace);
const matchingHorizontalPodAutoscaler =
horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
if (scaleTargetRef) {
const scaleTargetRefName = scaleTargetRef.name;
const scaleTargetRefKind = scaleTargetRef.kind;
// include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
return (
scaleTargetRefName === app.metadata?.name &&
scaleTargetRefKind === app.kind
);
}
return false;
}) || null;
getMatchingHorizontalPodAutoscaler(
horizontalPodAutoscalers,
namespace,
appName,
app.kind || ''
);
return matchingHorizontalPodAutoscaler;
},
{
@ -57,6 +52,29 @@ export function useApplicationHorizontalPodAutoscaler(
);
}
export function getMatchingHorizontalPodAutoscaler(
horizontalPodAutoscalers: HorizontalPodAutoscaler[],
appNamespace: string,
appName: string,
appKind: string
) {
const matchingHorizontalPodAutoscaler =
horizontalPodAutoscalers.find((horizontalPodAutoscaler) => {
if (horizontalPodAutoscaler.metadata?.namespace !== appNamespace) {
return false;
}
const scaleTargetRef = horizontalPodAutoscaler.spec?.scaleTargetRef;
if (scaleTargetRef) {
const scaleTargetRefName = scaleTargetRef.name;
const scaleTargetRefKind = scaleTargetRef.kind;
// include the horizontal pod autoscaler if the scale target ref name and kind match the application name and kind
return scaleTargetRefName === appName && scaleTargetRefKind === appKind;
}
return false;
}) || null;
return matchingHorizontalPodAutoscaler;
}
async function getNamespaceHorizontalPodAutoscalers(
environmentId: EnvironmentId,
namespace: string

View file

@ -1,4 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
@ -10,23 +11,34 @@ import { pluralize } from '@/portainer/helpers/strings';
import { parseKubernetesAxiosError } from '../../axiosError';
import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
import { Stack } from '../ListView/ApplicationsStacksDatatable/types';
import { deleteServices } from '../../services/service';
import { updateIngress } from '../../ingresses/service';
import { Ingress } from '../../ingresses/types';
import { queryKeys } from './query-keys';
export function useDeleteApplicationsMutation({
environmentId,
stacks,
ingresses,
reportStacks,
}: {
environmentId: EnvironmentId;
stacks: Stack[];
ingresses: Ingress[];
reportStacks?: boolean;
}) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (applications: ApplicationRowData[]) =>
deleteApplications(applications, stacks, environmentId),
onSuccess: ({ settledAppDeletions, settledStackDeletions }) => {
deleteApplications(applications, stacks, ingresses, environmentId),
// apps and stacks deletions results (success and error) are handled here
onSuccess: ({
settledAppDeletions,
settledStackDeletions,
settledIngressUpdates,
settledHpaDeletions,
}) => {
// one error notification per rejected item
settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
notifyError(
@ -37,6 +49,18 @@ export function useDeleteApplicationsMutation({
settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
notifyError(`Failed to remove stack '${item.Name}'`, new Error(reason));
});
settledIngressUpdates.rejectedItems.forEach(({ item, reason }) => {
notifyError(
`Failed to update ingress paths for '${item.Name}'`,
new Error(reason)
);
});
settledHpaDeletions.rejectedItems.forEach(({ item, reason }) => {
notifyError(
`Failed to remove horizontal pod autoscaler for '${item.metadata?.name}'`,
new Error(reason)
);
});
// one success notification for all fulfilled items
if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
@ -59,8 +83,13 @@ export function useDeleteApplicationsMutation({
.join(', ')
);
}
// dont notify successful ingress updates to avoid notification spam
queryClient.invalidateQueries(queryKeys.applications(environmentId));
},
// failed service deletions are handled here
onError: (error: unknown) => {
notifyError('Unable to remove applications', error as Error);
},
...withGlobalError('Unable to remove applications'),
});
}
@ -68,6 +97,7 @@ export function useDeleteApplicationsMutation({
async function deleteApplications(
applications: ApplicationRowData[],
stacks: Stack[],
ingresses: Ingress[],
environmentId: EnvironmentId
) {
const settledAppDeletions = await getAllSettledItems(
@ -84,7 +114,83 @@ async function deleteApplications(
(stack) => deleteStack(stack, environmentId)
);
return { settledAppDeletions, settledStackDeletions };
// update associated k8s ressources
const servicesToDelete = getServicesFromApplications(applications);
// axios error handling is done inside deleteServices already
await deleteServices({
environmentId,
data: servicesToDelete,
});
const hpasToDelete = applications
.map((app) => app.HorizontalPodAutoscaler)
.filter((hpa) => !!hpa);
const settledHpaDeletions = await getAllSettledItems(hpasToDelete, (hpa) =>
deleteHorizontalPodAutoscaler(hpa, environmentId)
);
const updatedIngresses = getUpdatedIngressesWithRemovedPaths(
ingresses,
servicesToDelete
);
const settledIngressUpdates = await getAllSettledItems(
updatedIngresses,
(ingress) => updateIngress(environmentId, ingress)
);
return {
settledAppDeletions,
settledStackDeletions,
settledIngressUpdates,
settledHpaDeletions,
};
}
function getServicesFromApplications(
applications: ApplicationRowData[]
): Record<string, string[]> {
return applications.reduce<Record<string, string[]>>(
(namespaceServicesMap, application) => {
const serviceNames =
application.Services?.map((service) => service.metadata?.name).filter(
(name): name is string => !!name
) || [];
if (namespaceServicesMap[application.ResourcePool]) {
return {
...namespaceServicesMap,
[application.ResourcePool]: [
...namespaceServicesMap[application.ResourcePool],
...serviceNames,
],
};
}
return {
...namespaceServicesMap,
[application.ResourcePool]: serviceNames,
};
},
{}
);
}
// return a list of ingresses, which need updated because their paths should be removed for deleted services.
function getUpdatedIngressesWithRemovedPaths(
ingresses: Ingress[],
servicesToDelete: Record<string, string[]>
) {
return ingresses.reduce<Ingress[]>((updatedIngresses, ingress) => {
if (!ingress.Paths) {
return updatedIngresses;
}
const servicesInNamespace = servicesToDelete[ingress.Namespace] || [];
// filter out the paths for services that are in the list of services to delete
const updatedIngressPaths =
ingress.Paths.filter(
(path) => !servicesInNamespace.includes(path.ServiceName)
) ?? [];
if (updatedIngressPaths.length !== ingress.Paths?.length) {
return [...updatedIngresses, { ...ingress, Paths: updatedIngressPaths }];
}
return updatedIngresses;
}, []);
}
async function deleteStack(stack: Stack, environmentId: EnvironmentId) {
@ -173,6 +279,22 @@ async function uninstallHelmApplication(
}
}
async function deleteHorizontalPodAutoscaler(
hpa: HorizontalPodAutoscaler,
environmentId: EnvironmentId
) {
try {
await axios.delete(
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v2/namespaces/${hpa.metadata?.namespace}/horizontalpodautoscalers/${hpa.metadata?.name}`
);
} catch (error) {
throw parseKubernetesAxiosError(
error,
'Unable to remove horizontal pod autoscaler'
);
}
}
// mutate the stacks array to remove the application
function removeApplicationFromStack(
application: ApplicationRowData,