diff --git a/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx new file mode 100644 index 000000000..8017f2e45 --- /dev/null +++ b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import { act } from 'react' +import { createRoot } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' +import { PodRenderer } from './PodRenderer' + +describe('PodRenderer nominated Node', () => { + it('links the nominated Node of an unbound Pod', async () => { + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) + const container = document.createElement('div') + const root = createRoot(container) + const onNavigate = vi.fn() + try { + await act(async () => root.render( + {}} + copied={null} + onNavigate={onNavigate} + />, + )) + expect(container.textContent).toContain('Nominated Node') + const button = [...container.querySelectorAll('button')].find(button => button.textContent === 'candidate') + expect(button).toBeDefined() + await act(async () => button!.click()) + expect(onNavigate).toHaveBeenCalledWith({ kind: 'nodes', namespace: '', name: 'candidate', group: undefined }) + } finally { + await act(async () => root.unmount()) + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: false }) + } + }) + + it('hides a nomination on a terminal or terminating unbound Pod', () => { + for (const data of [ + { metadata: { name: 'failed', namespace: 'app' }, spec: { containers: [] }, status: { phase: 'Failed', nominatedNodeName: 'stale' } }, + { metadata: { name: 'deleting', namespace: 'app', deletionTimestamp: '2026-10-07T00:00:00Z' }, spec: { containers: [] }, status: { phase: 'Pending', nominatedNodeName: 'stale' } }, + ]) { + const html = renderToStaticMarkup( {}} copied={null} />) + expect(html).not.toContain('Nominated Node') + expect(html).not.toContain('stale') + } + }) + + it('hides a nomination once the Pod is bound, even when it names another node', () => { + const html = renderToStaticMarkup( + {}} + copied={null} + />, + ) + expect(html).not.toContain('Nominated Node') + expect(html).not.toContain('stale') + expect(html).toContain('assigned') + }) +}) diff --git a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx index 884b6a668..0a2104779 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx @@ -453,6 +453,19 @@ export function PodRenderer({ : undefined } copyable onCopy={onCopy} copied={copied} /> + {/* A nomination outlives its meaning: the API server clears it on binding by + default only from Kubernetes 1.35, and pod GC fails a terminating unbound + Pod without clearing it. Only a Pending, unbound, live Pod can still use it. */} + {data.status?.nominatedNodeName && !data.spec?.nodeName && data.status?.phase === 'Pending' && !data.metadata?.deletionTimestamp && ( + + Nominated Node + + } + value={} + /> + )} {data.spec?.priorityClassName && } />}