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>pull/8797/head
parent
745bbb7d79
commit
98e6393274
|
@ -151,7 +151,7 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo
|
|||
|
||||
const application = {
|
||||
name: 'kubernetes.applications.application',
|
||||
url: '/:namespace/:name',
|
||||
url: '/:namespace/:name?resource-type',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'kubernetesApplicationView',
|
||||
|
|
|
@ -276,7 +276,7 @@
|
|||
</a>
|
||||
<a
|
||||
ng-if="!item.KubernetesApplications"
|
||||
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool })"
|
||||
ui-sref="kubernetes.applications.application({ name: item.Name, namespace: item.ResourcePool, 'resource-type': $ctrl.applicationTypeEnumToParamMap[item.ApplicationType] })"
|
||||
ng-click="$event.stopPropagation()"
|
||||
class="hyperlink"
|
||||
>{{ item.Name }}
|
||||
|
|
|
@ -33,6 +33,13 @@ angular.module('portainer.docker').controller('KubernetesApplicationsDatatableCo
|
|||
},
|
||||
};
|
||||
|
||||
this.applicationTypeEnumToParamMap = {
|
||||
[KubernetesApplicationTypes.DEPLOYMENT]: 'Deployment',
|
||||
[KubernetesApplicationTypes.DAEMONSET]: 'DaemonSet',
|
||||
[KubernetesApplicationTypes.STATEFULSET]: 'StatefulSet',
|
||||
[KubernetesApplicationTypes.POD]: 'Pod',
|
||||
};
|
||||
|
||||
this.expandAll = function () {
|
||||
this.state.expandAll = !this.state.expandAll;
|
||||
this.state.filteredDataSet.forEach((item) => this.expandItem(item, this.state.expandAll));
|
||||
|
|
|
@ -8,6 +8,10 @@ import { NamespaceAccessUsersSelector } from '@/react/kubernetes/namespaces/Acce
|
|||
import { CreateNamespaceRegistriesSelector } from '@/react/kubernetes/namespaces/CreateView/CreateNamespaceRegistriesSelector';
|
||||
import { KubeApplicationAccessPolicySelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationAccessPolicySelector';
|
||||
import { KubeApplicationDeploymentTypeSelector } from '@/react/kubernetes/applications/CreateView/KubeApplicationDeploymentTypeSelector';
|
||||
import { ApplicationSummaryWidget } from '@/react/kubernetes/applications/DetailsView';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withUserProvider } from '@/react/test-utils/withUserProvider';
|
||||
|
||||
export const componentsModule = angular
|
||||
.module('portainer.kubernetes.react.components', [])
|
||||
|
@ -82,4 +86,11 @@ export const componentsModule = angular
|
|||
'onChange',
|
||||
'supportGlobalDeployment',
|
||||
])
|
||||
)
|
||||
.component(
|
||||
'applicationSummaryWidget',
|
||||
r2a(
|
||||
withUIRouter(withReactQuery(withUserProvider(ApplicationSummaryWidget))),
|
||||
[]
|
||||
)
|
||||
).name;
|
||||
|
|
|
@ -25,133 +25,7 @@
|
|||
<uib-tabset active="ctrl.state.activeTab" justified="true" type="pills">
|
||||
<uib-tab index="0" classes="btn-sm" select="ctrl.selectTab(0)">
|
||||
<uib-tab-heading> <pr-icon icon="'svg-laptopcode'" class-name="'mr-1'"></pr-icon> Application </uib-tab-heading>
|
||||
<div style="padding: 20px">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td data-cy="k8sAppDetail-appName">
|
||||
{{ ctrl.application.Name }}
|
||||
<span class="label label-primary image-tag label-margins" ng-if="!ctrl.isSystemNamespace() && ctrl.isExternalApplication()">external</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stack</td>
|
||||
<td data-cy="k8sAppDetail-stackName">{{ ctrl.application.StackName || '-' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Namespace</td>
|
||||
<td data-cy="k8sAppDetail-resourcePoolName">
|
||||
<a ui-sref="kubernetes.resourcePools.resourcePool({ id: ctrl.application.ResourcePool })">{{ ctrl.application.ResourcePool }}</a>
|
||||
<span style="margin-left: 5px" class="label label-info image-tag" ng-if="ctrl.isSystemNamespace()">system</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Application Type</td>
|
||||
<td data-cy="k8sAppDetail-appType">
|
||||
{{ ctrl.application.ApplicationType | kubernetesApplicationTypeText }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD">
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.REPLICATED" data-cy="k8sAppDetail-deployType">Replicated</span>
|
||||
<span ng-if="ctrl.application.DeploymentType === ctrl.KubernetesApplicationDeploymentTypes.GLOBAL" data-cy="k8sAppDetail-appType">Global</span>
|
||||
<code data-cy="k8sAppDetail-runningPods">{{ ctrl.application.RunningPodsCount }}</code> /
|
||||
<code data-cy="k8sAppDetail-totalPods">{{ ctrl.application.TotalPodsCount }}</code>
|
||||
</td>
|
||||
<td ng-if="ctrl.application.ApplicationType === ctrl.KubernetesApplicationTypes.POD">
|
||||
{{ ctrl.application.Pods[0].Status }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="ctrl.application.Requests.Cpu || ctrl.application.Requests.Memory">
|
||||
<td>
|
||||
<div>Resource reservations</div>
|
||||
<div ng-if="ctrl.application.ApplicationType !== ctrl.KubernetesApplicationTypes.POD" class="text-muted small"> per instance </div>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-if="ctrl.application.Requests.Cpu" data-cy="k8sAppDetail-cpuReservation"
|
||||
>CPU {{ ctrl.application.Requests.Cpu | kubernetesApplicationCPUValue }}</div
|
||||
>
|
||||
<div ng-if="ctrl.application.Requests.Memory" data-cy="k8sAppDetail-memoryReservation">Memory {{ ctrl.application.Requests.Memory | humansize }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" class="vertical-center mr-1" data-cy="k8sAppDetail-owner">
|
||||
<pr-icon icon="'user'"></pr-icon> {{ ctrl.application.ApplicationOwner }}
|
||||
</span>
|
||||
<span class="vertical-center"> <pr-icon icon="'clock'" mode="'alt'"></pr-icon> {{ ctrl.application.CreationDate | getisodate }} </span>
|
||||
<span ng-if="ctrl.application.ApplicationOwner" data-cy="k8sAppDetail-creationMethod" class="vertical-center">
|
||||
<pr-icon icon="'clock'"></pr-icon> Deployed from {{ ctrl.state.appType }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<form class="form-horizontal" name="kubernetesApplicationNoteForm">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 vertical-center">
|
||||
<pr-icon icon="'edit'"></pr-icon> Note
|
||||
<button
|
||||
class="btn btn-xs btn-light vertical-center"
|
||||
ng-click="ctrl.state.expandedNote = !ctrl.state.expandedNote;"
|
||||
data-cy="k8sAppDetail-expandNoteButton"
|
||||
>{{ ctrl.state.expandedNote ? 'Collapse' : 'Expand' }}
|
||||
<pr-icon icon="'chevron-up'" ng-if="ctrl.state.expandedNote"></pr-icon>
|
||||
<pr-icon icon="'chevron-down'" ng-if="!ctrl.state.expandedNote"></pr-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.expandedNote">
|
||||
<div class="col-sm-12">
|
||||
<textarea
|
||||
class="form-control"
|
||||
name="application_note"
|
||||
id="application_note"
|
||||
ng-model="ctrl.formValues.Note"
|
||||
rows="5"
|
||||
placeholder="Enter a note about this application..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="ctrl.state.expandedNote">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
style="margin-left: 0px"
|
||||
type="button"
|
||||
ng-click="ctrl.updateApplication()"
|
||||
ng-disabled="ctrl.formValues.Note === ctrl.application.Note"
|
||||
data-cy="k8sAppDetail-saveNoteButton"
|
||||
>{{ ctrl.application.Note ? 'Update' : 'Save' }} note</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- <tr>
|
||||
<td colspan="2">
|
||||
<form class="form-horizontal" name="KubernetesApplicationRollbackForm">
|
||||
<div class="form-group">
|
||||
<label for="resource-pool-selector" class="col-sm-2 col-lg-1 control-label text-left">Version</label>
|
||||
<div class="col-sm-2">
|
||||
<select class="form-control" id="resource-pool-selector" ng-model="ctrl.formValues.SelectedRevision"
|
||||
ng-options="revision as revision.revision for revision in ctrl.application.Revisions"></select>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<button class="btn btn-primary btn-sm" style="margin-left: 0px;" type="button" ng-click="ctrl.rollbackApplication()"
|
||||
ng-disabled="ctrl.formValues.SelectedRevision.revision === ctrl.application.CurrentRevision.revision">Rollback</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr> -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<application-summary-widget></application-summary-widget>
|
||||
</uib-tab>
|
||||
|
||||
<uib-tab index="1" classes="btn-sm" select="ctrl.selectTab(1)">
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DashboardItem } from '@@/DashboardItem/DashboardItem';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { useNamespaces } from '../namespaces/queries';
|
||||
import { useApplicationsForCluster } from '../applications/queries';
|
||||
import { useApplicationsForCluster } from '../applications/application.queries';
|
||||
import { useConfigurationsForCluster } from '../configs/queries';
|
||||
import { usePVCsForCluster } from '../volumes/queries';
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { compact } from 'lodash';
|
|||
import { withError } from '@/react-tools/react-query';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { isFulfilled } from '@/react/utils';
|
||||
|
||||
import { getNamespaces } from '../namespaces/service';
|
||||
|
||||
|
@ -52,12 +53,6 @@ export function useServices(environmentId: EnvironmentId) {
|
|||
);
|
||||
}
|
||||
|
||||
function isFulfilled<T>(
|
||||
input: PromiseSettledResult<T>
|
||||
): input is PromiseFulfilledResult<T> {
|
||||
return input.status === 'fulfilled';
|
||||
}
|
||||
|
||||
export function useMutationDeleteServices(environmentId: EnvironmentId) {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation(deleteServices, {
|
||||
|
|
|
@ -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];
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { ApplicationSummaryWidget } from './ApplicationSummaryWidget';
|
|
@ -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)
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -7,6 +7,7 @@ import {
|
|||
withInvalidate,
|
||||
} from '@/react-tools/react-query';
|
||||
import { getServices } from '@/react/kubernetes/networks/services/service';
|
||||
import { isFulfilled } from '@/react/utils';
|
||||
|
||||
import {
|
||||
getIngresses,
|
||||
|
@ -193,9 +194,3 @@ export function useIngressControllers(
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
function isFulfilled<T>(
|
||||
input: PromiseSettledResult<T>
|
||||
): input is PromiseFulfilledResult<T> {
|
||||
return input.status === 'fulfilled';
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
export const systemNamespaces = [
|
||||
'kube-system',
|
||||
'kube-public',
|
||||
'kube-node-lease',
|
||||
'portainer',
|
||||
];
|
||||
|
||||
export function isSystemNamespace(namespace: string) {
|
||||
return systemNamespaces.includes(namespace || '');
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export function isFulfilled<T>(
|
||||
input: PromiseSettledResult<T>
|
||||
): input is PromiseFulfilledResult<T> {
|
||||
return input.status === 'fulfilled';
|
||||
}
|
|
@ -143,6 +143,7 @@
|
|||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/angular": "^1.8.3",
|
||||
"@types/file-saver": "^2.0.4",
|
||||
"@types/filesize-parser": "^1.5.1",
|
||||
"@types/jest": "^27.0.3",
|
||||
"@types/jquery": "^3.5.10",
|
||||
"@types/mustache": "^4.1.2",
|
||||
|
|
|
@ -4725,6 +4725,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.4.tgz#aaf9b96296150d737b2fefa535ced05ed8013d84"
|
||||
integrity sha512-sPZYQEIF/SOnLAvaz9lTuydniP+afBMtElRTdYkeV1QtEgvtJ7qolCPjly6O32QI8CbEmP5O/fztMXEDWfEcrg==
|
||||
|
||||
"@types/filesize-parser@^1.5.1":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/filesize-parser/-/filesize-parser-1.5.1.tgz#d1218f48ad160d7b089a555f3984439254196bd7"
|
||||
integrity sha512-kU4V/I/EUARPF3hxTRrZ1ad2P0/9sby1JoRbNcpvMIPqhkVlBqcNc+HutiLl6cS7iCId54Xd6X+BEp5vUgSY6Q==
|
||||
|
||||
"@types/fined@*":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/fined/-/fined-1.1.3.tgz#83f03e8f0a8d3673dfcafb18fce3571f6250e1bc"
|
||||
|
|
Loading…
Reference in New Issue