Skip to content
Open
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
13 changes: 9 additions & 4 deletions frontend/packages/console-app/locales/en/console-app.json
Original file line number Diff line number Diff line change
Expand Up @@ -354,15 +354,23 @@
"Mark unschedulable": "Mark unschedulable",
"View events": "View events",
"Activity": "Activity",
"Disk": "Disk",
"Network": "Network",
"NIC": "NIC",
"CPU": "CPU",
"Details": "Details",
"Node name": "Node name",
"Instance type": "Instance type",
"Not available": "Not available",
"Node addresses": "Node addresses",
"Uptime": "Uptime",
"Inventory": "Inventory",
"Image": "Image",
"Images": "Images",
"Image": "Image",
"Virtual machines (CNV)": "Virtual machines (CNV)",
"This count is based on your access permissions and might not include all virtual machines.": "This count is based on your access permissions and might not include all virtual machines.",
"Virtual machine": "Virtual machine",
"Virtual machines": "Virtual machines",
"Health checks": "Health checks",
"See details": "See details",
"{{ cpuMessage }}": "{{ cpuMessage }}",
Expand All @@ -374,7 +382,6 @@
"Only one {{ machineHealthCheckLabel }} resource should match this node.": "Only one {{ machineHealthCheckLabel }} resource should match this node.",
"Not configured": "Not configured",
"No conditions": "No conditions",
"CPU": "CPU",
"Memory": "Memory",
"Network in": "Network in",
"Network out": "Network out",
Expand Down Expand Up @@ -407,8 +414,6 @@
"Kubelet version": "Kubelet version",
"Kube-Proxy version": "Kube-Proxy version",
"Machine set": "Machine set",
"Virtual machines": "Virtual machines",
"This count is based on your access permissions and might not include all virtual machines.": "This count is based on your access permissions and might not include all virtual machines.",
"MachineConfigPool": "MachineConfigPool",
"{{formattedCores}} cores / {{totalCores}} cores": "{{formattedCores}} cores / {{totalCores}} cores",
"Node": "Node",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {
K8sGroupVersionKind,
K8sResourceKind,
WatchK8sResult,
} from '@console/dynamic-plugin-sdk/src';
import { useFlag } from '@console/dynamic-plugin-sdk/src/utils/flags';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks';
import { K8sKind } from '@console/internal/module/k8s';

export const BAREMETAL_FLAG = 'BAREMETAL';

export const BareMetalHostModel: K8sKind = {
label: 'Bare Metal Host',
labelPlural: 'Bare Metal Hosts',
apiVersion: 'v1alpha1',
apiGroup: 'metal3.io',
plural: 'baremetalhosts',
abbr: 'BMH',
namespaced: true,
kind: 'BareMetalHost',
id: 'baremetalhost',
crd: true,
};

export const BareMetalHostGroupVersionKind: K8sGroupVersionKind = {
group: 'metal3.io',
kind: 'BareMetalHost',
version: 'v1alpha1',
};

export const useIsBareMetalPluginActive = () => useFlag(BAREMETAL_FLAG);

export const useWatchBareMetalHost = (nodeName: string): WatchK8sResult<K8sResourceKind> => {
const isBareMetalPluginActive = useIsBareMetalPluginActive();

const [bareMetalHosts, bareMetalHostsLoaded, bareMetalHostsLoadError] = useK8sWatchResource<
K8sResourceKind[]
>(
isBareMetalPluginActive
? {
isList: true,
groupVersionKind: BareMetalHostGroupVersionKind,
fieldSelector: `metadata.name=${nodeName}`,
}
: undefined,
);

return [bareMetalHosts?.[0], bareMetalHostsLoaded, bareMetalHostsLoadError];
};
Comment on lines +33 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for BareMetalHost usage and namespace patterns
rg -n "BareMetalHost\|namespace\|fieldSelector" frontend/packages/console-app/src/components/nodes/NodeBareMetalUtils.ts -A 2 -B 2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

# Find all files that import or reference BareMetalHost
rg -l "BareMetalHost\|useWatchBareMetalHost" frontend/packages --type=ts --type=tsx

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Search for K8s namespace patterns in similar resource watchers
rg -n "useK8sWatchResource" frontend/packages/console-app/src/components --type=ts --type=tsx -A 3 | head -80

Repository: openshift/console

Length of output: 88


🏁 Script executed:

# Look for baremetal-related configuration or documentation
fd -e md -e ts -e tsx baremetal frontend/packages --type f

Repository: openshift/console

Length of output: 1873


🏁 Script executed:

# Find usages of useWatchBareMetalHost
rg -n "useWatchBareMetalHost" frontend/packages --type=ts -B 2 -A 3

Repository: openshift/console

Length of output: 2473


🏁 Script executed:

# Check the complete NodeBareMetalUtils.ts file
cat -n frontend/packages/console-app/src/components/nodes/NodeBareMetalUtils.ts

Repository: openshift/console

Length of output: 2213


🏁 Script executed:

# Search for BareMetalHost namespace patterns in metal3-plugin
rg -n "BareMetalHost" frontend/packages/metal3-plugin/src --type=ts -B 1 -A 1 | head -120

Repository: openshift/console

Length of output: 9649


🏁 Script executed:

# Check how useK8sWatchResource is used with namespaced resources
rg -n "useK8sWatchResource" frontend/packages/console-app/src/components --type=ts -B 2 -A 8 | head -100

Repository: openshift/console

Length of output: 13262


🏁 Script executed:

# Check how BareMetalInventoryItems calls the hook and if it extracts namespace
cat -n frontend/packages/console-app/src/components/nodes/node-dashboard/BareMetalInventoryItems.tsx | head -80

Repository: openshift/console

Length of output: 2958


🏁 Script executed:

# Look for namespace handling in metal3-plugin for BMH resources
rg -n "namespace.*BMH\|BMH.*namespace\|openshift-machine-api" frontend/packages/metal3-plugin/src --type=ts -B 2 -A 2 | head -100

Repository: openshift/console

Length of output: 43


🏁 Script executed:

# Check what the node object contains (obj parameter)
rg -n "obj\\.metadata" frontend/packages/console-app/src/components/nodes/node-dashboard/BareMetalInventoryItems.tsx -B 1 -A 1

Repository: openshift/console

Length of output: 191


🏁 Script executed:

# Double-check: does useK8sWatchResource support implicit namespace behavior?
# Look at how it's typically used in the codebase with and without namespace
rg -n "useK8sWatchResource" frontend/packages/console-app/src/components/persistent-volumes -A 5 | head -60

Repository: openshift/console

Length of output: 164


🏁 Script executed:

# Check if there's any Metal3 operator documentation about BMH namespace
rg -n "openshift-machine-api\|default.*namespace\|BMH.*namespace" frontend/packages --type=ts | head -20

Repository: openshift/console

Length of output: 43


🏁 Script executed:

# Search for useK8sWatchResource patterns with namespaced resources
rg -n "useK8sWatchResource" frontend/packages/console-app/src -A 6 | grep -A 6 "isList.*true" | head -80

Repository: openshift/console

Length of output: 7969


🏁 Script executed:

# Check metal3 constants or configuration for namespace hints
rg -n "namespace\|NAMESPACE\|openshift-machine" frontend/packages/metal3-plugin/src/constants -A 2 -B 2

Repository: openshift/console

Length of output: 43


🏁 Script executed:

# Look at actual K8s hook implementation to understand default behavior
find frontend/packages -name "*k8s*hook*" -o -name "*watch*" | grep -E "\.(ts|js)$" | head -10

Repository: openshift/console

Length of output: 285


🏁 Script executed:

# Check the watch hook type definitions to understand namespace behavior
cat -n frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watch-types.ts

Repository: openshift/console

Length of output: 1076


🏁 Script executed:

# Look at the watcher implementation
head -100 frontend/packages/console-dynamic-plugin-sdk/src/utils/k8s/hooks/k8s-watcher.ts

Repository: openshift/console

Length of output: 3090


Add namespace parameter to useWatchBareMetalHost—BMH is a namespaced resource but the hook watches cluster-wide.

Line 33: The hook signature omits the namespace parameter, and line 40–44 doesn't scope the watch to any namespace. Since BareMetalHostModel is marked namespaced: true (line 19), the resource watch should explicitly include namespace in the resource config; otherwise, useK8sWatchResource queries all namespaces and relies only on fieldSelector: metadata.name to filter results. This can fail if multiple BMH resources with identical names exist across namespaces.

Update the hook to accept and pass the namespace:

export const useWatchBareMetalHost = (nodeName: string, namespace: string): WatchK8sResult<K8sResourceKind> => {
  const isBareMetalPluginActive = useIsBareMetalPluginActive();

  const [bareMetalHosts, bareMetalHostsLoaded, bareMetalHostsLoadError] = useK8sWatchResource<
    K8sResourceKind[]
  >(
    isBareMetalPluginActive
      ? {
          isList: true,
          groupVersionKind: BareMetalHostGroupVersionKind,
          namespace,
          fieldSelector: `metadata.name=${nodeName}`,
        }
      : undefined,
  );

  return [bareMetalHosts?.[0], bareMetalHostsLoaded, bareMetalHostsLoadError];
};

Update the call site in BareMetalInventoryItems.tsx line 64 to pass the namespace from the node context.

🤖 Prompt for AI Agents
In `@frontend/packages/console-app/src/components/nodes/NodeBareMetalUtils.ts`
around lines 33 - 49, The hook useWatchBareMetalHost currently watches
BareMetalHost cluster-wide (using useK8sWatchResource with
BareMetalHostGroupVersionKind and a fieldSelector) but BareMetalHost is
namespaced; update the function signature to accept a namespace parameter and
pass that namespace into the resource config (namespace: namespace) when
constructing the useK8sWatchResource args so the watch is scoped correctly; then
update callers (e.g., BareMetalInventoryItems.tsx where useWatchBareMetalHost is
called) to supply the node's namespace.


export const metricsFromBareMetalHosts = (
bareMetalHost?: K8sResourceKind,
): { disks?: number; nics?: number; cpus?: number } => ({
disks: bareMetalHost?.status?.hardware?.storage?.length,
nics: bareMetalHost?.status?.hardware?.nics?.length,
cpus: bareMetalHost?.status?.hardware?.cpu?.count,
});
68 changes: 68 additions & 0 deletions frontend/packages/console-app/src/components/nodes/NodeVmUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
K8sGroupVersionKind,
K8sResourceKind,
WatchK8sResult,
} from '@console/dynamic-plugin-sdk/src';
import { useFlag } from '@console/dynamic-plugin-sdk/src/utils/flags';
import { useK8sWatchResource } from '@console/dynamic-plugin-sdk/src/utils/k8s/hooks';
import { K8sKind } from '@console/internal/module/k8s';

export const VirtualMachineModel: K8sKind = {
label: 'VirtualMachine',
labelPlural: 'VirtualMachines',
apiVersion: 'v1',
apiGroup: 'kubevirt.io',
plural: 'virtualmachines',
abbr: 'VM',
namespaced: true,
kind: 'VirtualMachine',
id: 'virtualmachine',
crd: true,
};

// TODO: Remove VMI retrieval and VMs count column if/when the plugin is able to add the VMs count column
export const VirtualMachineInstanceGroupVersionKind: K8sGroupVersionKind = {
group: 'kubevirt.io',
kind: 'VirtualMachineInstance',
version: 'v1',
};

export const useIsKubevirtPluginActive = () => {
const kubevirtFeature = useFlag('KUBEVIRT_DYNAMIC');

return (
Array.isArray(window.SERVER_FLAGS?.consolePlugins) &&
window.SERVER_FLAGS.consolePlugins.includes('kubevirt-plugin') &&
kubevirtFeature
);
};

// Return all matching VMIs, if no nodeName is given, return all VMIs.
export const filterVirtualMachineInstancesByNode = (vmis: K8sResourceKind[], nodeName?: string) =>
vmis?.filter((vm) => !nodeName || vm.status?.nodeName === nodeName) ?? [];

// Watch VMIs and return all matching VMIs by nodeName if given, if not, return all VMIs.
export const useWatchVirtualMachineInstances = (
nodeName?: string,
): WatchK8sResult<K8sResourceKind[]> => {
const isKubevirtPluginActive = useIsKubevirtPluginActive();

const [
virtualMachineInstances,
virtualMachineInstancesLoaded,
virtualMachineInstancesLoadError,
] = useK8sWatchResource<K8sResourceKind[]>(
isKubevirtPluginActive
? {
isList: true,
groupVersionKind: VirtualMachineInstanceGroupVersionKind,
}
: undefined,
);

return [
filterVirtualMachineInstancesByNode(virtualMachineInstances, nodeName),
virtualMachineInstancesLoaded,
virtualMachineInstancesLoadError,
];
};
51 changes: 14 additions & 37 deletions frontend/packages/console-app/src/components/nodes/NodesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@ import {
ConsoleDataViewRow,
ResourceFilters,
} from '@console/app/src/components/data-view/types';
import {
filterVirtualMachineInstancesByNode,
useIsKubevirtPluginActive,
useWatchVirtualMachineInstances,
} from '@console/app/src/components/nodes/NodeVmUtils';
import {
getGroupVersionKindForResource,
K8sModel,
ListPageBody,
useAccessReview,
useFlag,
} from '@console/dynamic-plugin-sdk/src/api/dynamic-core-api';
import {
K8sGroupVersionKind,
K8sResourceCommon,
K8sResourceKind,
NodeCertificateSigningRequestKind,
OwnerReference,
RowProps,
Expand Down Expand Up @@ -101,13 +103,6 @@ import { NodeStatusWithExtensions } from './NodeStatus';
import ClientCSRStatus from './status/CSRStatus';
import { GetNodeStatusExtensions, useNodeStatusExtensions } from './useNodeStatusExtensions';

// TODO: Remove VMI retrieval and VMs count column if/when the plugin is able to add the VMs count column
const VirtualMachineInstanceGroupVersionKind: K8sGroupVersionKind = {
group: 'kubevirt.io',
kind: 'VirtualMachineInstance',
version: 'v1',
};

const nodeColumnInfo = Object.freeze({
name: {
id: 'name',
Expand Down Expand Up @@ -902,40 +897,22 @@ export const NodesPage: FC<NodesPageProps> = ({ selector }) => {
CertificateSigningRequestKind[]
>(CertificateSigningRequestModel);

const kubevirtFeature = useFlag('KUBEVIRT_DYNAMIC');
const isKubevirtPluginActive =
Array.isArray(window.SERVER_FLAGS.consolePlugins) &&
window.SERVER_FLAGS.consolePlugins.includes('kubevirt-plugin') &&
kubevirtFeature;
// TODO: Remove VMs count column if/when the plugin is able to add the VMs count column
const isKubevirtPluginActive = useIsKubevirtPluginActive();

const [vmis, vmisLoaded, vmisLoadError] = useK8sWatchResource<K8sResourceKind[]>(
isKubevirtPluginActive
? {
isList: true,
groupVersionKind: VirtualMachineInstanceGroupVersionKind,
}
: undefined,
);
const [vmis, vmisLoaded, vmisLoadError] = useWatchVirtualMachineInstances();

const vmsByNode = useMemo(() => {
if (!isKubevirtPluginActive || !nodesLoaded || nodesLoadError || !vmisLoaded || vmisLoadError) {
return undefined;
}

const map = new Map<string, K8sResourceKind[]>(nodes.map((node) => [node.metadata.name, []]));
vmis.forEach((vmi) => {
const nodeName = vmi.status?.nodeName;
if (!nodeName) {
return;
}
const nodeVMs = map.get(nodeName);
if (nodeVMs) {
nodeVMs.push(vmi);
} else {
map.set(nodeName, [vmi]);
}
});
return map;
return new Map(
nodes.map((node) => [
`${node.metadata.name}`,
filterVirtualMachineInstancesByNode(vmis, node.metadata.name),
]),
);
}, [isKubevirtPluginActive, nodes, nodesLoadError, nodesLoaded, vmis, vmisLoadError, vmisLoaded]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type { FC } from 'react';
import { useContext } from 'react';
import {
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
} from '@patternfly/react-core';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom-v5-compat';
import {
BareMetalHostModel,
metricsFromBareMetalHosts,
useIsBareMetalPluginActive,
useWatchBareMetalHost,
} from '@console/app/src/components/nodes/NodeBareMetalUtils';
import { resourcePathFromModel } from '@console/internal/components/utils/resource-link';
import { DASH } from '@console/shared/src';
import { InventoryItem } from '@console/shared/src/components/dashboard/inventory-card/InventoryItem';
import { NodeDashboardContext } from './NodeDashboardContext';

type BareMetalInventoryItemsProps = {
loaded: boolean;
loadError?: unknown;
title: string;
itemsTitle?: string;
count: number | undefined;
linkTo?: string;
};

const BareMetalInventoryItem: FC<BareMetalInventoryItemsProps> = ({
loaded,
loadError,
title,
itemsTitle,
count,
linkTo,
}) => {
return (
<DescriptionListGroup>
<DescriptionListTerm>{title}</DescriptionListTerm>
<DescriptionListDescription>
{!loaded ? (
<InventoryItem title={title} isLoading count={0} />
) : loadError || count === undefined ? (
DASH
) : linkTo ? (
<Link to={linkTo}>
<InventoryItem title={itemsTitle ?? title} isLoading={false} count={count} />
</Link>
) : (
<InventoryItem title={itemsTitle ?? title} isLoading={false} count={count} />
)}
</DescriptionListDescription>
</DescriptionListGroup>
);
};

const BareMetalInventoryItems: FC = () => {
const { obj } = useContext(NodeDashboardContext);
const { t } = useTranslation();

const showBareMetal = useIsBareMetalPluginActive();

const [bareMetalHost, bareMetalHostLoaded, bareMetalHostLoadError] = useWatchBareMetalHost(
obj.metadata.name,
);

if (!showBareMetal) {
return null;
}

const { disks, nics, cpus } = metricsFromBareMetalHosts(bareMetalHost);

return (
<>
<BareMetalInventoryItem
loaded={bareMetalHostLoaded}
loadError={bareMetalHostLoadError || !bareMetalHost}
title={t('console-app~Disk')}
count={disks}
linkTo={
bareMetalHost
? `${resourcePathFromModel(
BareMetalHostModel,
bareMetalHost.metadata.name,
bareMetalHost.metadata.namespace,
)}/disks`
: undefined
}
/>
<BareMetalInventoryItem
loaded={bareMetalHostLoaded}
loadError={bareMetalHostLoadError || !bareMetalHost}
title={t('console-app~Network')}
itemsTitle={t('console-app~NIC')}
count={nics}
linkTo={
bareMetalHost
? `${resourcePathFromModel(
BareMetalHostModel,
bareMetalHost.metadata.name,
bareMetalHost.metadata.namespace,
)}/nics`
: undefined
}
/>
<BareMetalInventoryItem
loaded={bareMetalHostLoaded}
loadError={bareMetalHostLoadError || !bareMetalHost}
title={t('console-app~CPU')}
count={cpus}
/>
</>
);
};

export default BareMetalInventoryItems;
Loading