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
@@ -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(
<PodRenderer
data={{ metadata: { name: 'pending', namespace: 'app' }, spec: { containers: [] }, status: { phase: 'Pending', nominatedNodeName: 'candidate' } }}
onCopy={() => {}}
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(<PodRenderer data={data} onCopy={() => {}} 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(
<PodRenderer
data={{ metadata: { name: 'running', namespace: 'app' }, spec: { nodeName: 'assigned', containers: [] }, status: { phase: 'Running', nominatedNodeName: 'stale' } }}
onCopy={() => {}}
copied={null}
/>,
)
expect(html).not.toContain('Nominated Node')
expect(html).not.toContain('stale')
expect(html).toContain('assigned')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,19 @@ export function PodRenderer({
<Property label="Node" value={
data.spec?.nodeName ? <ResourceLink name={data.spec.nodeName} kind="nodes" onNavigate={onNavigate} /> : 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 && (
<Property
label={
<Tooltip content="The node this Pod is expected to run on, set by the scheduler or another component. It can change before the Pod is bound." position="right">
<span className="border-b border-dotted border-theme-text-tertiary cursor-help">Nominated Node</span>
</Tooltip>
}
value={<ResourceLink name={data.status.nominatedNodeName} kind="nodes" onNavigate={onNavigate} />}
/>
)}
{data.spec?.priorityClassName && <Property label="Priority Class" value={
<ResourceLink name={data.spec.priorityClassName} kind="priorityclasses" group="scheduling.k8s.io" onNavigate={onNavigate} />
} />}
Expand Down
Loading