From 89f3785c12d182af3e6cfb510be8cee1b6aaa8ca Mon Sep 17 00:00:00 2001 From: Nadav Erell Date: Wed, 7 Oct 2026 08:34:46 +0300 Subject: [PATCH 1/4] Link explicit scheduler nominated Nodes separately from placement --- .../renderers/PodNomination.test.tsx | 24 +++++++++++++++++++ .../resources/renderers/PodRenderer.tsx | 4 ++++ 2 files changed, 28 insertions(+) create mode 100644 packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx 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 0000000000..023c55dc1a --- /dev/null +++ b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx @@ -0,0 +1,24 @@ +// @vitest-environment jsdom +import { act } from 'react' +import { createRoot } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { expect, it, vi } from 'vitest' +import { PodRenderer } from './PodRenderer' +Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) + +it('links an explicit nominated Node without presenting it as assigned placement', async () => { + const element = document.createElement('div'); const root = createRoot(element); const onNavigate = vi.fn() + try { + await act(async () => root.render( {}} copied={null} onNavigate={onNavigate} />)) + expect(element.textContent).toContain('Nominated Node') + const button = [...element.querySelectorAll('button')].find(b => b.textContent === 'candidate') + expect(button).toBeDefined(); await act(async () => button!.click()) + expect(onNavigate).toHaveBeenCalledWith({kind:'nodes',group:'',namespace:'',name:'candidate'}) + expect(element.textContent).toContain('Pending') + } finally { await act(async () => root.unmount()) } +}) + +it('does not infer a nominated Node from ordinary assigned placement', () => { + const html = renderToStaticMarkup( {}} copied={null} />) + expect(html).not.toContain('Nominated Node'); 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 60aaa31928..ba1dca955b 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx @@ -453,6 +453,10 @@ export function PodRenderer({ : undefined } copyable onCopy={onCopy} copied={copied} /> + {data.status?.nominatedNodeName && ( + Nominated Node} + value={} /> + )} Date: Wed, 7 Oct 2026 12:09:24 +0300 Subject: [PATCH 2/4] Hide stale nominations on bound Pods Before Kubernetes 1.35 the API server does not clear status.nominatedNodeName when a Pod is bound, so a running Pod can still name a different node. Show the nomination only while the Pod is unbound, give the label the same tooltip affordance as QoS Class, and rewrite the test in the renderer-test style. --- .../renderers/PodNomination.test.tsx | 54 +++++++++++++------ .../resources/renderers/PodRenderer.tsx | 14 +++-- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx index 023c55dc1a..b9ae383889 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx @@ -2,23 +2,45 @@ import { act } from 'react' import { createRoot } from 'react-dom/client' import { renderToStaticMarkup } from 'react-dom/server' -import { expect, it, vi } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { PodRenderer } from './PodRenderer' -Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) -it('links an explicit nominated Node without presenting it as assigned placement', async () => { - const element = document.createElement('div'); const root = createRoot(element); const onNavigate = vi.fn() - try { - await act(async () => root.render( {}} copied={null} onNavigate={onNavigate} />)) - expect(element.textContent).toContain('Nominated Node') - const button = [...element.querySelectorAll('button')].find(b => b.textContent === 'candidate') - expect(button).toBeDefined(); await act(async () => button!.click()) - expect(onNavigate).toHaveBeenCalledWith({kind:'nodes',group:'',namespace:'',name:'candidate'}) - expect(element.textContent).toContain('Pending') - } finally { await act(async () => root.unmount()) } -}) +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('does not infer a nominated Node from ordinary assigned placement', () => { - const html = renderToStaticMarkup( {}} copied={null} />) - expect(html).not.toContain('Nominated Node'); expect(html).toContain('assigned') + 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 ba1dca955b..248b7105bd 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx @@ -453,9 +453,17 @@ export function PodRenderer({ : undefined } copyable onCopy={onCopy} copied={copied} /> - {data.status?.nominatedNodeName && ( - Nominated Node} - value={} /> + {/* Before Kubernetes 1.35 the API server does not clear a nomination on + binding, so a bound Pod can carry a stale, different node name. */} + {data.status?.nominatedNodeName && !data.spec?.nodeName && ( + + Nominated Node + + } + value={} + /> )} From 26f312f37e1651590366ee8de0c4a2207004eecd Mon Sep 17 00:00:00 2001 From: Nadav Erell Date: Wed, 7 Oct 2026 12:25:25 +0300 Subject: [PATCH 3/4] State the nomination-clearing version boundary precisely --- .../k8s-ui/src/components/resources/renderers/PodRenderer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx index 248b7105bd..32d3912726 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodRenderer.tsx @@ -453,8 +453,8 @@ export function PodRenderer({ : undefined } copyable onCopy={onCopy} copied={copied} /> - {/* Before Kubernetes 1.35 the API server does not clear a nomination on - binding, so a bound Pod can carry a stale, different node name. */} + {/* The API server clears a nomination on binding by default only from + Kubernetes 1.35, so a bound Pod can carry a stale, different node name. */} {data.status?.nominatedNodeName && !data.spec?.nodeName && ( Date: Wed, 7 Oct 2026 22:03:49 +0300 Subject: [PATCH 4/4] Show a nomination only for a Pending, live, unbound Pod Pod GC fails a terminating unbound Pod without clearing its nomination, and a finalizer can keep that Pod around, so the row pointed stuck-termination diagnosis toward placement. The tooltip no longer attributes the nomination to the scheduler alone; other components may set it. --- .../resources/renderers/PodNomination.test.tsx | 11 +++++++++++ .../components/resources/renderers/PodRenderer.tsx | 9 +++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx index b9ae383889..8017f2e458 100644 --- a/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx +++ b/packages/k8s-ui/src/components/resources/renderers/PodNomination.test.tsx @@ -31,6 +31,17 @@ describe('PodRenderer nominated Node', () => { } }) + 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( : undefined } copyable onCopy={onCopy} copied={copied} /> - {/* The API server clears a nomination on binding by default only from - Kubernetes 1.35, so a bound Pod can carry a stale, different node name. */} - {data.status?.nominatedNodeName && !data.spec?.nodeName && ( + {/* 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 }