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

refactor(app): summary widget migration [EE-5351] (#8796)

* refactor(app): summary widget migration [EE-5351]

* update converter and limit display

---------

Co-authored-by: testa113 <testa113>
This commit is contained in:
Ali 2023-05-03 15:55:25 +12:00 committed by GitHub
parent 745bbb7d79
commit 98e6393274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 964 additions and 304 deletions

View file

@ -0,0 +1,289 @@
import { User, Clock, Edit, ChevronDown, ChevronUp } from 'lucide-react';
import moment from 'moment';
import { useState } from 'react';
import { Pod } from 'kubernetes-types/core/v1';
import { useCurrentStateAndParams } from '@uirouter/react';
import { Authorized } from '@/react/hooks/useUser';
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { DetailsTable } from '@@/DetailsTable';
import { Badge } from '@@/Badge';
import { Link } from '@@/Link';
import { Button, LoadingButton } from '@@/buttons';
import { isSystemNamespace } from '../../namespaces/utils';
import {
appStackNameLabel,
appKindToDeploymentTypeMap,
appOwnerLabel,
appDeployMethodLabel,
appNoteAnnotation,
} from '../constants';
import {
applicationIsKind,
bytesToReadableFormat,
getResourceRequests,
getRunningPods,
getTotalPods,
isExternalApplication,
} from '../utils';
import {
useApplication,
usePatchApplicationMutation,
} from '../application.queries';
import { Application } from '../types';
export function ApplicationSummaryWidget() {
const stateAndParams = useCurrentStateAndParams();
const {
params: {
namespace,
name,
'resource-type': resourceType,
endpointId: environmentId,
},
} = stateAndParams;
const applicationQuery = useApplication(
environmentId,
namespace,
name,
resourceType
);
const application = applicationQuery.data;
const systemNamespace = isSystemNamespace(namespace);
const externalApplication = application && isExternalApplication(application);
const applicationRequests = application && getResourceRequests(application);
const applicationOwner = application?.metadata?.labels?.[appOwnerLabel];
const applicationDeployMethod = getApplicationDeployMethod(application);
const applicationNote =
application?.metadata?.annotations?.[appNoteAnnotation];
const [isNoteOpen, setIsNoteOpen] = useState(true);
const [applicationNoteFormValues, setApplicationNoteFormValues] = useState(
applicationNote || ''
);
const patchApplicationMutation = usePatchApplicationMutation(
environmentId,
namespace,
name
);
return (
<div className="p-5">
<DetailsTable>
<tr>
<td>Name</td>
<td>
<div
className="flex items-center gap-x-2"
data-cy="k8sAppDetail-appName"
>
{name}
{externalApplication && !systemNamespace && (
<Badge type="info">external</Badge>
)}
</div>
</td>
</tr>
<tr>
<td>Stack</td>
<td data-cy="k8sAppDetail-stackName">
{application?.metadata?.labels?.[appStackNameLabel] || '-'}
</td>
</tr>
<tr>
<td>Namespace</td>
<td>
<div
className="flex items-center gap-x-2"
data-cy="k8sAppDetail-resourcePoolName"
>
<Link
to="kubernetes.resourcePools.resourcePool"
params={{ id: namespace }}
>
{namespace}
</Link>
{systemNamespace && <Badge type="info">system</Badge>}
</div>
</td>
</tr>
<tr>
<td>Application type</td>
<td data-cy="k8sAppDetail-appType">{application?.kind || '-'}</td>
</tr>
{application?.kind && (
<tr>
<td>Status</td>
{applicationIsKind<Pod>('Pod', application) && (
<td data-cy="k8sAppDetail-appType">
{application?.status?.phase}
</td>
)}
{!applicationIsKind<Pod>('Pod', application) && (
<td data-cy="k8sAppDetail-appType">
{appKindToDeploymentTypeMap[application.kind]}
<code className="ml-1">
{getRunningPods(application)}
</code> / <code>{getTotalPods(application)}</code>
</td>
)}
</tr>
)}
{(!!applicationRequests?.cpu || !!applicationRequests?.memoryBytes) && (
<tr>
<td>
Resource reservations
{!applicationIsKind<Pod>('Pod', application) && (
<div className="text-muted small">per instance</div>
)}
</td>
<td>
{!!applicationRequests?.cpu && (
<div data-cy="k8sAppDetail-cpuReservation">
CPU {applicationRequests.cpu}
</div>
)}
{!!applicationRequests?.memoryBytes && (
<div data-cy="k8sAppDetail-memoryReservation">
Memory{' '}
{bytesToReadableFormat(applicationRequests.memoryBytes)}
</div>
)}
</td>
</tr>
)}
<tr>
<td>Creation</td>
<td>
<div className="flex flex-wrap items-center gap-3">
{applicationOwner && (
<span
className="flex items-center gap-1"
data-cy="k8sAppDetail-owner"
>
<User />
{applicationOwner}
</span>
)}
<span
className="flex items-center gap-1"
data-cy="k8sAppDetail-creationDate"
>
<Clock />
{moment(application?.metadata?.creationTimestamp).format(
'YYYY-MM-DD HH:mm:ss'
)}
</span>
{(!externalApplication || systemNamespace) && (
<span
className="flex items-center gap-1"
data-cy="k8sAppDetail-creationMethod"
>
<Clock />
Deployed from {applicationDeployMethod}
</span>
)}
</div>
</td>
</tr>
<tr>
<td colSpan={2}>
<form className="form-horizontal">
<div className="form-group">
<div className="col-sm-12 vertical-center">
<Edit /> Note
<Button
size="xsmall"
type="button"
color="light"
data-cy="k8sAppDetail-expandNoteButton"
onClick={() => setIsNoteOpen(!isNoteOpen)}
>
{isNoteOpen ? 'Collapse' : 'Expand'}
{isNoteOpen ? <ChevronUp /> : <ChevronDown />}
</Button>
</div>
</div>
{isNoteOpen && (
<>
<div className="form-group">
<div className="col-sm-12">
<textarea
className="form-control resize-y"
name="application_note"
id="application_note"
value={applicationNoteFormValues}
onChange={(e) =>
setApplicationNoteFormValues(e.target.value)
}
rows={5}
placeholder="Enter a note about this application..."
/>
</div>
</div>
<Authorized authorizations="K8sApplicationDetailsW">
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
color="primary"
size="small"
className="!ml-0"
type="button"
onClick={() => patchApplicationNote()}
disabled={
// disable if there is no change to the note, or it's updating
applicationNoteFormValues ===
(applicationNote || '') ||
patchApplicationMutation.isLoading
}
data-cy="k8sAppDetail-saveNoteButton"
isLoading={patchApplicationMutation.isLoading}
loadingText={applicationNote ? 'Updating' : 'Saving'}
>
{applicationNote ? 'Update' : 'Save'} note
</LoadingButton>
</div>
</div>
</Authorized>
</>
)}
</form>
</td>
</tr>
</DetailsTable>
</div>
);
async function patchApplicationNote() {
const path = `/metadata/annotations/${appNoteAnnotation}`;
const value = applicationNoteFormValues;
if (application?.kind) {
try {
await patchApplicationMutation.mutateAsync({
appKind: application.kind,
path,
value,
});
notifySuccess('Success', 'Application successfully updated');
} catch (error) {
notifyError(
`Failed to ${applicationNote ? 'update' : 'save'} note`,
error as Error
);
}
}
}
}
function getApplicationDeployMethod(application?: Application) {
if (!application?.metadata?.labels?.[appDeployMethodLabel])
return 'application form';
if (application?.metadata?.labels?.[appDeployMethodLabel] === 'content') {
return 'manifest';
}
return application?.metadata?.labels?.[appDeployMethodLabel];
}

View file

@ -0,0 +1 @@
export { ApplicationSummaryWidget } from './ApplicationSummaryWidget';

View file

@ -0,0 +1,90 @@
import { useMutation, useQuery } from 'react-query';
import { queryClient, withError } from '@/react-tools/react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import {
getApplicationsForCluster,
getApplication,
patchApplication,
} from './application.service';
import { AppKind } from './types';
const queryKeys = {
applicationsForCluster: (environmentId: EnvironmentId) => [
'environments',
environmentId,
'kubernetes',
'applications',
],
application: (
environmentId: EnvironmentId,
namespace: string,
name: string
) => [
'environments',
environmentId,
'kubernetes',
'applications',
namespace,
name,
],
};
// useQuery to get a list of all applications from an array of namespaces
export function useApplicationsForCluster(
environemtId: EnvironmentId,
namespaces?: string[]
) {
return useQuery(
queryKeys.applicationsForCluster(environemtId),
() => namespaces && getApplicationsForCluster(environemtId, namespaces),
{
...withError('Unable to retrieve applications'),
enabled: !!namespaces,
}
);
}
// useQuery to get an application by environmentId, namespace and name
export function useApplication(
environmentId: EnvironmentId,
namespace: string,
name: string,
appKind?: AppKind
) {
return useQuery(
queryKeys.application(environmentId, namespace, name),
() => getApplication(environmentId, namespace, name, appKind),
{
...withError('Unable to retrieve application'),
}
);
}
// useQuery to patch an application by environmentId, namespace, name and patch payload
export function usePatchApplicationMutation(
environmentId: EnvironmentId,
namespace: string,
name: string
) {
return useMutation(
({
appKind,
path,
value,
}: {
appKind: AppKind;
path: string;
value: string;
}) =>
patchApplication(environmentId, namespace, appKind, name, path, value),
{
onSuccess: () => {
queryClient.invalidateQueries(
queryKeys.application(environmentId, namespace, name)
);
},
}
);
}

View file

@ -0,0 +1,268 @@
import {
DaemonSetList,
StatefulSetList,
DeploymentList,
Deployment,
DaemonSet,
StatefulSet,
} from 'kubernetes-types/apps/v1';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { isFulfilled } from '@/react/utils';
import { getPod, getPods, patchPod } from './pod.service';
import { getNakedPods } from './utils';
import { AppKind, Application, ApplicationList } from './types';
// This file contains services for Kubernetes apps/v1 resources (Deployments, DaemonSets, StatefulSets)
export async function getApplicationsForCluster(
environmentId: EnvironmentId,
namespaces: string[]
) {
try {
const applications = await Promise.all(
namespaces.map((namespace) =>
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'
),
getPods(environmentId, namespace),
]);
// 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,
`Unable to retrieve applications in namespace ${namespace}`
);
}
}
// if not known, get the type of an application (Deployment, DaemonSet, StatefulSet or naked pod) by name
export async function getApplication(
environmentId: EnvironmentId,
namespace: string,
name: string,
appKind?: AppKind
) {
try {
// if resourceType is known, get the application by type and name
if (appKind) {
switch (appKind) {
case 'Deployment':
case 'DaemonSet':
case 'StatefulSet':
return await getApplicationByKind(
environmentId,
namespace,
appKind,
name
);
case 'Pod':
return await getPod(environmentId, namespace, name);
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
),
getApplicationByKind<DaemonSet>(
environmentId,
namespace,
'DaemonSet',
name
),
getApplicationByKind<StatefulSet>(
environmentId,
namespace,
'StatefulSet',
name
),
getPod(environmentId, namespace, name),
]);
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,
`Unable to retrieve application ${name} in namespace ${namespace}`
);
}
}
export async function patchApplication(
environmentId: EnvironmentId,
namespace: string,
appKind: AppKind,
name: string,
path: string,
value: string
) {
try {
switch (appKind) {
case 'Deployment':
return await patchApplicationByKind<Deployment>(
environmentId,
namespace,
appKind,
name,
path,
value
);
case 'DaemonSet':
return await patchApplicationByKind<DaemonSet>(
environmentId,
namespace,
appKind,
name,
path,
value
);
case 'StatefulSet':
return await patchApplicationByKind<StatefulSet>(
environmentId,
namespace,
appKind,
name,
path,
value
);
case 'Pod':
return await patchPod(environmentId, namespace, name, path, value);
default:
throw new Error(`Unknown application kind ${appKind}`);
}
} catch (e) {
throw parseAxiosError(
e as Error,
`Unable to patch application ${name} in namespace ${namespace}`
);
}
}
async function patchApplicationByKind<T extends Application>(
environmentId: EnvironmentId,
namespace: string,
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
name: string,
path: string,
value: string
) {
const payload = [
{
op: 'replace',
path,
value,
},
];
try {
const res = await axios.patch<T>(
buildUrl(environmentId, namespace, `${appKind}s`, name),
payload,
{
headers: {
'Content-Type': 'application/json-patch+json',
},
}
);
return res;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to patch application');
}
}
async function getApplicationByKind<T extends Application>(
environmentId: EnvironmentId,
namespace: string,
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet',
name: string
) {
try {
const { data } = await axios.get<T>(
buildUrl(environmentId, namespace, `${appKind}s`, name)
);
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`);
}
}
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,17 @@
import { AppKind, DeploymentType } from './types';
// Portainer specific labels
export const appStackNameLabel = 'io.portainer.kubernetes.application.stack';
export const appOwnerLabel = 'io.portainer.kubernetes.application.owner';
export const appNoteAnnotation = 'io.portainer.kubernetes.application.note';
export const appDeployMethodLabel = 'io.portainer.kubernetes.application.kind';
export const appKindToDeploymentTypeMap: Record<
AppKind,
DeploymentType | null
> = {
Deployment: 'Replicated',
StatefulSet: 'Replicated',
DaemonSet: 'Global',
Pod: null,
};

View file

@ -0,0 +1,66 @@
import { Pod, PodList } from 'kubernetes-types/core/v1';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
export async function getPods(environmentId: EnvironmentId, namespace: string) {
try {
const { data } = await axios.get<PodList>(
buildUrl(environmentId, namespace)
);
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve pods');
}
}
export async function getPod(
environmentId: EnvironmentId,
namespace: string,
name: string
) {
try {
const { data } = await axios.get<Pod>(
buildUrl(environmentId, namespace, name)
);
return data;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve pod');
}
}
export async function patchPod(
environmentId: EnvironmentId,
namespace: string,
name: string,
path: string,
value: string
) {
const payload = [
{
op: 'replace',
path,
value,
},
];
try {
return await axios.put<Pod>(
buildUrl(environmentId, namespace, name),
payload
);
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to update pod');
}
}
export function buildUrl(
environmentId: EnvironmentId,
namespace: string,
name?: string
) {
let baseUrl = `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`;
if (name) {
baseUrl += `/${name}`;
}
return baseUrl;
}

View file

@ -1,21 +0,0 @@
import { useQuery } from 'react-query';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { withError } from '@/react-tools/react-query';
import { getApplicationsListForCluster } from './service';
// useQuery to get a list of all applications from an array of namespaces
export function useApplicationsForCluster(
environemtId: EnvironmentId,
namespaces?: string[]
) {
return useQuery(
['environments', environemtId, 'kubernetes', 'applications'],
() => namespaces && getApplicationsListForCluster(environemtId, namespaces),
{
...withError('Unable to retrieve applications'),
enabled: !!namespaces,
}
);
}

View file

@ -1,141 +0,0 @@
import { Pod, PodList } from 'kubernetes-types/core/v1';
import {
Deployment,
DeploymentList,
DaemonSet,
DaemonSetList,
StatefulSet,
StatefulSetList,
} from 'kubernetes-types/apps/v1';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { EnvironmentId } from '@/react/portainer/environments/types';
export async function getApplicationsListForCluster(
environmentId: EnvironmentId,
namespaces: string[]
) {
try {
const applications = await Promise.all(
namespaces.map((namespace) =>
getApplicationsListForNamespace(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 and StatefulSets in one namespace
export async function getApplicationsListForNamespace(
environmentId: EnvironmentId,
namespace: string
) {
try {
const [deployments, daemonSets, statefulSets, pods] = await Promise.all([
getDeployments(environmentId, namespace),
getDaemonSets(environmentId, namespace),
getStatefulSets(environmentId, namespace),
getPods(environmentId, namespace),
]);
// 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,
`Unable to retrieve applications in namespace ${namespace}`
);
}
}
async function getDeployments(environmentId: EnvironmentId, namespace: string) {
try {
const { data } = await axios.get<DeploymentList>(
buildUrl(environmentId, namespace, 'deployments')
);
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve deployments');
}
}
async function getDaemonSets(environmentId: EnvironmentId, namespace: string) {
try {
const { data } = await axios.get<DaemonSetList>(
buildUrl(environmentId, namespace, 'daemonsets')
);
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve daemonsets');
}
}
async function getStatefulSets(
environmentId: EnvironmentId,
namespace: string
) {
try {
const { data } = await axios.get<StatefulSetList>(
buildUrl(environmentId, namespace, 'statefulsets')
);
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve statefulsets');
}
}
async function getPods(environmentId: EnvironmentId, namespace: string) {
try {
const { data } = await axios.get<PodList>(
`/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`
);
return data.items;
} catch (e) {
throw parseAxiosError(e as Error, 'Unable to retrieve pods');
}
}
function buildUrl(
environmentId: EnvironmentId,
namespace: string,
appResource: 'deployments' | 'daemonsets' | 'statefulsets'
) {
return `/endpoints/${environmentId}/kubernetes/apis/apps/v1/namespaces/${namespace}/${appResource}`;
}
function getNakedPods(
pods: Pod[],
deployments: Deployment[],
daemonSets: DaemonSet[],
statefulSets: StatefulSet[]
) {
// naked pods are pods which are not owned by a deployment, daemonset, statefulset or replicaset
// https://kubernetes.io/docs/concepts/configuration/overview/#naked-pods-vs-replicasets-deployments-and-jobs
const appLabels = [
...deployments.map((deployment) => deployment.spec?.selector.matchLabels),
...daemonSets.map((daemonSet) => daemonSet.spec?.selector.matchLabels),
...statefulSets.map(
(statefulSet) => statefulSet.spec?.selector.matchLabels
),
];
const nakedPods = pods.filter((pod) => {
const podLabels = pod.metadata?.labels;
// if the pod has no labels, it is naked
if (!podLabels) return true;
// if the pod has labels, but no app labels, it is naked
return !appLabels.some((appLabel) => {
if (!appLabel) return false;
return Object.entries(appLabel).every(
([key, value]) => podLabels[key] === value
);
});
});
return nakedPods;
}

View file

@ -0,0 +1,21 @@
import {
DaemonSet,
DaemonSetList,
Deployment,
DeploymentList,
StatefulSet,
StatefulSetList,
} from 'kubernetes-types/apps/v1';
import { Pod, PodList } from 'kubernetes-types/core/v1';
export type Application = Deployment | DaemonSet | StatefulSet | Pod;
export type ApplicationList =
| DeploymentList
| DaemonSetList
| StatefulSetList
| PodList;
export type AppKind = 'Deployment' | 'DaemonSet' | 'StatefulSet' | 'Pod';
export type DeploymentType = 'Replicated' | 'Global';

View file

@ -0,0 +1,167 @@
import { Deployment, DaemonSet, StatefulSet } from 'kubernetes-types/apps/v1';
import { Pod } from 'kubernetes-types/core/v1';
import filesizeParser from 'filesize-parser';
import { Application } from './types';
import { appOwnerLabel } from './constants';
export function getNakedPods(
pods: Pod[],
deployments: Deployment[],
daemonSets: DaemonSet[],
statefulSets: StatefulSet[]
) {
// naked pods are pods which are not owned by a deployment, daemonset, statefulset or replicaset
// https://kubernetes.io/docs/concepts/configuration/overview/#naked-pods-vs-replicasets-deployments-and-jobs
const appLabels = [
...deployments.map((deployment) => deployment.spec?.selector.matchLabels),
...daemonSets.map((daemonSet) => daemonSet.spec?.selector.matchLabels),
...statefulSets.map(
(statefulSet) => statefulSet.spec?.selector.matchLabels
),
];
const nakedPods = pods.filter((pod) => {
const podLabels = pod.metadata?.labels;
// if the pod has no labels, it is naked
if (!podLabels) return true;
// if the pod has labels, but no app labels, it is naked
return !appLabels.some((appLabel) => {
if (!appLabel) return false;
return Object.entries(appLabel).every(
([key, value]) => podLabels[key] === value
);
});
});
return nakedPods;
}
// type guard to check if an application is a deployment, daemonset statefulset or pod
export function applicationIsKind<T extends Application>(
appKind: 'Deployment' | 'DaemonSet' | 'StatefulSet' | 'Pod',
application?: Application
): application is T {
return application?.kind === appKind;
}
// the application is external if it has no owner label
export function isExternalApplication(application: Application) {
return !application.metadata?.labels?.[appOwnerLabel];
}
function getDeploymentRunningPods(deployment: Deployment): number {
const availableReplicas = deployment.status?.availableReplicas ?? 0;
const totalReplicas = deployment.status?.replicas ?? 0;
const unavailableReplicas = deployment.status?.unavailableReplicas ?? 0;
return availableReplicas || totalReplicas - unavailableReplicas;
}
function getDaemonSetRunningPods(daemonSet: DaemonSet): number {
const numberAvailable = daemonSet.status?.numberAvailable ?? 0;
const desiredNumberScheduled = daemonSet.status?.desiredNumberScheduled ?? 0;
const numberUnavailable = daemonSet.status?.numberUnavailable ?? 0;
return numberAvailable || desiredNumberScheduled - numberUnavailable;
}
function getStatefulSetRunningPods(statefulSet: StatefulSet): number {
return statefulSet.status?.readyReplicas ?? 0;
}
export function getRunningPods(
application: Deployment | DaemonSet | StatefulSet
): number {
switch (application.kind) {
case 'Deployment':
return getDeploymentRunningPods(application);
case 'DaemonSet':
return getDaemonSetRunningPods(application);
case 'StatefulSet':
return getStatefulSetRunningPods(application);
default:
throw new Error('Unknown application type');
}
}
export function getTotalPods(
application: Deployment | DaemonSet | StatefulSet
): number {
switch (application.kind) {
case 'Deployment':
return application.status?.replicas ?? 0;
case 'DaemonSet':
return application.status?.desiredNumberScheduled ?? 0;
case 'StatefulSet':
return application.status?.replicas ?? 0;
default:
throw new Error('Unknown application type');
}
}
function parseCpu(cpu: string) {
let res = parseInt(cpu, 10);
if (cpu.endsWith('m')) {
res /= 1000;
} else if (cpu.endsWith('n')) {
res /= 1000000000;
}
return res;
}
// bytesToReadableFormat converts bytes to a human readable string (e.g. '1.5 GB'), assuming base 10
// there's some discussion about whether base 2 or base 10 should be used for memory units
// https://www.quora.com/Is-1-GB-equal-to-1024-MB-or-1000-MB
export function bytesToReadableFormat(memoryBytes: number) {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let unitIndex = 0;
let memoryValue = memoryBytes;
while (memoryValue > 1000 && unitIndex < units.length) {
memoryValue /= 1000;
unitIndex++;
}
return `${memoryValue.toFixed(1)} ${units[unitIndex]}`;
}
// getResourceRequests returns the total cpu and memory requests for all containers in an application
export function getResourceRequests(application: Application) {
const appContainers = applicationIsKind<Pod>('Pod', application)
? application.spec?.containers
: application.spec?.template.spec?.containers;
if (!appContainers) return null;
const requests = appContainers.reduce(
(acc, container) => {
const cpu = container.resources?.requests?.cpu;
const memory = container.resources?.requests?.memory;
if (cpu) acc.cpu += parseCpu(cpu);
if (memory) acc.memoryBytes += filesizeParser(memory, { base: 10 });
return acc;
},
{ cpu: 0, memoryBytes: 0 }
);
return requests;
}
// getResourceLimits returns the total cpu and memory limits for all containers in an application
export function getResourceLimits(application: Application) {
const appContainers = applicationIsKind<Pod>('Pod', application)
? application.spec?.containers
: application.spec?.template.spec?.containers;
if (!appContainers) return null;
const limits = appContainers.reduce(
(acc, container) => {
const cpu = container.resources?.limits?.cpu;
const memory = container.resources?.limits?.memory;
if (cpu) acc.cpu += parseCpu(cpu);
if (memory) acc.memory += filesizeParser(memory, { base: 10 });
return acc;
},
{ cpu: 0, memory: 0 }
);
return limits;
}