2024-10-24 23:28:05 +00:00
|
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
2024-11-01 08:03:49 +00:00
|
|
|
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v2';
|
2024-10-24 23:28:05 +00:00
|
|
|
|
|
|
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
|
|
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|
|
|
import { getAllSettledItems } from '@/portainer/helpers/promise-utils';
|
|
|
|
import { withGlobalError } from '@/react-tools/react-query';
|
|
|
|
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
|
|
|
import { pluralize } from '@/portainer/helpers/strings';
|
|
|
|
|
|
|
|
import { parseKubernetesAxiosError } from '../../axiosError';
|
|
|
|
import { ApplicationRowData } from '../ListView/ApplicationsDatatable/types';
|
|
|
|
import { Stack } from '../ListView/ApplicationsStacksDatatable/types';
|
2024-11-01 08:03:49 +00:00
|
|
|
import { deleteServices } from '../../services/service';
|
|
|
|
import { updateIngress } from '../../ingresses/service';
|
|
|
|
import { Ingress } from '../../ingresses/types';
|
2024-10-24 23:28:05 +00:00
|
|
|
|
|
|
|
import { queryKeys } from './query-keys';
|
|
|
|
|
|
|
|
export function useDeleteApplicationsMutation({
|
|
|
|
environmentId,
|
|
|
|
stacks,
|
2024-11-01 08:03:49 +00:00
|
|
|
ingresses,
|
2024-10-24 23:28:05 +00:00
|
|
|
reportStacks,
|
|
|
|
}: {
|
|
|
|
environmentId: EnvironmentId;
|
|
|
|
stacks: Stack[];
|
2024-11-01 08:03:49 +00:00
|
|
|
ingresses: Ingress[];
|
2024-10-24 23:28:05 +00:00
|
|
|
reportStacks?: boolean;
|
|
|
|
}) {
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
|
|
mutationFn: (applications: ApplicationRowData[]) =>
|
2024-11-01 08:03:49 +00:00
|
|
|
deleteApplications(applications, stacks, ingresses, environmentId),
|
|
|
|
// apps and stacks deletions results (success and error) are handled here
|
|
|
|
onSuccess: ({
|
|
|
|
settledAppDeletions,
|
|
|
|
settledStackDeletions,
|
|
|
|
settledIngressUpdates,
|
|
|
|
settledHpaDeletions,
|
|
|
|
}) => {
|
2024-10-24 23:28:05 +00:00
|
|
|
// one error notification per rejected item
|
|
|
|
settledAppDeletions.rejectedItems.forEach(({ item, reason }) => {
|
|
|
|
notifyError(
|
|
|
|
`Failed to remove application '${item.Name}'`,
|
|
|
|
new Error(reason)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
settledStackDeletions.rejectedItems.forEach(({ item, reason }) => {
|
|
|
|
notifyError(`Failed to remove stack '${item.Name}'`, new Error(reason));
|
|
|
|
});
|
2024-11-01 08:03:49 +00:00
|
|
|
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)
|
|
|
|
);
|
|
|
|
});
|
2024-10-24 23:28:05 +00:00
|
|
|
|
|
|
|
// one success notification for all fulfilled items
|
|
|
|
if (settledAppDeletions.fulfilledItems.length && !reportStacks) {
|
|
|
|
notifySuccess(
|
|
|
|
`${pluralize(
|
|
|
|
settledAppDeletions.fulfilledItems.length,
|
|
|
|
'Application'
|
|
|
|
)} successfully removed`,
|
|
|
|
settledAppDeletions.fulfilledItems.map((item) => item.Name).join(', ')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
if (settledStackDeletions.fulfilledItems.length && reportStacks) {
|
|
|
|
notifySuccess(
|
|
|
|
`${pluralize(
|
|
|
|
settledStackDeletions.fulfilledItems.length,
|
|
|
|
'Stack'
|
|
|
|
)} successfully removed`,
|
|
|
|
settledStackDeletions.fulfilledItems
|
|
|
|
.map((item) => item.Name)
|
|
|
|
.join(', ')
|
|
|
|
);
|
|
|
|
}
|
2024-11-01 08:03:49 +00:00
|
|
|
// dont notify successful ingress updates to avoid notification spam
|
2024-10-24 23:28:05 +00:00
|
|
|
queryClient.invalidateQueries(queryKeys.applications(environmentId));
|
|
|
|
},
|
2024-11-01 08:03:49 +00:00
|
|
|
// failed service deletions are handled here
|
|
|
|
onError: (error: unknown) => {
|
|
|
|
notifyError('Unable to remove applications', error as Error);
|
|
|
|
},
|
2024-10-24 23:28:05 +00:00
|
|
|
...withGlobalError('Unable to remove applications'),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteApplications(
|
|
|
|
applications: ApplicationRowData[],
|
|
|
|
stacks: Stack[],
|
2024-11-01 08:03:49 +00:00
|
|
|
ingresses: Ingress[],
|
2024-10-24 23:28:05 +00:00
|
|
|
environmentId: EnvironmentId
|
|
|
|
) {
|
|
|
|
const settledAppDeletions = await getAllSettledItems(
|
|
|
|
applications,
|
|
|
|
(application) => deleteApplication(application, stacks, environmentId)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Delete stacks that have no applications left (stacks object has been mutated by deleteApplication)
|
|
|
|
const stacksToDelete = stacks.filter(
|
|
|
|
(stack) => stack.Applications.length === 0
|
|
|
|
);
|
|
|
|
const settledStackDeletions = await getAllSettledItems(
|
|
|
|
stacksToDelete,
|
|
|
|
(stack) => deleteStack(stack, environmentId)
|
|
|
|
);
|
|
|
|
|
2024-11-01 08:03:49 +00:00
|
|
|
// 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(
|
2024-11-10 19:26:56 +00:00
|
|
|
(name) => name !== undefined
|
2024-11-01 08:03:49 +00:00
|
|
|
) || [];
|
2024-11-10 19:26:56 +00:00
|
|
|
const existingServices =
|
|
|
|
namespaceServicesMap[application.ResourcePool] || [];
|
|
|
|
const uniqueServicesForNamespace = Array.from(
|
|
|
|
new Set([...existingServices, ...serviceNames])
|
|
|
|
);
|
2024-11-01 08:03:49 +00:00
|
|
|
return {
|
|
|
|
...namespaceServicesMap,
|
2024-11-10 19:26:56 +00:00
|
|
|
[application.ResourcePool]: uniqueServicesForNamespace,
|
2024-11-01 08:03:49 +00:00
|
|
|
};
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}, []);
|
2024-10-24 23:28:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteStack(stack: Stack, environmentId: EnvironmentId) {
|
|
|
|
try {
|
|
|
|
await axios.delete(`/stacks/name/${stack.Name}`, {
|
|
|
|
params: {
|
|
|
|
external: false,
|
|
|
|
name: stack.Name,
|
|
|
|
endpointId: environmentId,
|
|
|
|
namespace: stack.ResourcePool,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
} catch (error) {
|
|
|
|
throw parseAxiosError(error, 'Unable to remove stack');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteApplication(
|
|
|
|
application: ApplicationRowData,
|
|
|
|
stacks: Stack[],
|
|
|
|
environmentId: EnvironmentId
|
|
|
|
) {
|
|
|
|
switch (application.ApplicationType) {
|
|
|
|
case 'Deployment':
|
|
|
|
case 'DaemonSet':
|
|
|
|
case 'StatefulSet':
|
|
|
|
await deleteKubernetesApplication(application, stacks, environmentId);
|
|
|
|
break;
|
|
|
|
case 'Pod':
|
|
|
|
await deletePodApplication(application, stacks, environmentId);
|
|
|
|
break;
|
|
|
|
case 'Helm':
|
|
|
|
await uninstallHelmApplication(application, environmentId);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
throw new Error(
|
|
|
|
`Unknown application type: ${application.ApplicationType}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function deleteKubernetesApplication(
|
|
|
|
application: ApplicationRowData,
|
|
|
|
stacks: Stack[],
|
|
|
|
environmentId: EnvironmentId
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
await axios.delete(
|
|
|
|
`/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${
|
|
|
|
application.ResourcePool
|
|
|
|
}/${application.ApplicationType.toLowerCase()}s/${application.Name}`
|
|
|
|
);
|
|
|
|
removeApplicationFromStack(application, stacks);
|
|
|
|
} catch (error) {
|
|
|
|
throw parseKubernetesAxiosError(error, 'Unable to remove application');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function deletePodApplication(
|
|
|
|
application: ApplicationRowData,
|
|
|
|
stacks: Stack[],
|
|
|
|
environmentId: EnvironmentId
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
await axios.delete(
|
|
|
|
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${application.ResourcePool}/pods/${application.Name}`
|
|
|
|
);
|
|
|
|
removeApplicationFromStack(application, stacks);
|
|
|
|
} catch (error) {
|
|
|
|
throw parseKubernetesAxiosError(error, 'Unable to remove application');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function uninstallHelmApplication(
|
|
|
|
application: ApplicationRowData,
|
|
|
|
environmentId: EnvironmentId
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
await axios.delete(
|
|
|
|
`/endpoints/${environmentId}/kubernetes/helm/${application.Name}`,
|
|
|
|
{ params: { namespace: application.ResourcePool } }
|
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
// parseAxiosError, because it's a regular portainer api error
|
|
|
|
throw parseAxiosError(error, 'Unable to remove application');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-01 08:03:49 +00:00
|
|
|
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'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-24 23:28:05 +00:00
|
|
|
// mutate the stacks array to remove the application
|
|
|
|
function removeApplicationFromStack(
|
|
|
|
application: ApplicationRowData,
|
|
|
|
stacks: Stack[]
|
|
|
|
) {
|
|
|
|
const stack = stacks.find(
|
|
|
|
(stack) =>
|
|
|
|
stack.Name === application.StackName &&
|
|
|
|
stack.ResourcePool === application.ResourcePool
|
|
|
|
);
|
|
|
|
if (stack) {
|
|
|
|
stack.Applications = stack.Applications.filter(
|
|
|
|
(app) => app.Name !== application.Name
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|