Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -46,32 +47,30 @@ export const KubeObjectGlance = memo(({ resource }: { resource: KubeObject }) =>
);
}, []);

const kind = resource.kind;

const sections = [];

if (kind === 'Pod') {
sections.push(<PodGlance pod={resource as Pod} />);
if (Pod.isClassOf(resource)) {
sections.push(<PodGlance pod={resource} />);
}

if (kind === 'Deployment') {
sections.push(<DeploymentGlance deployment={resource as Deployment} />);
if (Deployment.isClassOf(resource)) {
sections.push(<DeploymentGlance deployment={resource} />);
}

if (kind === 'Service') {
sections.push(<ServiceGlance service={resource as Service} />);
if (Service.isClassOf(resource)) {
sections.push(<ServiceGlance service={resource} />);
}

if (kind === 'Endpoints') {
sections.push(<EndpointsGlance endpoints={resource as Endpoints} />);
if (Endpoints.isClassOf(resource)) {
sections.push(<EndpointsGlance endpoints={resource} />);
}

if (kind === 'ReplicaSet' || kind === 'StatefulSet') {
sections.push(<ReplicaSetGlance set={resource as ReplicaSet} />);
if (ReplicaSet.isClassOf(resource) || StatefulSet.isClassOf(resource)) {
sections.push(<ReplicaSetGlance set={resource} />);
}

if (kind === 'HorizontalPodAutoscaler') {
sections.push(<HorizontalPodAutoscalerGlance hpa={resource as HPA} />);
if (HPA.isClassOf(resource)) {
sections.push(<HorizontalPodAutoscalerGlance hpa={resource} />);
}

if (events.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/components/resourceMap/graph/graphFiltering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -62,8 +63,8 @@ const makeRelation = <From extends KubeObjectClass, To extends KubeObjectClass>(
to: To,
selector: (a: InstanceType<From>, b: InstanceType<To>) => unknown
): Relation => ({
fromSource: from.kind,
toSource: to.kind,
fromSource: makeKubeSourceId(from),
toSource: makeKubeSourceId(to),
predicate(fromNode, toNode) {
const fromObject = fromNode.kubeObject as InstanceType<From>;
const toObject = toNode.kubeObject as InstanceType<To>;
Expand All @@ -73,7 +74,7 @@ const makeRelation = <From extends KubeObjectClass, To extends KubeObjectClass>(
});

const makeOwnerRelation = (cl: KubeObjectClass): Relation => ({
fromSource: cl.kind,
fromSource: makeKubeSourceId(cl),
predicate(from, to) {
const obj = from.kubeObject as KubeObject;

Expand All @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: <KubeIcon kind={cl.kind as any} />,
useData() {
Expand All @@ -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, []);
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/lib/k8s/KubeObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,41 @@ export class KubeObject<T extends KubeObjectInterface | KubeEvent = any> {
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<K extends KubeObjectClass>(
this: K,
maybeInstance: KubeObject
): maybeInstance is InstanceType<K> {
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.
Expand Down
Loading