diff --git a/pkg/koyeb/sandbox_create.go b/pkg/koyeb/sandbox_create.go index c6b52ea7..a57d511a 100644 --- a/pkg/koyeb/sandbox_create.go +++ b/pkg/koyeb/sandbox_create.go @@ -163,6 +163,35 @@ func parseSandboxDefinitionFlags(ctx *CLIContext, cmd *cobra.Command, def *koyeb } } + // Require at least one sleep delay when min-scale is 0 and the user explicitly + // changed either the scale or sleep delay flags. + sleepChanged := flags.Lookup("light-sleep-delay").Changed || flags.Lookup("deep-sleep-delay").Changed + if minScale == 0 && (flags.Lookup("min-scale").Changed || sleepChanged) { + hasSleepTarget := false + for _, target := range scaling.GetTargets() { + if target.HasSleepIdleDelay() { + sid := target.GetSleepIdleDelay() + if (sid.LightSleepValue != nil && *sid.LightSleepValue > 0) || + (sid.DeepSleepValue != nil && *sid.DeepSleepValue > 0) { + hasSleepTarget = true + break + } + } + } + if !hasSleepTarget { + return &errors.CLIError{ + What: "Error while configuring the sandbox", + Why: "setting min-scale to 0 requires a sleep delay policy", + Additional: []string{ + "When min-scale is 0, a sleep delay must be configured to define when the service scales to zero.", + "Use --light-sleep-delay and/or --deep-sleep-delay to set the sleep policy.", + }, + Orig: nil, + Solution: "Add --light-sleep-delay and/or --deep-sleep-delay to your command and try again", + } + } + } + def.SetScalings([]koyeb.DeploymentScaling{*scaling}) return nil diff --git a/pkg/koyeb/services.go b/pkg/koyeb/services.go index 94891fb3..99a35e95 100644 --- a/pkg/koyeb/services.go +++ b/pkg/koyeb/services.go @@ -1367,8 +1367,8 @@ func (h *ServiceHandler) setScalingsTargets(flags *pflag.FlagSet, scaling *koyeb sid.DeepSleepValue = nil } } - // Remove the target entirely if both values are unset - if sid.LightSleepValue == nil && sid.DeepSleepValue == nil { + // Remove the target entirely when min-scale is not 0 + if scaling.GetMin() != 0 { continue } target.SetSleepIdleDelay(sid) @@ -1396,6 +1396,37 @@ func (h *ServiceHandler) setScalingsTargets(flags *pflag.FlagSet, scaling *koyeb } scaling.Targets = newTargets } + + // Require at least one sleep delay when min-scale is 0 and the user explicitly + // changed either the scale or sleep delay flags. + scaleChanged := flags.Lookup("min-scale").Changed || flags.Lookup("scale").Changed + sleepChanged := flags.Lookup("light-sleep-delay").Changed || flags.Lookup("deep-sleep-delay").Changed + if scaling.GetMin() == 0 && (scaleChanged || sleepChanged) { + hasSleepTarget := false + for _, target := range scaling.GetTargets() { + if target.HasSleepIdleDelay() { + sid := target.GetSleepIdleDelay() + if (sid.LightSleepValue != nil && *sid.LightSleepValue > 0) || + (sid.DeepSleepValue != nil && *sid.DeepSleepValue > 0) { + hasSleepTarget = true + break + } + } + } + if !hasSleepTarget { + return &errors.CLIError{ + What: "Error while configuring the service", + Why: "setting min-scale to 0 requires a sleep delay policy", + Additional: []string{ + "When min-scale is 0, a sleep delay must be configured to define when the service scales to zero.", + "Use --light-sleep-delay and/or --deep-sleep-delay to set the sleep policy.", + }, + Orig: nil, + Solution: "Add --light-sleep-delay and/or --deep-sleep-delay to your command and try again", + } + } + } + return nil } diff --git a/pkg/koyeb/services_test.go b/pkg/koyeb/services_test.go index 032bba7b..6de687b0 100644 --- a/pkg/koyeb/services_test.go +++ b/pkg/koyeb/services_test.go @@ -551,7 +551,7 @@ func TestSetSleepDelayFlags(t *testing.T) { }, }, }, - "disable both sleep delays removes target": { + "error when disabling both sleep delays with min-scale zero": { args: []string{"--light-sleep-delay", "0", "--deep-sleep-delay", "0"}, minScale: 0, currentTargets: []koyeb.DeploymentScalingTarget{ @@ -562,7 +562,7 @@ func TestSetSleepDelayFlags(t *testing.T) { }, }, }, - expected: []koyeb.DeploymentScalingTarget{}, + expectedErr: true, }, "preserve other targets when setting sleep delay": { args: []string{"--light-sleep-delay", "5m"}, @@ -620,6 +620,56 @@ func TestSetSleepDelayFlags(t *testing.T) { minScale: 2, expectedErr: true, }, + "error when min-scale is zero without sleep delays": { + args: []string{"--min-scale", "0"}, + minScale: 0, + currentTargets: nil, + expectedErr: true, + }, + "no error when min-scale is zero with existing sleep target and no flags changed": { + args: []string{}, + minScale: 0, + currentTargets: []koyeb.DeploymentScalingTarget{ + { + SleepIdleDelay: &koyeb.DeploymentScalingTargetSleepIdleDelay{ + LightSleepValue: koyeb.PtrInt64(300), + }, + }, + }, + expected: []koyeb.DeploymentScalingTarget{ + { + SleepIdleDelay: &koyeb.DeploymentScalingTargetSleepIdleDelay{ + LightSleepValue: koyeb.PtrInt64(300), + }, + }, + }, + }, + "error when disabling deep sleep with zero-value light sleep from API": { + args: []string{"--deep-sleep-delay", "0"}, + minScale: 0, + currentTargets: []koyeb.DeploymentScalingTarget{ + { + SleepIdleDelay: &koyeb.DeploymentScalingTargetSleepIdleDelay{ + LightSleepValue: koyeb.PtrInt64(0), + DeepSleepValue: koyeb.PtrInt64(60), + }, + }, + }, + expectedErr: true, + }, + "error when disabling sleep delay with existing zero values": { + args: []string{"--light-sleep-delay", "0"}, + minScale: 0, + currentTargets: []koyeb.DeploymentScalingTarget{ + { + SleepIdleDelay: &koyeb.DeploymentScalingTargetSleepIdleDelay{ + LightSleepValue: koyeb.PtrInt64(0), + DeepSleepValue: koyeb.PtrInt64(0), + }, + }, + }, + expectedErr: true, + }, } for name, tc := range tests {