diff --git a/cmd/ftl/cmd_serve.go b/cmd/ftl/cmd_serve.go index 3b81608c00..d094be8e23 100644 --- a/cmd/ftl/cmd_serve.go +++ b/cmd/ftl/cmd_serve.go @@ -8,11 +8,14 @@ import ( "os" "path/filepath" "strconv" + "strings" + "time" "github.com/alecthomas/errors" "github.com/alecthomas/kong" "golang.org/x/sync/errgroup" + "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/controller" "github.com/TBD54566975/ftl/backend/runner" @@ -24,8 +27,16 @@ type serveCmd struct { Runners int `short:"r" help:"Number of runners to start." default:"10"` } +const ftlContainerName = "ftl-db" + func (s *serveCmd) Run(ctx context.Context) error { logger := log.FromContext(ctx) + + dsn, err := setupDB(ctx) + if err != nil { + return errors.WithStack(err) + } + logger.Infof("Starting %d controller(s) and %d runner(s)", s.Controllers, s.Runners) wg, ctx := errgroup.WithContext(ctx) @@ -38,6 +49,7 @@ func (s *serveCmd) Run(ctx context.Context) error { controllerAddresses = append(controllerAddresses, nextBind) config := controller.Config{ Bind: nextBind, + DSN: dsn, } if err := kong.ApplyDefaults(&config); err != nil { return errors.WithStack(err) @@ -105,6 +117,71 @@ func (s *serveCmd) Run(ctx context.Context) error { return nil } +func setupDB(ctx context.Context) (string, error) { + logger := log.FromContext(ctx) + logger.Infof("Checking for FTL database") + + nameFlag := fmt.Sprintf("name=^/%s$", ftlContainerName) + output, err := exec.Capture(ctx, ".", "docker", "ps", "-a", "--filter", nameFlag, "--format", "{{.Names}}") + if err != nil { + logger.Errorf(err, "%s", output) + return "", errors.WithStack(err) + } + + recreate := false + + if len(output) == 0 { + logger.Infof("Creating FTL database") + + err = exec.Command(ctx, logger.GetLevel(), "./", "docker", "run", + "-d", // run detached so we can follow with other commands + "--name", ftlContainerName, + "--user", "postgres", + "--restart", "always", + "-e", "POSTGRES_PASSWORD=secret", + "-p", "5432", // dynamically allocate port + "--health-cmd=pg_isready", + "--health-interval=1s", + "--health-timeout=60s", + "--health-retries=60", + "--health-start-period=80s", + "postgres:latest", "postgres", + ).Run() + + if err != nil { + return "", errors.WithStack(err) + } + + err = pollContainerHealth(ctx, ftlContainerName, 10*time.Second) + if err != nil { + return "", err + } + + recreate = true + } + + // grab the port from docker for this container + port, err := exec.Capture(ctx, ".", "docker", "inspect", "--format", "{{ (index (index .NetworkSettings.Ports \"5432/tcp\") 0).HostPort }}", ftlContainerName) + if err != nil { + return "", errors.WithStack(err) + } + + dsn := fmt.Sprintf("postgres://postgres:secret@localhost:%s/%s?sslmode=disable", strings.TrimSpace(string(port)), ftlContainerName) + dsnFlag := fmt.Sprintf("--dsn=%s", dsn) + + if recreate { + logger.Infof("Initializing FTL schema") + err = exec.Command(ctx, logger.GetLevel(), ".", "ftl-initdb", "--recreate", dsnFlag).Run() + } else { + err = exec.Command(ctx, logger.GetLevel(), ".", "ftl-initdb", dsnFlag).Run() + } + if err != nil { + return "", errors.WithStack(err) + } + + return dsn, nil +} + func incrementPort(baseURL *url.URL) (*url.URL, error) { newURL := *baseURL @@ -116,3 +193,28 @@ func incrementPort(baseURL *url.URL) (*url.URL, error) { newURL.Host = fmt.Sprintf("%s:%d", baseURL.Hostname(), newPort+1) return &newURL, nil } + +func pollContainerHealth(ctx context.Context, containerName string, timeout time.Duration) error { + logger := log.FromContext(ctx) + logger.Infof("Waiting for %s to be healthy", containerName) + + pollCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + for { + select { + case <-pollCtx.Done(): + return errors.New("timed out waiting for container to be healthy") + case <-time.After(1 * time.Millisecond): + output, err := exec.Capture(pollCtx, ".", "docker", "inspect", "--format", "{{.State.Health.Status}}", containerName) + if err != nil { + return errors.WithStack(err) + } + + status := strings.TrimSpace(string(output)) + if status == "healthy" { + return nil + } + } + } +} diff --git a/console/client/src/protos/xyz/block/ftl/v1/console/console_pb.ts b/console/client/src/protos/xyz/block/ftl/v1/console/console_pb.ts index 38998e27c8..54c87fb8fe 100644 --- a/console/client/src/protos/xyz/block/ftl/v1/console/console_pb.ts +++ b/console/client/src/protos/xyz/block/ftl/v1/console/console_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v1.3.3 with parameter "target=ts" +// @generated by protoc-gen-es v1.4.0 with parameter "target=ts" // @generated from file xyz/block/ftl/v1/console/console.proto (package xyz.block.ftl.v1.console, syntax proto3) /* eslint-disable */ // @ts-nocheck diff --git a/console/client/src/protos/xyz/block/ftl/v1/ftl_pb.ts b/console/client/src/protos/xyz/block/ftl/v1/ftl_pb.ts index ffe4ba3844..7ba96bee31 100644 --- a/console/client/src/protos/xyz/block/ftl/v1/ftl_pb.ts +++ b/console/client/src/protos/xyz/block/ftl/v1/ftl_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v1.3.3 with parameter "target=ts" +// @generated by protoc-gen-es v1.4.0 with parameter "target=ts" // @generated from file xyz/block/ftl/v1/ftl.proto (package xyz.block.ftl.v1, syntax proto3) /* eslint-disable */ // @ts-nocheck diff --git a/console/client/src/protos/xyz/block/ftl/v1/schema/runtime_pb.ts b/console/client/src/protos/xyz/block/ftl/v1/schema/runtime_pb.ts index 83ad8cd2fe..aad6e6b524 100644 --- a/console/client/src/protos/xyz/block/ftl/v1/schema/runtime_pb.ts +++ b/console/client/src/protos/xyz/block/ftl/v1/schema/runtime_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v1.3.3 with parameter "target=ts" +// @generated by protoc-gen-es v1.4.0 with parameter "target=ts" // @generated from file xyz/block/ftl/v1/schema/runtime.proto (package xyz.block.ftl.v1.schema, syntax proto3) /* eslint-disable */ // @ts-nocheck diff --git a/console/client/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts b/console/client/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts index e3b44bd758..00b16c6893 100644 --- a/console/client/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts +++ b/console/client/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v1.3.3 with parameter "target=ts" +// @generated by protoc-gen-es v1.4.0 with parameter "target=ts" // @generated from file xyz/block/ftl/v1/schema/schema.proto (package xyz.block.ftl.v1.schema, syntax proto3) /* eslint-disable */ // @ts-nocheck