Skip to content
Open
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
Expand Up @@ -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()
Expand Down Expand Up @@ -109,20 +109,28 @@ function LimitsFields({ prefix }: { prefix: OverridePrefix }) {
}

export interface NodepoolModalProps {
type: 'stable' | 'default' | 'gpu'
type: 'stable' | 'default' | 'gpu' | 'cronjob'
cluster: Cluster
onChange: (data: Omit<KarpenterNodePool, 'requirements'>) => void
// TODO: Narrow this type once KarpenterCronjobNodePoolOverride is available in the generated TS client
onChange: (
data: Omit<KarpenterNodePool, 'requirements'> & { cronjob_override?: KarpenterStableNodePoolOverride }
) => void
defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride
}

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<KarpenterNodePool, 'requirements'> & {
cronjob_override?: KarpenterStableNodePoolOverride
}

export function NodepoolModal({ type, cluster, onChange, defaultValues }: NodepoolModalProps) {
const { closeModal } = useModal()

const methods = useForm<Omit<KarpenterNodePool, 'requirements'>>({
const methods = useForm<NodepoolFormData>({
mode: 'onChange',
defaultValues: {
default_override: {
Expand All @@ -148,20 +156,36 @@ 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: '',
})),
},
},
},
})

const prefix: OverridePrefix = match(type)
.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<KarpenterNodePool, 'requirements'> = match(type)
const payload: NodepoolFormData = match(type)
.with('default', () => ({
default_override: {
limits: {
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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}
Expand Down Expand Up @@ -295,7 +342,7 @@ export function NodepoolModal({ type, cluster, onChange, defaultValues }: Nodepo
</div>
</div>
))
.with('stable_override', 'gpu_override', (prefix) => (
.with('stable_override', 'gpu_override', 'cronjob_override', (prefix) => (
<div className="flex flex-col gap-4 rounded border border-neutral-250 bg-neutral-100 p-4">
<Controller
name={`${prefix}.consolidation.enabled`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const formatWeekdays = (days: string[]): string => {

export interface NodepoolsResourcesSettingsProps {
cluster: Cluster
filter: 'default' | 'gpu'
filter: 'default' | 'gpu' | 'cronjob'
}

export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourcesSettingsProps) {
Expand All @@ -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,
Expand All @@ -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 (
<div>
Expand Down Expand Up @@ -349,6 +355,92 @@ export function NodepoolsResourcesSettings({ cluster, filter }: NodepoolsResourc
</div>
</div>
))
.with('cronjob', () => (
<div className="flex flex-col gap-4 rounded border border-neutral-200 bg-neutral-150 p-4 text-sm">
<div className="flex justify-between gap-10">
<div className="flex flex-col gap-1.5">
<p className="font-medium text-neutral-400">Cronjob nodepool</p>
<span className="text-ssm text-neutral-350">
Dedicated to cronjob workloads. Cronjob pods are automatically scheduled on this nodepool when
enabled. Consolidation can be configured independently from the default nodepool.
</span>
</div>
<Button
type="button"
variant="surface"
color="neutral"
onClick={() =>
openModal({
content: (
<NodepoolModal
type="cronjob"
cluster={cluster}
onChange={(data) => {
// TODO: Remove cast once qovery-typescript-axios is regenerated with KarpenterCronjobNodePoolOverride
setValue('karpenter.qovery_node_pools.cronjob_override' as any, {
...watchCronjob,
// TODO: Remove cast once qovery-typescript-axios is regenerated with KarpenterCronjobNodePoolOverride
limits: (data as any).cronjob_override?.limits,
// TODO: Remove cast once qovery-typescript-axios is regenerated with KarpenterCronjobNodePoolOverride
consolidation: (data as any).cronjob_override?.consolidation,
})
}}
defaultValues={watchCronjob}
/>
),
})
}
>
<Icon iconName="pen" iconStyle="solid" />
</Button>
</div>
<div className="flex justify-between gap-4">
<div className="flex w-1/2 flex-col gap-1">
<span className="text-neutral-350">Consolidation</span>
<div className="flex flex-col justify-between gap-4 text-sm text-neutral-400">
{watchCronjob?.consolidation?.enabled ? (
<span className="flex flex-col justify-center">
<span className="flex gap-1.5">
{formatWeekdays(watchCronjob?.consolidation?.days)},
<Tooltip content={`Schedule (${cluster.region})`}>
<span className="text-sm">
<Icon iconName="circle-info" iconStyle="regular" />
</span>
</Tooltip>
</span>
<span>
{startCronjob} to {endCronjob}
</span>
{watchCronjob?.consolidate_after && (
<span className="text-neutral-350">Consolidate after: {watchCronjob.consolidate_after}</span>
)}
</span>
) : (
<span>Disabled</span>
)}
</div>
</div>
<div className="flex w-1/2 flex-col gap-1">
<span className="text-neutral-350">Resources limit</span>
{watchCronjob?.limits?.enabled ? (
<span>
{watchCronjob.limits.max_cpu_in_vcpu && (
<span>vCPU limit: {watchCronjob?.limits?.max_cpu_in_vcpu} vCPU; </span>
)}
{watchCronjob.limits.max_memory_in_gibibytes && (
<>
<br />
<span>Memory limit: {watchCronjob?.limits?.max_memory_in_gibibytes} GiB</span>
</>
)}
</span>
) : (
<span>No limit</span>
)}
</div>
</div>
</div>
))
.exhaustive()}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 (
<div className="flex flex-col gap-10">
{props.cloudProvider === 'AWS' && watchClusterType === KubernetesEnum.MANAGED && (
Expand Down Expand Up @@ -546,6 +562,49 @@ export function ClusterResourcesSettings(props: ClusterResourcesSettingsProps) {
)}
</AnimatePresence>
</BlockContent>

<BlockContent title="Cronjob nodepools configuration" className="mb-0" classNameContent="p-0">
<div className="flex flex-col gap-3 p-4">
<InputToggle
value={isCronjobEnabled}
onChange={handleCronjobEnabledChange}
name="cronjob_enabled"
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
/>
<Callout.Root color="sky">
<Callout.Icon>
<Icon iconName="info-circle" iconStyle="regular" />
</Callout.Icon>
<Callout.Text>
<Callout.TextDescription>
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.
</Callout.TextDescription>
</Callout.Text>
</Callout.Root>
</div>

<AnimatePresence>
{isCronjobEnabled && (
<motion.div
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
className="overflow-hidden"
>
{watchKarpenterEnabled && props.cluster && (
<NodepoolsResourcesSettings cluster={props.cluster} filter="cronjob" />
)}
</motion.div>
)}
</AnimatePresence>
</BlockContent>
</>
)}

Expand Down
Loading