From 5ffae6814fe6f65e3613a4afd64715045309d6e9 Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Fri, 27 Mar 2026 17:54:47 +0100 Subject: [PATCH] feat: add cronjob nodepool UI to console --- .../nodepool-modal/nodepool-modal.tsx | 59 ++++++++++-- .../nodepools-resources-settings.tsx | 94 ++++++++++++++++++- .../cluster-resources-settings.tsx | 59 ++++++++++++ 3 files changed, 205 insertions(+), 7 deletions(-) diff --git a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx index 8c3b4a73824..ad6cedc2639 100644 --- a/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx +++ b/libs/domains/clusters/feature/src/lib/nodepools-resources-settings/nodepool-modal/nodepool-modal.tsx @@ -11,7 +11,7 @@ import { P, match } from 'ts-pattern' import { Callout, Icon, InputSelect, InputText, InputToggle, ModalCrud, Tooltip, useModal } from '@qovery/shared/ui' import { upperCaseFirstLetter } from '@qovery/shared/util-js' -type OverridePrefix = 'stable_override' | 'default_override' | 'gpu_override' +type OverridePrefix = 'stable_override' | 'default_override' | 'gpu_override' | 'cronjob_override' function LimitsFields({ prefix }: { prefix: OverridePrefix }) { const { control, watch } = useFormContext() @@ -109,9 +109,12 @@ function LimitsFields({ prefix }: { prefix: OverridePrefix }) { } export interface NodepoolModalProps { - type: 'stable' | 'default' | 'gpu' + type: 'stable' | 'default' | 'gpu' | 'cronjob' cluster: Cluster - onChange: (data: Omit) => void + // TODO: Narrow this type once KarpenterCronjobNodePoolOverride is available in the generated TS client + onChange: ( + data: Omit & { cronjob_override?: KarpenterStableNodePoolOverride } + ) => void defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride } @@ -119,10 +122,15 @@ const CPU_MIN = 6 const MEMORY_MIN = 10 const GPU_MIN = 0 +// TODO: Remove this extended type once KarpenterCronjobNodePoolOverride is available in the generated TS client +type NodepoolFormData = Omit & { + cronjob_override?: KarpenterStableNodePoolOverride +} + export function NodepoolModal({ type, cluster, onChange, defaultValues }: NodepoolModalProps) { const { closeModal } = useModal() - const methods = useForm>({ + const methods = useForm({ mode: 'onChange', defaultValues: { default_override: { @@ -148,6 +156,21 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo limits: defaultValues?.limits, consolidate_after: defaultValues?.consolidate_after, }, + cronjob_override: { + ...defaultValues, + ...{ + consolidation: match(defaultValues) + .with({ consolidation: P.not(P.nullish) }, ({ consolidation }) => ({ + ...consolidation, + start_time: consolidation.start_time.replace('PT', ''), + duration: consolidation.duration.replace('PT', ''), + })) + .otherwise(() => ({ + start_time: '', + duration: '', + })), + }, + }, }, }) @@ -155,13 +178,14 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo .with('default', () => 'default_override' as const) .with('stable', () => 'stable_override' as const) .with('gpu', () => 'gpu_override' as const) + .with('cronjob', () => 'cronjob_override' as const) .exhaustive() const watchConsolidation = methods.watch( `${prefix === 'default_override' ? 'stable_override' : prefix}.consolidation.enabled` ) const onSubmit = methods.handleSubmit(async (data) => { - const payload: Omit = match(type) + const payload: NodepoolFormData = match(type) .with('default', () => ({ default_override: { limits: { @@ -215,6 +239,27 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo consolidate_after: data.gpu_override?.consolidate_after, }, })) + .with('cronjob', () => ({ + cronjob_override: { + limits: { + enabled: data.cronjob_override?.limits?.enabled ?? false, + max_cpu_in_vcpu: data.cronjob_override?.limits?.max_cpu_in_vcpu ?? CPU_MIN, + max_memory_in_gibibytes: data.cronjob_override?.limits?.max_memory_in_gibibytes ?? MEMORY_MIN, + max_gpu: data.cronjob_override?.limits?.max_gpu ?? GPU_MIN, + }, + consolidation: { + enabled: data.cronjob_override?.consolidation?.enabled ?? false, + days: data.cronjob_override?.consolidation?.days ?? [], + start_time: data.cronjob_override?.consolidation?.start_time + ? `PT${data.cronjob_override.consolidation.start_time}` + : '', + duration: data.cronjob_override?.consolidation?.duration + ? `PT${data.cronjob_override.consolidation.duration.toUpperCase()}` + : '', + }, + consolidate_after: data.cronjob_override?.consolidate_after, + }, + })) .exhaustive() onChange(payload) @@ -234,6 +279,7 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo .with('stable', () => 'Nodepool stable') .with('default', () => 'Nodepool default') .with('gpu', () => 'Nodepool gpu') + .with('cronjob', () => 'Nodepool cronjob') .exhaustive()} description={match(type) .with( @@ -246,6 +292,7 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo () => 'Designed to handle general workloads and serves as the foundation for deploying most applications.' ) .with('gpu', () => 'Used for GPU workloads, such as machine learning and data processing.') + .with('cronjob', () => 'Dedicated to cronjob workloads, providing isolated nodes for scheduled tasks.') .exhaustive()} onSubmit={onSubmit} onClose={closeModal} @@ -295,7 +342,7 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo )) - .with('stable_override', 'gpu_override', (prefix) => ( + .with('stable_override', 'gpu_override', 'cronjob_override', (prefix) => (
{ export interface NodepoolsResourcesSettingsProps { cluster: Cluster - filter: 'default' | 'gpu' + filter: 'default' | 'gpu' | 'cronjob' } export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourcesSettingsProps) { @@ -82,6 +82,8 @@ export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourc const watchStable = watch('karpenter.qovery_node_pools.stable_override') const watchDefault = watch('karpenter.qovery_node_pools.default_override') const watchGpu = watch('karpenter.qovery_node_pools.gpu_override') + // TODO: Replace with proper type once KarpenterCronjobNodePoolOverride is available in the generated TS client + const watchCronjob = watch('karpenter.qovery_node_pools.cronjob_override' as any) const { start: startStable, end: endStable } = formatTimeRange( watchStable?.consolidation?.start_time, @@ -91,6 +93,10 @@ export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourc watchGpu?.consolidation?.start_time, watchGpu?.consolidation?.duration ) + const { start: startCronjob, end: endCronjob } = formatTimeRange( + watchCronjob?.consolidation?.start_time, + watchCronjob?.consolidation?.duration + ) return (
@@ -349,6 +355,92 @@ export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourc
)) + .with('cronjob', () => ( +
+
+
+

Cronjob nodepool

+ + Dedicated to cronjob workloads. Cronjob pods are automatically scheduled on this nodepool when + enabled. Consolidation can be configured independently from the default nodepool. + +
+ +
+
+
+ Consolidation +
+ {watchCronjob?.consolidation?.enabled ? ( + + + {formatWeekdays(watchCronjob?.consolidation?.days)}, + + + + + + + + {startCronjob} to {endCronjob} + + {watchCronjob?.consolidate_after && ( + Consolidate after: {watchCronjob.consolidate_after} + )} + + ) : ( + Disabled + )} +
+
+
+ Resources limit + {watchCronjob?.limits?.enabled ? ( + + {watchCronjob.limits.max_cpu_in_vcpu && ( + vCPU limit: {watchCronjob?.limits?.max_cpu_in_vcpu} vCPU; + )} + {watchCronjob.limits.max_memory_in_gibibytes && ( + <> +
+ Memory limit: {watchCronjob?.limits?.max_memory_in_gibibytes} GiB + + )} +
+ ) : ( + No limit + )} +
+
+
+ )) .exhaustive()} diff --git a/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx b/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx index aa994598c62..1e5e6cdaeb5 100644 --- a/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx +++ b/libs/shared/console-shared/src/lib/cluster-settings/ui/cluster-resources-settings/cluster-resources-settings.tsx @@ -67,6 +67,10 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { const isKarpenter = Boolean(props.cluster?.features?.find((f) => f.id === 'KARPENTER')) const [isGpuEnabled, setIsGpuEnabled] = useState(!!watchKarpenter?.qovery_node_pools?.gpu_override) + // TODO: Replace cast once KarpenterCronjobNodePoolOverride is available in the generated TS client + const [isCronjobEnabled, setIsCronjobEnabled] = useState( + !!(watchKarpenter?.qovery_node_pools as any)?.cronjob_override + ) const { data: cloudProviderInstanceTypes } = useCloudProviderInstanceTypes( match(props.cloudProvider || CloudVendorEnum.AWS) @@ -173,6 +177,18 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { setIsGpuEnabled(value) } + // TODO: Replace casts once KarpenterCronjobNodePoolOverride is available in the generated TS client + const handleCronjobEnabledChange = (value: boolean) => { + if (!value) { + setValue('karpenter.qovery_node_pools.cronjob_override' as any, undefined) + } else { + setValue('karpenter.qovery_node_pools.cronjob_override' as any, { + ...(watchKarpenter?.qovery_node_pools as any)?.cronjob_override, + }) + } + setIsCronjobEnabled(value) + } + return (
{props.cloudProvider === 'AWS' && watchClusterType === KubernetesEnum.MANAGED && ( @@ -546,6 +562,49 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { )} + + +
+ + + + + + + + After enabling or disabling this setting, we strongly recommend redeploying all your cron jobs to + ensure they run with the correct node targeting. A fallback mechanism prevents pods from getting + stuck, but redeploying guarantees optimal scheduling. + + + +
+ + + {isCronjobEnabled && ( + + {watchKarpenterEnabled && props.cluster && ( + + )} + + )} + +
)}