diff --git a/kmesh/src/components/nodeinfo/Detail.tsx b/kmesh/src/components/nodeinfo/Detail.tsx index 77537c7a2e..ade72953eb 100644 --- a/kmesh/src/components/nodeinfo/Detail.tsx +++ b/kmesh/src/components/nodeinfo/Detail.tsx @@ -2,11 +2,14 @@ * Detail view for a single KmeshNodeInfo resource. * Shows full IPsec security state for one node: SPI, addresses, Pod CIDRs, and boot ID. */ +import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { MainInfoSection, + SectionBox, SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; +import { Link as HeadlampLink } from '@kinvolk/headlamp-plugin/lib/components/common'; import Alert from '@mui/material/Alert'; import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; @@ -15,8 +18,54 @@ import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import React from 'react'; import { useParams } from 'react-router-dom'; +import { useKmeshDaemonPods } from '../../hooks/useKmeshDaemonPods'; import { KmeshNodeInfo } from '../../resources/kmeshNodeInfo'; +/** Related resources for a KmeshNodeInfo: the underlying Node and the kmesh-daemon Pod on it. */ +function NodeInfoRelatedResources({ nodeName }: { nodeName: string }) { + const [node] = K8s.ResourceClasses.Node.useGet(nodeName); + const { pods: daemonPods } = useKmeshDaemonPods(); + const daemonPod = daemonPods.find(p => p.nodeName === nodeName) ?? null; + const [daemonPodObject] = K8s.ResourceClasses.Pod.useGet( + daemonPod?.name ?? '', + daemonPod?.namespace ?? '' + ); + + return ( + + row.label, + }, + { + label: 'Resource', + getter: (row: { label: string; resources: any[] }) => + row.resources.length > 0 ? ( + + {row.resources.map(resource => ( + + {resource.getName()} + + ))} + + ) : ( + + Not found + + ), + }, + ]} + /> + + ); +} + /** * Detail view for a single KmeshNodeInfo resource. */ @@ -169,6 +218,10 @@ export default function KmeshNodeInfoDetail() { ]} /> + + + + ); } diff --git a/kmesh/src/components/waypoints/Detail.test.tsx b/kmesh/src/components/waypoints/Detail.test.tsx new file mode 100644 index 0000000000..0a22dc072a --- /dev/null +++ b/kmesh/src/components/waypoints/Detail.test.tsx @@ -0,0 +1,129 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +const { mockUseGet, mockPodUseList, mockServiceUseList, mockNamespaceUseList, mockUseParams } = + vi.hoisted(() => ({ + mockUseGet: vi.fn(), + mockPodUseList: vi.fn(), + mockServiceUseList: vi.fn(), + mockNamespaceUseList: vi.fn(), + mockUseParams: vi.fn(() => ({})), + })); + +vi.mock('react-router-dom', () => ({ + useParams: mockUseParams, +})); + +vi.mock('@kinvolk/headlamp-plugin/lib', () => ({ + K8s: { + ResourceClasses: { + Pod: { useList: mockPodUseList }, + Service: { useList: mockServiceUseList }, + Namespace: { useList: mockNamespaceUseList }, + }, + }, +})); + +vi.mock('@kinvolk/headlamp-plugin/lib/CommonComponents', () => ({ + ObjectEventList: () => null, + SectionBox: ({ title, children }: any) =>
{children}
, + SimpleTable: ({ data, columns }: any) => ( + + + {data.map((row: any, i: number) => ( + + {columns.map((col: any) => ( + + ))} + + ))} + +
{col.getter(row)}
+ ), + StatusLabel: ({ children }: any) => {children}, +})); + +vi.mock('@kinvolk/headlamp-plugin/lib/components/common', () => ({ + MainInfoSection: ({ extraInfo }: any) => ( +
+ {extraInfo?.map((info: any) => ( +
+ {info.name}: {String(info.value)} +
+ ))} +
+ ), + Link: ({ kubeObject, children }: any) => {children ?? kubeObject?.getName()}, +})); + +vi.mock('../../resources/waypoint', () => ({ + Waypoint: { useGet: mockUseGet }, +})); + +import WaypointDetail from './Detail'; + +beforeEach(() => { + mockPodUseList.mockReturnValue([[]]); + mockServiceUseList.mockReturnValue([[]]); + mockNamespaceUseList.mockReturnValue([[]]); +}); + +afterEach(() => { + cleanup(); + mockUseGet.mockReset(); + mockPodUseList.mockReset(); + mockServiceUseList.mockReset(); + mockNamespaceUseList.mockReset(); + mockUseParams.mockReset().mockReturnValue({}); +}); + +function waypoint(overrides: Record = {}) { + return { + spec: { gatewayClassName: 'kmesh-waypoint' }, + status: { conditions: [] }, + image: 'kmesh/waypoint:latest', + currentStatus: 'Programmed', + getName: () => 'my-waypoint', + metadata: { uid: 'waypoint-uid', namespace: 'default' }, + ...overrides, + }; +} + +function pod(name: string) { + return { getName: () => name, metadata: { uid: `pod-${name}` } }; +} + +function service(name: string) { + return { getName: () => name, metadata: { uid: `svc-${name}` } }; +} + +function namespace(name: string, labels: Record = {}) { + return { getName: () => name, metadata: { uid: `ns-${name}`, labels } }; +} + +describe('WaypointDetail related resources', () => { + it('shows "None found" for all related resource rows when nothing matches', () => { + mockUseParams.mockReturnValue({ namespace: 'default', name: 'my-waypoint' }); + mockUseGet.mockReturnValue([waypoint(), null]); + + render(); + + expect(screen.getAllByText('None found')).toHaveLength(3); + }); + + it('renders links for proxy pods, the proxy service, and enrolled namespaces', () => { + mockUseParams.mockReturnValue({ namespace: 'default', name: 'my-waypoint' }); + mockUseGet.mockReturnValue([waypoint(), null]); + mockPodUseList.mockReturnValue([[pod('my-waypoint-abcde')]]); + mockServiceUseList.mockReturnValue([[service('my-waypoint')]]); + mockNamespaceUseList.mockReturnValue([ + [namespace('team-a', { 'istio.io/use-waypoint': 'my-waypoint' }), namespace('team-b')], + ]); + + render(); + + expect(screen.getByText('my-waypoint-abcde')).toBeTruthy(); + expect(screen.getByText('team-a')).toBeTruthy(); + expect(screen.queryByText('team-b')).toBeNull(); + }); +}); diff --git a/kmesh/src/components/waypoints/Detail.tsx b/kmesh/src/components/waypoints/Detail.tsx index 2d0e8d98f3..7c8b7fae18 100644 --- a/kmesh/src/components/waypoints/Detail.tsx +++ b/kmesh/src/components/waypoints/Detail.tsx @@ -1,14 +1,25 @@ +import { K8s } from '@kinvolk/headlamp-plugin/lib'; import { ObjectEventList, SectionBox, SimpleTable, StatusLabel, } from '@kinvolk/headlamp-plugin/lib/CommonComponents'; -import { MainInfoSection } from '@kinvolk/headlamp-plugin/lib/components/common'; +import { + Link as HeadlampLink, + MainInfoSection, +} from '@kinvolk/headlamp-plugin/lib/components/common'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; import { useParams } from 'react-router-dom'; import { Waypoint } from '../../resources/waypoint'; import { kmeshRoutePaths } from '../../utils/kmeshRoutes'; +/** Label the Gateway API deployer sets on the proxy Pod/Service it creates for a Gateway. */ +const GATEWAY_NAME_LABEL = 'istio.io/gateway-name'; +/** Label a Namespace carries when its workloads are enrolled to use a specific waypoint. */ +const USE_WAYPOINT_LABEL = 'istio.io/use-waypoint'; + /** * Props for the Waypoint Detail view component. * @@ -92,6 +103,70 @@ function ConditionsTable({ conditions }: { conditions?: any[] }) { ); } +interface RelatedResourceRow { + label: string; + resources: any[]; +} + +function RelatedResourcesTable({ rows }: { rows: RelatedResourceRow[] }) { + return ( + + row.label, + }, + { + label: 'Resources', + getter: (row: RelatedResourceRow) => + row.resources.length > 0 ? ( + + {row.resources.map(resource => ( + + {resource.getName()} + + ))} + + ) : ( + + None found + + ), + }, + ]} + /> + + ); +} + +function WaypointRelatedResources({ name, namespace }: { name: string; namespace: string }) { + const [proxyPods] = K8s.ResourceClasses.Pod.useList({ + namespace, + labelSelector: `${GATEWAY_NAME_LABEL}=${name}`, + }); + const [proxyServices] = K8s.ResourceClasses.Service.useList({ + namespace, + labelSelector: `${GATEWAY_NAME_LABEL}=${name}`, + }); + const [namespaces] = K8s.ResourceClasses.Namespace.useList(); + + const enrolledNamespaces = (namespaces ?? []).filter( + ns => ns.metadata?.labels?.[USE_WAYPOINT_LABEL] === name + ); + + return ( + + ); +} + function WaypointDetailContent({ name, namespace, @@ -126,6 +201,7 @@ function WaypointDetailContent({ ]} /> {waypoint && } + {waypoint && } {waypoint && } );