portainer/app/react/kubernetes/applications/application.queries.ts

380 lines
11 KiB
TypeScript

import { UseQueryResult, useMutation, useQuery } from 'react-query';
import { Pod } from 'kubernetes-types/core/v1';
import { queryClient, withError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { getNamespaceServices } from '../services/service';
import {
getApplicationsForCluster,
getApplication,
patchApplication,
getApplicationRevisionList,
} from './application.service';
import type { AppKind, Application, ApplicationPatch } from './types';
import { deletePod, getNamespacePods } from './pod.service';
import { getNamespaceHorizontalPodAutoscalers } from './autoscaling.service';
import { applicationIsKind, matchLabelsToLabelSelectorValue } from './utils';
const queryKeys = {
applicationsForCluster: (environmentId: EnvironmentId) => [
'environments',
environmentId,
'kubernetes',
'applications',
],
application: (
environmentId: EnvironmentId,
namespace: string,
name: string,
yaml?: boolean
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
yaml,
],
applicationRevisions: (
environmentId: EnvironmentId,
namespace: string,
name: string,
labelSelector?: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'revisions',
labelSelector,
],
applicationServices: (
environmentId: EnvironmentId,
namespace: string,
name: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'services',
],
ingressesForApplication: (
environmentId: EnvironmentId,
namespace: string,
name: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'ingresses',
],
applicationHorizontalPodAutoscalers: (
environmentId: EnvironmentId,
namespace: string,
name: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'horizontalpodautoscalers',
],
applicationPods: (
environmentId: EnvironmentId,
namespace: string,
name: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'pods',
],
};
// useQuery to get a list of all applications from an array of namespaces
export function useApplicationsQuery(
environemtId: EnvironmentId,
namespaces?: string[]
) {
return useQuery(
queryKeys.applicationsForCluster(environemtId),
() => getApplicationsForCluster(environemtId, namespaces),
{
...withError('Unable to retrieve applications'),
enabled: !!namespaces?.length,
}
);
}
// when yaml is set to true, the expected return type is a string
export function useApplication<T extends Application | string = Application>(
environmentId: EnvironmentId,
namespace: string,
name: string,
appKind?: AppKind,
options?: { autoRefreshRate?: number; yaml?: boolean }
): UseQueryResult<T> {
return useQuery(
queryKeys.application(environmentId, namespace, name, options?.yaml),
() =>
getApplication<T>(environmentId, namespace, name, appKind, options?.yaml),
{
...withError('Unable to retrieve application'),
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
}
);
}
// test if I can get the previous revision
// useQuery to get an application's previous revision by environmentId, namespace, appKind and labelSelector
export function useApplicationRevisionList(
environmentId: EnvironmentId,
namespace: string,
name: string,
deploymentUid?: string,
labelSelector?: string,
appKind?: AppKind
) {
return useQuery(
queryKeys.applicationRevisions(
environmentId,
namespace,
name,
labelSelector
),
() =>
getApplicationRevisionList(
environmentId,
namespace,
deploymentUid,
appKind,
labelSelector
),
{
...withError('Unable to retrieve application revisions'),
enabled: !!labelSelector && !!appKind && !!deploymentUid,
}
);
}
// useApplicationServices returns a query for services that are related to the application (this doesn't include ingresses)
// Filtering the services by the application selector labels is done in the front end because:
// - The label selector query param in the kubernetes API filters by metadata.labels, but we need to filter the services by spec.selector
// - The field selector query param in the kubernetes API can filter the services by spec.selector, but it doesn't support chaining with 'OR',
// so we can't filter by services with at least one matching label. See: https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#chained-selectors
export function useApplicationServices(
environmentId: EnvironmentId,
namespace: string,
appName: string,
app?: Application
) {
return useQuery(
queryKeys.applicationServices(environmentId, namespace, appName),
async () => {
if (!app) {
return [];
}
// get the selector labels for the application
const appSelectorLabels = applicationIsKind<Pod>('Pod', app)
? app.metadata?.labels
: app.spec?.template?.metadata?.labels;
// get all services in the namespace and filter them by the application selector labels
const services = await getNamespaceServices(environmentId, namespace);
const filteredServices = services.filter((service) => {
if (service.spec?.selector && appSelectorLabels) {
const serviceSelectorLabels = service.spec.selector;
// include the service if the service selector label matches at least one application selector label
return Object.keys(appSelectorLabels).some(
(key) =>
serviceSelectorLabels[key] &&
serviceSelectorLabels[key] === appSelectorLabels[key]
);
}
return false;
});
return filteredServices;
},
{ ...withError(`Unable to get services for ${appName}`), enabled: !!app }
);
}
// useApplicationHorizontalPodAutoscalers returns a query for horizontal pod autoscalers that are related to the application
export function useApplicationHorizontalPodAutoscaler(
environmentId: EnvironmentId,
namespace: string,
appName: string,
app?: Application
) {
return useQuery(
queryKeys.applicationHorizontalPodAutoscalers(
environmentId,
namespace,
appName
),
async () => {
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;
return matchingHorizontalPodAutoscaler;
},
{
...withError(
`Unable to get horizontal pod autoscaler${
app ? ` for ${app.metadata?.name}` : ''
}`
),
enabled: !!app,
}
);
}
// useApplicationPods returns a query for pods that are related to the application by the application selector labels
export function useApplicationPods(
environmentId: EnvironmentId,
namespace: string,
appName: string,
app?: Application,
options?: { autoRefreshRate?: number }
) {
return useQuery(
queryKeys.applicationPods(environmentId, namespace, appName),
async () => {
if (applicationIsKind<Pod>('Pod', app)) {
return [app];
}
const appSelector = app?.spec?.selector;
const labelSelector = matchLabelsToLabelSelectorValue(
appSelector?.matchLabels
);
// get all pods in the namespace using the application selector as the label selector query param
const pods = await getNamespacePods(
environmentId,
namespace,
labelSelector
);
return pods;
},
{
...withError(`Unable to get pods for ${appName}`),
enabled: !!app,
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
}
);
}
// useQuery to patch an application by environmentId, namespace, name and patch payload
export function usePatchApplicationMutation(
environmentId: EnvironmentId,
namespace: string,
name: string
) {
return useMutation(
({
appKind,
patch,
contentType = 'application/json-patch+json',
}: {
appKind: AppKind;
patch: ApplicationPatch;
contentType?:
| 'application/json-patch+json'
| 'application/strategic-merge-patch+json';
}) =>
patchApplication(
environmentId,
namespace,
appKind,
name,
patch,
contentType
),
{
onSuccess: () => {
queryClient.invalidateQueries(
queryKeys.application(environmentId, namespace, name)
);
},
// patch application is used for patching and rollbacks, so handle the error where it's used instead of here
}
);
}
// useRedeployApplicationMutation gets all the pods for an application (using the matchLabels field in the labelSelector query param) and then deletes all of them, so that they are recreated
export function useRedeployApplicationMutation(
environmentId: number,
namespace: string,
name: string
) {
return useMutation(
async ({ labelSelector }: { labelSelector: string }) => {
try {
// get only the pods that match the labelSelector for the application
const pods = await getNamespacePods(
environmentId,
namespace,
labelSelector
);
// delete all the pods to redeploy the application
await Promise.all(
pods.map((pod) => {
if (pod?.metadata?.name) {
return deletePod(environmentId, namespace, pod.metadata.name);
}
return Promise.resolve();
})
);
} catch (error) {
throw new Error(`Unable to redeploy application: ${error}`);
}
},
{
onSuccess: () => {
queryClient.invalidateQueries(
queryKeys.application(environmentId, namespace, name)
);
},
...withError('Unable to redeploy application'),
}
);
}