diff --git a/backend/controller/admin/admin.go b/backend/controller/admin/admin.go index 203e5e91ce..b947f43722 100644 --- a/backend/controller/admin/admin.go +++ b/backend/controller/admin/admin.go @@ -9,20 +9,29 @@ import ( ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" + "github.com/TBD54566975/ftl/backend/schema" cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/TBD54566975/ftl/go-runtime/encoding" + "github.com/TBD54566975/ftl/internal/log" ) type AdminService struct { - cm *cf.Manager[cf.Configuration] - sm *cf.Manager[cf.Secrets] + schr SchemaRetriever + cm *cf.Manager[cf.Configuration] + sm *cf.Manager[cf.Secrets] } var _ ftlv1connect.AdminServiceHandler = (*AdminService)(nil) -func NewAdminService(cm *cf.Manager[cf.Configuration], sm *cf.Manager[cf.Secrets]) *AdminService { +type SchemaRetriever interface { + GetActiveSchema(ctx context.Context) (*schema.Schema, error) +} + +func NewAdminService(cm *cf.Manager[cf.Configuration], sm *cf.Manager[cf.Secrets], schr SchemaRetriever) *AdminService { return &AdminService{ - cm: cm, - sm: sm, + schr: schr, + cm: cm, + sm: sm, } } @@ -34,7 +43,7 @@ func (s *AdminService) Ping(ctx context.Context, req *connect.Request[ftlv1.Ping func (s *AdminService) ConfigList(ctx context.Context, req *connect.Request[ftlv1.ListConfigRequest]) (*connect.Response[ftlv1.ListConfigResponse], error) { listing, err := s.cm.List(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list configs: %w", err) } configs := []*ftlv1.ListConfigResponse_Config{} @@ -73,13 +82,13 @@ func (s *AdminService) ConfigList(ctx context.Context, req *connect.Request[ftlv // ConfigGet returns the configuration value for a given ref string. func (s *AdminService) ConfigGet(ctx context.Context, req *connect.Request[ftlv1.GetConfigRequest]) (*connect.Response[ftlv1.GetConfigResponse], error) { var value any - err := s.cm.Get(ctx, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), &value) + err := s.cm.Get(ctx, refFromConfigRef(req.Msg.GetRef()), &value) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get from config manager: %w", err) } vb, err := json.MarshalIndent(value, "", " ") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal value: %w", err) } return connect.NewResponse(&ftlv1.GetConfigResponse{Value: vb}), nil } @@ -101,20 +110,25 @@ func configProviderKey(p *ftlv1.ConfigProvider) string { // ConfigSet sets the configuration at the given ref to the provided value. func (s *AdminService) ConfigSet(ctx context.Context, req *connect.Request[ftlv1.SetConfigRequest]) (*connect.Response[ftlv1.SetConfigResponse], error) { - pkey := configProviderKey(req.Msg.Provider) - err := s.cm.SetJSON(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), req.Msg.Value) + err := s.validateAgainstSchema(ctx, false, refFromConfigRef(req.Msg.GetRef()), req.Msg.Value) if err != nil { return nil, err } + + pkey := configProviderKey(req.Msg.Provider) + err = s.cm.SetJSON(ctx, pkey, refFromConfigRef(req.Msg.GetRef()), req.Msg.Value) + if err != nil { + return nil, fmt.Errorf("failed to set config: %w", err) + } return connect.NewResponse(&ftlv1.SetConfigResponse{}), nil } // ConfigUnset unsets the config value at the given ref. func (s *AdminService) ConfigUnset(ctx context.Context, req *connect.Request[ftlv1.UnsetConfigRequest]) (*connect.Response[ftlv1.UnsetConfigResponse], error) { pkey := configProviderKey(req.Msg.Provider) - err := s.cm.Unset(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name)) + err := s.cm.Unset(ctx, pkey, refFromConfigRef(req.Msg.GetRef())) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unset config: %w", err) } return connect.NewResponse(&ftlv1.UnsetConfigResponse{}), nil } @@ -123,7 +137,7 @@ func (s *AdminService) ConfigUnset(ctx context.Context, req *connect.Request[ftl func (s *AdminService) SecretsList(ctx context.Context, req *connect.Request[ftlv1.ListSecretsRequest]) (*connect.Response[ftlv1.ListSecretsResponse], error) { listing, err := s.sm.List(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list secrets: %w", err) } secrets := []*ftlv1.ListSecretsResponse_Secret{} for _, secret := range listing { @@ -162,13 +176,13 @@ func (s *AdminService) SecretsList(ctx context.Context, req *connect.Request[ftl // SecretGet returns the secret value for a given ref string. func (s *AdminService) SecretGet(ctx context.Context, req *connect.Request[ftlv1.GetSecretRequest]) (*connect.Response[ftlv1.GetSecretResponse], error) { var value any - err := s.sm.Get(ctx, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), &value) + err := s.sm.Get(ctx, refFromConfigRef(req.Msg.GetRef()), &value) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get from secret manager: %w", err) } vb, err := json.MarshalIndent(value, "", " ") if err != nil { - return nil, err + return nil, fmt.Errorf("failed to marshal value: %w", err) } return connect.NewResponse(&ftlv1.GetSecretResponse{Value: vb}), nil } @@ -194,20 +208,80 @@ func secretProviderKey(p *ftlv1.SecretProvider) string { // SecretSet sets the secret at the given ref to the provided value. func (s *AdminService) SecretSet(ctx context.Context, req *connect.Request[ftlv1.SetSecretRequest]) (*connect.Response[ftlv1.SetSecretResponse], error) { - pkey := secretProviderKey(req.Msg.Provider) - err := s.sm.SetJSON(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name), req.Msg.Value) + err := s.validateAgainstSchema(ctx, true, refFromConfigRef(req.Msg.GetRef()), req.Msg.Value) if err != nil { return nil, err } + + pkey := secretProviderKey(req.Msg.Provider) + err = s.sm.SetJSON(ctx, pkey, refFromConfigRef(req.Msg.GetRef()), req.Msg.Value) + if err != nil { + return nil, fmt.Errorf("failed to set secret: %w", err) + } return connect.NewResponse(&ftlv1.SetSecretResponse{}), nil } // SecretUnset unsets the secret value at the given ref. func (s *AdminService) SecretUnset(ctx context.Context, req *connect.Request[ftlv1.UnsetSecretRequest]) (*connect.Response[ftlv1.UnsetSecretResponse], error) { pkey := secretProviderKey(req.Msg.Provider) - err := s.sm.Unset(ctx, pkey, cf.NewRef(*req.Msg.Ref.Module, req.Msg.Ref.Name)) + err := s.sm.Unset(ctx, pkey, refFromConfigRef(req.Msg.GetRef())) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unset secret: %w", err) } return connect.NewResponse(&ftlv1.UnsetSecretResponse{}), nil } + +func refFromConfigRef(cr *ftlv1.ConfigRef) cf.Ref { + return cf.NewRef(cr.GetModule(), cr.GetName()) +} + +func (s *AdminService) validateAgainstSchema(ctx context.Context, isSecret bool, ref cf.Ref, value json.RawMessage) error { + logger := log.FromContext(ctx) + + // Globals aren't in the module schemas, so we have nothing to validate against. + if !ref.Module.Ok() { + return nil + } + + // If we can't retrieve an active schema, skip validation. + sch, err := s.schr.GetActiveSchema(ctx) + if err != nil { + logger.Debugf("skipping validation; could not get the active schema: %v", err) + return nil + } + + r := schema.RefKey{Module: ref.Module.Default(""), Name: ref.Name}.ToRef() + decl, ok := sch.Resolve(r).Get() + if !ok { + logger.Debugf("skipping validation; declaration %q not found", ref.Name) + return nil + } + + var fieldType schema.Type + if isSecret { + decl, ok := decl.(*schema.Secret) + if !ok { + return fmt.Errorf("%q is not a secret declaration", ref.Name) + } + fieldType = decl.Type + } else { + decl, ok := decl.(*schema.Config) + if !ok { + return fmt.Errorf("%q is not a config declaration", ref.Name) + } + fieldType = decl.Type + } + + var v any + err = encoding.Unmarshal(value, &v) + if err != nil { + return fmt.Errorf("could not unmarshal JSON value: %w", err) + } + + err = schema.ValidateJSONValue(fieldType, []string{ref.Name}, v, sch) + if err != nil { + return fmt.Errorf("JSON validation failed: %w", err) + } + + return nil +} diff --git a/backend/controller/admin/admin_test.go b/backend/controller/admin/admin_test.go index a0add822bf..636d46044f 100644 --- a/backend/controller/admin/admin_test.go +++ b/backend/controller/admin/admin_test.go @@ -13,6 +13,7 @@ import ( "github.com/alecthomas/types/optional" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" + "github.com/TBD54566975/ftl/backend/schema" cf "github.com/TBD54566975/ftl/common/configuration" "github.com/TBD54566975/ftl/internal/log" ) @@ -31,7 +32,7 @@ func TestAdminService(t *testing.T) { cf.InlineProvider[cf.Secrets]{}, }) assert.NoError(t, err) - admin := NewAdminService(cm, sm) + admin := NewAdminService(cm, sm, &diskSchemaRetriever{}) assert.NotZero(t, admin) expectedEnvarValue, err := json.MarshalIndent(map[string]string{"bar": "barfoo"}, "", " ") @@ -140,3 +141,130 @@ func testAdminSecrets( assert.Equal(t, entry.Value, string(resp.Msg.Value)) } } + +var testSchema = schema.MustValidate(&schema.Schema{ + Modules: []*schema.Module{ + { + Name: "batmobile", + Comments: []string{"A batmobile comment"}, + Decls: []schema.Decl{ + &schema.Secret{ + Comments: []string{"top secret"}, + Name: "owner", + Type: &schema.String{}, + }, + &schema.Secret{ + Comments: []string{"ultra secret"}, + Name: "horsepower", + Type: &schema.Int{}, + }, + &schema.Config{ + Comments: []string{"car color"}, + Name: "color", + Type: &schema.Ref{Module: "batmobile", Name: "Color"}, + }, + &schema.Config{ + Comments: []string{"car capacity"}, + Name: "capacity", + Type: &schema.Ref{Module: "batmobile", Name: "Capacity"}, + }, + &schema.Enum{ + Comments: []string{"Car colors"}, + Name: "Color", + Type: &schema.String{}, + Variants: []*schema.EnumVariant{ + {Name: "Black", Value: &schema.StringValue{Value: "Black"}}, + {Name: "Blue", Value: &schema.StringValue{Value: "Blue"}}, + {Name: "Green", Value: &schema.StringValue{Value: "Green"}}, + }, + }, + &schema.Enum{ + Comments: []string{"Car capacities"}, + Name: "Capacity", + Type: &schema.Int{}, + Variants: []*schema.EnumVariant{ + {Name: "One", Value: &schema.IntValue{Value: int(1)}}, + {Name: "Two", Value: &schema.IntValue{Value: int(2)}}, + {Name: "Four", Value: &schema.IntValue{Value: int(4)}}, + }, + }, + }, + }, + }, +}) + +type mockSchemaRetriever struct { +} + +func (d *mockSchemaRetriever) GetActiveSchema(ctx context.Context) (*schema.Schema, error) { + return testSchema, nil +} + +func TestAdminValidation(t *testing.T) { + config := tempConfigPath(t, "testdata/ftl-project.toml", "admin") + ctx := log.ContextWithNewDefaultLogger(context.Background()) + + cm, err := cf.NewConfigurationManager(ctx, cf.ProjectConfigResolver[cf.Configuration]{Config: config}) + assert.NoError(t, err) + + sm, err := cf.New(ctx, + cf.ProjectConfigResolver[cf.Secrets]{Config: config}, + []cf.Provider[cf.Secrets]{ + cf.EnvarProvider[cf.Secrets]{}, + cf.InlineProvider[cf.Secrets]{}, + }) + assert.NoError(t, err) + admin := NewAdminService(cm, sm, &mockSchemaRetriever{}) + assert.NotZero(t, admin) + + testSetConfig(t, ctx, admin, "batmobile", "color", "Black", "") + testSetConfig(t, ctx, admin, "batmobile", "color", "Red", "JSON validation failed: Red is not a valid variant of enum batmobile.Color") + testSetConfig(t, ctx, admin, "batmobile", "capacity", 2, "") + testSetConfig(t, ctx, admin, "batmobile", "capacity", 3, "JSON validation failed: %!s(float64=3) is not a valid variant of enum batmobile.Capacity") + + testSetSecret(t, ctx, admin, "batmobile", "owner", "Bruce Wayne", "") + testSetSecret(t, ctx, admin, "batmobile", "owner", 99, "JSON validation failed: owner has wrong type, expected String found float64") + testSetSecret(t, ctx, admin, "batmobile", "horsepower", 1000, "") + testSetSecret(t, ctx, admin, "batmobile", "horsepower", "thousand", "JSON validation failed: horsepower has wrong type, expected Int found string") + + testSetConfig(t, ctx, admin, "", "city", "Gotham", "") + testSetSecret(t, ctx, admin, "", "universe", "DC", "") +} + +// nolint +func testSetConfig(t testing.TB, ctx context.Context, admin *AdminService, module string, name string, jsonVal any, expectedError string) { + t.Helper() + buffer, err := json.Marshal(jsonVal) + assert.NoError(t, err) + + configRef := &ftlv1.ConfigRef{Name: name} + if module != "" { + configRef.Module = &module + } + + _, err = admin.ConfigSet(ctx, connect.NewRequest(&ftlv1.SetConfigRequest{ + Provider: ftlv1.ConfigProvider_CONFIG_INLINE.Enum(), + Ref: configRef, + Value: buffer, + })) + assert.EqualError(t, err, expectedError) +} + +// nolint +func testSetSecret(t testing.TB, ctx context.Context, admin *AdminService, module string, name string, jsonVal any, expectedError string) { + t.Helper() + buffer, err := json.Marshal(jsonVal) + assert.NoError(t, err) + + configRef := &ftlv1.ConfigRef{Name: name} + if module != "" { + configRef.Module = &module + } + + _, err = admin.SecretSet(ctx, connect.NewRequest(&ftlv1.SetSecretRequest{ + Provider: ftlv1.SecretProvider_SECRET_INLINE.Enum(), + Ref: configRef, + Value: buffer, + })) + assert.EqualError(t, err, expectedError) +} diff --git a/backend/controller/admin/local_client.go b/backend/controller/admin/local_client.go index 73c659b0fb..ad87330a29 100644 --- a/backend/controller/admin/local_client.go +++ b/backend/controller/admin/local_client.go @@ -2,8 +2,14 @@ package admin import ( "context" + "fmt" + "path/filepath" + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/buildengine" "github.com/TBD54566975/ftl/common/configuration" + "github.com/TBD54566975/ftl/common/projectconfig" + "github.com/alecthomas/types/optional" ) // localClient reads and writes to local projectconfig files without making any network @@ -13,8 +19,43 @@ type localClient struct { *AdminService } +type diskSchemaRetriever struct { + // Omit to use the project root as the deploy root. + deployRoot optional.Option[string] +} + func newLocalClient(ctx context.Context) *localClient { cm := configuration.ConfigFromContext(ctx) sm := configuration.SecretsFromContext(ctx) - return &localClient{NewAdminService(cm, sm)} + return &localClient{NewAdminService(cm, sm, &diskSchemaRetriever{})} +} + +func (s *diskSchemaRetriever) GetActiveSchema(ctx context.Context) (*schema.Schema, error) { + path, ok := projectconfig.DefaultConfigPath().Get() + if !ok { + return nil, fmt.Errorf("no project config path available") + } + projConfig, err := projectconfig.Load(ctx, path) + if err != nil { + return nil, fmt.Errorf("could not load project config: %w", err) + } + modules, err := buildengine.DiscoverModules(ctx, projConfig.AbsModuleDirs()) + if err != nil { + return nil, fmt.Errorf("could not discover modules: %w", err) + } + + sch := &schema.Schema{} + for _, m := range modules { + schemaPath := m.Config.Abs().Schema + if r, ok := s.deployRoot.Get(); ok { + schemaPath = filepath.Join(r, m.Config.Module, m.Config.DeployDir, m.Config.Schema) + } + + module, err := schema.ModuleFromProtoFile(schemaPath) + if err != nil { + return nil, fmt.Errorf("could not load module schema: %w", err) + } + sch.Upsert(module) + } + return sch, nil } diff --git a/backend/controller/admin/local_client_test.go b/backend/controller/admin/local_client_test.go new file mode 100644 index 0000000000..1d4f9eab4e --- /dev/null +++ b/backend/controller/admin/local_client_test.go @@ -0,0 +1,65 @@ +//go:build integration + +package admin + +import ( + "context" + "testing" + + cf "github.com/TBD54566975/ftl/common/configuration" + in "github.com/TBD54566975/ftl/integration" + "github.com/TBD54566975/ftl/internal/log" + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/types/optional" +) + +func TestDiskSchemaRetrieverWithBuildArtefact(t *testing.T) { + in.RunWithoutController(t, "ftl-project-dr.toml", + in.CopyModule("dischema"), + in.Build("dischema"), + func(t testing.TB, ic in.TestContext) { + dsr := &diskSchemaRetriever{deployRoot: optional.Some[string](ic.WorkingDir())} + sch, err := dsr.GetActiveSchema(ic.Context) + assert.NoError(t, err) + + module, ok := sch.Module("dischema").Get() + assert.Equal(t, ok, true) + assert.Equal(t, "dischema", module.Name) + }, + ) +} + +func TestDiskSchemaRetrieverWithNoSchema(t *testing.T) { + in.RunWithoutController(t, "ftl-project-dr.toml", + in.CopyModule("dischema"), + func(t testing.TB, ic in.TestContext) { + dsr := &diskSchemaRetriever{} + _, err := dsr.GetActiveSchema(ic.Context) + assert.Error(t, err) + }, + ) +} + +func TestAdminNoValidationWithNoSchema(t *testing.T) { + config := tempConfigPath(t, "testdata/ftl-project.toml", "admin") + ctx := log.ContextWithNewDefaultLogger(context.Background()) + + cm, err := cf.NewConfigurationManager(ctx, cf.ProjectConfigResolver[cf.Configuration]{Config: config}) + assert.NoError(t, err) + + sm, err := cf.New(ctx, + cf.ProjectConfigResolver[cf.Secrets]{Config: config}, + []cf.Provider[cf.Secrets]{ + cf.EnvarProvider[cf.Secrets]{}, + cf.InlineProvider[cf.Secrets]{}, + }) + assert.NoError(t, err) + + dsr := &diskSchemaRetriever{deployRoot: optional.Some(string(t.TempDir()))} + _, err = dsr.GetActiveSchema(ctx) + assert.Error(t, err) + + admin := NewAdminService(cm, sm, dsr) + testSetConfig(t, ctx, admin, "batmobile", "color", "Red", "") + testSetSecret(t, ctx, admin, "batmobile", "owner", 99, "") +} diff --git a/backend/controller/admin/testdata/go/dischema/dischema.go b/backend/controller/admin/testdata/go/dischema/dischema.go new file mode 100644 index 0000000000..f780f6a518 --- /dev/null +++ b/backend/controller/admin/testdata/go/dischema/dischema.go @@ -0,0 +1,23 @@ +package dischema + +import ( + "context" + "fmt" + + "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. +) + +var defaultUser = ftl.Config[string]("defaultUser") + +type EchoRequest struct { + Name ftl.Option[string] `json:"name"` +} + +type EchoResponse struct { + Message string `json:"message"` +} + +//ftl:verb +func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { + return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default(defaultUser.Get(ctx)))}, nil +} diff --git a/backend/controller/admin/testdata/go/dischema/ftl.toml b/backend/controller/admin/testdata/go/dischema/ftl.toml new file mode 100644 index 0000000000..19ad570a97 --- /dev/null +++ b/backend/controller/admin/testdata/go/dischema/ftl.toml @@ -0,0 +1,2 @@ +module = "dischema" +language = "go" diff --git a/backend/controller/admin/testdata/go/dischema/go.mod b/backend/controller/admin/testdata/go/dischema/go.mod new file mode 100644 index 0000000000..60aa2617ca --- /dev/null +++ b/backend/controller/admin/testdata/go/dischema/go.mod @@ -0,0 +1,43 @@ +module ftl/dischema + +go 1.22.2 + +toolchain go1.22.4 + +require github.com/TBD54566975/ftl v1.1.5 + +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/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/swaggest/jsonschema-go v0.3.70 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.4 // 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.23.0 // indirect + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) diff --git a/backend/controller/admin/testdata/go/dischema/go.sum b/backend/controller/admin/testdata/go/dischema/go.sum new file mode 100644 index 0000000000..e2d61f4b4e --- /dev/null +++ b/backend/controller/admin/testdata/go/dischema/go.sum @@ -0,0 +1,142 @@ +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/ftl v1.1.5 h1:PZq322WiBWUFnOHv51bnDjT/SbztEUVkEAHyGJA1HWY= +github.com/TBD54566975/ftl v1.1.5/go.mod h1:Z64GLoDe603sxe7Nytpx6Le/bB/Jlk8m52MVz+JKXtM= +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/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.34 h1:P9n315P8LdpxusnYQ0X7MP1CZXwBK5ae5RZrd+GdSZE= +github.com/bool64/dev v0.2.34/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/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.70 h1:8Vx5nm5t/6DBFw2+WC0/Vp1ZVe9/4mpuA0tuAe0wwCI= +github.com/swaggest/jsonschema-go v0.3.70/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE= +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.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= +github.com/zalando/go-keyring v0.2.4/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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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.50.9 h1:hIWf1uz55lorXQhfoEoezdUHjxzuO6ceshET/yWjSjk= +modernc.org/libc v1.50.9/go.mod h1:15P6ublJ9FJR8YQCGy8DeQ2Uwur7iW9Hserr/T3OFZE= +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.0 h1:8YhPUs/HTnlEgErn/jSYQTwHN/ex8CjHHjg+K9iG7LM= +modernc.org/sqlite v1.30.0/go.mod h1:cgkTARJ9ugeXSNaLBPK3CqbOe7Ec7ZhWPoMFGldEYEw= +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/backend/controller/admin/testdata/go/ftl-project-dr.toml b/backend/controller/admin/testdata/go/ftl-project-dr.toml new file mode 100644 index 0000000000..a65b02f7ff --- /dev/null +++ b/backend/controller/admin/testdata/go/ftl-project-dr.toml @@ -0,0 +1,9 @@ +name = "dr" +ftl-min-version = "dev" +hermit = false +no-git = false + +[global] + +[commands] + startup = ["echo disk retreiver"] diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 5f249672cd..b6663acab5 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -142,7 +142,7 @@ func Start(ctx context.Context, config Config, runnerScaling scaling.RunnerScali cm := cf.ConfigFromContext(ctx) sm := cf.SecretsFromContext(ctx) - admin := admin.NewAdminService(cm, sm) + admin := admin.NewAdminService(cm, sm, svc.dal) console := NewConsoleService(svc.dal) ingressHandler := http.Handler(svc) @@ -308,7 +308,7 @@ func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - sch, err := s.getActiveSchema(r.Context()) + sch, err := s.dal.GetActiveSchema(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -931,7 +931,7 @@ func (s *Service) callWithRequest( return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("body is required")) } - sch, err := s.getActiveSchema(ctx) + sch, err := s.dal.GetActiveSchema(ctx) if err != nil { return nil, err } @@ -1685,18 +1685,6 @@ func (s *Service) syncSchema(ctx context.Context) { } } -func (s *Service) getActiveSchema(ctx context.Context) (*schema.Schema, error) { - deployments, err := s.dal.GetActiveDeployments(ctx) - if err != nil { - return nil, err - } - return schema.ValidateSchema(&schema.Schema{ - Modules: slices.Map(deployments, func(d dal.Deployment) *schema.Module { - return d.Schema - }), - }) -} - func extractIngressRoutingEntries(req *ftlv1.CreateDeploymentRequest) []dal.IngressRoutingEntry { var ingressRoutes []dal.IngressRoutingEntry for _, decl := range req.Schema.Decls { diff --git a/backend/controller/dal/dal.go b/backend/controller/dal/dal.go index e1a12ee840..249adbc35b 100644 --- a/backend/controller/dal/dal.go +++ b/backend/controller/dal/dal.go @@ -851,6 +851,23 @@ func (d *DAL) GetActiveDeployments(ctx context.Context) ([]Deployment, error) { }) } +// GetActiveSchema returns the schema for all active deployments. +func (d *DAL) GetActiveSchema(ctx context.Context) (*schema.Schema, error) { + deployments, err := d.GetActiveDeployments(ctx) + if err != nil { + return nil, err + } + sch, err := schema.ValidateSchema(&schema.Schema{ + Modules: slices.Map(deployments, func(d Deployment) *schema.Module { + return d.Schema + }), + }) + if err != nil { + return nil, fmt.Errorf("could not validate schema: %w", err) + } + return sch, nil +} + func (d *DAL) GetDeploymentsWithMinReplicas(ctx context.Context) ([]Deployment, error) { rows, err := d.db.GetDeploymentsWithMinReplicas(ctx) if err != nil { diff --git a/backend/controller/ingress/ingress.go b/backend/controller/ingress/ingress.go index afa896058a..fbcafd7192 100644 --- a/backend/controller/ingress/ingress.go +++ b/backend/controller/ingress/ingress.go @@ -1,14 +1,10 @@ package ingress import ( - "encoding/base64" "encoding/json" "fmt" "math/rand" - "reflect" - "strconv" "strings" - "time" "github.com/TBD54566975/ftl/backend/controller/dal" dalerrs "github.com/TBD54566975/ftl/backend/dal" @@ -16,12 +12,6 @@ import ( "github.com/TBD54566975/ftl/internal/slices" ) -type path []string - -func (p path) String() string { - return strings.TrimLeft(strings.Join(p, ""), ".") -} - func GetIngressRoute(routes []dal.IngressRoute, method string, path string) (*dal.IngressRoute, error) { var matchedRoutes = slices.Filter(routes, func(route dal.IngressRoute) bool { return matchSegments(route.Path, path, func(segment, value string) {}) @@ -66,7 +56,11 @@ func ValidateCallBody(body []byte, verb *schema.Verb, sch *schema.Schema) error return fmt.Errorf("HTTP request body is not valid JSON: %w", err) } - return validateValue(verb.Request, []string{verb.Request.String()}, requestMap, sch) + err = schema.ValidateJSONValue(verb.Request, []string{verb.Request.String()}, requestMap, sch) + if err != nil { + return fmt.Errorf("could not validate HTTP request body: %w", err) + } + return nil } func getBodyField(ref *schema.Ref, sch *schema.Schema) (*schema.Field, error) { @@ -88,190 +82,3 @@ func getBodyField(ref *schema.Ref, sch *schema.Schema) (*schema.Field, error) { return bodyField, nil } - -func validateValue(fieldType schema.Type, path path, value any, sch *schema.Schema) error { //nolint:maintidx - var typeMatches bool - switch fieldType := fieldType.(type) { - case *schema.Any: - typeMatches = true - - case *schema.Unit: - // TODO: Use type assertions consistently in this function rather than reflection. - rv := reflect.ValueOf(value) - if rv.Kind() != reflect.Map || rv.Len() != 0 { - return fmt.Errorf("%s must be an empty map", path) - } - return nil - - case *schema.Time: - str, ok := value.(string) - if !ok { - return fmt.Errorf("time %s must be an RFC3339 formatted string", path) - } - _, err := time.Parse(time.RFC3339Nano, str) - if err != nil { - return fmt.Errorf("time %s must be an RFC3339 formatted string: %w", path, err) - } - return nil - - case *schema.Int: - switch value := value.(type) { - case int64, float64: - typeMatches = true - case string: - if _, err := strconv.ParseInt(value, 10, 64); err == nil { - typeMatches = true - } - } - - case *schema.Float: - switch value := value.(type) { - case float64: - typeMatches = true - case string: - if _, err := strconv.ParseFloat(value, 64); err == nil { - typeMatches = true - } - } - - case *schema.String: - _, typeMatches = value.(string) - - case *schema.Bool: - switch value := value.(type) { - case bool: - typeMatches = true - case string: - if _, err := strconv.ParseBool(value); err == nil { - typeMatches = true - } - } - - case *schema.Array: - rv := reflect.ValueOf(value) - if rv.Kind() != reflect.Slice { - return fmt.Errorf("%s is not a slice", path) - } - elementType := fieldType.Element - for i := range rv.Len() { - elemPath := append(path, fmt.Sprintf("[%d]", i)) //nolint:gocritic - elem := rv.Index(i).Interface() - if err := validateValue(elementType, elemPath, elem, sch); err != nil { - return err - } - } - typeMatches = true - - case *schema.Map: - rv := reflect.ValueOf(value) - if rv.Kind() != reflect.Map { - return fmt.Errorf("%s is not a map", path) - } - keyType := fieldType.Key - valueType := fieldType.Value - for _, key := range rv.MapKeys() { - elemPath := append(path, fmt.Sprintf("[%q]", key)) //nolint:gocritic - elem := rv.MapIndex(key).Interface() - if err := validateValue(keyType, elemPath, key.Interface(), sch); err != nil { - return err - } - if err := validateValue(valueType, elemPath, elem, sch); err != nil { - return err - } - } - typeMatches = true - case *schema.Ref: - decl, ok := sch.Resolve(fieldType).Get() - if !ok { - return fmt.Errorf("unknown ref %v", fieldType) - } - - switch d := decl.(type) { - case *schema.Data: - if valueMap, ok := value.(map[string]any); ok { - if err := validateRequestMap(fieldType, path, valueMap, sch); err != nil { - return err - } - typeMatches = true - } - case *schema.TypeAlias: - return validateValue(d.Type, path, value, sch) - case *schema.Enum: - var inputName any - inputName = value - for _, v := range d.Variants { - switch t := v.Value.(type) { - case *schema.StringValue: - if valueStr, ok := value.(string); ok { - if t.Value == valueStr { - typeMatches = true - break - } - } - case *schema.IntValue: - if valueInt, ok := value.(int); ok { - if t.Value == valueInt { - typeMatches = true - break - } - } - case *schema.TypeValue: - if reqVariant, ok := value.(map[string]any); ok { - vName, ok := reqVariant["name"] - if !ok { - return fmt.Errorf(`missing name field in enum type %q: expected structure is `+ - "{\"name\": \"\", \"value\": }", value) - } - vNameStr, ok := vName.(string) - if !ok { - return fmt.Errorf(`invalid type for enum %q; name field must be a string, was %T`, - fieldType, vName) - } - inputName = fmt.Sprintf("%q", vNameStr) - - vValue, ok := reqVariant["value"] - if !ok { - return fmt.Errorf(`missing value field in enum type %q: expected structure is `+ - "{\"name\": \"\", \"value\": }", value) - } - - if v.Name == vNameStr { - return validateValue(t.Value, path, vValue, sch) - } - } else { - return fmt.Errorf(`malformed enum type %s: expected structure is `+ - "{\"name\": \"\", \"value\": }", path) - } - } - } - if !typeMatches { - return fmt.Errorf("%s is not a valid variant of enum %s", inputName, fieldType) - } - - case *schema.Config, *schema.Database, *schema.Secret, *schema.Verb, *schema.FSM, *schema.Topic, *schema.Subscription: - - } - - case *schema.Bytes: - _, typeMatches = value.([]byte) - if bodyStr, ok := value.(string); ok { - _, err := base64.StdEncoding.DecodeString(bodyStr) - if err != nil { - return fmt.Errorf("%s is not a valid base64 string", path) - } - typeMatches = true - } - - case *schema.Optional: - if value == nil { - typeMatches = true - } else { - return validateValue(fieldType.Type, path, value, sch) - } - } - - if !typeMatches { - return fmt.Errorf("%s has wrong type, expected %s found %T", path, fieldType, value) - } - return nil -} diff --git a/backend/controller/ingress/ingress_test.go b/backend/controller/ingress/ingress_test.go index 1d502f2645..a448580fbc 100644 --- a/backend/controller/ingress/ingress_test.go +++ b/backend/controller/ingress/ingress_test.go @@ -143,7 +143,7 @@ func TestValidation(t *testing.T) { sch, err := schema.ParseString("", test.schema) assert.NoError(t, err) - err = validateRequestMap(&schema.Ref{Module: "test", Name: "Test"}, nil, test.request, sch) + err = schema.ValidateRequestMap(&schema.Ref{Module: "test", Name: "Test"}, nil, test.request, sch) if test.err != "" { assert.EqualError(t, err, test.err) } else { @@ -407,7 +407,7 @@ func TestEnumValidation(t *testing.T) { } for _, test := range tests { - err := validateValue(test.validateRoot, []string{test.validateRoot.String()}, test.req, sch) + err := schema.ValidateJSONValue(test.validateRoot, []string{test.validateRoot.String()}, test.req, sch) if test.err == "" { assert.NoError(t, err) } else { diff --git a/backend/controller/ingress/request.go b/backend/controller/ingress/request.go index 4c8ed01e38..d9bb18a2b4 100644 --- a/backend/controller/ingress/request.go +++ b/backend/controller/ingress/request.go @@ -2,7 +2,6 @@ package ingress import ( "encoding/json" - "errors" "fmt" "io" "net/http" @@ -84,7 +83,7 @@ func BuildRequestBody(route *dal.IngressRoute, r *http.Request, sch *schema.Sche return nil, err } - err = validateRequestMap(request, []string{request.String()}, requestMap, sch) + err = schema.ValidateRequestMap(request, []string{request.String()}, requestMap, sch) if err != nil { return nil, err } @@ -235,45 +234,6 @@ func buildRequestMap(route *dal.IngressRoute, r *http.Request, ref *schema.Ref, return requestMap, nil } -func validateRequestMap(ref *schema.Ref, path path, request map[string]any, sch *schema.Schema) error { - data, err := sch.ResolveMonomorphised(ref) - if err != nil { - return err - } - - var errs []error - for _, field := range data.Fields { - fieldPath := append(path, "."+field.Name) //nolint:gocritic - - value, haveValue := request[field.Name] - if !haveValue && !allowMissingField(field) { - errs = append(errs, fmt.Errorf("%s is required", fieldPath)) - continue - } - - if haveValue { - err := validateValue(field.Type, fieldPath, value, sch) - if err != nil { - errs = append(errs, err) - } - } - - } - - return errors.Join(errs...) -} - -// Fields of these types can be omitted from the JSON representation. -func allowMissingField(field *schema.Field) bool { - switch field.Type.(type) { - case *schema.Optional, *schema.Any, *schema.Array, *schema.Map, *schema.Bytes, *schema.Unit: - return true - - case *schema.Bool, *schema.Ref, *schema.Float, *schema.Int, *schema.String, *schema.Time: - } - return false -} - func parseQueryParams(values url.Values, data *schema.Data) (map[string]any, error) { if jsonStr, ok := values["@json"]; ok { if len(values) > 1 { diff --git a/backend/schema/jsonvalidate.go b/backend/schema/jsonvalidate.go new file mode 100644 index 0000000000..3e5336b8c2 --- /dev/null +++ b/backend/schema/jsonvalidate.go @@ -0,0 +1,251 @@ +package schema + +import ( + "encoding/base64" + "errors" + "fmt" + "reflect" + "strconv" + "strings" + "time" +) + +type path []string + +func (p path) String() string { + return strings.TrimLeft(strings.Join(p, ""), ".") +} + +// ValidateJSONValue validates a given JSON value against the provided schema. +func ValidateJSONValue(fieldType Type, path path, value any, sch *Schema) error { //nolint:maintidx + var typeMatches bool + switch fieldType := fieldType.(type) { + case *Any: + typeMatches = true + + case *Unit: + // TODO: Use type assertions consistently in this function rather than reflection. + rv := reflect.ValueOf(value) + if rv.Kind() != reflect.Map || rv.Len() != 0 { + return fmt.Errorf("%s must be an empty map", path) + } + return nil + + case *Time: + str, ok := value.(string) + if !ok { + return fmt.Errorf("time %s must be an RFC3339 formatted string", path) + } + _, err := time.Parse(time.RFC3339Nano, str) + if err != nil { + return fmt.Errorf("time %s must be an RFC3339 formatted string: %w", path, err) + } + return nil + + case *Int: + switch value := value.(type) { + case int64, float64: + typeMatches = true + case string: + fmt.Printf("found string %s\n", value) + if _, err := strconv.ParseInt(value, 10, 64); err == nil { + typeMatches = true + } + } + + case *Float: + switch value := value.(type) { + case float64: + typeMatches = true + case string: + if _, err := strconv.ParseFloat(value, 64); err == nil { + typeMatches = true + } + } + + case *String: + _, typeMatches = value.(string) + + case *Bool: + switch value := value.(type) { + case bool: + typeMatches = true + case string: + if _, err := strconv.ParseBool(value); err == nil { + typeMatches = true + } + } + + case *Array: + rv := reflect.ValueOf(value) + if rv.Kind() != reflect.Slice { + return fmt.Errorf("%s is not a slice", path) + } + elementType := fieldType.Element + for i := range rv.Len() { + elemPath := append(path, fmt.Sprintf("[%d]", i)) //nolint:gocritic + elem := rv.Index(i).Interface() + if err := ValidateJSONValue(elementType, elemPath, elem, sch); err != nil { + return err + } + } + typeMatches = true + + case *Map: + rv := reflect.ValueOf(value) + if rv.Kind() != reflect.Map { + return fmt.Errorf("%s is not a map", path) + } + keyType := fieldType.Key + valueType := fieldType.Value + for _, key := range rv.MapKeys() { + elemPath := append(path, fmt.Sprintf("[%q]", key)) //nolint:gocritic + elem := rv.MapIndex(key).Interface() + if err := ValidateJSONValue(keyType, elemPath, key.Interface(), sch); err != nil { + return err + } + if err := ValidateJSONValue(valueType, elemPath, elem, sch); err != nil { + return err + } + } + typeMatches = true + case *Ref: + decl, ok := sch.Resolve(fieldType).Get() + if !ok { + return fmt.Errorf("unknown ref %v", fieldType) + } + + switch d := decl.(type) { + case *Data: + if valueMap, ok := value.(map[string]any); ok { + if err := ValidateRequestMap(fieldType, path, valueMap, sch); err != nil { + return err + } + typeMatches = true + } + case *TypeAlias: + return ValidateJSONValue(d.Type, path, value, sch) + case *Enum: + var inputName any + inputName = value + for _, v := range d.Variants { + switch t := v.Value.(type) { + case *StringValue: + if valueStr, ok := value.(string); ok { + if t.Value == valueStr { + typeMatches = true + break + } + } + case *IntValue: + switch value := value.(type) { + case int, int64: + if t.Value == value { + typeMatches = true + break + } + case float64: + if float64(t.Value) == value { + typeMatches = true + break + } + } + case *TypeValue: + if reqVariant, ok := value.(map[string]any); ok { + vName, ok := reqVariant["name"] + if !ok { + return fmt.Errorf(`missing name field in enum type %q: expected structure is `+ + "{\"name\": \"\", \"value\": }", value) + } + vNameStr, ok := vName.(string) + if !ok { + return fmt.Errorf(`invalid type for enum %q; name field must be a string, was %T`, + fieldType, vName) + } + inputName = fmt.Sprintf("%q", vNameStr) + + vValue, ok := reqVariant["value"] + if !ok { + return fmt.Errorf(`missing value field in enum type %q: expected structure is `+ + "{\"name\": \"\", \"value\": }", value) + } + + if v.Name == vNameStr { + return ValidateJSONValue(t.Value, path, vValue, sch) + } + } else { + return fmt.Errorf(`malformed enum type %s: expected structure is `+ + "{\"name\": \"\", \"value\": }", path) + } + } + } + if !typeMatches { + return fmt.Errorf("%s is not a valid variant of enum %s", inputName, fieldType) + } + + case *Config, *Database, *Secret, *Verb, *FSM, *Topic, *Subscription: + + } + + case *Bytes: + _, typeMatches = value.([]byte) + if bodyStr, ok := value.(string); ok { + _, err := base64.StdEncoding.DecodeString(bodyStr) + if err != nil { + return fmt.Errorf("%s is not a valid base64 string", path) + } + typeMatches = true + } + + case *Optional: + if value == nil { + typeMatches = true + } else { + return ValidateJSONValue(fieldType.Type, path, value, sch) + } + } + + if !typeMatches { + return fmt.Errorf("%s has wrong type, expected %s found %T", path, fieldType, value) + } + return nil +} + +func ValidateRequestMap(ref *Ref, path path, request map[string]any, sch *Schema) error { + data, err := sch.ResolveMonomorphised(ref) + if err != nil { + return err + } + + var errs []error + for _, field := range data.Fields { + fieldPath := append(path, "."+field.Name) //nolint:gocritic + + value, haveValue := request[field.Name] + if !haveValue && !allowMissingField(field) { + errs = append(errs, fmt.Errorf("%s is required", fieldPath)) + continue + } + + if haveValue { + err := ValidateJSONValue(field.Type, fieldPath, value, sch) + if err != nil { + errs = append(errs, err) + } + } + + } + + return errors.Join(errs...) +} + +// Fields of these types can be omitted from the JSON representation. +func allowMissingField(field *Field) bool { + switch field.Type.(type) { + case *Optional, *Any, *Array, *Map, *Bytes, *Unit: + return true + + case *Bool, *Ref, *Float, *Int, *String, *Time: + } + return false +} diff --git a/common/projectconfig/integration_test.go b/common/projectconfig/integration_test.go index 02c60d92b8..2e79d26e31 100644 --- a/common/projectconfig/integration_test.go +++ b/common/projectconfig/integration_test.go @@ -59,3 +59,37 @@ func TestFindConfig(t *testing.T) { checkConfig("."), ) } + +func TestConfigValidation(t *testing.T) { + in.Run(t, "./validateconfig/ftl-project.toml", + in.CopyModule("validateconfig"), + + // Global sets never error. + in.Chdir("validateconfig", in.Exec("ftl", "config", "set", "key", "--inline", "valueTwo")), + in.Chdir("validateconfig", in.Exec("ftl", "config", "set", "key", "--inline", "2")), + in.ExecWithExpectedOutput("\"2\"\n", "ftl", "config", "get", "key"), + + // No deploy yet; module sets don't error if decl isn't found. + in.Exec("ftl", "config", "set", "validateconfig.defaultName", "--inline", "somename"), + in.ExecWithExpectedOutput("\"somename\"\n", "ftl", "config", "get", "validateconfig.defaultName"), + in.Exec("ftl", "config", "set", "validateconfig.count", "--inline", "1"), + in.ExecWithExpectedOutput("\"1\"\n", "ftl", "config", "get", "validateconfig.count"), + + // This is a mismatched type, but should pass without an active deployment. + in.Exec("ftl", "config", "set", "validateconfig.count", "--inline", "one"), + + // Deploy; validation should now be run on config sets. + in.Deploy("validateconfig"), + in.Exec("ftl", "config", "set", "validateconfig.defaultName", "--inline", "somenametwo"), + in.ExecWithExpectedOutput("\"somenametwo\"\n", "ftl", "config", "get", "validateconfig.defaultName"), + in.Exec("ftl", "config", "set", "validateconfig.count", "--inline", "2"), + in.ExecWithExpectedOutput("\"2\"\n", "ftl", "config", "get", "validateconfig.count"), + + // With a deploy, set should fail validation on bad data type. + in.ExecWithExpectedError("ftl: error: unknown: JSON validation failed: count has wrong type, expected Int found string", + "ftl", "config", "set", "validateconfig.count", "--inline", "three"), + + in.ExecWithExpectedOutput("key = \"2\"\nvalidateconfig.count = \"2\"\nvalidateconfig.defaultName = \"somenametwo\"\n", + "ftl", "config", "list", "--values"), + ) +} diff --git a/common/projectconfig/testdata/go/validate-configs-ftl-project.toml b/common/projectconfig/testdata/go/validate-configs-ftl-project.toml new file mode 100644 index 0000000000..4ce8cbd2cd --- /dev/null +++ b/common/projectconfig/testdata/go/validate-configs-ftl-project.toml @@ -0,0 +1,5 @@ +name = "testdata" + +[global] + [global.configuration] + key = "inline://InZhbHVlIg" diff --git a/common/projectconfig/testdata/go/validateconfig/ftl-project.toml b/common/projectconfig/testdata/go/validateconfig/ftl-project.toml new file mode 100644 index 0000000000..5e2018aa4b --- /dev/null +++ b/common/projectconfig/testdata/go/validateconfig/ftl-project.toml @@ -0,0 +1 @@ +name = "testdata" diff --git a/common/projectconfig/testdata/go/validateconfig/ftl.toml b/common/projectconfig/testdata/go/validateconfig/ftl.toml new file mode 100644 index 0000000000..6e79705a5d --- /dev/null +++ b/common/projectconfig/testdata/go/validateconfig/ftl.toml @@ -0,0 +1,2 @@ +module = "validateconfig" +language = "go" diff --git a/common/projectconfig/testdata/go/validateconfig/go.mod b/common/projectconfig/testdata/go/validateconfig/go.mod new file mode 100644 index 0000000000..c1a34b2764 --- /dev/null +++ b/common/projectconfig/testdata/go/validateconfig/go.mod @@ -0,0 +1,47 @@ +module ftl/validateconfig + +go 1.22.2 + +toolchain go1.22.4 + +require github.com/TBD54566975/ftl v1.1.5 + +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 +) + +replace github.com/TBD54566975/ftl => ../../../../.. diff --git a/common/projectconfig/testdata/go/validateconfig/go.sum b/common/projectconfig/testdata/go/validateconfig/go.sum new file mode 100644 index 0000000000..1e8d4689ef --- /dev/null +++ b/common/projectconfig/testdata/go/validateconfig/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/common/projectconfig/testdata/go/validateconfig/validateconfig.go b/common/projectconfig/testdata/go/validateconfig/validateconfig.go new file mode 100644 index 0000000000..60a8dd2158 --- /dev/null +++ b/common/projectconfig/testdata/go/validateconfig/validateconfig.go @@ -0,0 +1,24 @@ +package validateconfig + +import ( + "context" + "fmt" + + "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. +) + +var defaultName = ftl.Config[string]("default") +var count = ftl.Config[int]("count") + +type EchoRequest struct { + Name ftl.Option[string] `json:"name"` +} + +type EchoResponse struct { + Message string `json:"message"` +} + +//ftl:verb +func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { + return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default(defaultName.Get(ctx)))}, nil +} diff --git a/docs/content/docs/reference/externaltypes.md b/docs/content/docs/reference/externaltypes.md new file mode 100644 index 0000000000..2ef289610d --- /dev/null +++ b/docs/content/docs/reference/externaltypes.md @@ -0,0 +1,59 @@ ++++ +title = "External Types" +description = "Using external types in your modules" +date = 2024-07-12T18:00:00+00:00 +updated = 2024-07-12T18:00:00+00:00 +draft = false +weight = 110 +sort_by = "weight" +template = "docs/page.html" + +[extra] +toc = true +top = false ++++ + +## Using external types + +To use an external type in your FTL module schema, declare a type alias over the external type: + +```go +//ftl:typealias +type FtlType external.OtherType +``` + +The external type is widened to `Any` in the FTL schema, and the corresponding type alias will include metadata +for the runtime-specific type mapping: + +``` +typealias FtlType Any + +typemap go "github.com/external.OtherType" +``` + +Users can achieve functionally equivalent behavior to using the external type directly by using the declared +alias (`FtlType`) throughout their code. Direct usage of the external type in schema declarations is not supported; +instead, the type alias must be used. + +FTL will automatically serialize and deserialize the external type to the strong type indicated by the mapping. + +## Cross-Runtime Type Mappings + +FTL also provides the capability to declare type mappings for other runtimes. For instance, to include a type mapping for Kotlin, you can +annotate your type alias declaration as follows: + +```go +//ftl:typealias +//ftl:typemap kotlin "com.external.other.OtherType" +type FtlType external.OtherType +``` + +In the FTL schema, this will appear as: + +``` +typealias FtlType Any + +typemap go "github.com/external.OtherType" + +typemap kotlin "com.external.other.OtherType" +``` + +This allows FTL to decode the type properly in other languages, for seamless +interoperability across different runtimes. \ No newline at end of file diff --git a/integration/actions.go b/integration/actions.go index 1ecdc2def6..772ebda325 100644 --- a/integration/actions.go +++ b/integration/actions.go @@ -141,6 +141,17 @@ func ExecWithExpectedOutput(want string, cmd string, args ...string) Action { } } +// ExecWithExpectedError runs a command from the test working directory, and +// expects it to fail with the given error message. +func ExecWithExpectedError(want string, cmd string, args ...string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Executing: %s %s", cmd, shellquote.Join(args...)) + output, err := ftlexec.Capture(ic, ic.workDir, cmd, args...) + assert.Error(t, err) + assert.Contains(t, string(output), want) + } +} + // ExecWithOutput runs a command from the test working directory. // On success capture() is executed with the output // On error, an error with the output is returned.