diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 52a56f6cb3..859fb3a91b 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -38,8 +38,6 @@ import ( "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/common/configuration" - "github.com/TBD54566975/ftl/common/modulecontext" frontend "github.com/TBD54566975/ftl/frontend" "github.com/TBD54566975/ftl/internal/cors" "github.com/TBD54566975/ftl/internal/log" @@ -649,28 +647,7 @@ func (s *Service) GetModuleContext(ctx context.Context, req *connect.Request[ftl if err != nil { return nil, err } - schemas = slices.Filter(schemas, func(s *schema.Module) bool { - return s.Name == req.Msg.Module - }) - if len(schemas) == 0 { - return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no schema found for module %q", req.Msg.Module)) - } else if len(schemas) > 1 { - return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("multiple schemas found for module %q", req.Msg.Module)) - } - - b := modulecontext.NewBuilder(req.Msg.Module) - b.AddConfigFromManager(configuration.ConfigFromContext(ctx)) - b.AddSecretsFromManager(configuration.SecretsFromContext(ctx)) - b.AddDSNsFromEnvarsForModule(schemas[0]) - moduleCtx, err := b.Build(ctx) - if err != nil { - return nil, err - } - out, err := moduleCtx.ToProto(ctx) - if err != nil { - return nil, err - } - return connect.NewResponse(out), nil + return moduleContextToProto(ctx, req.Msg.Module, schemas) } func (s *Service) Call(ctx context.Context, req *connect.Request[ftlv1.CallRequest]) (*connect.Response[ftlv1.CallResponse], error) { diff --git a/backend/controller/modulecontext.go b/backend/controller/modulecontext.go new file mode 100644 index 0000000000..54d4c4afa3 --- /dev/null +++ b/backend/controller/modulecontext.go @@ -0,0 +1,97 @@ +package controller + +import ( + "context" + "fmt" + "os" + "strings" + + "connectrpc.com/connect" + ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/common/configuration" + cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/TBD54566975/ftl/internal/slices" +) + +func moduleContextToProto(ctx context.Context, name string, schemas []*schema.Module) (*connect.Response[ftlv1.ModuleContextResponse], error) { + schemas = slices.Filter(schemas, func(s *schema.Module) bool { + return s.Name == name + }) + if len(schemas) == 0 { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("no schema found for module %q", name)) + } else if len(schemas) > 1 { + return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("multiple schemas found for module %q", name)) + } + + // configs + configManager := configuration.ConfigFromContext(ctx) + configList, err := configManager.List(ctx) + if err != nil { + return nil, err + } + configProtos := []*ftlv1.ModuleContextResponse_Config{} + for _, entry := range configList { + data, err := configManager.GetData(ctx, entry.Ref) + if err != nil { + return nil, err + } + configProtos = append(configProtos, &ftlv1.ModuleContextResponse_Config{ + Ref: configRefToProto(entry.Ref), + Data: data, + }) + } + + // secrets + secretsManager := configuration.SecretsFromContext(ctx) + secretsList, err := secretsManager.List(ctx) + if err != nil { + return nil, err + } + secretProtos := []*ftlv1.ModuleContextResponse_Secret{} + for _, entry := range secretsList { + data, err := secretsManager.GetData(ctx, entry.Ref) + if err != nil { + return nil, err + } + secretProtos = append(secretProtos, &ftlv1.ModuleContextResponse_Secret{ + Ref: configRefToProto(entry.Ref), + Data: data, + }) + } + + // DSNs + dsnProtos := []*ftlv1.ModuleContextResponse_DSN{} + for _, decl := range schemas[0].Decls { + dbDecl, ok := decl.(*schema.Database) + if !ok { + continue + } + key := fmt.Sprintf("FTL_POSTGRES_DSN_%s_%s", strings.ToUpper(name), strings.ToUpper(dbDecl.Name)) + dsn, ok := os.LookupEnv(key) + if !ok { + return nil, fmt.Errorf("missing environment variable %q", key) + } + dsnProtos = append(dsnProtos, &ftlv1.ModuleContextResponse_DSN{ + Name: dbDecl.Name, + Type: ftlv1.ModuleContextResponse_POSTGRES, + Dsn: dsn, + }) + } + + return connect.NewResponse(&ftlv1.ModuleContextResponse{ + Configs: configProtos, + Secrets: secretProtos, + Databases: dsnProtos, + }), nil +} + +func configRefToProto(r cf.Ref) *ftlv1.ModuleContextResponse_Ref { + protoRef := &ftlv1.ModuleContextResponse_Ref{ + Name: r.Name, + } + if module, ok := r.Module.Get(); ok { + protoRef.Module = &module + } + return protoRef +} diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go index d5be67becd..8a1b938ba5 100644 --- a/common/modulecontext/builder.go +++ b/common/modulecontext/builder.go @@ -3,11 +3,8 @@ package modulecontext import ( "context" "fmt" - "os" - "strings" 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/slices" "github.com/alecthomas/types/optional" @@ -23,8 +20,6 @@ type refValuePair struct { resolver resolver } -func (r refValuePair) configOrSecretItem() {} - type dsnEntry struct { name string dbType DBType @@ -43,34 +38,19 @@ type stringResolver interface { } var _ stringResolver = valueResolver{} -var _ stringResolver = envarResolver{} - -type configManager[R cf.Role] struct { - manager *cf.Manager[R] -} - -func (m configManager[R]) configOrSecretItem() {} - -// sumtype:decl -type configOrSecretItem interface { - configOrSecretItem() -} - -var _ configOrSecretItem = refValuePair{} -var _ configOrSecretItem = configManager[cf.Configuration]{} type Builder struct { moduleName string - configs []configOrSecretItem - secrets []configOrSecretItem + configs []refValuePair + secrets []refValuePair dsns []dsnEntry } func NewBuilder(moduleName string) *Builder { return &Builder{ moduleName: moduleName, - configs: []configOrSecretItem{}, - secrets: []configOrSecretItem{}, + configs: []refValuePair{}, + secrets: []refValuePair{}, dsns: []dsnEntry{}, } } @@ -78,10 +58,10 @@ func NewBuilder(moduleName string) *Builder { func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) *Builder { return &Builder{ moduleName: moduleName, - configs: slices.Map(response.Configs, func(c *ftlv1.ModuleContextResponse_Config) configOrSecretItem { + configs: slices.Map(response.Configs, func(c *ftlv1.ModuleContextResponse_Config) refValuePair { return refValuePair{ref: refFromProto(c.Ref), resolver: dataResolver{data: c.Data}} }), - secrets: slices.Map(response.Secrets, func(s *ftlv1.ModuleContextResponse_Secret) configOrSecretItem { + secrets: slices.Map(response.Secrets, func(s *ftlv1.ModuleContextResponse_Secret) refValuePair { return refValuePair{ref: refFromProto(s.Ref), resolver: dataResolver{data: s.Data}} }), dsns: slices.Map(response.Databases, func(d *ftlv1.ModuleContextResponse_DSN) dsnEntry { @@ -105,10 +85,10 @@ func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { dbProvider: NewDBProvider(), } - if err := buildConfigOrSecrets[cf.Configuration](ctx, b, *moduleCtx.configManager, b.configs); err != nil { + if err := buildConfigOrSecrets[cf.Configuration](ctx, *moduleCtx.configManager, b.configs); err != nil { return nil, err } - if err := buildConfigOrSecrets[cf.Secrets](ctx, b, *moduleCtx.secretsManager, b.secrets); err != nil { + if err := buildConfigOrSecrets[cf.Secrets](ctx, *moduleCtx.secretsManager, b.secrets); err != nil { return nil, err } @@ -135,73 +115,25 @@ func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], e return manager, nil } -func buildConfigOrSecrets[R cf.Role](ctx context.Context, b *Builder, manager cf.Manager[R], items []configOrSecretItem) error { +func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], items []refValuePair) error { for _, item := range items { - switch item := item.(type) { - case refValuePair: - isData, value, data, err := item.resolver.Resolve() - if err != nil { + isData, value, data, err := item.resolver.Resolve() + if err != nil { + return err + } + if isData { + if err := manager.SetData(ctx, cf.Ref(item.ref), data); err != nil { return err } - if isData { - if err := manager.SetData(ctx, cf.Ref(item.ref), data); err != nil { - return err - } - } else { - if err := manager.Set(ctx, cf.Ref(item.ref), value); err != nil { - return err - } - } - case configManager[R]: - list, err := item.manager.List(ctx) - if err != nil { + } else { + if err := manager.Set(ctx, cf.Ref(item.ref), value); err != nil { return err } - for _, e := range list { - if m, isModuleSpecific := e.Module.Get(); isModuleSpecific && b.moduleName != m { - continue - } - data, err := item.manager.GetData(ctx, e.Ref) - if err != nil { - return err - } - if err := manager.SetData(ctx, e.Ref, data); err != nil { - return err - } - } } } return nil } -func (b *Builder) AddDSNsFromEnvarsForModule(module *schema.Module) *Builder { - // remove in favor of a non-envar approach once it is available - for _, decl := range module.Decls { - dbDecl, ok := decl.(*schema.Database) - if !ok { - continue - } - b.dsns = append(b.dsns, dsnEntry{ - name: dbDecl.Name, - dbType: DBTypePostgres, - resolver: envarResolver{ - name: fmt.Sprintf("FTL_POSTGRES_DSN_%s_%s", strings.ToUpper(module.Name), strings.ToUpper(dbDecl.Name)), - }, - }) - } - return b -} - -func (b *Builder) AddConfigFromManager(cm *cf.Manager[cf.Configuration]) *Builder { - b.configs = append(b.configs, configManager[cf.Configuration]{manager: cm}) - return b -} - -func (b *Builder) AddSecretsFromManager(sm *cf.Manager[cf.Secrets]) *Builder { - b.secrets = append(b.secrets, configManager[cf.Secrets]{manager: sm}) - return b -} - func refFromProto(r *ftlv1.ModuleContextResponse_Ref) ref { return ref{ Module: optional.Ptr(r.Module), @@ -232,23 +164,3 @@ type dataResolver struct { func (r dataResolver) Resolve() (isData bool, value any, data []byte, err error) { return true, nil, r.data, nil } - -type envarResolver struct { - name string -} - -func (r envarResolver) Resolve() (isData bool, value any, data []byte, err error) { - value, err = r.ResolveString() - if err != nil { - return false, nil, nil, err - } - return false, value, nil, nil -} - -func (r envarResolver) ResolveString() (string, error) { - value, ok := os.LookupEnv(r.name) - if !ok { - return "", fmt.Errorf("missing environment variable %q", r.name) - } - return value, nil -} diff --git a/common/modulecontext/builder_test.go b/common/modulecontext/builder_test.go index 343375855b..9aa6da9dc0 100644 --- a/common/modulecontext/builder_test.go +++ b/common/modulecontext/builder_test.go @@ -1,48 +1 @@ package modulecontext - -import ( - "context" - "testing" - - cf "github.com/TBD54566975/ftl/common/configuration" - "github.com/TBD54566975/ftl/internal/log" - "github.com/alecthomas/assert/v2" - "github.com/alecthomas/types/optional" -) - -type jsonableStruct struct { - Str string `json:"string"` - TestStruct *jsonableStruct `json:"struct,omitempty"` -} - -func TestConfigManagerInAndOut(t *testing.T) { - ctx := log.ContextWithNewDefaultLogger(context.Background()) - - cm, err := newInMemoryConfigManager[cf.Configuration](ctx) - assert.NoError(t, err) - - moduleName := "test" - strValue := "HelloWorld" - intValue := 42 - structValue := jsonableStruct{Str: "HelloWorld", TestStruct: &jsonableStruct{Str: "HelloWorld"}} - cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "str"}, strValue) - cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "int"}, intValue) - cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "struct"}, structValue) - - builder := NewBuilder(moduleName).AddConfigFromManager(cm) - - moduleCtx, err := builder.Build(ctx) - assert.NoError(t, err) - - var outStr string - moduleCtx.configManager.Get(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "str"}, &outStr) - assert.Equal(t, strValue, outStr, "expected string value to be set and retrieved correctly") - - var outInt int - moduleCtx.configManager.Get(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "int"}, &outInt) - assert.Equal(t, intValue, outInt, "expected int value to be set and retrieved correctly") - - var outStruct jsonableStruct - moduleCtx.configManager.Get(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "struct"}, &outStruct) - assert.Equal(t, structValue, outStruct, "expected struct value to be set and retrieved correctly") -} diff --git a/common/modulecontext/modulecontext.go b/common/modulecontext/modulecontext.go index c5a19aaa23..72e946c87a 100644 --- a/common/modulecontext/modulecontext.go +++ b/common/modulecontext/modulecontext.go @@ -3,7 +3,6 @@ package modulecontext import ( "context" - ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" cf "github.com/TBD54566975/ftl/common/configuration" ) @@ -19,78 +18,3 @@ func (m *ModuleContext) ApplyToContext(ctx context.Context) context.Context { ctx = cf.ContextWithSecrets(ctx, m.secretsManager) return ctx } - -func (m *ModuleContext) ToProto(ctx context.Context) (*ftlv1.ModuleContextResponse, error) { - configList, err := m.configManager.List(ctx) - if err != nil { - return nil, err - } - configProtos := []*ftlv1.ModuleContextResponse_Config{} - for _, entry := range configList { - p, err := m.configEntryToConfigProto(ctx, entry) - if err != nil { - return nil, err - } - configProtos = append(configProtos, p) - } - - secretsList, err := m.secretsManager.List(ctx) - if err != nil { - return nil, err - } - secretProtos := []*ftlv1.ModuleContextResponse_Secret{} - for _, entry := range secretsList { - p, err := m.configEntryToSecretProto(ctx, entry) - if err != nil { - return nil, err - } - secretProtos = append(secretProtos, p) - } - - dsnProtos := []*ftlv1.ModuleContextResponse_DSN{} - for name, entry := range m.dbProvider.entries { - dsnProtos = append(dsnProtos, &ftlv1.ModuleContextResponse_DSN{ - Name: name, - Type: ftlv1.ModuleContextResponse_DBType(entry.dbType), - Dsn: entry.dsn, - }) - } - - return &ftlv1.ModuleContextResponse{ - Configs: configProtos, - Secrets: secretProtos, - Databases: dsnProtos, - }, nil -} - -func (m *ModuleContext) configEntryToConfigProto(ctx context.Context, e cf.Entry) (*ftlv1.ModuleContextResponse_Config, error) { - data, err := m.configManager.GetData(ctx, e.Ref) - if err != nil { - return nil, err - } - return &ftlv1.ModuleContextResponse_Config{ - Ref: m.configRefToProto(e.Ref), - Data: data, - }, nil -} - -func (m *ModuleContext) configEntryToSecretProto(ctx context.Context, e cf.Entry) (*ftlv1.ModuleContextResponse_Secret, error) { - data, err := m.secretsManager.GetData(ctx, e.Ref) - if err != nil { - return nil, err - } - return &ftlv1.ModuleContextResponse_Secret{ - Ref: m.configRefToProto(e.Ref), - Data: data, - }, nil -} - -func (m *ModuleContext) configRefToProto(r cf.Ref) *ftlv1.ModuleContextResponse_Ref { - protoRef := &ftlv1.ModuleContextResponse_Ref{ - Name: r.Name, - } - if module, ok := r.Module.Get(); ok { - protoRef.Module = &module - } - return protoRef -}