2023-05-03 03:55:25 +00:00
|
|
|
import {
|
|
|
|
DaemonSetList,
|
|
|
|
StatefulSetList,
|
|
|
|
DeploymentList,
|
|
|
|
Deployment,
|
|
|
|
DaemonSet,
|
|
|
|
StatefulSet,
|
2023-05-29 03:06:14 +00:00
|
|
|
ReplicaSetList,
|
|
|
|
ControllerRevisionList,
|
2023-05-03 03:55:25 +00:00
|
|
|
} from 'kubernetes-types/apps/v1';
|
|
|
|
|
|
|
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|
|
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
2023-05-14 02:26:11 +00:00
|
|
|
import { isFulfilled } from '@/portainer/helpers/promise-utils';
|
2023-05-03 03:55:25 +00:00
|
|
|
|
2023-05-29 03:06:14 +00:00
|
|
|
import { getPod, getNamespacePods, patchPod } from './pod.service';
|
|
|
|
import { filterRevisionsByOwnerUid, getNakedPods } from './utils';
|
|
|
|
import {
|
|
|
|
AppKind,
|
|
|
|
Application,
|
|
|
|
ApplicationList,
|
|
|
|
ApplicationPatch,
|
|
|
|
} from './types';
|
|
|
|
import { appRevisionAnnotation } from './constants';
|
2023-05-03 03:55:25 +00:00
|
|
|
|
|
|
|
// This file contains services for Kubernetes apps/v1 resources (Deployments, DaemonSets, StatefulSets)
|
|
|
|
|
|
|
|
export async function getApplicationsForCluster(
|
|
|
|
environmentId: EnvironmentId,
|
2023-06-11 21:46:48 +00:00
|
|
|
namespaceNames?: string[]
|
2023-05-03 03:55:25 +00:00
|
|
|
) {
|
|
|
|
try {
|
2023-06-11 21:46:48 +00:00
|
|
|
if (!namespaceNames) {
|
|
|
|
return [];
|
|
|
|
}
|
2023-05-03 03:55:25 +00:00
|
|
|
const applications = await Promise.all(
|
2023-06-11 21:46:48 +00:00
|
|
|
namespaceNames.map((namespace) =>
|
2023-05-03 03:55:25 +00:00
|
|
|
getApplicationsForNamespace(environmentId, namespace)
|
|
|
|
)
|
|
|
|
);
|
|
|
|
return applications.flat();
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(
|
|
|
|
e as Error,
|
|
|
|
'Unable to retrieve applications for cluster'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// get a list of all Deployments, DaemonSets, StatefulSets and naked pods (https://portainer.atlassian.net/browse/CE-2) in one namespace
|
|
|
|
async function getApplicationsForNamespace(
|
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const [deployments, daemonSets, statefulSets, pods] = await Promise.all([
|
|
|
|
getApplicationsByKind<DeploymentList>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
'Deployment'
|
|
|
|
),
|
|
|
|
getApplicationsByKind<DaemonSetList>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
'DaemonSet'
|
|
|
|
),
|
|
|
|
getApplicationsByKind<StatefulSetList>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
'StatefulSet'
|
|
|
|
),
|
2023-05-29 03:06:14 +00:00
|
|
|
getNamespacePods(environmentId, namespace),
|
2023-05-03 03:55:25 +00:00
|
|
|
]);
|
|
|
|
// find all pods which are 'naked' (not owned by a deployment, daemonset or statefulset)
|
|
|
|
const nakedPods = getNakedPods(pods, deployments, daemonSets, statefulSets);
|
|
|
|
return [...deployments, ...daemonSets, ...statefulSets, ...nakedPods];
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(
|
|
|
|
e as Error,
|
2023-06-11 21:46:48 +00:00
|
|
|
`Unable to retrieve applications in namespace '${namespace}'`
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if not known, get the type of an application (Deployment, DaemonSet, StatefulSet or naked pod) by name
|
2023-08-27 21:01:35 +00:00
|
|
|
export async function getApplication<
|
2023-09-04 15:20:36 +00:00
|
|
|
T extends Application | string = Application,
|
2023-08-27 21:01:35 +00:00
|
|
|
>(
|
2023-05-03 03:55:25 +00:00
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
|
|
|
name: string,
|
2023-08-27 21:01:35 +00:00
|
|
|
appKind?: AppKind,
|
|
|
|
yaml?: boolean
|
2023-05-03 03:55:25 +00:00
|
|
|
) {
|
|
|
|
try {
|
|
|
|
// if resourceType is known, get the application by type and name
|
|
|
|
if (appKind) {
|
|
|
|
switch (appKind) {
|
|
|
|
case 'Deployment':
|
|
|
|
case 'DaemonSet':
|
|
|
|
case 'StatefulSet':
|
2023-08-27 21:01:35 +00:00
|
|
|
return await getApplicationByKind<T>(
|
2023-05-03 03:55:25 +00:00
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
appKind,
|
2023-08-27 21:01:35 +00:00
|
|
|
name,
|
|
|
|
yaml
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
case 'Pod':
|
2023-08-27 21:01:35 +00:00
|
|
|
return await getPod(environmentId, namespace, name, yaml);
|
2023-05-03 03:55:25 +00:00
|
|
|
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',
|
2023-08-27 21:01:35 +00:00
|
|
|
name,
|
|
|
|
yaml
|
2023-05-03 03:55:25 +00:00
|
|
|
),
|
|
|
|
getApplicationByKind<DaemonSet>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
'DaemonSet',
|
2023-08-27 21:01:35 +00:00
|
|
|
name,
|
|
|
|
yaml
|
2023-05-03 03:55:25 +00:00
|
|
|
),
|
|
|
|
getApplicationByKind<StatefulSet>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
'StatefulSet',
|
2023-08-27 21:01:35 +00:00
|
|
|
name,
|
|
|
|
yaml
|
2023-05-03 03:55:25 +00:00
|
|
|
),
|
2023-08-27 21:01:35 +00:00
|
|
|
getPod(environmentId, namespace, name, yaml),
|
2023-05-03 03:55:25 +00:00
|
|
|
]);
|
|
|
|
|
|
|
|
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');
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(
|
|
|
|
e as Error,
|
2023-06-11 21:46:48 +00:00
|
|
|
`Unable to retrieve application ${name} in namespace '${namespace}'`
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function patchApplication(
|
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
|
|
|
appKind: AppKind,
|
|
|
|
name: string,
|
2023-05-29 03:06:14 +00:00
|
|
|
patch: ApplicationPatch
|
2023-05-03 03:55:25 +00:00
|
|
|
) {
|
|
|
|
try {
|
|
|
|
switch (appKind) {
|
|
|
|
case 'Deployment':
|
|
|
|
return await patchApplicationByKind<Deployment>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
appKind,
|
|
|
|
name,
|
2023-05-29 03:06:14 +00:00
|
|
|
patch
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
case 'DaemonSet':
|
|
|
|
return await patchApplicationByKind<DaemonSet>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
appKind,
|
|
|
|
name,
|
2023-05-29 03:06:14 +00:00
|
|
|
patch,
|
|
|
|
'application/strategic-merge-patch+json'
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
case 'StatefulSet':
|
|
|
|
return await patchApplicationByKind<StatefulSet>(
|
|
|
|
environmentId,
|
|
|
|
namespace,
|
|
|
|
appKind,
|
|
|
|
name,
|
2023-05-29 03:06:14 +00:00
|
|
|
patch,
|
|
|
|
'application/strategic-merge-patch+json'
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
case 'Pod':
|
2023-05-29 03:06:14 +00:00
|
|
|
return await patchPod(environmentId, namespace, name, patch);
|
2023-05-03 03:55:25 +00:00
|
|
|
default:
|
|
|
|
throw new Error(`Unknown application kind ${appKind}`);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(
|
|
|
|
e as Error,
|
2023-06-11 21:46:48 +00:00
|
|
|
`Unable to patch application ${name} in namespace '${namespace}'`
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function patchApplicationByKind<T extends Application>(
|
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
|
|
|
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
|
|
|
|
name: string,
|
2023-05-29 03:06:14 +00:00
|
|
|
patch: ApplicationPatch,
|
|
|
|
contentType = 'application/json-patch+json'
|
2023-05-03 03:55:25 +00:00
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const res = await axios.patch<T>(
|
|
|
|
buildUrl(environmentId, namespace, `${appKind}s`, name),
|
2023-05-29 03:06:14 +00:00
|
|
|
patch,
|
2023-05-03 03:55:25 +00:00
|
|
|
{
|
|
|
|
headers: {
|
2023-05-29 03:06:14 +00:00
|
|
|
'Content-Type': contentType,
|
2023-05-03 03:55:25 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
return res;
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(e as Error, 'Unable to patch application');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-27 21:01:35 +00:00
|
|
|
async function getApplicationByKind<
|
2023-09-04 15:20:36 +00:00
|
|
|
T extends Application | string = Application,
|
2023-08-27 21:01:35 +00:00
|
|
|
>(
|
2023-05-03 03:55:25 +00:00
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
|
|
|
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
|
2023-08-27 21:01:35 +00:00
|
|
|
name: string,
|
|
|
|
yaml?: boolean
|
2023-05-03 03:55:25 +00:00
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const { data } = await axios.get<T>(
|
2023-08-27 21:01:35 +00:00
|
|
|
buildUrl(environmentId, namespace, `${appKind}s`, name),
|
|
|
|
{
|
|
|
|
headers: { Accept: yaml ? 'application/yaml' : 'application/json' },
|
|
|
|
}
|
2023-05-03 03:55:25 +00:00
|
|
|
);
|
|
|
|
return data;
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(e as Error, 'Unable to retrieve application');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getApplicationsByKind<T extends ApplicationList>(
|
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
|
|
|
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet'
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
const { data } = await axios.get<T>(
|
|
|
|
buildUrl(environmentId, namespace, `${appKind}s`)
|
|
|
|
);
|
|
|
|
return data.items as T['items'];
|
|
|
|
} catch (e) {
|
|
|
|
throw parseAxiosError(e as Error, `Unable to retrieve ${appKind}s`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-29 03:06:14 +00:00
|
|
|
export 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}`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export 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 parseAxiosError(e as Error, 'Unable to retrieve ReplicaSets');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export 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 parseAxiosError(e as Error, 'Unable to retrieve ControllerRevisions');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-03 03:55:25 +00:00
|
|
|
function buildUrl(
|
|
|
|
environmentId: EnvironmentId,
|
|
|
|
namespace: string,
|
2023-05-29 03:06:14 +00:00
|
|
|
appKind:
|
|
|
|
| 'Deployments'
|
|
|
|
| 'DaemonSets'
|
|
|
|
| 'StatefulSets'
|
|
|
|
| 'ReplicaSets'
|
|
|
|
| 'ControllerRevisions',
|
2023-05-03 03:55:25 +00:00
|
|
|
name?: string
|
|
|
|
) {
|
|
|
|
let baseUrl = `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appKind.toLowerCase()}`;
|
|
|
|
if (name) {
|
|
|
|
baseUrl += `/${name}`;
|
|
|
|
}
|
|
|
|
return baseUrl;
|
|
|
|
}
|