From d8e5f56abf5148280adeca3d92feac3910b50a29 Mon Sep 17 00:00:00 2001 From: Alexis Guerville Date: Fri, 18 Jul 2025 15:53:38 +0200 Subject: [PATCH 1/2] Add --wait-timeout and return error when timing out or service is erroring --- pkg/koyeb/apps.go | 7 ++++--- pkg/koyeb/apps_init.go | 27 +++++++++++++++++++++------ pkg/koyeb/deploy.go | 9 +++++---- pkg/koyeb/flags.go | 11 ++++++++++- pkg/koyeb/services.go | 12 ++++++------ pkg/koyeb/services_create.go | 24 ++++++++++++++++++------ pkg/koyeb/services_redeploy.go | 23 +++++++++++++++-------- pkg/koyeb/services_update.go | 27 ++++++++++++++++++--------- 8 files changed, 97 insertions(+), 43 deletions(-) diff --git a/pkg/koyeb/apps.go b/pkg/koyeb/apps.go index a782e322..2083c5a1 100644 --- a/pkg/koyeb/apps.go +++ b/pkg/koyeb/apps.go @@ -1,6 +1,8 @@ package koyeb import ( + "time" + "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" "github.com/spf13/cobra" ) @@ -46,12 +48,11 @@ func NewAppCmd() *cobra.Command { createService.SetDefinition(*createDefinition) - wait, _ := cmd.Flags().GetBool("wait") - - return h.Init(ctx, cmd, args, createApp, createService, wait) + return h.Init(ctx, cmd, args, createApp, createService) }), } initAppCmd.Flags().Bool("wait", false, "Waits until app deployment is done") + initAppCmd.Flags().Duration("wait-timeout", 5*time.Minute, "Duration the wait will last until timeout") appCmd.AddCommand(initAppCmd) serviceHandler.addServiceDefinitionFlags(initAppCmd.Flags()) diff --git a/pkg/koyeb/apps_init.go b/pkg/koyeb/apps_init.go index 7c69a291..92ebf0f5 100644 --- a/pkg/koyeb/apps_init.go +++ b/pkg/koyeb/apps_init.go @@ -13,7 +13,13 @@ import ( "github.com/spf13/cobra" ) -func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, createApp *koyeb.CreateApp, createService *koyeb.CreateService, wait bool) error { +func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, createApp *koyeb.CreateApp, createService *koyeb.CreateService) error { + wait, _ := cmd.Flags().GetBool("wait") + waitTimeout, err := cmd.Flags().GetDuration("wait-timeout") + if err != nil { + return err + } + uid := uuid.Must(uuid.NewV4()) createService.SetAppId(uid.String()) _, resp, err := ctx.Client.ServicesApi.CreateService(ctx.Context).DryRun(true).Service(*createService).Execute() @@ -66,7 +72,7 @@ func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, cr serviceRes.Service.GetId()[:8], ) - ctxd, cancel := context.WithTimeout(ctx.Context, 5*time.Minute) + ctxd, cancel := context.WithTimeout(ctx.Context, waitTimeout) defer cancel() for range ticker(ctxd, 2*time.Second) { @@ -79,9 +85,15 @@ func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, cr ) } - if getServiceRes.Service != nil && getServiceRes.Service.Status != nil && - *getServiceRes.Service.Status != koyeb.SERVICESTATUS_STARTING { - return nil + if getServiceRes.Service != nil && getServiceRes.Service.Status != nil { + switch status := *getServiceRes.Service.Status; status { + case koyeb.SERVICESTATUS_DELETED, koyeb.SERVICESTATUS_DEGRADED, koyeb.SERVICESTATUS_UNHEALTHY: + return fmt.Errorf("Service %s deployment ended in status: %s", serviceRes.Service.GetId()[:8], status) + case koyeb.SERVICESTATUS_STARTING, koyeb.SERVICESTATUS_RESUMING, koyeb.SERVICESTATUS_DELETING, koyeb.SERVICESTATUS_PAUSING: + break + default: + return nil + } } } @@ -89,7 +101,10 @@ func (h *AppHandler) Init(ctx *CLIContext, cmd *cobra.Command, args []string, cr serviceRes.Service.GetId()[:8], serviceRes.Service.GetId()[:8], ) - return nil + return fmt.Errorf("service deployment still in progress, --wait timed out. To access the build logs, run: `koyeb service logs %s -t build`. For the runtime logs, run `koyeb service logs %s`", + serviceRes.Service.GetId()[:8], + serviceRes.Service.GetId()[:8], + ) } return nil diff --git a/pkg/koyeb/deploy.go b/pkg/koyeb/deploy.go index 839e94ed..b1dfb215 100644 --- a/pkg/koyeb/deploy.go +++ b/pkg/koyeb/deploy.go @@ -3,6 +3,7 @@ package koyeb import ( "fmt" "strconv" + "time" "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" "github.com/koyeb/koyeb-cli/pkg/koyeb/errors" @@ -15,7 +16,6 @@ func NewDeployCmd() *cobra.Command { appHandler := NewAppHandler() archiveHandler := NewArchiveHandler() serviceHandler := NewServiceHandler() - wait := false deployCmd := &cobra.Command{ Use: "deploy /", @@ -86,7 +86,7 @@ func NewDeployCmd() *cobra.Command { createService.SetDefinition(*createDefinition) log.Infof("Creating the new service `%s`", serviceName) - if err := serviceHandler.Create(ctx, cmd, []string{args[1]}, createService, wait); err != nil { + if err := serviceHandler.Create(ctx, cmd, []string{args[1]}, createService); err != nil { return err } } else { @@ -143,7 +143,7 @@ func NewDeployCmd() *cobra.Command { updateService.SetDefinition(*updateDefinition) log.Infof("Updating the existing service `%s`", serviceName) - if err := serviceHandler.Update(ctx, cmd, []string{args[1]}, updateService, wait); err != nil { + if err := serviceHandler.Update(ctx, cmd, []string{args[1]}, updateService); err != nil { return err } } @@ -151,7 +151,8 @@ func NewDeployCmd() *cobra.Command { }), } deployCmd.Flags().String("app", "", "Service application. Can also be provided in the service name with the format /") - deployCmd.Flags().BoolVar(&wait, "wait", false, "Waits until the deployment is done") + deployCmd.Flags().Bool("wait", false, "Waits until the deployment is done") + deployCmd.Flags().Duration("wait-timeout", 5*time.Minute, "Duration the wait will last until timeout") serviceHandler.addServiceDefinitionFlagsForAllSources(deployCmd.Flags()) serviceHandler.addServiceDefinitionFlagsForArchiveSource(deployCmd.Flags()) diff --git a/pkg/koyeb/flags.go b/pkg/koyeb/flags.go index a8cb9abd..19bfe2a5 100644 --- a/pkg/koyeb/flags.go +++ b/pkg/koyeb/flags.go @@ -1,6 +1,15 @@ package koyeb -import "github.com/spf13/cobra" +import ( + "time" + + "github.com/spf13/cobra" +) + +func GetDurationFlags(cmd *cobra.Command, name string) time.Duration { + val, _ := cmd.Flags().GetDuration(name) + return val +} func GetBoolFlags(cmd *cobra.Command, name string) bool { val, _ := cmd.Flags().GetBool(name) diff --git a/pkg/koyeb/services.go b/pkg/koyeb/services.go index 599fe4e2..ff7bafbf 100644 --- a/pkg/koyeb/services.go +++ b/pkg/koyeb/services.go @@ -5,6 +5,7 @@ import ( "regexp" "strconv" "strings" + "time" "github.com/koyeb/koyeb-api-client-go/api/v1/koyeb" "github.com/koyeb/koyeb-cli/pkg/koyeb/dates" @@ -61,14 +62,13 @@ $> koyeb service create myservice --app myapp --docker nginx --port 80:tcp createDefinition.Name = koyeb.PtrString(serviceName) createService.SetDefinition(*createDefinition) - wait, _ := cmd.Flags().GetBool("wait") - - return h.Create(ctx, cmd, args, createService, wait) + return h.Create(ctx, cmd, args, createService) }), } h.addServiceDefinitionFlags(createServiceCmd.Flags()) createServiceCmd.Flags().StringP("app", "a", "", "Service application") createServiceCmd.Flags().Bool("wait", false, "Waits until service deployment is done") + createServiceCmd.Flags().Duration("wait-timeout", 5*time.Minute, "Duration the wait will last until timeout") serviceCmd.AddCommand(createServiceCmd) getServiceCmd := &cobra.Command{ @@ -224,9 +224,7 @@ $> koyeb service update myapp/myservice --port 80:tcp --route '!/' saveOnly, _ := cmd.Flags().GetBool("save-only") updateService.SetSaveOnly(saveOnly) - wait, _ := cmd.Flags().GetBool("wait") - - return h.Update(ctx, cmd, args, updateService, wait) + return h.Update(ctx, cmd, args, updateService) }), } h.addServiceDefinitionFlags(updateServiceCmd.Flags()) @@ -236,6 +234,7 @@ $> koyeb service update myapp/myservice --port 80:tcp --route '!/' updateServiceCmd.Flags().Bool("skip-build", false, "If there has been at least one past successfully build deployment, use the last one instead of rebuilding. WARNING: this can lead to unexpected behavior if the build depends, for example, on environment variables.") updateServiceCmd.Flags().Bool("save-only", false, "Save the new configuration without deploying it") updateServiceCmd.Flags().Bool("wait", false, "Waits until the service deployment is done") + updateServiceCmd.Flags().Duration("wait-timeout", 5*time.Minute, "Duration the wait will last until timeout") serviceCmd.AddCommand(updateServiceCmd) redeployServiceCmd := &cobra.Command{ @@ -247,6 +246,7 @@ $> koyeb service update myapp/myservice --port 80:tcp --route '!/' redeployServiceCmd.Flags().StringP("app", "a", "", "Service application") redeployServiceCmd.Flags().Bool("skip-build", false, "If there has been at least one past successfully build deployment, use the last one instead of rebuilding. WARNING: this can lead to unexpected behavior if the build depends, for example, on environment variables.") redeployServiceCmd.Flags().Bool("wait", false, "Waits until service deployment is done.") + redeployServiceCmd.Flags().Duration("wait-timeout", 5*time.Minute, "Duration the wait will last until timeout") serviceCmd.AddCommand(redeployServiceCmd) redeployServiceCmd.Flags().Bool("use-cache", false, "Use cache to redeploy") diff --git a/pkg/koyeb/services_create.go b/pkg/koyeb/services_create.go index 767b17c1..1271a973 100644 --- a/pkg/koyeb/services_create.go +++ b/pkg/koyeb/services_create.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createService *koyeb.CreateService, wait bool) error { +func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []string, createService *koyeb.CreateService) error { appID, err := h.parseAppName(cmd, args[0]) if err != nil { return err @@ -22,6 +22,9 @@ func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []stri return err } + wait, _ := cmd.Flags().GetBool("wait") + waitTimeout, _ := cmd.Flags().GetDuration("wait-timeout") + resApp, resp, err := ctx.Client.AppsApi.GetApp(ctx.Context, app).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( @@ -56,7 +59,7 @@ func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []stri }() if wait { - ctxd, cancel := context.WithTimeout(ctx.Context, 5*time.Minute) + ctxd, cancel := context.WithTimeout(ctx.Context, waitTimeout) defer cancel() for range ticker(ctxd, 2*time.Second) { @@ -69,9 +72,15 @@ func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []stri ) } - if res.Service != nil && res.Service.Status != nil && - *res.Service.Status != koyeb.SERVICESTATUS_STARTING { - return nil + if res.Service != nil && res.Service.Status != nil { + switch status := *res.Service.Status; status { + case koyeb.SERVICESTATUS_DELETED, koyeb.SERVICESTATUS_DEGRADED, koyeb.SERVICESTATUS_UNHEALTHY: + return fmt.Errorf("Service %s deployment ended in status: %s", res.Service.GetId()[:8], status) + case koyeb.SERVICESTATUS_STARTING, koyeb.SERVICESTATUS_RESUMING, koyeb.SERVICESTATUS_DELETING, koyeb.SERVICESTATUS_PAUSING: + break + default: + return nil + } } } @@ -79,7 +88,10 @@ func (h *ServiceHandler) Create(ctx *CLIContext, cmd *cobra.Command, args []stri res.Service.GetId()[:8], res.Service.GetId()[:8], ) - return nil + return fmt.Errorf("service deployment still in progress, --wait timed out. To access the build logs, run: `koyeb service logs %s -t build`. For the runtime logs, run `koyeb service logs %s`", + res.Service.GetId()[:8], + res.Service.GetId()[:8], + ) } return nil diff --git a/pkg/koyeb/services_redeploy.go b/pkg/koyeb/services_redeploy.go index cc74b566..ca9dc97d 100644 --- a/pkg/koyeb/services_redeploy.go +++ b/pkg/koyeb/services_redeploy.go @@ -25,6 +25,7 @@ func (h *ServiceHandler) ReDeploy(ctx *CLIContext, cmd *cobra.Command, args []st useCache := GetBoolFlags(cmd, "use-cache") skipBuild := GetBoolFlags(cmd, "skip-build") wait := GetBoolFlags(cmd, "wait") + waitTimeout := GetDurationFlags(cmd, "wait-timeout") redeployBody := *koyeb.NewRedeployRequestInfoWithDefaults() redeployBody.UseCache = &useCache @@ -44,7 +45,7 @@ func (h *ServiceHandler) ReDeploy(ctx *CLIContext, cmd *cobra.Command, args []st ) if wait { - ctxd, cancel := context.WithTimeout(ctx.Context, 5*time.Minute) + ctxd, cancel := context.WithTimeout(ctx.Context, waitTimeout) defer cancel() for range ticker(ctxd, 2*time.Second) { @@ -57,12 +58,15 @@ func (h *ServiceHandler) ReDeploy(ctx *CLIContext, cmd *cobra.Command, args []st ) } - if res.Deployment != nil && res.Deployment.Status != nil && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_ALLOCATING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_PROVISIONING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_PENDING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_STARTING { - return nil + if res.Deployment != nil && res.Deployment.Status != nil { + switch status := *res.Deployment.Status; status { + case koyeb.DEPLOYMENTSTATUS_ERROR, koyeb.DEPLOYMENTSTATUS_DEGRADED, koyeb.DEPLOYMENTSTATUS_UNHEALTHY, koyeb.DEPLOYMENTSTATUS_CANCELED, koyeb.DEPLOYMENTSTATUS_STOPPED, koyeb.DEPLOYMENTSTATUS_ERRORING: + return fmt.Errorf("Deployment %s update ended in status: %s", res.Deployment.GetId()[:8], status) + case koyeb.DEPLOYMENTSTATUS_STARTING, koyeb.DEPLOYMENTSTATUS_PENDING, koyeb.DEPLOYMENTSTATUS_PROVISIONING, koyeb.DEPLOYMENTSTATUS_ALLOCATING: + break + default: + return nil + } } } @@ -70,7 +74,10 @@ func (h *ServiceHandler) ReDeploy(ctx *CLIContext, cmd *cobra.Command, args []st res.Deployment.GetId()[:8], res.Deployment.GetId()[:8], ) - return nil + return fmt.Errorf("service deployment still in progress, --wait timed out. To access the build logs, run: `koyeb deployment logs %s -t build`. For the runtime logs, run `koyeb deployment logs %s`", + res.Deployment.GetId()[:8], + res.Deployment.GetId()[:8], + ) } log.Infof("Service %s redeployed.", serviceName) diff --git a/pkg/koyeb/services_update.go b/pkg/koyeb/services_update.go index 30dad0b7..0ea27a8a 100644 --- a/pkg/koyeb/services_update.go +++ b/pkg/koyeb/services_update.go @@ -11,7 +11,7 @@ import ( "github.com/spf13/cobra" ) -func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []string, updateService *koyeb.UpdateService, wait bool) error { +func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []string, updateService *koyeb.UpdateService) error { serviceName, err := h.parseServiceName(cmd, args[0]) if err != nil { return err @@ -22,6 +22,9 @@ func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []stri return err } + wait, _ := cmd.Flags().GetBool("wait") + waitTimeout, _ := cmd.Flags().GetDuration("wait-timeout") + res, resp, err := ctx.Client.ServicesApi.UpdateService(ctx.Context, service).Service(*updateService).Execute() if err != nil { return errors.NewCLIErrorFromAPIError( @@ -47,7 +50,7 @@ func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []stri }() if wait { - ctxd, cancel := context.WithTimeout(ctx.Context, 5*time.Minute) + ctxd, cancel := context.WithTimeout(ctx.Context, waitTimeout) defer cancel() for range ticker(ctxd, 2*time.Second) { @@ -60,12 +63,15 @@ func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []stri ) } - if res.Deployment != nil && res.Deployment.Status != nil && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_ALLOCATING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_PROVISIONING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_PENDING && - *res.Deployment.Status != koyeb.DEPLOYMENTSTATUS_STARTING { - return nil + if res.Deployment != nil && res.Deployment.Status != nil { + switch status := *res.Deployment.Status; status { + case koyeb.DEPLOYMENTSTATUS_ERROR, koyeb.DEPLOYMENTSTATUS_DEGRADED, koyeb.DEPLOYMENTSTATUS_UNHEALTHY, koyeb.DEPLOYMENTSTATUS_CANCELED, koyeb.DEPLOYMENTSTATUS_STOPPED, koyeb.DEPLOYMENTSTATUS_ERRORING: + return fmt.Errorf("Deployment %s update ended in status: %s", res.Deployment.GetId()[:8], status) + case koyeb.DEPLOYMENTSTATUS_STARTING, koyeb.DEPLOYMENTSTATUS_PENDING, koyeb.DEPLOYMENTSTATUS_PROVISIONING, koyeb.DEPLOYMENTSTATUS_ALLOCATING: + break + default: + return nil + } } } @@ -73,7 +79,10 @@ func (h *ServiceHandler) Update(ctx *CLIContext, cmd *cobra.Command, args []stri res.Service.GetId()[:8], res.Service.GetId()[:8], ) - return nil + return fmt.Errorf("service deployment still in progress, --wait timed out. To access the build logs, run: `koyeb service logs %s -t build`. For the runtime logs, run `koyeb service logs %s`", + res.Service.GetId()[:8], + res.Service.GetId()[:8], + ) } return nil } From 5fca1fc5ec5d12c96f618cae786ffbad854b0c82 Mon Sep 17 00:00:00 2001 From: Alexis Guerville Date: Fri, 18 Jul 2025 15:58:55 +0200 Subject: [PATCH 2/2] v5.7.0 --- CHANGES.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 4f38cf9b..45a2edb8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,8 @@ -## v5.7.0 (unreleased) +## v5.8.0 (unreleased) + +## v5.7.0 (2025-07-18) +* Add `--wait` and `--wait-timeout` to `app init`, `service create`, `service update`, `service redeploy` and `deploy`. + * By default the `--wait-timeout` duration will be 5 minutes, but can be changed by using like this: `--wait-timeout 1m` ## v5.6.0 (2025-07-17) * Add koyeb compose which is docker compose like functionality.