From 1a247dd861f5b11882e2997d80979b0639a86dea Mon Sep 17 00:00:00 2001 From: Guillaume Da Silva Date: Thu, 9 Apr 2026 09:16:12 +0200 Subject: [PATCH 1/2] feat(clusters): add cronjob nodepool support --- .../nodepool-modal/nodepool-modal.tsx | 53 +++++++++- .../nodepools-resources-settings.tsx | 97 +++++++++++++++++-- .../cluster-resources-settings.tsx | 55 +++++++++++ package.json | 2 +- yarn.lock | 10 +- 5 files changed, 199 insertions(+), 18 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..6f44a99e387 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 @@ -1,7 +1,9 @@ import { differenceInMinutes, isValid } from 'date-fns' import { type Cluster, + type KarpenterCronjobNodePoolOverride, type KarpenterDefaultNodePoolOverride, + type KarpenterGpuNodePoolOverride, type KarpenterNodePool, type KarpenterStableNodePoolOverride, WeekdayEnum, @@ -11,7 +13,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,10 +111,14 @@ function LimitsFields({ prefix }: { prefix: OverridePrefix }) { } export interface NodepoolModalProps { - type: 'stable' | 'default' | 'gpu' + type: 'stable' | 'default' | 'gpu' | 'cronjob' cluster: Cluster onChange: (data: Omit) => void - defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride + defaultValues?: + | KarpenterStableNodePoolOverride + | KarpenterDefaultNodePoolOverride + | KarpenterGpuNodePoolOverride + | KarpenterCronjobNodePoolOverride } const CPU_MIN = 6 @@ -148,6 +154,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,6 +176,7 @@ 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` @@ -215,6 +237,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 +277,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 +290,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 +340,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 +76,7 @@ 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') + const watchCronjob = watch('karpenter.qovery_node_pools.cronjob_override') const { start: startStable, end: endStable } = formatTimeRange( watchStable?.consolidation?.start_time, @@ -91,6 +86,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 +348,88 @@ 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 8f86acb1593..9df0c1bfcbc 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 @@ -66,6 +66,7 @@ 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) + const [isCronjobEnabled, setIsCronjobEnabled] = useState(!!watchKarpenter?.qovery_node_pools?.cronjob_override) const { data: cloudProviderInstanceTypes } = useCloudProviderInstanceTypes( match(props.cloudProvider || CloudVendorEnum.AWS) @@ -170,6 +171,17 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { setIsGpuEnabled(value) } + const handleCronjobEnabledChange = (value: boolean) => { + if (!value) { + setValue('karpenter.qovery_node_pools.cronjob_override', undefined) + } else { + setValue('karpenter.qovery_node_pools.cronjob_override', { + ...watchKarpenter?.qovery_node_pools?.cronjob_override, + }) + } + setIsCronjobEnabled(value) + } + return (
{props.cloudProvider === 'AWS' && watchClusterType === KubernetesEnum.MANAGED && ( @@ -541,6 +553,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 && ( + + )} + + )} + +
)} diff --git a/package.json b/package.json index dd3ce952c1a..676355dd229 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "mermaid": "11.6.0", "monaco-editor": "0.53.0", "posthog-js": "1.260.1", - "qovery-typescript-axios": "1.1.855", + "qovery-typescript-axios": "1.1.858", "react": "18.3.1", "react-country-flag": "3.0.2", "react-datepicker": "4.12.0", diff --git a/yarn.lock b/yarn.lock index 9842dd2fd20..2cb55d43cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5451,7 +5451,7 @@ __metadata: prettier: 3.2.5 prettier-plugin-tailwindcss: 0.5.14 pretty-quick: 4.0.0 - qovery-typescript-axios: 1.1.855 + qovery-typescript-axios: 1.1.858 qovery-ws-typescript-axios: 0.1.506 react: 18.3.1 react-country-flag: 3.0.2 @@ -24269,12 +24269,12 @@ __metadata: languageName: node linkType: hard -"qovery-typescript-axios@npm:1.1.855": - version: 1.1.855 - resolution: "qovery-typescript-axios@npm:1.1.855" +"qovery-typescript-axios@npm:1.1.858": + version: 1.1.858 + resolution: "qovery-typescript-axios@npm:1.1.858" dependencies: axios: 1.12.2 - checksum: e456e82d9b2027d4e862ceb7c720857aa52b8d36340961a6622c1478ce34126ed507d427d0abb65f17fba91a05a4cb3ff76c86ff2d5f259c8e31ebd59f599d84 + checksum: b6f8e62fe69c54c63efa904edbcb4d1536b75861b261990843d13f12f9e646ca66c42c57a4a501eb9cec92e9ca72614ea4c5a8104d933f2ab715a2990ce189e1 languageName: node linkType: hard From 81ff5dbc3adcf7e3387f23d1a6eca509cdee449e Mon Sep 17 00:00:00 2001 From: RemiBonnet Date: Thu, 9 Apr 2026 09:26:47 +0200 Subject: [PATCH 2/2] refactor(cluster-resources): remove unnecessary class from cronjob nodepool description component --- .../ui/cluster-resources-settings/cluster-resources-settings.tsx | 1 - 1 file changed, 1 deletion(-) 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 9df0c1bfcbc..68e0490c370 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 @@ -563,7 +563,6 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) { title="Enable cronjob nodepools" description="Creates a dedicated nodepool for cronjob workloads with isolated nodes. Cronjob scaling will not impact long-running services on the default nodepool." forceAlignTop - className="items-center" small />