mirror of https://github.com/portainer/portainer
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,
|
getApplicationRevisionList,
|
||||||
} from './application.service';
|
} from './application.service';
|
||||||
import type { AppKind, Application, ApplicationPatch } from './types';
|
import type { AppKind, Application, ApplicationPatch } from './types';
|
||||||
import { deletePod, getNamespacePods } from './pod.service';
|
import { deletePod } from './pod.service';
|
||||||
import { getNamespaceHorizontalPodAutoscalers } from './autoscaling.service';
|
import { getNamespaceHorizontalPodAutoscalers } from './autoscaling.service';
|
||||||
import { applicationIsKind, matchLabelsToLabelSelectorValue } from './utils';
|
import { applicationIsKind, matchLabelsToLabelSelectorValue } from './utils';
|
||||||
|
import { getNamespacePods } from './usePods';
|
||||||
|
|
||||||
const queryKeys = {
|
const queryKeys = {
|
||||||
applicationsForCluster: (environmentId: EnvironmentId) =>
|
applicationsForCluster: (environmentId: EnvironmentId) =>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import { isFulfilled } from '@/portainer/helpers/promise-utils';
|
||||||
|
|
||||||
import { parseKubernetesAxiosError } from '../axiosError';
|
import { parseKubernetesAxiosError } from '../axiosError';
|
||||||
|
|
||||||
import { getPod, getNamespacePods, patchPod } from './pod.service';
|
import { getPod, patchPod } from './pod.service';
|
||||||
import { filterRevisionsByOwnerUid, getNakedPods } from './utils';
|
import { filterRevisionsByOwnerUid, getNakedPods } from './utils';
|
||||||
import {
|
import {
|
||||||
AppKind,
|
AppKind,
|
||||||
|
@ -24,6 +24,7 @@ import {
|
||||||
ApplicationPatch,
|
ApplicationPatch,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { appRevisionAnnotation } from './constants';
|
import { appRevisionAnnotation } from './constants';
|
||||||
|
import { getNamespacePods } from './usePods';
|
||||||
|
|
||||||
// This file contains services for Kubernetes apps/v1 resources (Deployments, DaemonSets, StatefulSets)
|
// 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 { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||||
|
@ -7,37 +7,6 @@ import { parseKubernetesAxiosError } from '../axiosError';
|
||||||
|
|
||||||
import { ApplicationPatch } from './types';
|
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>(
|
export async function getPod<T extends Pod | string = Pod>(
|
||||||
environmentId: EnvironmentId,
|
environmentId: EnvironmentId,
|
||||||
namespace: string,
|
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 { useMemo } from 'react';
|
||||||
import { FileCode } from 'lucide-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 { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
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 { pluralize } from '@/portainer/helpers/strings';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
||||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
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 { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||||
import { AddButton } from '@@/buttons';
|
|
||||||
import { useTableState } from '@@/datatables/useTableState';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||||
|
import { AddButton } from '@@/buttons/AddButton';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useConfigMapsForCluster,
|
useConfigMapsForCluster,
|
||||||
|
@ -55,10 +57,11 @@ export function ConfigMapsDatatable() {
|
||||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
const podsQuery = usePods(environmentId, namespaceNames);
|
||||||
environmentId,
|
const jobsQuery = useJobs(environmentId, namespaceNames);
|
||||||
namespaceNames
|
const cronJobsQuery = useCronJobs(environmentId, namespaceNames);
|
||||||
);
|
const isInUseLoading =
|
||||||
|
podsQuery.isLoading || jobsQuery.isLoading || cronJobsQuery.isLoading;
|
||||||
|
|
||||||
const filteredConfigMaps = useMemo(
|
const filteredConfigMaps = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -71,8 +74,10 @@ export function ConfigMapsDatatable() {
|
||||||
);
|
);
|
||||||
const configMapRowData = useConfigMapRowData(
|
const configMapRowData = useConfigMapRowData(
|
||||||
filteredConfigMaps,
|
filteredConfigMaps,
|
||||||
applications ?? [],
|
podsQuery.data ?? [],
|
||||||
applicationsQuery.isLoading,
|
jobsQuery.data ?? [],
|
||||||
|
cronJobsQuery.data ?? [],
|
||||||
|
isInUseLoading,
|
||||||
namespaces
|
namespaces
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -112,8 +117,10 @@ export function ConfigMapsDatatable() {
|
||||||
// and wraps with useMemo to prevent unnecessary calculations
|
// and wraps with useMemo to prevent unnecessary calculations
|
||||||
function useConfigMapRowData(
|
function useConfigMapRowData(
|
||||||
configMaps: ConfigMap[],
|
configMaps: ConfigMap[],
|
||||||
applications: Application[],
|
pods: Pod[],
|
||||||
applicationsLoading: boolean,
|
jobs: Job[],
|
||||||
|
cronJobs: CronJob[],
|
||||||
|
isInUseLoading: boolean,
|
||||||
namespaces?: Namespaces
|
namespaces?: Namespaces
|
||||||
): ConfigMapRowData[] {
|
): ConfigMapRowData[] {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -122,12 +129,13 @@ function useConfigMapRowData(
|
||||||
...configMap,
|
...configMap,
|
||||||
inUse:
|
inUse:
|
||||||
// if the apps are loading, set inUse to true to hide the 'unused' badge
|
// 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
|
isSystem: namespaces
|
||||||
? namespaces?.[configMap.metadata?.namespace ?? '']?.IsSystem
|
? namespaces?.[configMap.metadata?.namespace ?? '']?.IsSystem
|
||||||
: false,
|
: 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 pod, job, or cronjob in the same namespace
|
||||||
|
*/
|
||||||
// getIsConfigMapInUse returns true if the configmap is referenced by any
|
|
||||||
// application in the cluster
|
|
||||||
export function getIsConfigMapInUse(
|
export function getIsConfigMapInUse(
|
||||||
configMap: ConfigMap,
|
configMap: ConfigMap,
|
||||||
applications: Application[]
|
pods: Pod[],
|
||||||
|
jobs: Job[],
|
||||||
|
cronJobs: CronJob[]
|
||||||
) {
|
) {
|
||||||
return applications.some((app) => {
|
// get all podspecs from pods, jobs and cronjobs that are in the same namespace
|
||||||
const appSpec = applicationIsKind<Pod>('Pod', app)
|
const podsInNamespace = pods
|
||||||
? app?.spec
|
.filter((pod) => pod.metadata?.namespace === configMap.metadata?.namespace)
|
||||||
: app?.spec?.template?.spec;
|
.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) => {
|
// check if the configmap is referenced by any pod, job or cronjob in the namespace
|
||||||
const valueFromEnv = container.env?.some(
|
const isReferenced = allPodSpecs.some((podSpec) => {
|
||||||
(envVar) =>
|
if (!podSpec || !configMap.metadata?.name) {
|
||||||
envVar.valueFrom?.configMapKeyRef?.name === configMap.metadata?.name
|
return false;
|
||||||
);
|
}
|
||||||
const envFromEnv = container.envFrom?.some(
|
return doesPodSpecReferenceConfigMap(podSpec, configMap.metadata?.name);
|
||||||
(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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 { useMemo } from 'react';
|
||||||
import { Lock } from 'lucide-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 { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
import { Authorized, useAuthorizations } from '@/react/hooks/useUser';
|
||||||
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
import { DefaultDatatableSettings } from '@/react/kubernetes/datatables/DefaultDatatableSettings';
|
||||||
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
import { createStore } from '@/react/kubernetes/datatables/default-kube-datatable-store';
|
||||||
import { SystemResourceDescription } from '@/react/kubernetes/datatables/SystemResourceDescription';
|
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 { pluralize } from '@/portainer/helpers/strings';
|
||||||
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
import { useNamespacesQuery } from '@/react/kubernetes/namespaces/queries/useNamespacesQuery';
|
||||||
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
import { Namespaces } from '@/react/kubernetes/namespaces/types';
|
||||||
import { CreateFromManifestButton } from '@/react/kubernetes/components/CreateFromManifestButton';
|
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 { Datatable, TableSettingsMenu } from '@@/datatables';
|
||||||
import { AddButton } from '@@/buttons';
|
import { AddButton } from '@@/buttons';
|
||||||
|
@ -55,10 +57,11 @@ export function SecretsDatatable() {
|
||||||
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
autoRefreshRate: tableState.autoRefreshRate * 1000,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { data: applications, ...applicationsQuery } = useApplicationsQuery(
|
const podsQuery = usePods(environmentId, namespaceNames);
|
||||||
environmentId,
|
const jobsQuery = useJobs(environmentId, namespaceNames);
|
||||||
namespaceNames
|
const cronJobsQuery = useCronJobs(environmentId, namespaceNames);
|
||||||
);
|
const isInUseLoading =
|
||||||
|
podsQuery.isLoading || jobsQuery.isLoading || cronJobsQuery.isLoading;
|
||||||
|
|
||||||
const filteredSecrets = useMemo(
|
const filteredSecrets = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -71,8 +74,10 @@ export function SecretsDatatable() {
|
||||||
);
|
);
|
||||||
const secretRowData = useSecretRowData(
|
const secretRowData = useSecretRowData(
|
||||||
filteredSecrets,
|
filteredSecrets,
|
||||||
applications ?? [],
|
podsQuery.data ?? [],
|
||||||
applicationsQuery.isLoading,
|
jobsQuery.data ?? [],
|
||||||
|
cronJobsQuery.data ?? [],
|
||||||
|
isInUseLoading,
|
||||||
namespaces
|
namespaces
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -112,8 +117,10 @@ export function SecretsDatatable() {
|
||||||
// and wraps with useMemo to prevent unnecessary calculations
|
// and wraps with useMemo to prevent unnecessary calculations
|
||||||
function useSecretRowData(
|
function useSecretRowData(
|
||||||
secrets: Secret[],
|
secrets: Secret[],
|
||||||
applications: Application[],
|
pods: Pod[],
|
||||||
applicationsLoading: boolean,
|
jobs: Job[],
|
||||||
|
cronJobs: CronJob[],
|
||||||
|
isInUseLoading: boolean,
|
||||||
namespaces?: Namespaces
|
namespaces?: Namespaces
|
||||||
): SecretRowData[] {
|
): SecretRowData[] {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -122,12 +129,12 @@ function useSecretRowData(
|
||||||
...secret,
|
...secret,
|
||||||
inUse:
|
inUse:
|
||||||
// if the apps are loading, set inUse to true to hide the 'unused' badge
|
// 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
|
isSystem: namespaces
|
||||||
? namespaces?.[secret.metadata?.namespace ?? '']?.IsSystem
|
? namespaces?.[secret.metadata?.namespace ?? '']?.IsSystem
|
||||||
: false,
|
: 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
|
// check if the secret is referenced by any pod, job or cronjob in the namespace
|
||||||
// application in the cluster
|
const isReferenced = allPodSpecs.some((podSpec) => {
|
||||||
export function getIsSecretInUse(secret: Secret, applications: Application[]) {
|
if (!podSpec || !secret.metadata?.name) {
|
||||||
return applications.some((app) => {
|
return false;
|
||||||
const appSpec = applicationIsKind<Pod>('Pod', app)
|
}
|
||||||
? app?.spec
|
return doesPodSpecReferenceSecret(podSpec, secret.metadata?.name);
|
||||||
: 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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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