From 52ff329efd4fea28d2fe45540b49b0e4c581a322 Mon Sep 17 00:00:00 2001 From: Abhayraj Jaiswal Date: Wed, 26 Aug 2026 19:14:55 +0000 Subject: [PATCH] kueue: Add queue maintenance mode controls and drain actions Implement QueueMaintenanceControl card with AuthVisible RBAC checks, stopPolicy formatters, type-only imports, targeted action loading states, and drain confirmation modal. Signed-off-by: Abhayraj Jaiswal --- kueue/locales/en/translation.json | 19 ++ kueue/package.json | 5 + kueue/src/components/clusterqueues/Detail.tsx | 26 ++- .../components/common/KueueResourceLinks.tsx | 2 +- .../common/QueueMaintenanceControl.tsx | 173 ++++++++++++++++++ kueue/src/components/localqueues/Detail.tsx | 10 + .../resources/queueControlFormatters.test.ts | 84 +++++++++ kueue/src/resources/queueControlFormatters.ts | 89 +++++++++ 8 files changed, 399 insertions(+), 9 deletions(-) create mode 100644 kueue/locales/en/translation.json create mode 100644 kueue/src/components/common/QueueMaintenanceControl.tsx create mode 100644 kueue/src/resources/queueControlFormatters.test.ts create mode 100644 kueue/src/resources/queueControlFormatters.ts diff --git a/kueue/locales/en/translation.json b/kueue/locales/en/translation.json new file mode 100644 index 0000000000..c2166f7312 --- /dev/null +++ b/kueue/locales/en/translation.json @@ -0,0 +1,19 @@ +{ + "Failed to update queue stop policy.": "Failed to update queue stop policy.", + "Queue Maintenance & Stop Policy": "Queue Maintenance & Stop Policy", + "Current State": "Current State", + "Clear Stop Policy (None)": "Clear Stop Policy (None)", + "Pause Admissions (Hold)": "Pause Admissions (Hold)", + "Drain Queue (HoldAndDrain)": "Drain Queue (HoldAndDrain)", + "Confirm Queue Drainage": "Confirm Queue Drainage", + "Are you sure you want to set {{queueType}} \"{{name}}\" to HoldAndDrain? This will block new admissions, evict admitted workloads, and cancel reservations for Reserving workloads.": "Are you sure you want to set {{queueType}} \"{{name}}\" to HoldAndDrain? This will block new admissions, evict admitted workloads, and cancel reservations for Reserving workloads.", + "Cancel": "Cancel", + "Confirm & Drain": "Confirm & Drain", + "Drain (HoldAndDrain)": "Drain (HoldAndDrain)", + "Paused (Hold)": "Paused (Hold)", + "No maintenance stop policy (None)": "No maintenance stop policy (None)", + "Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.": "Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.", + "Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.": "Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.", + "No maintenance stop policy is applied to this queue.": "No maintenance stop policy is applied to this queue.", + "Custom stop policy: {{policy}}": "Custom stop policy: {{policy}}" +} diff --git a/kueue/package.json b/kueue/package.json index 341946f782..989865c91c 100644 --- a/kueue/package.json +++ b/kueue/package.json @@ -38,5 +38,10 @@ "overrides": { "ws": "8.21.0", "vitest": "^3.2.6" + }, + "headlamp": { + "i18n": [ + "en" + ] } } diff --git a/kueue/src/components/clusterqueues/Detail.tsx b/kueue/src/components/clusterqueues/Detail.tsx index 5b35b5dcaf..69f26d051d 100644 --- a/kueue/src/components/clusterqueues/Detail.tsx +++ b/kueue/src/components/clusterqueues/Detail.tsx @@ -13,25 +13,26 @@ import { renderCohortLink, renderResourceFlavorLink, } from '../common/KueueResourceLinks'; +import QueueMaintenanceControl from '../common/QueueMaintenanceControl'; import { RelatedLocalQueuesSection, RelatedWorkloadsSection } from '../common/RelatedResources'; -/** Flattened row rendered for status flavor reservations or flavor usage. */ +/** Flattened row rendered in the ClusterQueue flavor reservations and usage tables. */ interface FlavorUsageRow { - /** ResourceFlavor name reported by ClusterQueue status. */ + /** ResourceFlavor name associated with this reservation or usage. */ flavor: string; - /** Resource name reported under the flavor. */ + /** Resource name within the flavor, for example cpu or memory. */ resource: string; - /** Total reserved or used quantity for the resource. */ + /** Total quota used or reserved by this resource and flavor pair. */ total?: string | number; - /** Quantity currently borrowed from the cohort. */ + /** Optional quota borrowed from the cohort for this resource. */ borrowed?: string | number; } -/** Admission check row from spec.admissionChecksStrategy. */ +/** Flattened row rendered in the ClusterQueue admission checks table. */ interface AdmissionCheckRow { - /** AdmissionCheck resource name. */ + /** AdmissionCheck name configured on the ClusterQueue. */ name: string; - /** Flavor names this AdmissionCheck applies to; empty means all flavors. */ + /** Specific ResourceFlavor names this check applies to, or all if empty. */ flavors: string[]; } @@ -66,6 +67,14 @@ function getAdmissionCheckRows(clusterQueue: ClusterQueue): AdmissionCheckRow[] ); } +/** Build the queue maintenance control section. */ +function getMaintenanceControlSection(clusterQueue: ClusterQueue) { + return { + id: 'maintenance-controls', + section: , + }; +} + /** Build the extra detail section that shows spec.resourceGroups as a table. */ function getResourceGroupsSection(clusterQueue: ClusterQueue) { const rows = getResourceGroupRows(clusterQueue.resourceGroups); @@ -285,6 +294,7 @@ export default function ClusterQueueDetail() { extraSections={clusterQueue => clusterQueue ? [ + getMaintenanceControlSection(clusterQueue), getConditionsSection(clusterQueue), getResourceGroupsSection(clusterQueue), getAdmissionChecksSection(clusterQueue), diff --git a/kueue/src/components/common/KueueResourceLinks.tsx b/kueue/src/components/common/KueueResourceLinks.tsx index af095fb6b4..6e22bb4706 100644 --- a/kueue/src/components/common/KueueResourceLinks.tsx +++ b/kueue/src/components/common/KueueResourceLinks.tsx @@ -17,7 +17,7 @@ export function renderAdmissionCheckLink(admissionCheckName?: string) { /** Render a Cohort reference as a detail-page link when present. */ export function renderCohortLink(cohortName?: string) { - if (!cohortName) { + if (!cohortName || cohortName === '-') { return '-'; } diff --git a/kueue/src/components/common/QueueMaintenanceControl.tsx b/kueue/src/components/common/QueueMaintenanceControl.tsx new file mode 100644 index 0000000000..9a62c96703 --- /dev/null +++ b/kueue/src/components/common/QueueMaintenanceControl.tsx @@ -0,0 +1,173 @@ +import { useTranslation } from '@kinvolk/headlamp-plugin/lib'; +import { AuthVisible, SectionBox } from '@kinvolk/headlamp-plugin/lib/components/common'; +import type { KubeObject } from '@kinvolk/headlamp-plugin/lib/k8s/cluster'; +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Paper, + Stack, + Typography, +} from '@mui/material'; +import React, { useState } from 'react'; +import type { StopPolicy } from '../../resources/clusterQueue'; +import { + createStopPolicyPatch, + getStopPolicyColor, + getStopPolicyDescription, + getStopPolicyLabel, +} from '../../resources/queueControlFormatters'; + +export type QueueResource = KubeObject & { + stopPolicy?: string; +}; + +interface QueueMaintenanceControlProps { + queue: QueueResource; + queueType: 'ClusterQueue' | 'LocalQueue'; +} + +export default function QueueMaintenanceControl({ + queue, + queueType, +}: QueueMaintenanceControlProps) { + const { t } = useTranslation(); + const rawPolicy = queue.stopPolicy; + const currentPolicy: StopPolicy = + !rawPolicy || rawPolicy === '-' || rawPolicy === 'None' ? 'None' : (rawPolicy as StopPolicy); + const [pendingPolicy, setPendingPolicy] = useState(null); + const [error, setError] = useState(null); + const [confirmDrainOpen, setConfirmDrainOpen] = useState(false); + + const isLoading = pendingPolicy !== null; + const currentLabel = getStopPolicyLabel(currentPolicy, t); + const currentDescription = getStopPolicyDescription(currentPolicy, t); + + const applyPolicy = async (targetPolicy: StopPolicy) => { + setPendingPolicy(targetPolicy); + setError(null); + try { + await queue.patch(createStopPolicyPatch(targetPolicy)); + } catch (err: any) { + setError(err?.message || t('Failed to update queue stop policy.')); + } finally { + setPendingPolicy(null); + setConfirmDrainOpen(false); + } + }; + + return ( + + + {error && ( + setError(null)}> + {error} + + )} + + + + + + {t('Current State')}: + + + + + {currentDescription} + + + + + + {currentPolicy !== 'None' && ( + + )} + + {currentPolicy !== 'Hold' && ( + + )} + + {currentPolicy !== 'HoldAndDrain' && ( + + )} + + + + + {/* Confirmation modal for draining queue */} + setConfirmDrainOpen(false)}> + {t('Confirm Queue Drainage')} + + + {t( + 'Are you sure you want to set {{queueType}} "{{name}}" to HoldAndDrain? This will block new admissions, evict admitted workloads, and cancel reservations for Reserving workloads.', + { queueType, name: queue.metadata?.name || '' } + )} + + + + + + + + + + ); +} diff --git a/kueue/src/components/localqueues/Detail.tsx b/kueue/src/components/localqueues/Detail.tsx index 7ac0ccda9b..3848b91ef0 100644 --- a/kueue/src/components/localqueues/Detail.tsx +++ b/kueue/src/components/localqueues/Detail.tsx @@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom'; import { LocalQueue } from '../../resources/localQueue'; import KueueAdminResourceAccess from '../common/KueueAdminResourceAccess'; import { renderClusterQueueLink } from '../common/KueueResourceLinks'; +import QueueMaintenanceControl from '../common/QueueMaintenanceControl'; import { RelatedWorkloadsSection } from '../common/RelatedResources'; /** Build the standard Headlamp conditions section for LocalQueue status. */ @@ -17,6 +18,14 @@ function getConditionsSection(localQueue: LocalQueue) { }; } +/** Build the queue maintenance control section for LocalQueue. */ +function getMaintenanceControlSection(localQueue: LocalQueue) { + return { + id: 'maintenance-controls', + section: , + }; +} + export default function LocalQueueDetail() { const { namespace, name } = useParams<{ namespace: string; name: string }>(); @@ -65,6 +74,7 @@ export default function LocalQueueDetail() { extraSections={localQueue => localQueue ? [ + getMaintenanceControlSection(localQueue), getConditionsSection(localQueue), { id: 'related-workloads', diff --git a/kueue/src/resources/queueControlFormatters.test.ts b/kueue/src/resources/queueControlFormatters.test.ts new file mode 100644 index 0000000000..5bc4051f9f --- /dev/null +++ b/kueue/src/resources/queueControlFormatters.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; +import { + createStopPolicyPatch, + getStopPolicyColor, + getStopPolicyDescription, + getStopPolicyLabel, +} from './queueControlFormatters'; + +describe('queueControlFormatters', () => { + describe('getStopPolicyColor', () => { + it('returns error for HoldAndDrain', () => { + expect(getStopPolicyColor('HoldAndDrain')).toBe('error'); + }); + + it('returns warning for Hold', () => { + expect(getStopPolicyColor('Hold')).toBe('warning'); + }); + + it('returns success for None, empty, dash, or undefined', () => { + expect(getStopPolicyColor('None')).toBe('success'); + expect(getStopPolicyColor('')).toBe('success'); + expect(getStopPolicyColor('-')).toBe('success'); + expect(getStopPolicyColor(undefined)).toBe('success'); + }); + + it('returns default for unknown string', () => { + expect(getStopPolicyColor('CustomPolicy')).toBe('default'); + }); + }); + + describe('getStopPolicyLabel', () => { + it('returns correct label for standard policies', () => { + expect(getStopPolicyLabel('HoldAndDrain')).toBe('Drain (HoldAndDrain)'); + expect(getStopPolicyLabel('Hold')).toBe('Paused (Hold)'); + expect(getStopPolicyLabel('None')).toBe('No maintenance stop policy (None)'); + expect(getStopPolicyLabel('-')).toBe('No maintenance stop policy (None)'); + expect(getStopPolicyLabel(undefined)).toBe('No maintenance stop policy (None)'); + }); + + it('supports localization function', () => { + const mockT = (key: string) => `[translated] ${key}`; + expect(getStopPolicyLabel('HoldAndDrain', mockT)).toBe('[translated] Drain (HoldAndDrain)'); + expect(getStopPolicyLabel('Hold', mockT)).toBe('[translated] Paused (Hold)'); + expect(getStopPolicyLabel('None', mockT)).toBe( + '[translated] No maintenance stop policy (None)' + ); + }); + }); + + describe('getStopPolicyDescription', () => { + it('returns explanatory description for each policy state', () => { + expect(getStopPolicyDescription('HoldAndDrain')).toContain('Drain mode'); + expect(getStopPolicyDescription('HoldAndDrain')).toContain('admitted workloads are evicted'); + expect(getStopPolicyDescription('HoldAndDrain')).toContain( + 'reservations for Reserving workloads are cancelled' + ); + expect(getStopPolicyDescription('Hold')).toContain('Pause mode'); + expect(getStopPolicyDescription('Hold')).toContain( + 'reservations for Reserving workloads are cancelled' + ); + expect(getStopPolicyDescription('None')).toContain('No maintenance stop policy'); + expect(getStopPolicyDescription('-')).toContain('No maintenance stop policy'); + expect(getStopPolicyDescription(undefined)).toContain('No maintenance stop policy'); + }); + + it('supports localization function', () => { + const mockT = (key: string) => `[translated] ${key}`; + expect(getStopPolicyDescription('HoldAndDrain', mockT)).toContain('[translated]'); + expect(getStopPolicyDescription('Hold', mockT)).toContain('[translated]'); + expect(getStopPolicyDescription('None', mockT)).toContain('[translated]'); + }); + }); + + describe('createStopPolicyPatch', () => { + it('creates standard spec patch', () => { + expect(createStopPolicyPatch('Hold')).toEqual({ + spec: { stopPolicy: 'Hold' }, + }); + expect(createStopPolicyPatch('None')).toEqual({ + spec: { stopPolicy: 'None' }, + }); + }); + }); +}); diff --git a/kueue/src/resources/queueControlFormatters.ts b/kueue/src/resources/queueControlFormatters.ts new file mode 100644 index 0000000000..cc2f283473 --- /dev/null +++ b/kueue/src/resources/queueControlFormatters.ts @@ -0,0 +1,89 @@ +import type { StopPolicy } from './clusterQueue'; + +export type StopPolicyColor = 'success' | 'warning' | 'error' | 'default'; + +/** + * Returns a theme-friendly color representing the queue's maintenance state. + */ +export function getStopPolicyColor(stopPolicy?: StopPolicy | string): StopPolicyColor { + switch (stopPolicy) { + case 'HoldAndDrain': + return 'error'; + case 'Hold': + return 'warning'; + case 'None': + case undefined: + case '': + case '-': + return 'success'; + default: + return 'default'; + } +} + +/** + * Returns a human-readable title for the StopPolicy state, optionally localized. + */ +export function getStopPolicyLabel( + stopPolicy?: StopPolicy | string, + t?: (key: string) => string +): string { + switch (stopPolicy) { + case 'HoldAndDrain': + return t ? t('Drain (HoldAndDrain)') : 'Drain (HoldAndDrain)'; + case 'Hold': + return t ? t('Paused (Hold)') : 'Paused (Hold)'; + case 'None': + case undefined: + case '': + case '-': + return t ? t('No maintenance stop policy (None)') : 'No maintenance stop policy (None)'; + default: + return stopPolicy; + } +} + +/** + * Returns a detailed operational description explaining the impact of the policy, optionally localized. + */ +export function getStopPolicyDescription( + stopPolicy?: StopPolicy | string, + t?: (key: string, options?: Record) => string +): string { + switch (stopPolicy) { + case 'HoldAndDrain': + return t + ? t( + 'Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.' + ) + : 'Queue is in Drain mode: new admissions are blocked, admitted workloads are evicted, and reservations for Reserving workloads are cancelled.'; + case 'Hold': + return t + ? t( + 'Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.' + ) + : 'Queue is in Pause mode: new admissions are blocked, reservations for Reserving workloads are cancelled, and in-flight workloads continue running.'; + case 'None': + case undefined: + case '': + case '-': + return t + ? t('No maintenance stop policy is applied to this queue.') + : 'No maintenance stop policy is applied to this queue.'; + default: + return t + ? t('Custom stop policy: {{policy}}', { policy: stopPolicy }) + : `Custom stop policy: ${stopPolicy}`; + } +} + +/** + * Returns the target patch object for updating the queue's stop policy. + */ +export function createStopPolicyPatch(stopPolicy: StopPolicy) { + return { + spec: { + stopPolicy, + }, + }; +}