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:
parent
cc75167437
commit
959c527be7
42 changed files with 1378 additions and 1293 deletions
37
app/react/kubernetes/applications/queries/getPods.ts
Normal file
37
app/react/kubernetes/applications/queries/getPods.ts
Normal 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}'`
|
||||
);
|
||||
}
|
||||
}
|
105
app/react/kubernetes/applications/queries/query-keys.ts
Normal file
105
app/react/kubernetes/applications/queries/query-keys.ts
Normal 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,
|
||||
};
|
168
app/react/kubernetes/applications/queries/useApplication.ts
Normal file
168
app/react/kubernetes/applications/queries/useApplication.ts
Normal 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;
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
54
app/react/kubernetes/applications/queries/useApplications.ts
Normal file
54
app/react/kubernetes/applications/queries/useApplications.ts
Normal 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');
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue