diff --git a/frontend/cli/cmd_deploy.go b/frontend/cli/cmd_deploy.go index fc32fbca44..7be5e01953 100644 --- a/frontend/cli/cmd_deploy.go +++ b/frontend/cli/cmd_deploy.go @@ -2,6 +2,10 @@ package main import ( "context" + "time" + + "github.com/TBD54566975/golang-tools/go/ssa/testdata/src/fmt" + "github.com/alecthomas/types/optional" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1beta1/provisioner/provisionerconnect" @@ -11,10 +15,11 @@ import ( ) type deployCmd struct { - Replicas int32 `short:"n" help:"Number of replicas to deploy." default:"1"` - NoWait bool `help:"Do not wait for deployment to complete." default:"false"` - UseProvisioner bool `help:"Use the ftl-provisioner to deploy the application." default:"false" hidden:"true"` - Build buildCmd `embed:""` + Replicas int32 `short:"n" help:"Number of replicas to deploy." default:"1"` + NoWait bool `help:"Do not wait for deployment to complete." default:"false"` + UseProvisioner bool `help:"Use the ftl-provisioner to deploy the application." default:"false" hidden:"true"` + Build buildCmd `embed:""` + Timeout time.Duration `short:"t" help:"Timeout for the deployment."` } func (d *deployCmd) Run(ctx context.Context, projConfig projectconfig.Config) error { @@ -24,7 +29,6 @@ func (d *deployCmd) Run(ctx context.Context, projConfig projectconfig.Config) er } else { client = rpc.ClientFromContext[ftlv1connect.ControllerServiceClient](ctx) } - bindAllocator, err := bindAllocatorWithoutController() if err != nil { return err @@ -37,5 +41,14 @@ func (d *deployCmd) Run(ctx context.Context, projConfig projectconfig.Config) er if err != nil { return err } - return engine.BuildAndDeploy(ctx, d.Replicas, !d.NoWait) + tm := optional.None[time.Duration]() + if d.Timeout.Milliseconds() > 0 { + tm = optional.Some(d.Timeout) + } + + err = engine.BuildAndDeploy(ctx, d.Replicas, !d.NoWait, tm) + if err != nil { + return fmt.Errorf("failed to deploy: %w", err) + } + return nil } diff --git a/internal/buildengine/engine.go b/internal/buildengine/engine.go index 51d2d21592..50c7b27674 100644 --- a/internal/buildengine/engine.go +++ b/internal/buildengine/engine.go @@ -507,7 +507,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration }() // Build and deploy all modules first. - err = e.BuildAndDeploy(ctx, 1, true) + err = e.BuildAndDeploy(ctx, 1, true, optional.None[time.Duration]()) if err != nil { logger.Errorf(err, "initial deploy failed") } @@ -540,7 +540,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration } e.moduleMetas.Store(config.Module, meta) e.rawEngineUpdates <- ModuleAdded{Module: config.Module} - _ = e.BuildAndDeploy(ctx, 1, true, config.Module) //nolint:errcheck + _ = e.BuildAndDeploy(ctx, 1, true, optional.None[time.Duration](), config.Module) //nolint:errcheck } case watch.WatchEventModuleRemoved: err := terminateModuleDeployment(ctx, e.client, event.Config.Module) @@ -577,7 +577,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration meta.module.Config = validConfig e.moduleMetas.Store(event.Config.Module, meta) - _ = e.BuildAndDeploy(ctx, 1, true, event.Config.Module) //nolint:errcheck + _ = e.BuildAndDeploy(ctx, 1, true, optional.None[time.Duration](), event.Config.Module) //nolint:errcheck } case change := <-schemaChanges: if change.ChangeType == ftlv1.DeploymentChangeType_DEPLOYMENT_REMOVED { @@ -604,7 +604,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration dependentModuleNames := e.getDependentModuleNames(change.Name) if len(dependentModuleNames) > 0 { logger.Infof("%s's schema changed; processing %s", change.Name, strings.Join(dependentModuleNames, ", ")) - _ = e.BuildAndDeploy(ctx, 1, true, dependentModuleNames...) //nolint:errcheck + _ = e.BuildAndDeploy(ctx, 1, true, optional.None[time.Duration](), dependentModuleNames...) //nolint:errcheck } case event := <-e.rebuildRequests: @@ -619,7 +619,7 @@ func (e *Engine) watchForModuleChanges(ctx context.Context, period time.Duration break readLoop } } - _ = e.BuildAndDeploy(ctx, 1, true, modules...) //nolint:errcheck + _ = e.BuildAndDeploy(ctx, 1, true, optional.None[time.Duration](), modules...) //nolint:errcheck } } } @@ -755,7 +755,7 @@ func (e *Engine) getDependentModuleNames(moduleName string) []string { } // BuildAndDeploy attempts to build and deploy all local modules. -func (e *Engine) BuildAndDeploy(ctx context.Context, replicas int32, waitForDeployOnline bool, moduleNames ...string) error { +func (e *Engine) BuildAndDeploy(ctx context.Context, replicas int32, waitForDeployOnline bool, timeout optional.Option[time.Duration], moduleNames ...string) error { logger := log.FromContext(ctx) if len(moduleNames) == 0 { moduleNames = e.Modules() @@ -768,6 +768,11 @@ func (e *Engine) BuildAndDeploy(ctx context.Context, replicas int32, waitForDepl buildGroup.Go(func() error { e.modulesToBuild.Store(module.Config.Module, false) e.rawEngineUpdates <- ModuleDeployStarted{Module: module.Config.Module} + var cancel context.CancelFunc + if tm, ok := timeout.Get(); ok { + buildCtx, cancel = context.WithTimeout(buildCtx, tm) + defer cancel() + } err := Deploy(buildCtx, e.projectConfig, module, module.Deploy, replicas, waitForDeployOnline, e.client) if err != nil { e.rawEngineUpdates <- ModuleDeployFailed{Module: module.Config.Module, Error: err} diff --git a/internal/integration/actions.go b/internal/integration/actions.go index 909f4b8662..0e189d7bf7 100644 --- a/internal/integration/actions.go +++ b/internal/integration/actions.go @@ -224,7 +224,7 @@ func ExpectError(action Action, expectedErrorMsg ...string) Action { func Deploy(module string) Action { return Chain( func(t testing.TB, ic TestContext) { - args := []string{"deploy"} + args := []string{"deploy", "-t", "1m"} if ic.Provisioner != nil { args = append(args, "--use-provisioner", "--provisioner-endpoint=http://localhost:8893") }