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
@@ -1,7 +1,9 @@
import { differenceInMinutes, isValid } from 'date-fns'
import {
type Cluster,
type KarpenterCronjobNodePoolOverride,
type KarpenterDefaultNodePoolOverride,
type KarpenterGpuNodePoolOverride,
type KarpenterNodePool,
type KarpenterStableNodePoolOverride,
WeekdayEnum,
Expand All @@ -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()
Expand Down Expand Up @@ -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<KarpenterNodePool, 'requirements'>) => void
defaultValues?: KarpenterStableNodePoolOverride | KarpenterDefaultNodePoolOverride
defaultValues?:
| KarpenterStableNodePoolOverride
| KarpenterDefaultNodePoolOverride
| KarpenterGpuNodePoolOverride
| KarpenterCronjobNodePoolOverride
}

const CPU_MIN = 6
Expand Down Expand Up @@ -148,13 +154,29 @@ 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`
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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}
Expand Down Expand Up @@ -295,7 +340,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
@@ -1,11 +1,5 @@
import { add, format, parse } from 'date-fns'
import {
type Cluster,
type KarpenterDefaultNodePoolOverride,
type KarpenterGpuNodePoolOverride,
type KarpenterStableNodePoolOverride,
WeekdayEnum,
} from 'qovery-typescript-axios'
import { type Cluster, WeekdayEnum } from 'qovery-typescript-axios'
import { useFormContext } from 'react-hook-form'
import { match } from 'ts-pattern'
import { type ClusterResourcesData } from '@qovery/shared/interfaces'
Expand Down Expand Up @@ -72,7 +66,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 +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,
Expand All @@ -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 (
<div>
Expand Down Expand Up @@ -349,6 +348,88 @@ 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) => {
setValue('karpenter.qovery_node_pools.cronjob_override', {
...watchCronjob,
...data.cronjob_override,
})
}}
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 @@ -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)
Expand Down Expand Up @@ -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 (
<div className="flex flex-col gap-10">
{props.cloudProvider === 'AWS' && watchClusterType === KubernetesEnum.MANAGED && (
Expand Down Expand Up @@ -541,6 +553,48 @@ 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
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading