From 14a365045dd223c61d395937e07fe4f8bb87efd0 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Fri, 3 May 2024 09:13:33 +1200 Subject: [PATCH] fix(configs): update unused badge logic [EE-6608] (#11500) Co-authored-by: testa113 --- .../applications/application.queries.ts | 3 +- .../applications/application.service.ts | 3 +- .../kubernetes/applications/pod.service.ts | 33 +------ .../kubernetes/applications/useCronJobs.ts | 77 +++++++++++++++ app/react/kubernetes/applications/useJobs.ts | 74 ++++++++++++++ app/react/kubernetes/applications/usePods.ts | 74 ++++++++++++++ .../ConfigMapsDatatable.tsx | 36 ++++--- .../ConfigMapsDatatable/utils.test.ts | 99 +++++++++++++++++++ .../ListView/ConfigMapsDatatable/utils.ts | 86 +++++++++++----- .../SecretsDatatable/SecretsDatatable.tsx | 33 ++++--- .../ListView/SecretsDatatable/utils.test.ts | 99 +++++++++++++++++++ .../ListView/SecretsDatatable/utils.ts | 86 +++++++++++----- 12 files changed, 590 insertions(+), 113 deletions(-) create mode 100644 app/react/kubernetes/applications/useCronJobs.ts create mode 100644 app/react/kubernetes/applications/useJobs.ts create mode 100644 app/react/kubernetes/applications/usePods.ts create mode 100644 app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.test.ts create mode 100644 app/react/kubernetes/configs/ListView/SecretsDatatable/utils.test.ts diff --git a/app/react/kubernetes/applications/application.queries.ts b/app/react/kubernetes/applications/application.queries.ts index 4ea154000..5588d533c 100644 --- a/app/react/kubernetes/applications/application.queries.ts +++ b/app/react/kubernetes/applications/application.queries.ts @@ -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) => diff --git a/app/react/kubernetes/applications/application.service.ts b/app/react/kubernetes/applications/application.service.ts index b3c8ce8db..8e7911d7e 100644 --- a/app/react/kubernetes/applications/application.service.ts +++ b/app/react/kubernetes/applications/application.service.ts @@ -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) diff --git a/app/react/kubernetes/applications/pod.service.ts b/app/react/kubernetes/applications/pod.service.ts index 5e99665b1..f5f3ed683 100644 --- a/app/react/kubernetes/applications/pod.service.ts +++ b/app/react/kubernetes/applications/pod.service.ts @@ -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( - buildUrl(environmentId, namespace), - { - params: { - labelSelector, - }, - } - ); - const items = (data.items || []).map( - (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( environmentId: EnvironmentId, namespace: string, diff --git a/app/react/kubernetes/applications/useCronJobs.ts b/app/react/kubernetes/applications/useCronJobs.ts new file mode 100644 index 000000000..69a8e2362 --- /dev/null +++ b/app/react/kubernetes/applications/useCronJobs.ts @@ -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( + `/endpoints/${environmentId}/kubernetes/apis/batch/v1/namespaces/${namespace}/cronjobs`, + { + params: { + labelSelector, + }, + } + ); + const items = (data.items || []).map( + (cronJob) => + { + ...cronJob, + kind: 'CronJob', + apiVersion: data.apiVersion, + } + ); + return items; + } catch (e) { + throw parseKubernetesAxiosError( + e, + `Unable to retrieve CronJobs in namespace '${namespace}'` + ); + } +} diff --git a/app/react/kubernetes/applications/useJobs.ts b/app/react/kubernetes/applications/useJobs.ts new file mode 100644 index 000000000..2fe98f0ec --- /dev/null +++ b/app/react/kubernetes/applications/useJobs.ts @@ -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( + `/endpoints/${environmentId}/kubernetes/apis/batch/v1/namespaces/${namespace}/jobs`, + { + params: { + labelSelector, + }, + } + ); + const items = (data.items || []).map( + (job) => + { + ...job, + kind: 'Job', + apiVersion: data.apiVersion, + } + ); + return items; + } catch (e) { + throw parseKubernetesAxiosError( + e, + `Unable to retrieve Jobs in namespace '${namespace}'` + ); + } +} diff --git a/app/react/kubernetes/applications/usePods.ts b/app/react/kubernetes/applications/usePods.ts new file mode 100644 index 000000000..e1de4a164 --- /dev/null +++ b/app/react/kubernetes/applications/usePods.ts @@ -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( + `/endpoints/${environmentId}/kubernetes/api/v1/namespaces/${namespace}/pods`, + { + params: { + labelSelector, + }, + } + ); + const items = (data.items || []).map( + (pod) => + { + ...pod, + kind: 'Pod', + apiVersion: data.apiVersion, + } + ); + return items; + } catch (e) { + throw parseKubernetesAxiosError( + e, + `Unable to retrieve Pods in namespace '${namespace}'` + ); + } +} diff --git a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx index f4bdeb2af..39cdbd1bb 100644 --- a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx +++ b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/ConfigMapsDatatable.tsx @@ -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] ); } diff --git a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.test.ts b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.test.ts new file mode 100644 index 000000000..11283cb68 --- /dev/null +++ b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.test.ts @@ -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); + }); +}); diff --git a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.ts b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.ts index 343b89278..6bac4df85 100644 --- a/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.ts +++ b/app/react/kubernetes/configs/ListView/ConfigMapsDatatable/utils.ts @@ -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', 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; } diff --git a/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx b/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx index 6ab1537ed..704a58c61 100644 --- a/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx +++ b/app/react/kubernetes/configs/ListView/SecretsDatatable/SecretsDatatable.tsx @@ -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] ); } diff --git a/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.test.ts b/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.test.ts new file mode 100644 index 000000000..e964d5537 --- /dev/null +++ b/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.test.ts @@ -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); + }); +}); diff --git a/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.ts b/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.ts index b6d1d015d..ee9e3f0cc 100644 --- a/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.ts +++ b/app/react/kubernetes/configs/ListView/SecretsDatatable/utils.ts @@ -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', 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; }