diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx index 79a1b6a2761..cd042c5398b 100644 --- a/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx +++ b/frontend/src/components/resourceMap/KubeObjectGlance/KubeObjectGlance.tsx @@ -26,6 +26,7 @@ import HPA from '../../../lib/k8s/hpa'; import Pod from '../../../lib/k8s/pod'; import ReplicaSet from '../../../lib/k8s/replicaSet'; import Service from '../../../lib/k8s/service'; +import StatefulSet from '../../../lib/k8s/statefulSet'; import { DateLabel } from '../../common/Label'; import { DeploymentGlance } from './DeploymentGlance'; import { EndpointsGlance } from './EndpointsGlance'; @@ -46,32 +47,30 @@ export const KubeObjectGlance = memo(({ resource }: { resource: KubeObject }) => ); }, []); - const kind = resource.kind; - const sections = []; - if (kind === 'Pod') { - sections.push(); + if (Pod.isClassOf(resource)) { + sections.push(); } - if (kind === 'Deployment') { - sections.push(); + if (Deployment.isClassOf(resource)) { + sections.push(); } - if (kind === 'Service') { - sections.push(); + if (Service.isClassOf(resource)) { + sections.push(); } - if (kind === 'Endpoints') { - sections.push(); + if (Endpoints.isClassOf(resource)) { + sections.push(); } - if (kind === 'ReplicaSet' || kind === 'StatefulSet') { - sections.push(); + if (ReplicaSet.isClassOf(resource) || StatefulSet.isClassOf(resource)) { + sections.push(); } - if (kind === 'HorizontalPodAutoscaler') { - sections.push(); + if (HPA.isClassOf(resource)) { + sections.push(); } if (events.length > 0) { diff --git a/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx b/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx index 619c8d9c053..8afc26eae6c 100644 --- a/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx +++ b/frontend/src/components/resourceMap/KubeObjectGlance/ReplicaSetGlance.tsx @@ -16,10 +16,11 @@ import { Box } from '@mui/system'; import { useTranslation } from 'react-i18next'; -import ReplicaSet from '../../../lib/k8s/replicaSet'; +import type ReplicaSet from '../../../lib/k8s/replicaSet'; +import type StatefulSet from '../../../lib/k8s/statefulSet'; import { StatusLabel } from '../../common/Label'; -export function ReplicaSetGlance({ set }: { set: ReplicaSet }) { +export function ReplicaSetGlance({ set }: { set: ReplicaSet | StatefulSet }) { const { t } = useTranslation(); const ready = set.status?.readyReplicas || 0; const desired = set.spec?.replicas || 0; diff --git a/frontend/src/components/resourceMap/graph/graphFiltering.test.ts b/frontend/src/components/resourceMap/graph/graphFiltering.test.ts index 894fbc6a1f7..fc7c45337f1 100644 --- a/frontend/src/components/resourceMap/graph/graphFiltering.test.ts +++ b/frontend/src/components/resourceMap/graph/graphFiltering.test.ts @@ -14,32 +14,41 @@ * limitations under the License. */ +import App from '../../../App'; import { KubeMetadata } from '../../../lib/k8s/KubeMetadata'; -import { KubeObject } from '../../../lib/k8s/KubeObject'; +import Pod from '../../../lib/k8s/pod'; import { filterGraph, GraphFilter } from './graphFiltering'; import { GraphEdge, GraphNode } from './graphModel'; +// circular dependency fix +// eslint-disable-next-line no-unused-vars +const _dont_delete_me = App; + describe('filterGraph', () => { const nodes: GraphNode[] = [ { id: '1', - kubeObject: { metadata: { namespace: 'ns1', name: 'node1' } as KubeMetadata } as KubeObject, + kubeObject: new Pod({ + kind: 'Pod', + metadata: { namespace: 'ns1', name: 'node1' }, + status: {}, + } as any), }, { id: '2', - kubeObject: { + kubeObject: new Pod({ kind: 'Pod', metadata: { namespace: 'ns2' } as KubeMetadata, status: { phase: 'Failed' }, - } as any, + } as any), }, { id: '3', - kubeObject: { metadata: { namespace: 'ns3' } as KubeMetadata } as KubeObject, + kubeObject: new Pod({ kind: 'Pod', metadata: { namespace: 'ns3' }, status: {} } as any), }, { id: '4', - kubeObject: { metadata: { namespace: 'ns3' } as KubeMetadata } as KubeObject, + kubeObject: new Pod({ kind: 'Pod', metadata: { namespace: 'ns3' }, status: {} } as any), }, ]; diff --git a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx index 1daf0819650..f721e4367bd 100644 --- a/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx +++ b/frontend/src/components/resourceMap/nodes/KubeObjectStatus.tsx @@ -47,7 +47,7 @@ function getPodStatus(pod: Pod): KubeObjectStatus { * Not all kinds of resources have a status and/or supported */ export function getStatus(w: KubeObject): KubeObjectStatus { - if (w.kind === 'Pod') return getPodStatus(w as Pod); + if (Pod.isClassOf(w)) return getPodStatus(w); if (['DaemonSet', 'ReplicaSet', 'StatefulSet', 'Deployment'].includes(w.kind)) { const workload = w as Workload; diff --git a/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx b/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx new file mode 100644 index 00000000000..2991d92777f --- /dev/null +++ b/frontend/src/components/resourceMap/sources/definitions/graphDefinitionUtils.tsx @@ -0,0 +1,24 @@ +/* + * Copyright 2025 The Kubernetes Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { KubeObjectClass } from '../../../../lib/k8s/KubeObject'; + +/** Create a unique API resource ID consisting of the API group name and kind */ +export function makeKubeSourceId(kubeObjectClass: KubeObjectClass) { + const group = kubeObjectClass.apiGroupName; + + return group ? group + '/' + kubeObjectClass.kind : kubeObjectClass.kind; +} diff --git a/frontend/src/components/resourceMap/sources/definitions/relations.tsx b/frontend/src/components/resourceMap/sources/definitions/relations.tsx index 8ef61360136..03c58e84ff9 100644 --- a/frontend/src/components/resourceMap/sources/definitions/relations.tsx +++ b/frontend/src/components/resourceMap/sources/definitions/relations.tsx @@ -45,6 +45,7 @@ import StatefulSet from '../../../../lib/k8s/statefulSet'; import ValidatingWebhookConfiguration from '../../../../lib/k8s/validatingWebhookConfiguration'; import { useNamespaces } from '../../../../redux/filterSlice'; import { Relation } from '../../graph/graphModel'; +import { makeKubeSourceId } from './graphDefinitionUtils'; /** * Check if the given item has matching labels @@ -62,8 +63,8 @@ const makeRelation = ( to: To, selector: (a: InstanceType, b: InstanceType) => unknown ): Relation => ({ - fromSource: from.kind, - toSource: to.kind, + fromSource: makeKubeSourceId(from), + toSource: makeKubeSourceId(to), predicate(fromNode, toNode) { const fromObject = fromNode.kubeObject as InstanceType; const toObject = toNode.kubeObject as InstanceType; @@ -73,7 +74,7 @@ const makeRelation = ( }); const makeOwnerRelation = (cl: KubeObjectClass): Relation => ({ - fromSource: cl.kind, + fromSource: makeKubeSourceId(cl), predicate(from, to) { const obj = from.kubeObject as KubeObject; @@ -85,7 +86,7 @@ const makeOwnerRelation = (cl: KubeObjectClass): Relation => ({ }); const makeOwnerRelationReversed = (cl: KubeObjectClass): Relation => ({ - fromSource: cl.kind, + fromSource: makeKubeSourceId(cl), predicate(from, to) { const obj = to.kubeObject as KubeObject; diff --git a/frontend/src/components/resourceMap/sources/definitions/sources.tsx b/frontend/src/components/resourceMap/sources/definitions/sources.tsx index 333d091857a..6142a40a9c4 100644 --- a/frontend/src/components/resourceMap/sources/definitions/sources.tsx +++ b/frontend/src/components/resourceMap/sources/definitions/sources.tsx @@ -51,12 +51,13 @@ import { useNamespaces } from '../../../../redux/filterSlice'; import { GraphSource } from '../../graph/graphModel'; import { getKindGroupColor, KubeIcon } from '../../kubeIcon/KubeIcon'; import { makeKubeObjectNode } from '../GraphSources'; +import { makeKubeSourceId } from './graphDefinitionUtils'; /** * Create a GraphSource from KubeObject class definition */ const makeKubeSource = (cl: KubeObjectClass): GraphSource => ({ - id: cl.kind, + id: makeKubeSourceId(cl), label: cl.apiName, icon: , useData() { @@ -72,6 +73,8 @@ const generateCRSources = (crds: CRD[]): GraphSource[] => { for (const crd of crds) { const [group] = crd.getMainAPIGroup(); const source = makeKubeSource(crd.makeCRClass()); + // Add crd prefix to avoid id clashes with resources already defined in other places + source.id = 'crd-' + source.id; if (!groupedSources.has(group)) { groupedSources.set(group, []); diff --git a/frontend/src/lib/k8s/KubeObject.ts b/frontend/src/lib/k8s/KubeObject.ts index 8b5dcf5b994..0b20e587dc3 100644 --- a/frontend/src/lib/k8s/KubeObject.ts +++ b/frontend/src/lib/k8s/KubeObject.ts @@ -122,6 +122,41 @@ export class KubeObject { return this.kind; } + /** + * Get name of the API group of this resource + * for example will return batch for CronJob + * + * For core group, like Pods, it will return undefined + * + * API group reference https://kubernetes.io/docs/reference/using-api/#api-groups + */ + static get apiGroupName(): string | undefined { + // Get any of the versions, group will be the same + const apiVersion = typeof this.apiVersion === 'string' ? this.apiVersion : this.apiVersion[0]; + + if (!apiVersion.includes('/')) return; + + return apiVersion.split('/')[0]; + } + + /** + * Type guard to check if a KubeObject instance belongs to this class. + * Compares API group name and kind to determine if the instance matches. + * This works even if class definitions are duplicated and should be used + * instead of `instanceof`. + * + * @param maybeInstance - The KubeObject instance to check. + * @returns True if the instance is of this class type, with narrowed type. + */ + static isClassOf( + this: K, + maybeInstance: KubeObject + ): maybeInstance is InstanceType { + return ( + maybeInstance._class().apiGroupName === this.apiGroupName && maybeInstance.kind === this.kind + ); + } + static get pluralName(): string { // This is a naive way to get the plural name of the object by default. It will // work in most cases, but for exceptions (like Ingress), we must override this.