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

refactor(apps): migrate applications view to react [r8s-124] (#28)

This commit is contained in:
Ali 2024-10-25 12:28:05 +13:00 committed by GitHub
parent cc75167437
commit 959c527be7
42 changed files with 1378 additions and 1293 deletions

View file

@ -0,0 +1,37 @@
import { Pod, PodList } from 'kubernetes-types/core/v1';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { parseKubernetesAxiosError } from '../../axiosError';
export async function getPods(
environmentId: EnvironmentId,
namespace: string,
labelSelector?: string
) {
try {
const { data } = await axios.get<PodList>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`,
{
params: {
labelSelector,
},
}
);
const items = (data.items || []).map(
(pod) =>
<Pod>{
...pod,
kind: 'Pod',
apiVersion: data.apiVersion,
}
);
return items;
} catch (e) {
throw parseKubernetesAxiosError(
e,
`Unable to retrieve Pods in namespace '${namespace}'`
);
}
}

View file

@ -0,0 +1,105 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
export type GetAppsParams = {
namespace?: string;
nodeName?: string;
withDependencies?: boolean;
};
export const queryKeys = {
applications: (environmentId: EnvironmentId, params?: GetAppsParams) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
params,
] as const,
application: (
environmentId: EnvironmentId,
namespace: string,
name: string,
yaml?: boolean
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
yaml,
] as const,
applicationRevisions: (
environmentId: EnvironmentId,
namespace: string,
name: string,
labelSelector?: string
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'revisions',
labelSelector,
] as const,
applicationServices: (
environmentId: EnvironmentId,
namespace: string,
name: string
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'services',
] as const,
ingressesForApplication: (
environmentId: EnvironmentId,
namespace: string,
name: string
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'ingresses',
] as const,
applicationHorizontalPodAutoscalers: (
environmentId: EnvironmentId,
namespace: string,
name: string
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'horizontalpodautoscalers',
] as const,
applicationPods: (
environmentId: EnvironmentId,
namespace: string,
name: string
) =>
[
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
'pods',
] as const,
};

View file

@ -0,0 +1,168 @@
import { UseQueryResult, useQuery } from '@tanstack/react-query';
import { DaemonSet, Deployment, StatefulSet } from 'kubernetes-types/apps/v1';
import { Pod } from 'kubernetes-types/core/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { isFulfilled } from '@/portainer/helpers/promise-utils';
import axios from '@/portainer/services/axios';
import { AppKind, Application } from '../types';
import { parseKubernetesAxiosError } from '../../axiosError';
import { queryKeys } from './query-keys';
/**
* @returns an application from the Kubernetes API proxy (deployment, statefulset, daemonSet or pod)
* 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),
{
...withGlobalError('Unable to retrieve application'),
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
}
);
}
// if not known, get the type of an application (Deployment, DaemonSet, StatefulSet or naked pod) by name
async function getApplication<T extends Application | string = Application>(
environmentId: EnvironmentId,
namespace: string,
name: string,
appKind?: AppKind,
yaml?: boolean
) {
// if resourceType is known, get the application by type and name
if (appKind) {
switch (appKind) {
case 'Deployment':
case 'DaemonSet':
case 'StatefulSet':
return getApplicationByKind<T>(
environmentId,
namespace,
appKind,
name,
yaml
);
case 'Pod':
return getPod(environmentId, namespace, name, yaml);
default:
throw new Error('Unknown resource type');
}
}
// if resourceType is not known, get the application by name and return the first one that is fulfilled
const [deployment, daemonSet, statefulSet, pod] = await Promise.allSettled([
getApplicationByKind<Deployment>(
environmentId,
namespace,
'Deployment',
name,
yaml
),
getApplicationByKind<DaemonSet>(
environmentId,
namespace,
'DaemonSet',
name,
yaml
),
getApplicationByKind<StatefulSet>(
environmentId,
namespace,
'StatefulSet',
name,
yaml
),
getPod(environmentId, namespace, name, yaml),
]);
if (isFulfilled(deployment)) {
return deployment.value;
}
if (isFulfilled(daemonSet)) {
return daemonSet.value;
}
if (isFulfilled(statefulSet)) {
return statefulSet.value;
}
if (isFulfilled(pod)) {
return pod.value;
}
throw new Error('Unable to retrieve application');
}
async function getPod<T extends Pod | string = Pod>(
environmentId: EnvironmentId,
namespace: string,
name: string,
yaml?: boolean
) {
try {
const { data } = await axios.get<T>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods/${name}`,
{
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
}
);
return data;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve pod');
}
}
async function getApplicationByKind<
T extends Application | string = Application,
>(
environmentId: EnvironmentId,
namespace: string,
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
name: string,
yaml?: boolean
) {
try {
const { data } = await axios.get<T>(
buildUrl(environmentId, namespace, `${appKind}s`, name),
{
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
// this logic is to get the latest YAML response
// axios-cache-adapter looks for the response headers to determine if the response should be cached
// to avoid writing requestInterceptor, adding a query param to the request url
params: yaml
? {
_: Date.now(),
}
: null,
}
);
return data;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve application');
}
}
function buildUrl(
environmentId: EnvironmentId,
namespace: string,
appKind: 'Deployments' | 'DaemonSets' | 'StatefulSets',
name?: string
) {
let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
if (name) {
baseUrl += `/${name}`;
}
return baseUrl;
}

View file

@ -0,0 +1,76 @@
import { useQuery } from '@tanstack/react-query';
import { HorizontalPodAutoscalerList } from 'kubernetes-types/autoscaling/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import type { Application } from '../types';
import { parseKubernetesAxiosError } from '../../axiosError';
import { queryKeys } from './query-keys';
// 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;
},
{
...withGlobalError(
`Unable to get horizontal pod autoscaler${
app ? ` for ${app.metadata?.name}` : ''
}`
),
enabled: !!app,
}
);
}
async function getNamespaceHorizontalPodAutoscalers(
environmentId: EnvironmentId,
namespace: string
) {
try {
const { data: autoScalarList } =
await axios.get<HorizontalPodAutoscalerList>(
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers`
);
return autoScalarList.items;
} catch (e) {
throw parseKubernetesAxiosError(
e,
'Unable to retrieve horizontal pod autoscalers'
);
}
}

View file

@ -0,0 +1,44 @@
import { useQuery } from '@tanstack/react-query';
import { Pod } from 'kubernetes-types/core/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { applicationIsKind, matchLabelsToLabelSelectorValue } from '../utils';
import { Application } from '../types';
import { queryKeys } from './query-keys';
import { getPods } from './getPods';
// 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 getPods(environmentId, namespace, labelSelector);
return pods;
},
{
...withGlobalError(`Unable to get pods for ${appName}`),
enabled: !!app,
refetchInterval() {
return options?.autoRefreshRate ?? false;
},
}
);
}

View file

@ -0,0 +1,173 @@
import { useQuery } from '@tanstack/react-query';
import {
ReplicaSetList,
ControllerRevisionList,
} from 'kubernetes-types/apps/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { AppKind } from '../types';
import { appRevisionAnnotation } from '../constants';
import { filterRevisionsByOwnerUid } from '../utils';
import { parseKubernetesAxiosError } from '../../axiosError';
import { queryKeys } from './query-keys';
// 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
),
{
...withGlobalError('Unable to retrieve application revisions'),
enabled: !!labelSelector && !!appKind && !!deploymentUid,
}
);
}
async function getApplicationRevisionList(
environmentId: EnvironmentId,
namespace: string,
deploymentUid?: string,
appKind?: AppKind,
labelSelector?: string
) {
if (!deploymentUid) {
throw new Error('deploymentUid is required');
}
try {
switch (appKind) {
case 'Deployment': {
const replicaSetList = await getReplicaSetList(
environmentId,
namespace,
labelSelector
);
const replicaSets = replicaSetList.items;
// keep only replicaset(s) which are owned by the deployment with the given uid
const replicaSetsWithOwnerId = filterRevisionsByOwnerUid(
replicaSets,
deploymentUid
);
// keep only replicaset(s) that have been a version of the Deployment
const replicaSetsWithRevisionAnnotations =
replicaSetsWithOwnerId.filter(
(rs) => !!rs.metadata?.annotations?.[appRevisionAnnotation]
);
return {
...replicaSetList,
items: replicaSetsWithRevisionAnnotations,
} as ReplicaSetList;
}
case 'DaemonSet':
case 'StatefulSet': {
const controllerRevisionList = await getControllerRevisionList(
environmentId,
namespace,
labelSelector
);
const controllerRevisions = controllerRevisionList.items;
// ensure the controller reference(s) is owned by the deployment with the given uid
const controllerRevisionsWithOwnerId = filterRevisionsByOwnerUid(
controllerRevisions,
deploymentUid
);
return {
...controllerRevisionList,
items: controllerRevisionsWithOwnerId,
} as ControllerRevisionList;
}
default:
throw new Error(`Unknown application kind ${appKind}`);
}
} catch (e) {
throw parseAxiosError(
e as Error,
`Unable to retrieve revisions for ${appKind}`
);
}
}
async function getReplicaSetList(
environmentId: EnvironmentId,
namespace: string,
labelSelector?: string
) {
try {
const { data } = await axios.get<ReplicaSetList>(
buildUrl(environmentId, namespace, 'ReplicaSets'),
{
params: {
labelSelector,
},
}
);
return data;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to retrieve ReplicaSets');
}
}
async function getControllerRevisionList(
environmentId: EnvironmentId,
namespace: string,
labelSelector?: string
) {
try {
const { data } = await axios.get<ControllerRevisionList>(
buildUrl(environmentId, namespace, 'ControllerRevisions'),
{
params: {
labelSelector,
},
}
);
return data;
} catch (e) {
throw parseKubernetesAxiosError(
e,
'Unable to retrieve ControllerRevisions'
);
}
}
function buildUrl(
environmentId: EnvironmentId,
namespace: string,
appKind:
| 'Deployments'
| 'DaemonSets'
| 'StatefulSets'
| 'ReplicaSets'
| 'ControllerRevisions',
name?: string
) {
let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
if (name) {
baseUrl += `/${name}`;
}
return baseUrl;
}

View file

@ -0,0 +1,57 @@
import { useQuery } from '@tanstack/react-query';
import { Pod } from 'kubernetes-types/core/v1';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { getNamespaceServices } from '../../services/service';
import type { Application } from '../types';
import { applicationIsKind } from '../utils';
import { queryKeys } from './query-keys';
// 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;
},
{
...withGlobalError(`Unable to get services for ${appName}`),
enabled: !!app,
}
);
}

View file

@ -0,0 +1,54 @@
import { useQuery } from '@tanstack/react-query';
import { withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { Application } from '../ListView/ApplicationsDatatable/types';
import { queryKeys } from './query-keys';
type GetAppsParams = {
namespace?: string;
nodeName?: string;
withDependencies?: boolean;
};
type GetAppsQueryOptions = {
refetchInterval?: number;
} & GetAppsParams;
/**
* @returns a UseQueryResult that can be used to fetch a list of applications from an array of namespaces.
* This api call goes to the portainer api, not the kubernetes proxy api.
*/
export function useApplications(
environmentId: EnvironmentId,
queryOptions?: GetAppsQueryOptions
) {
const { refetchInterval, ...params } = queryOptions ?? {};
return useQuery(
queryKeys.applications(environmentId, params),
() => getApplications(environmentId, params),
{
refetchInterval,
...withGlobalError('Unable to retrieve applications'),
}
);
}
// get all applications from a namespace
export async function getApplications(
environmentId: EnvironmentId,
params?: GetAppsParams
) {
try {
const { data } = await axios.get<Application[]>(
`/kubernetes/${environmentId}/applications`,
{ params }
);
return data;
} catch (e) {
throw parseAxiosError(e, 'Unable to retrieve applications');
}
}

View file

@ -0,0 +1,191 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
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';
import { queryKeys } from './query-keys';
export function useDeleteApplicationsMutation({
environmentId,
stacks,
reportStacks,
}: {
environmentId: EnvironmentId;
stacks: Stack[];
reportStacks?: boolean;
}) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (applications: ApplicationRowData[]) =>
deleteApplications(applications, stacks, environmentId),
onSuccess: ({ settledAppDeletions, settledStackDeletions }) => {
// 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));
});
// 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(', ')
);
}
queryClient.invalidateQueries(queryKeys.applications(environmentId));
},
...withGlobalError('Unable to remove applications'),
});
}
async function deleteApplications(
applications: ApplicationRowData[],
stacks: Stack[],
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)
);
return { settledAppDeletions, settledStackDeletions };
}
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');
}
}
// 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
);
}
}

View file

@ -0,0 +1,70 @@
import { HorizontalPodAutoscaler } from 'kubernetes-types/autoscaling/v1';
import { useQuery } from '@tanstack/react-query';
import axios from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { withGlobalError } from '@/react-tools/react-query';
import { parseKubernetesAxiosError } from '../../axiosError';
// when yaml is set to true, the expected return type is a string
export function useHorizontalPodAutoScaler<
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler,
>(
environmentId: EnvironmentId,
namespace: string,
name?: string,
options?: { yaml?: boolean }
) {
return useQuery(
[
'environments',
environmentId,
'kubernetes',
'namespaces',
namespace,
'horizontalpodautoscalers',
name,
options?.yaml,
],
() =>
name
? getNamespaceHorizontalPodAutoscaler<T>(
environmentId,
namespace,
name,
options
)
: undefined,
{
...withGlobalError('Unable to get horizontal pod autoscaler'),
enabled: !!name,
}
);
}
export async function getNamespaceHorizontalPodAutoscaler<
T extends HorizontalPodAutoscaler | string = HorizontalPodAutoscaler,
>(
environmentId: EnvironmentId,
namespace: string,
name: string,
options?: { yaml?: boolean }
) {
try {
const { data: autoScalar } = await axios.get<T>(
`/endpoints/${environmentId}/kubernetes/apis/autoscaling/v1/namespaces/${namespace}/horizontalpodautoscalers/${name}`,
{
headers: {
Accept: options?.yaml ? 'application/yaml' : 'application/json',
},
}
);
return autoScalar;
} catch (e) {
throw parseKubernetesAxiosError(
e,
'Unable to retrieve horizontal pod autoscaler'
);
}
}

View file

@ -0,0 +1,155 @@
import { useMutation } from '@tanstack/react-query';
import { Deployment, DaemonSet, StatefulSet } from 'kubernetes-types/apps/v1';
import { Pod } from 'kubernetes-types/core/v1';
import { queryClient } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import type { AppKind, ApplicationPatch, Application } from '../types';
import { parseKubernetesAxiosError } from '../../axiosError';
import { queryKeys } from './query-keys';
// 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
}
);
}
async function patchApplication(
environmentId: EnvironmentId,
namespace: string,
appKind: AppKind,
name: string,
patch: ApplicationPatch,
contentType: string = 'application/json-patch+json'
) {
switch (appKind) {
case 'Deployment':
return patchApplicationByKind<Deployment>(
environmentId,
namespace,
appKind,
name,
patch,
contentType
);
case 'DaemonSet':
return patchApplicationByKind<DaemonSet>(
environmentId,
namespace,
appKind,
name,
patch,
contentType
);
case 'StatefulSet':
return patchApplicationByKind<StatefulSet>(
environmentId,
namespace,
appKind,
name,
patch,
contentType
);
case 'Pod':
return patchPod(environmentId, namespace, name, patch);
default:
throw new Error(`Unknown application kind ${appKind}`);
}
}
async function patchApplicationByKind<T extends Application>(
environmentId: EnvironmentId,
namespace: string,
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
name: string,
patch: ApplicationPatch,
contentType = 'application/json-patch+json'
) {
try {
const res = await axios.patch<T>(
buildUrl(environmentId, namespace, `${appKind}s`, name),
patch,
{
headers: {
'Content-Type': contentType,
},
}
);
return res;
} catch (e) {
throw parseKubernetesAxiosError(e, 'Unable to patch application');
}
}
async function patchPod(
environmentId: EnvironmentId,
namespace: string,
name: string,
patch: ApplicationPatch
) {
try {
return await axios.patch<Pod>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods/${name}`,
patch,
{
headers: {
'Content-Type': 'application/json-patch+json',
},
}
);
} catch (e) {
throw parseAxiosError(e, 'Unable to update pod');
}
}
function buildUrl(
environmentId: EnvironmentId,
namespace: string,
appKind:
| 'Deployments'
| 'DaemonSets'
| 'StatefulSets'
| 'ReplicaSets'
| 'ControllerRevisions',
name?: string
) {
let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
if (name) {
baseUrl += `/${name}`;
}
return baseUrl;
}

View file

@ -0,0 +1,70 @@
import { useMutation } from '@tanstack/react-query';
import { Pod } from 'kubernetes-types/core/v1';
import { queryClient, withGlobalError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import axios from '@/portainer/services/axios';
import { parseKubernetesAxiosError } from '../../axiosError';
import { queryKeys } from './query-keys';
import { getPods } from './getPods';
// 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 getPods(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)
);
},
...withGlobalError('Unable to redeploy application'),
}
);
}
async function deletePod(
environmentId: EnvironmentId,
namespace: string,
name: string
) {
try {
return await axios.delete<Pod>(buildUrl(environmentId, namespace, name));
} catch (e) {
throw parseKubernetesAxiosError(e as Error, 'Unable to delete pod');
}
}
function buildUrl(
environmentId: EnvironmentId,
namespace: string,
name?: string
) {
let baseUrl = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`;
if (name) {
baseUrl += `/${name}`;
}
return baseUrl;
}