fix(configs): update unused badge logic [EE-6608] (#11500)
Co-authored-by: testa113 <testa113>pull/11764/head
parent
9b6779515e
commit
14a365045d
|
@ -13,9 +13,10 @@ import {
|
|||
getApplicationRevisionList,
|
||||
} from './application.service';
|
||||
import type { AppKind, Application, ApplicationPatch } from './types';
|
||||
import { deletePod, getNamespacePods } from './pod.service';
|
||||
import { deletePod } from './pod.service';
|
||||
import { getNamespaceHorizontalPodAutoscalers } from './autoscaling.service';
|
||||
import { applicationIsKind, matchLabelsToLabelSelectorValue } from './utils';
|
||||
import { getNamespacePods } from './usePods';
|
||||
|
||||
const queryKeys = {
|
||||
applicationsForCluster: (environmentId: EnvironmentId) =>
|
||||
|
|
|
@ -15,7 +15,7 @@ import { isFulfilled } from '@/portainer/helpers/promise-utils';
|
|||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
import { getPod, getNamespacePods, patchPod } from './pod.service';
|
||||
import { getPod, patchPod } from './pod.service';
|
||||
import { filterRevisionsByOwnerUid, getNakedPods } from './utils';
|
||||
import {
|
||||
AppKind,
|
||||
|
@ -24,6 +24,7 @@ import {
|
|||
ApplicationPatch,
|
||||
} from './types';
|
||||
import { appRevisionAnnotation } from './constants';
|
||||
import { getNamespacePods } from './usePods';
|
||||
|
||||
// This file contains services for Kubernetes apps/v1 resources (Deployments, DaemonSets, StatefulSets)
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||
import { Pod } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
@ -7,37 +7,6 @@ import { parseKubernetesAxiosError } from '../axiosError';
|
|||
|
||||
import { ApplicationPatch } from './types';
|
||||
|
||||
export async function getNamespacePods(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
labelSelector?: string
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<PodList>(
|
||||
buildUrl(environmentId, namespace),
|
||||
{
|
||||
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}'`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPod<T extends Pod | string = Pod>(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { CronJob, CronJobList } from 'kubernetes-types/batch/v1';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
const queryKeys = {
|
||||
cronJobsForCluster: (environmentId: EnvironmentId) => [
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'cronjobs',
|
||||
],
|
||||
};
|
||||
|
||||
export function useCronJobs(
|
||||
environmentId: EnvironmentId,
|
||||
namespaces?: string[]
|
||||
) {
|
||||
return useQuery(
|
||||
queryKeys.cronJobsForCluster(environmentId),
|
||||
() => getCronJobsForCluster(environmentId, namespaces),
|
||||
{
|
||||
...withError('Unable to retrieve CronJobs'),
|
||||
enabled: !!namespaces?.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCronJobsForCluster(
|
||||
environmentId: EnvironmentId,
|
||||
namespaceNames?: string[]
|
||||
) {
|
||||
if (!namespaceNames) {
|
||||
return [];
|
||||
}
|
||||
const jobs = await Promise.all(
|
||||
namespaceNames.map((namespace) =>
|
||||
getNamespaceCronJobs(environmentId, namespace)
|
||||
)
|
||||
);
|
||||
return jobs.flat();
|
||||
}
|
||||
|
||||
export async function getNamespaceCronJobs(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
labelSelector?: string
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<CronJobList>(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/batch/v1/namespaces/${namespace}/cronjobs`,
|
||||
{
|
||||
params: {
|
||||
labelSelector,
|
||||
},
|
||||
}
|
||||
);
|
||||
const items = (data.items || []).map(
|
||||
(cronJob) =>
|
||||
<CronJob>{
|
||||
...cronJob,
|
||||
kind: 'CronJob',
|
||||
apiVersion: data.apiVersion,
|
||||
}
|
||||
);
|
||||
return items;
|
||||
} catch (e) {
|
||||
throw parseKubernetesAxiosError(
|
||||
e,
|
||||
`Unable to retrieve CronJobs in namespace '${namespace}'`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { Job, JobList } from 'kubernetes-types/batch/v1';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
const queryKeys = {
|
||||
jobsForCluster: (environmentId: EnvironmentId) => [
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'jobs',
|
||||
],
|
||||
};
|
||||
|
||||
export function useJobs(environmentId: EnvironmentId, namespaces?: string[]) {
|
||||
return useQuery(
|
||||
queryKeys.jobsForCluster(environmentId),
|
||||
() => getJobsForCluster(environmentId, namespaces),
|
||||
{
|
||||
...withError('Unable to retrieve Jobs'),
|
||||
enabled: !!namespaces?.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getJobsForCluster(
|
||||
environmentId: EnvironmentId,
|
||||
namespaceNames?: string[]
|
||||
) {
|
||||
if (!namespaceNames) {
|
||||
return [];
|
||||
}
|
||||
const jobs = await Promise.all(
|
||||
namespaceNames.map((namespace) =>
|
||||
getNamespaceJobs(environmentId, namespace)
|
||||
)
|
||||
);
|
||||
return jobs.flat();
|
||||
}
|
||||
|
||||
export async function getNamespaceJobs(
|
||||
environmentId: EnvironmentId,
|
||||
namespace: string,
|
||||
labelSelector?: string
|
||||
) {
|
||||
try {
|
||||
const { data } = await axios.get<JobList>(
|
||||
`/endpoints/${environmentId}/kubernetes/apis/batch/v1/namespaces/${namespace}/jobs`,
|
||||
{
|
||||
params: {
|
||||
labelSelector,
|
||||
},
|
||||
}
|
||||
);
|
||||
const items = (data.items || []).map(
|
||||
(job) =>
|
||||
<Job>{
|
||||
...job,
|
||||
kind: 'Job',
|
||||
apiVersion: data.apiVersion,
|
||||
}
|
||||
);
|
||||
return items;
|
||||
} catch (e) {
|
||||
throw parseKubernetesAxiosError(
|
||||
e,
|
||||
`Unable to retrieve Jobs in namespace '${namespace}'`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Pod, PodList } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { withError } from '@/react-tools/react-query';
|
||||
import axios from '@/portainer/services/axios';
|
||||
|
||||
import { parseKubernetesAxiosError } from '../axiosError';
|
||||
|
||||
const queryKeys = {
|
||||
podsForCluster: (environmentId: EnvironmentId) => [
|
||||
'environments',
|
||||
environmentId,
|
||||
'kubernetes',
|
||||
'pods',
|
||||
],
|
||||
};
|
||||
|
||||
export function usePods(environemtId: EnvironmentId, namespaces?: string[]) {
|
||||
return useQuery(
|
||||
queryKeys.podsForCluster(environemtId),
|
||||
() => getPodsForCluster(environemtId, namespaces),
|
||||
{
|
||||
...withError('Unable to retrieve Pods'),
|
||||
enabled: !!namespaces?.length,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getPodsForCluster(
|
||||
environmentId: EnvironmentId,
|
||||
namespaceNames?: string[]
|
||||
) {
|
||||
if (!namespaceNames) {
|
||||
return [];
|
||||
}
|
||||
const pods = await Promise.all(
|
||||
namespaceNames.map((namespace) =>
|
||||
getNamespacePods(environmentId, namespace)
|
||||
)
|
||||
);
|
||||
return pods.flat();
|
||||
}
|
||||
|
||||
export async function getNamespacePods(
|
||||
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}'`
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,23 +1,25 @@
|
|||
import { useMemo } from 'react';
|
||||
import { FileCode } from 'lucide-react';
|
||||
import { ConfigMap } from 'kubernetes-types/core/v1';
|
||||
import { ConfigMap, Pod } from 'kubernetes-types/core/v1';
|
||||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { useApplicationsQuery } from '@/react/kubernetes/applications/application.queries';
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { pluralize } from '@/portainer/helpers/strings';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
||||
import { usePods } from '@/react/kubernetes/applications/usePods';
|
||||
import { useJobs } from '@/react/kubernetes/applications/useJobs';
|
||||
import { useCronJobs } from '@/react/kubernetes/applications/useCronJobs';
|
||||
|
||||
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||
import { AddButton } from '@@/buttons';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
import { AddButton } from '@@/buttons/AddButton';
|
||||
|
||||
import {
|
||||
useConfigMapsForCluster,
|
||||
|
@ -55,10 +57,11 @@ export function ConfigMapsDatatable() {
|
|||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
}
|
||||
);
|
||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
const podsQuery = usePods(environmentId, namespaceNames);
|
||||
const jobsQuery = useJobs(environmentId, namespaceNames);
|
||||
const cronJobsQuery = useCronJobs(environmentId, namespaceNames);
|
||||
const isInUseLoading =
|
||||
podsQuery.isLoading || jobsQuery.isLoading || cronJobsQuery.isLoading;
|
||||
|
||||
const filteredConfigMaps = useMemo(
|
||||
() =>
|
||||
|
@ -71,8 +74,10 @@ export function ConfigMapsDatatable() {
|
|||
);
|
||||
const configMapRowData = useConfigMapRowData(
|
||||
filteredConfigMaps,
|
||||
applications ?? [],
|
||||
applicationsQuery.isLoading,
|
||||
podsQuery.data ?? [],
|
||||
jobsQuery.data ?? [],
|
||||
cronJobsQuery.data ?? [],
|
||||
isInUseLoading,
|
||||
namespaces
|
||||
);
|
||||
|
||||
|
@ -112,8 +117,10 @@ export function ConfigMapsDatatable() {
|
|||
// and wraps with useMemo to prevent unnecessary calculations
|
||||
function useConfigMapRowData(
|
||||
configMaps: ConfigMap[],
|
||||
applications: Application[],
|
||||
applicationsLoading: boolean,
|
||||
pods: Pod[],
|
||||
jobs: Job[],
|
||||
cronJobs: CronJob[],
|
||||
isInUseLoading: boolean,
|
||||
namespaces?: Namespaces
|
||||
): ConfigMapRowData[] {
|
||||
return useMemo(
|
||||
|
@ -122,12 +129,13 @@ function useConfigMapRowData(
|
|||
...configMap,
|
||||
inUse:
|
||||
// if the apps are loading, set inUse to true to hide the 'unused' badge
|
||||
applicationsLoading || getIsConfigMapInUse(configMap, applications),
|
||||
isInUseLoading ||
|
||||
getIsConfigMapInUse(configMap, pods, jobs, cronJobs),
|
||||
isSystem: namespaces
|
||||
? namespaces?.[configMap.metadata?.namespace ?? '']?.IsSystem
|
||||
: false,
|
||||
})),
|
||||
[configMaps, applicationsLoading, applications, namespaces]
|
||||
[configMaps, isInUseLoading, pods, jobs, cronJobs, namespaces]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import { ConfigMap, Pod } from 'kubernetes-types/core/v1';
|
||||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
|
||||
import { getIsConfigMapInUse } from './utils';
|
||||
|
||||
describe('getIsConfigMapInUse', () => {
|
||||
it('should return false when no resources reference the configMap', () => {
|
||||
const configMap: ConfigMap = {
|
||||
metadata: { name: 'my-configmap', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsConfigMapInUse(configMap, pods, jobs, cronJobs)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when a pod references the configMap', () => {
|
||||
const configMap: ConfigMap = {
|
||||
metadata: { name: 'my-configmap', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ configMapRef: { name: 'my-configmap' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsConfigMapInUse(configMap, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a job references the configMap', () => {
|
||||
const configMap: ConfigMap = {
|
||||
metadata: { name: 'my-configmap', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ configMapRef: { name: 'my-configmap' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsConfigMapInUse(configMap, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a cronJob references the configMap', () => {
|
||||
const configMap: ConfigMap = {
|
||||
metadata: { name: 'my-configmap', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
schedule: '0 0 * * *',
|
||||
jobTemplate: {
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ configMapRef: { name: 'my-configmap' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getIsConfigMapInUse(configMap, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,33 +1,67 @@
|
|||
import { ConfigMap, Pod } from 'kubernetes-types/core/v1';
|
||||
import { ConfigMap, Pod, PodSpec } from 'kubernetes-types/core/v1';
|
||||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { applicationIsKind } from '@/react/kubernetes/applications/utils';
|
||||
|
||||
// getIsConfigMapInUse returns true if the configmap is referenced by any
|
||||
// application in the cluster
|
||||
/**
|
||||
* getIsConfigMapInUse returns true if the configmap is referenced by any pod, job, or cronjob in the same namespace
|
||||
*/
|
||||
export function getIsConfigMapInUse(
|
||||
configMap: ConfigMap,
|
||||
applications: Application[]
|
||||
pods: Pod[],
|
||||
jobs: Job[],
|
||||
cronJobs: CronJob[]
|
||||
) {
|
||||
return applications.some((app) => {
|
||||
const appSpec = applicationIsKind<Pod>('Pod', app)
|
||||
? app?.spec
|
||||
: app?.spec?.template?.spec;
|
||||
// get all podspecs from pods, jobs and cronjobs that are in the same namespace
|
||||
const podsInNamespace = pods
|
||||
.filter((pod) => pod.metadata?.namespace === configMap.metadata?.namespace)
|
||||
.map((pod) => pod.spec);
|
||||
const jobsInNamespace = jobs
|
||||
.filter((job) => job.metadata?.namespace === configMap.metadata?.namespace)
|
||||
.map((job) => job.spec?.template.spec);
|
||||
const cronJobsInNamespace = cronJobs
|
||||
.filter(
|
||||
(cronJob) => cronJob.metadata?.namespace === configMap.metadata?.namespace
|
||||
)
|
||||
.map((cronJob) => cronJob.spec?.jobTemplate.spec?.template.spec);
|
||||
const allPodSpecs = [
|
||||
...podsInNamespace,
|
||||
...jobsInNamespace,
|
||||
...cronJobsInNamespace,
|
||||
];
|
||||
|
||||
const hasEnvVarReference = appSpec?.containers.some((container) => {
|
||||
const valueFromEnv = container.env?.some(
|
||||
(envVar) =>
|
||||
envVar.valueFrom?.configMapKeyRef?.name === configMap.metadata?.name
|
||||
);
|
||||
const envFromEnv = container.envFrom?.some(
|
||||
(envVar) => envVar.configMapRef?.name === configMap.metadata?.name
|
||||
);
|
||||
return valueFromEnv || envFromEnv;
|
||||
});
|
||||
const hasVolumeReference = appSpec?.volumes?.some(
|
||||
(volume) => volume.configMap?.name === configMap.metadata?.name
|
||||
);
|
||||
|
||||
return hasEnvVarReference || hasVolumeReference;
|
||||
// check if the configmap is referenced by any pod, job or cronjob in the namespace
|
||||
const isReferenced = allPodSpecs.some((podSpec) => {
|
||||
if (!podSpec || !configMap.metadata?.name) {
|
||||
return false;
|
||||
}
|
||||
return doesPodSpecReferenceConfigMap(podSpec, configMap.metadata?.name);
|
||||
});
|
||||
|
||||
return isReferenced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a PodSpec references a specific ConfigMap.
|
||||
* @param podSpec - The PodSpec object to check.
|
||||
* @param configmapName - The name of the ConfigMap to check for references.
|
||||
* @returns A boolean indicating whether the PodSpec references the ConfigMap.
|
||||
*/
|
||||
function doesPodSpecReferenceConfigMap(
|
||||
podSpec: PodSpec,
|
||||
configmapName: string
|
||||
) {
|
||||
const hasEnvVarReference = podSpec?.containers.some((container) => {
|
||||
const valueFromEnv = container.env?.some(
|
||||
(envVar) => envVar.valueFrom?.configMapKeyRef?.name === configmapName
|
||||
);
|
||||
const envFromEnv = container.envFrom?.some(
|
||||
(envVar) => envVar.configMapRef?.name === configmapName
|
||||
);
|
||||
return valueFromEnv || envFromEnv;
|
||||
});
|
||||
|
||||
const hasVolumeReference = podSpec?.volumes?.some(
|
||||
(volume) => volume.configMap?.name === configmapName
|
||||
);
|
||||
|
||||
return hasEnvVarReference || hasVolumeReference;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Lock } from 'lucide-react';
|
||||
import { Secret } from 'kubernetes-types/core/v1';
|
||||
import { Pod, Secret } from 'kubernetes-types/core/v1';
|
||||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
||||
import { useApplicationsQuery } from '@/react/kubernetes/applications/application.queries';
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { pluralize } from '@/portainer/helpers/strings';
|
||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
||||
import { usePods } from '@/react/kubernetes/applications/usePods';
|
||||
import { useJobs } from '@/react/kubernetes/applications/useJobs';
|
||||
import { useCronJobs } from '@/react/kubernetes/applications/useCronJobs';
|
||||
|
||||
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||
import { AddButton } from '@@/buttons';
|
||||
|
@ -55,10 +57,11 @@ export function SecretsDatatable() {
|
|||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||
}
|
||||
);
|
||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
||||
environmentId,
|
||||
namespaceNames
|
||||
);
|
||||
const podsQuery = usePods(environmentId, namespaceNames);
|
||||
const jobsQuery = useJobs(environmentId, namespaceNames);
|
||||
const cronJobsQuery = useCronJobs(environmentId, namespaceNames);
|
||||
const isInUseLoading =
|
||||
podsQuery.isLoading || jobsQuery.isLoading || cronJobsQuery.isLoading;
|
||||
|
||||
const filteredSecrets = useMemo(
|
||||
() =>
|
||||
|
@ -71,8 +74,10 @@ export function SecretsDatatable() {
|
|||
);
|
||||
const secretRowData = useSecretRowData(
|
||||
filteredSecrets,
|
||||
applications ?? [],
|
||||
applicationsQuery.isLoading,
|
||||
podsQuery.data ?? [],
|
||||
jobsQuery.data ?? [],
|
||||
cronJobsQuery.data ?? [],
|
||||
isInUseLoading,
|
||||
namespaces
|
||||
);
|
||||
|
||||
|
@ -112,8 +117,10 @@ export function SecretsDatatable() {
|
|||
// and wraps with useMemo to prevent unnecessary calculations
|
||||
function useSecretRowData(
|
||||
secrets: Secret[],
|
||||
applications: Application[],
|
||||
applicationsLoading: boolean,
|
||||
pods: Pod[],
|
||||
jobs: Job[],
|
||||
cronJobs: CronJob[],
|
||||
isInUseLoading: boolean,
|
||||
namespaces?: Namespaces
|
||||
): SecretRowData[] {
|
||||
return useMemo(
|
||||
|
@ -122,12 +129,12 @@ function useSecretRowData(
|
|||
...secret,
|
||||
inUse:
|
||||
// if the apps are loading, set inUse to true to hide the 'unused' badge
|
||||
applicationsLoading || getIsSecretInUse(secret, applications),
|
||||
isInUseLoading || getIsSecretInUse(secret, pods, jobs, cronJobs),
|
||||
isSystem: namespaces
|
||||
? namespaces?.[secret.metadata?.namespace ?? '']?.IsSystem
|
||||
: false,
|
||||
})),
|
||||
[secrets, applicationsLoading, applications, namespaces]
|
||||
[secrets, isInUseLoading, pods, jobs, cronJobs, namespaces]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
import { Secret, Pod } from 'kubernetes-types/core/v1';
|
||||
|
||||
import { getIsSecretInUse } from './utils';
|
||||
|
||||
describe('getIsSecretInUse', () => {
|
||||
it('should return false when no resources reference the secret', () => {
|
||||
const secret: Secret = {
|
||||
metadata: { name: 'my-secret', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsSecretInUse(secret, pods, jobs, cronJobs)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when a pod references the secret', () => {
|
||||
const secret: Secret = {
|
||||
metadata: { name: 'my-secret', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ secretRef: { name: 'my-secret' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsSecretInUse(secret, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a job references the secret', () => {
|
||||
const secret: Secret = {
|
||||
metadata: { name: 'my-secret', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ secretRef: { name: 'my-secret' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const cronJobs: CronJob[] = [];
|
||||
|
||||
expect(getIsSecretInUse(secret, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true when a cronJob references the secret', () => {
|
||||
const secret: Secret = {
|
||||
metadata: { name: 'my-secret', namespace: 'default' },
|
||||
};
|
||||
const pods: Pod[] = [];
|
||||
const jobs: Job[] = [];
|
||||
const cronJobs: CronJob[] = [
|
||||
{
|
||||
metadata: { namespace: 'default' },
|
||||
spec: {
|
||||
schedule: '0 0 * * *',
|
||||
jobTemplate: {
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: 'container1',
|
||||
envFrom: [{ secretRef: { name: 'my-secret' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
expect(getIsSecretInUse(secret, pods, jobs, cronJobs)).toBe(true);
|
||||
});
|
||||
});
|
|
@ -1,30 +1,64 @@
|
|||
import { Secret, Pod } from 'kubernetes-types/core/v1';
|
||||
import { Secret, Pod, PodSpec } from 'kubernetes-types/core/v1';
|
||||
import { CronJob, Job } from 'kubernetes-types/batch/v1';
|
||||
|
||||
import { Application } from '@/react/kubernetes/applications/types';
|
||||
import { applicationIsKind } from '@/react/kubernetes/applications/utils';
|
||||
/**
|
||||
* getIsSecretInUse returns true if the secret is referenced by any pod, job, or cronjob in the same namespace
|
||||
*/
|
||||
export function getIsSecretInUse(
|
||||
secret: Secret,
|
||||
pods: Pod[],
|
||||
jobs: Job[],
|
||||
cronJobs: CronJob[]
|
||||
) {
|
||||
// get all podspecs from pods, jobs and cronjobs that are in the same namespace
|
||||
const podsInNamespace = pods
|
||||
.filter((pod) => pod.metadata?.namespace === secret.metadata?.namespace)
|
||||
.map((pod) => pod.spec);
|
||||
const jobsInNamespace = jobs
|
||||
.filter((job) => job.metadata?.namespace === secret.metadata?.namespace)
|
||||
.map((job) => job.spec?.template.spec);
|
||||
const cronJobsInNamespace = cronJobs
|
||||
.filter(
|
||||
(cronJob) => cronJob.metadata?.namespace === secret.metadata?.namespace
|
||||
)
|
||||
.map((cronJob) => cronJob.spec?.jobTemplate.spec?.template.spec);
|
||||
const allPodSpecs = [
|
||||
...podsInNamespace,
|
||||
...jobsInNamespace,
|
||||
...cronJobsInNamespace,
|
||||
];
|
||||
|
||||
// getIsSecretInUse returns true if the secret is referenced by any
|
||||
// application in the cluster
|
||||
export function getIsSecretInUse(secret: Secret, applications: Application[]) {
|
||||
return applications.some((app) => {
|
||||
const appSpec = applicationIsKind<Pod>('Pod', app)
|
||||
? app?.spec
|
||||
: app?.spec?.template?.spec;
|
||||
|
||||
const hasEnvVarReference = appSpec?.containers.some((container) => {
|
||||
const valueFromEnv = container.env?.some(
|
||||
(envVar) =>
|
||||
envVar.valueFrom?.secretKeyRef?.name === secret.metadata?.name
|
||||
);
|
||||
const envFromEnv = container.envFrom?.some(
|
||||
(envVar) => envVar.secretRef?.name === secret.metadata?.name
|
||||
);
|
||||
return valueFromEnv || envFromEnv;
|
||||
});
|
||||
const hasVolumeReference = appSpec?.volumes?.some(
|
||||
(volume) => volume.secret?.secretName === secret.metadata?.name
|
||||
);
|
||||
|
||||
return hasEnvVarReference || hasVolumeReference;
|
||||
// check if the secret is referenced by any pod, job or cronjob in the namespace
|
||||
const isReferenced = allPodSpecs.some((podSpec) => {
|
||||
if (!podSpec || !secret.metadata?.name) {
|
||||
return false;
|
||||
}
|
||||
return doesPodSpecReferenceSecret(podSpec, secret.metadata?.name);
|
||||
});
|
||||
|
||||
return isReferenced;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a PodSpec references a specific Secret.
|
||||
* @param podSpec - The PodSpec object to check.
|
||||
* @param secretName - The name of the Secret to check for references.
|
||||
* @returns A boolean indicating whether the PodSpec references the Secret.
|
||||
*/
|
||||
function doesPodSpecReferenceSecret(podSpec: PodSpec, secretName: string) {
|
||||
const hasEnvVarReference = podSpec?.containers.some((container) => {
|
||||
const valueFromEnv = container.env?.some(
|
||||
(envVar) => envVar.valueFrom?.secretKeyRef?.name === secretName
|
||||
);
|
||||
const envFromEnv = container.envFrom?.some(
|
||||
(envVar) => envVar.secretRef?.name === secretName
|
||||
);
|
||||
return valueFromEnv || envFromEnv;
|
||||
});
|
||||
|
||||
const hasVolumeReference = podSpec?.volumes?.some(
|
||||
(volume) => volume.secret?.secretName === secretName
|
||||
);
|
||||
|
||||
return hasEnvVarReference || hasVolumeReference;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue