diff --git a/.golangci.yml b/.golangci.yml index 93bac24c44..daba8a5c1c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -84,7 +84,7 @@ linters-settings: - pkg: github.com/pkg/errors desc: "use fmt.Errorf or errors.New" - pkg: github.com/stretchr/testify - desc: "use fmt.Errorf or errors.New" + desc: "use github.com/alecthomas/assert/v2" - pkg: github.com/alecthomas/errors desc: "use fmt.Errorf or errors.New" - pkg: braces.dev/errtrace diff --git a/buildengine/engine.go b/buildengine/engine.go index 128860e70d..75fbc4e691 100644 --- a/buildengine/engine.go +++ b/buildengine/engine.go @@ -23,6 +23,7 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" + "github.com/TBD54566975/ftl/internal/slices" ) type CompilerBuildError struct { @@ -205,13 +206,19 @@ func (e *Engine) buildGraph(moduleName string, out map[string][]string) error { if _, ok := out[moduleName]; ok { return nil } + foundModule := false if meta, ok := e.moduleMetas.Load(moduleName); ok { + foundModule = true deps = meta.module.Dependencies - } else if sch, ok := e.controllerSchema.Load(moduleName); ok { - deps = sch.Imports() - } else { + } + if sch, ok := e.controllerSchema.Load(moduleName); ok { + foundModule = true + deps = append(deps, sch.Imports()...) + } + if !foundModule { return fmt.Errorf("module %q not found", moduleName) } + deps = slices.Unique(deps) out[moduleName] = deps for _, dep := range deps { if err := e.buildGraph(dep, out); err != nil { diff --git a/cmd/ftl/cmd_box.go b/cmd/ftl/cmd_box.go index 53e55a7bd9..d09214c887 100644 --- a/cmd/ftl/cmd_box.go +++ b/cmd/ftl/cmd_box.go @@ -6,7 +6,9 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" + "text/template" "github.com/otiai10/copy" @@ -18,11 +20,23 @@ import ( "github.com/TBD54566975/ftl/internal/log" ) +// Test locally by running: +// +// Rebuild the image: +// docker build -t ftl0/ftl-box:latest --platform=linux/amd64 -f Dockerfile.box . +// +// Build the box: +// ftl box echo --compose=echo-compose.yml + +const boxftlProjectFile = `module-dirs = ["/root/modules"] +` + const boxDockerFile = `FROM {{.BaseImage}} WORKDIR /root -COPY modules /root +COPY modules /root/modules +COPY ftl-project.toml /root EXPOSE 8891 EXPOSE 8892 @@ -31,15 +45,75 @@ ENTRYPOINT ["/root/ftl", "box-run", "/root/modules"] ` +const boxComposeFile = `name: {{.Name}}-box +services: + db: + image: postgres + platform: linux/{{.GOARCH}} + command: postgres + user: postgres + restart: always + environment: + POSTGRES_PASSWORD: secret + expose: + - 5432 + healthcheck: + test: ["CMD-SHELL", "pg_isready"] + interval: 1s + timeout: 60s + retries: 60 + start_period: 80s + {{.Name}}: + image: {{.Name}} + platform: linux/amd64 + depends_on: + db: + condition: service_healthy + links: + - db + ports: + - "8891:8891" + - "8892:8892" + environment: + LOG_LEVEL: debug + FTL_CONFIG: /root/ftl-project.toml + FTL_CONTROLLER_DSN: postgres://postgres:secret@db:5432/ftl?sslmode=disable +` + +func init() { + if strings.Contains(boxComposeFile, "\t") { + panic("tabs in boxComposeFile in cmd_box.go") + } +} + type boxCmd struct { BaseImage string `help:"Name of the ftl-box Docker image to use as a base." default:"ftl0/ftl-box:${version}"` Parallelism int `short:"j" help:"Number of modules to build in parallel." default:"${numcpu}"` - Image string `arg:"" help:"Name of image to build."` + Compose string `help:"Path to a compose file to generate."` + Name string `arg:"" help:"Name of the project."` Dirs []string `arg:"" help:"Base directories containing modules (defaults to modules in project config)." type:"existingdir" optional:""` } func (b *boxCmd) Help() string { - return `` + return ` +To build a new box with echo and time from examples/go: + + ftl box echo --compose=echo-compose.yml + +To run the box: + + docker compose -f echo-compose.yml up --recreate --watch + +Interact with the box: + + ftl schema + ftl ps + ftl call echo echo '{name:"Alice"}' + +Bring the box down: + + docker compose -f echo-compose.yml down --rmi local + ` } func (b *boxCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceClient, projConfig projectconfig.Config) error { @@ -80,6 +154,7 @@ func (b *boxCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceC return err } files = append(files, filepath.Join(config.Dir, "ftl.toml")) + files = append(files, config.Schema) for _, file := range files { relFile, err := filepath.Rel(config.Dir, file) if err != nil { @@ -104,11 +179,45 @@ func (b *boxCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceC } baseImage = baseImageParts[0] + ":" + version } - dockerFile := strings.ReplaceAll(boxDockerFile, "{{.BaseImage}}", baseImage) - err = os.WriteFile(filepath.Join(workDir, "Dockerfile"), []byte(dockerFile), 0600) + err = writeFile(filepath.Join(workDir, "Dockerfile"), boxDockerFile, struct{ BaseImage string }{BaseImage: baseImage}) if err != nil { return fmt.Errorf("failed to write Dockerfile: %w", err) } - logger.Infof("Building image %s", b.Image) - return exec.Command(ctx, log.Debug, workDir, "docker", "build", "-t", b.Image, "--progress=plain", "--platform=linux/amd64", ".").RunBuffered(ctx) + err = writeFile(filepath.Join(workDir, "ftl-project.toml"), boxftlProjectFile, nil) + if err != nil { + return fmt.Errorf("failed to write ftl-project.toml: %w", err) + } + logger.Infof("Building image %s", b.Name) + err = exec.Command(ctx, log.Debug, workDir, "docker", "build", "-t", b.Name, "--progress=plain", "--platform=linux/amd64", ".").RunBuffered(ctx) + if err != nil { + return fmt.Errorf("failed to build image: %w", err) + } + if b.Compose != "" { + err = writeFile(b.Compose, boxComposeFile, struct { + Name string + GOARCH string + }{ + Name: b.Name, + GOARCH: runtime.GOARCH, + }) + if err != nil { + return fmt.Errorf("failed to write compose file: %w", err) + } + logger.Infof("Wrote compose file %s", b.Compose) + } + return nil +} + +func writeFile(path, content string, context any) error { + t := template.Must(template.New(path).Parse(content)) + w, err := os.Create(path) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer w.Close() //nolint:errcheck + err = t.Execute(w, context) + if err != nil { + return fmt.Errorf("failed to write %q: %w", path, err) + } + return nil } diff --git a/cmd/ftl/cmd_box_run.go b/cmd/ftl/cmd_box_run.go index d825a026ab..0f9cb08b52 100644 --- a/cmd/ftl/cmd_box_run.go +++ b/cmd/ftl/cmd_box_run.go @@ -15,8 +15,10 @@ import ( "github.com/TBD54566975/ftl/backend/controller/scaling/localscaling" "github.com/TBD54566975/ftl/backend/controller/sql/databasetesting" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" + "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/buildengine" "github.com/TBD54566975/ftl/internal/bind" + "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/model" "github.com/TBD54566975/ftl/internal/rpc" ) @@ -77,6 +79,22 @@ func (b *boxRunCmd) Run(ctx context.Context) error { return fmt.Errorf("failed to create build engine: %w", err) } + logger := log.FromContext(ctx) + + // Manually import the schema for each module to get the dependency graph. + err = engine.Each(func(m buildengine.Module) error { + logger.Debugf("Loading schema for module %q", m.Config.Module) + mod, err := schema.ModuleFromProtoFile(m.Config.Abs().Schema) + if err != nil { + return fmt.Errorf("failed to read schema for module %q: %w", m.Config.Module, err) + } + engine.Import(ctx, mod) + return nil + }) + if err != nil { + return fmt.Errorf("failed to load schemas: %w", err) + } + if err := engine.Deploy(ctx, 1, true); err != nil { return fmt.Errorf("failed to deploy: %w", err) } diff --git a/cmd/ftl/integration_test.go b/cmd/ftl/integration_test.go new file mode 100644 index 0000000000..c2fc0e5547 --- /dev/null +++ b/cmd/ftl/integration_test.go @@ -0,0 +1,31 @@ +//go:build integration + +package main + +import ( + "context" + "testing" + + "github.com/alecthomas/assert/v2" + + . "github.com/TBD54566975/ftl/integration" + "github.com/TBD54566975/ftl/internal/exec" + "github.com/TBD54566975/ftl/internal/log" +) + +func TestBox(t *testing.T) { + // Need a longer timeout to wait for FTL inside Docker. + t.Setenv("FTL_INTEGRATION_TEST_TIMEOUT", "30s") + Infof("Building local ftl0/ftl-box:latest Docker image") + ctx := log.ContextWithNewDefaultLogger(context.Background()) + err := exec.Command(ctx, log.Debug, "../..", "docker", "build", "-t", "ftl0/ftl-box:latest", "--progress=plain", "--platform=linux/amd64", "-f", "Dockerfile.box", ".").Run() + assert.NoError(t, err) + RunWithoutController(t, "", + CopyModule("time"), + CopyModule("echo"), + Exec("ftl", "box", "echo", "--compose=echo-compose.yml"), + Exec("docker", "compose", "-f", "echo-compose.yml", "up", "--wait"), + Call("echo", "echo", Obj{"name": "Alice"}, nil), + Exec("docker", "compose", "-f", "echo-compose.yml", "down", "--rmi", "local"), + ) +} diff --git a/cmd/ftl/testdata/go/echo/echo.go b/cmd/ftl/testdata/go/echo/echo.go new file mode 100644 index 0000000000..4733a5db57 --- /dev/null +++ b/cmd/ftl/testdata/go/echo/echo.go @@ -0,0 +1,32 @@ +// This is the echo module. +package echo + +import ( + "context" + "fmt" + + "ftl/time" + + "github.com/TBD54566975/ftl/go-runtime/ftl" +) + +// An echo request. +type EchoRequest struct { + Name ftl.Option[string] `json:"name"` +} + +type EchoResponse struct { + Message string `json:"message"` +} + +// Echo returns a greeting with the current time. +// +//ftl:verb +func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { + tresp, err := ftl.Call(ctx, time.Time, time.TimeRequest{}) + if err != nil { + return EchoResponse{}, err + } + + return EchoResponse{Message: fmt.Sprintf("Hello, %s!!! It is %s!", req.Name.Default("world"), tresp.Time)}, nil +} diff --git a/cmd/ftl/testdata/go/echo/ftl.toml b/cmd/ftl/testdata/go/echo/ftl.toml new file mode 100644 index 0000000000..72ce292aa2 --- /dev/null +++ b/cmd/ftl/testdata/go/echo/ftl.toml @@ -0,0 +1,2 @@ +module = "echo" +language = "go" diff --git a/cmd/ftl/testdata/go/echo/go.mod b/cmd/ftl/testdata/go/echo/go.mod new file mode 100644 index 0000000000..4f5f42b839 --- /dev/null +++ b/cmd/ftl/testdata/go/echo/go.mod @@ -0,0 +1,45 @@ +module ftl/echo + +go 1.22.2 + +replace github.com/TBD54566975/ftl => ../../../../.. + +require github.com/TBD54566975/ftl v0.248.0 + +require ( + connectrpc.com/connect v1.16.1 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.7.0 // indirect + github.com/alecthomas/atomic v0.1.0-alpha2 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/types v0.16.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/puzpuzpuz/xsync/v3 v3.2.0 // indirect + github.com/swaggest/jsonschema-go v0.3.72 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.5 // indirect + go.opentelemetry.io/otel v1.27.0 // indirect + go.opentelemetry.io/otel/metric v1.27.0 // indirect + go.opentelemetry.io/otel/trace v1.27.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/cmd/ftl/testdata/go/echo/go.sum b/cmd/ftl/testdata/go/echo/go.sum new file mode 100644 index 0000000000..1e8d4689ef --- /dev/null +++ b/cmd/ftl/testdata/go/echo/go.sum @@ -0,0 +1,144 @@ +connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= +connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= +connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= +github.com/TBD54566975/scaffolder v1.0.0 h1:QUFSy2wVzumLDg7IHcKC6AP+IYyqWe9Wxiu72nZn5qU= +github.com/TBD54566975/scaffolder v1.0.0/go.mod h1:auVpczIbOAdIhYDVSruIw41DanxOKB9bSvjf6MEl7Fs= +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8= +github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.16.0 h1:o9+JSwCRB6DDaWDeR/Mg7v/zh3R+MlknM6DrnDyY7U0= +github.com/alecthomas/types v0.16.0/go.mod h1:Tswm0qQpjpVq8rn70OquRsUtFxbQKub/8TMyYYGI0+k= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk= +github.com/bool64/dev v0.2.35/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/puzpuzpuz/xsync/v3 v3.2.0 h1:9AzuUeF88YC5bK8u2vEG1Fpvu4wgpM1wfPIExfaaDxQ= +github.com/puzpuzpuz/xsync/v3 v3.2.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNyNzvnVHv4= +github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8= +github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.27.0 h1:9BZoF3yMK/O1AafMiQTVu0YDj5Ea4hPhxCs7sGva+cg= +go.opentelemetry.io/otel v1.27.0/go.mod h1:DMpAK8fzYRzs+bi3rS5REupisuqTheUlSZJ1WnZaPAQ= +go.opentelemetry.io/otel/metric v1.27.0 h1:hvj3vdEKyeCi4YaYfNjv2NUje8FqKqUY8IlF0FxV/ik= +go.opentelemetry.io/otel/metric v1.27.0/go.mod h1:mVFgmRlhljgBiuk/MP/oKylr4hs85GZAylncepAX/ak= +go.opentelemetry.io/otel/sdk v1.27.0 h1:mlk+/Y1gLPLn84U4tI8d3GNJmGT/eXe3ZuOXN9kTWmI= +go.opentelemetry.io/otel/sdk v1.27.0/go.mod h1:Ha9vbLwJE6W86YstIywK2xFfPjbWlCuwPtMkKdz/Y4A= +go.opentelemetry.io/otel/sdk/metric v1.27.0 h1:5uGNOlpXi+Hbo/DRoI31BSb1v+OGcpv2NemcCrOL8gI= +go.opentelemetry.io/otel/sdk/metric v1.27.0/go.mod h1:we7jJVrYN2kh3mVBlswtPU22K0SA+769l93J6bsyvqw= +go.opentelemetry.io/otel/trace v1.27.0 h1:IqYb813p7cmbHk0a5y6pD5JPakbVfftRXABGt5/Rscw= +go.opentelemetry.io/otel/trace v1.27.0/go.mod h1:6RiD1hkAprV4/q+yd2ln1HG9GoPx39SuvvstaLBl+l4= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= +golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= +modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk= +modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/cmd/ftl/testdata/go/time/ftl.toml b/cmd/ftl/testdata/go/time/ftl.toml new file mode 100644 index 0000000000..c7449d155e --- /dev/null +++ b/cmd/ftl/testdata/go/time/ftl.toml @@ -0,0 +1,5 @@ +module = "time" +language = "go" + +[go.replace] +"github.com/TBD54566975/ftl" = "../.." diff --git a/cmd/ftl/testdata/go/time/go.mod b/cmd/ftl/testdata/go/time/go.mod new file mode 100644 index 0000000000..ce2a3974c8 --- /dev/null +++ b/cmd/ftl/testdata/go/time/go.mod @@ -0,0 +1,5 @@ +module ftl/time + +go 1.22.2 + +replace github.com/TBD54566975/ftl => ../../../../.. diff --git a/cmd/ftl/testdata/go/time/go.sum b/cmd/ftl/testdata/go/time/go.sum new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/ftl/testdata/go/time/time.go b/cmd/ftl/testdata/go/time/time.go new file mode 100644 index 0000000000..f2d3cc1975 --- /dev/null +++ b/cmd/ftl/testdata/go/time/time.go @@ -0,0 +1,23 @@ +package time + +import ( + "context" + "time" +) + +type TimeRequest struct{} +type TimeResponse struct { + Time time.Time +} + +// Time returns the current time. +// +//ftl:verb export +func Time(ctx context.Context, req TimeRequest) (TimeResponse, error) { + return TimeResponse{Time: time.Now()}, nil +} + +//ftl:verb +func Internal(ctx context.Context, req TimeRequest) (TimeResponse, error) { + return TimeResponse{Time: time.Now()}, nil +} diff --git a/common/moduleconfig/moduleconfig.go b/common/moduleconfig/moduleconfig.go index dc47f0f78e..bdc0bbcd27 100644 --- a/common/moduleconfig/moduleconfig.go +++ b/common/moduleconfig/moduleconfig.go @@ -1,6 +1,7 @@ package moduleconfig import ( + "errors" "fmt" "go/parser" "go/token" @@ -169,6 +170,9 @@ func replacementWatches(moduleDir, deployDir string) ([]string, error) { goModPath := filepath.Join(moduleDir, "go.mod") goModBytes, err := os.ReadFile(goModPath) if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, nil + } return nil, fmt.Errorf("failed to read %s: %w", goModPath, err) } goModFile, err := modfile.Parse(goModPath, goModBytes, nil) diff --git a/integration/actions.go b/integration/actions.go index b781e30213..33b8e90dd4 100644 --- a/integration/actions.go +++ b/integration/actions.go @@ -54,7 +54,7 @@ func CopyModule(module string) Action { return Chain( CopyDir(module, module), func(t testing.TB, ic TestContext) { - err := ftlexec.Command(ic, log.Debug, filepath.Join(ic.workDir, module), "go", "mod", "edit", "-replace", "github.com/TBD54566975/ftl="+ic.rootDir).RunBuffered(ic) + err := ftlexec.Command(ic, log.Debug, filepath.Join(ic.workDir, module), "go", "mod", "edit", "-replace", "github.com/TBD54566975/ftl="+ic.RootDir).RunBuffered(ic) assert.NoError(t, err) }, ) diff --git a/integration/harness.go b/integration/harness.go index 8b7353f277..a293e101f4 100644 --- a/integration/harness.go +++ b/integration/harness.go @@ -26,14 +26,14 @@ import ( "github.com/TBD54566975/ftl/internal/rpc" ) -var integrationTestTimeout = func() time.Duration { +func integrationTestTimeout() time.Duration { timeout := optional.Zero(os.Getenv("FTL_INTEGRATION_TEST_TIMEOUT")).Default("5s") d, err := time.ParseDuration(timeout) if err != nil { panic(err) } return d -}() +} func Infof(format string, args ...any) { fmt.Printf("\033[32m\033[1mINFO: "+format+"\033[0m\n", args...) @@ -106,7 +106,7 @@ func run(t *testing.T, ftlConfigPath string, startController bool, actions ...Ac ic := TestContext{ Context: ctx, - rootDir: rootDir, + RootDir: rootDir, testData: filepath.Join(cwd, "testdata", "go"), workDir: tmpDir, binDir: binDir, @@ -135,7 +135,7 @@ type TestContext struct { // Temporary directory the test is executing in. workDir string // Root of FTL repo. - rootDir string + RootDir string // Path to testdata directory for the current language. testData string // Path to the "bin" directory. @@ -150,7 +150,7 @@ func (i TestContext) WorkingDir() string { return i.workDir } // AssertWithRetry asserts that the given action passes within the timeout. func (i TestContext) AssertWithRetry(t testing.TB, assertion Action) { - waitCtx, done := context.WithTimeout(i, integrationTestTimeout) + waitCtx, done := context.WithTimeout(i, integrationTestTimeout()) defer done() for { err := i.runAssertionOnce(t, assertion) diff --git a/internal/slices/slices.go b/internal/slices/slices.go index 64b8816ccc..ed4d9bdd03 100644 --- a/internal/slices/slices.go +++ b/internal/slices/slices.go @@ -103,3 +103,15 @@ func FindVariant[T any, U any](slice []U) (T, bool) { var zero T return zero, false } + +func Unique[T comparable](slice []T) []T { + seen := make(map[T]struct{}) + result := make([]T, 0, len(slice)) + for _, v := range slice { + if _, ok := seen[v]; !ok { + seen[v] = struct{}{} + result = append(result, v) + } + } + return result +}