From a464fdc2988d40619fce63a27a4034f9277a93eb Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 11:10:16 +1000 Subject: [PATCH 01/10] feat: module context over gRPC --- backend/controller/controller.go | 30 ++- cmd/ftl/main.go | 18 +- common/configuration/in_memory_provider.go | 44 ++++ common/configuration/in_memory_resolver.go | 45 ++++ common/configuration/manager.go | 45 ++-- common/modulecontext/builder.go | 254 +++++++++++++++++++++ common/modulecontext/builder_test.go | 1 + common/modulecontext/db_provider.go | 81 +++++++ common/modulecontext/db_provider_test.go | 24 ++ common/modulecontext/modulecontext.go | 96 ++++++++ go-runtime/ftl/database.go | 31 ++- go-runtime/server/server.go | 18 +- 12 files changed, 645 insertions(+), 42 deletions(-) create mode 100644 common/configuration/in_memory_provider.go create mode 100644 common/configuration/in_memory_resolver.go create mode 100644 common/modulecontext/builder.go create mode 100644 common/modulecontext/builder_test.go create mode 100644 common/modulecontext/db_provider.go create mode 100644 common/modulecontext/db_provider_test.go create mode 100644 common/modulecontext/modulecontext.go diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 8f9884afac..9c4e8fe922 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -38,6 +38,8 @@ 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" @@ -642,7 +644,33 @@ nextModule: // GetModuleContext retrieves config, secrets and DSNs for a module. func (s *Service) GetModuleContext(ctx context.Context, req *connect.Request[ftlv1.ModuleContextRequest]) (*connect.Response[ftlv1.ModuleContextResponse], error) { - return nil, fmt.Errorf("not implemented") + // get module schema + schemas, err := s.dal.GetActiveDeploymentSchemas(ctx) + 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 = b.AddConfigFromManager(configuration.ConfigFromContext(ctx)) + b = b.AddSecretsFromManager(configuration.SecretsFromContext(ctx)) + b = 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 } func (s *Service) Call(ctx context.Context, req *connect.Request[ftlv1.CallRequest]) (*connect.Response[ftlv1.CallResponse], error) { diff --git a/cmd/ftl/main.go b/cmd/ftl/main.go index 697ab3ed05..59cb4991a6 100644 --- a/cmd/ftl/main.go +++ b/cmd/ftl/main.go @@ -8,7 +8,6 @@ import ( "os/signal" "runtime" "strconv" - "strings" "syscall" "github.com/alecthomas/kong" @@ -92,10 +91,19 @@ func main() { kctx.BindTo(sr, (*cf.Resolver[cf.Secrets])(nil)) kctx.BindTo(cr, (*cf.Resolver[cf.Configuration])(nil)) - // Propagate to runner processes. - // TODO: This is a bit of a hack until we get proper configuration - // management through the Controller. - os.Setenv("FTL_CONFIG", strings.Join(projectconfig.ConfigPaths(cli.ConfigFlag), ",")) + // Add config manager to context. + cm, err := cf.NewConfigurationManager(ctx, cr) + if err != nil { + kctx.Fatalf(err.Error()) + } + ctx = cf.ContextWithConfig(ctx, cm) + + // Add secrets manager to context. + sm, err := cf.NewSecretsManager(ctx, sr) + if err != nil { + kctx.Fatalf(err.Error()) + } + ctx = cf.ContextWithSecrets(ctx, sm) // Handle signals. sigch := make(chan os.Signal, 1) diff --git a/common/configuration/in_memory_provider.go b/common/configuration/in_memory_provider.go new file mode 100644 index 0000000000..3c62eff3a6 --- /dev/null +++ b/common/configuration/in_memory_provider.go @@ -0,0 +1,44 @@ +package configuration + +import ( + "context" + "fmt" + "net/url" +) + +// InMemoryProvider is a configuration provider that keeps values in memory +type InMemoryProvider[R Role] struct { + values map[Ref][]byte +} + +var _ MutableProvider[Configuration] = &InMemoryProvider[Configuration]{} + +func NewInMemoryProvider[R Role]() *InMemoryProvider[R] { + return &InMemoryProvider[R]{values: map[Ref][]byte{}} +} + +func (p *InMemoryProvider[R]) Role() R { var r R; return r } +func (p *InMemoryProvider[R]) Key() string { return "grpc" } + +func (p *InMemoryProvider[R]) Load(ctx context.Context, ref Ref, key *url.URL) ([]byte, error) { + if bytes, found := p.values[ref]; found { + return bytes, nil + } + return nil, fmt.Errorf("key %q not found", ref.Name) +} + +func (p *InMemoryProvider[R]) Writer() bool { + return true +} + +// Store a configuration value and return its key. +func (p *InMemoryProvider[R]) Store(ctx context.Context, ref Ref, value []byte) (*url.URL, error) { + p.values[ref] = value + return &url.URL{Scheme: p.Key()}, nil +} + +// Delete a configuration value. +func (p *InMemoryProvider[R]) Delete(ctx context.Context, ref Ref) error { + delete(p.values, ref) + return nil +} diff --git a/common/configuration/in_memory_resolver.go b/common/configuration/in_memory_resolver.go new file mode 100644 index 0000000000..ee0ca59eb5 --- /dev/null +++ b/common/configuration/in_memory_resolver.go @@ -0,0 +1,45 @@ +package configuration + +import ( + "context" + "fmt" + "net/url" +) + +type InMemoryResolver[R Role] struct { + keyMap map[Ref]*url.URL +} + +var _ Resolver[Configuration] = InMemoryResolver[Configuration]{} +var _ Resolver[Secrets] = InMemoryResolver[Secrets]{} + +func NewInMemoryResolver[R Role]() *InMemoryResolver[R] { + return &InMemoryResolver[R]{keyMap: map[Ref]*url.URL{}} +} + +func (k InMemoryResolver[R]) Role() R { var r R; return r } + +func (k InMemoryResolver[R]) Get(ctx context.Context, ref Ref) (*url.URL, error) { + if key, found := k.keyMap[ref]; found { + return key, nil + } + return nil, fmt.Errorf("key %q not found", ref.Name) +} + +func (k InMemoryResolver[R]) List(ctx context.Context) ([]Entry, error) { + entries := []Entry{} + for ref, url := range k.keyMap { + entries = append(entries, Entry{Ref: ref, Accessor: url}) + } + return entries, nil +} + +func (k InMemoryResolver[R]) Set(ctx context.Context, ref Ref, key *url.URL) error { + k.keyMap[ref] = key + return nil +} + +func (k InMemoryResolver[R]) Unset(ctx context.Context, ref Ref) error { + delete(k.keyMap, ref) + return nil +} diff --git a/common/configuration/manager.go b/common/configuration/manager.go index 5718e54718..5fe6f872c6 100644 --- a/common/configuration/manager.go +++ b/common/configuration/manager.go @@ -64,10 +64,9 @@ func (m *Manager[R]) Mutable() error { return fmt.Errorf("no writeable configuration provider available, specify one of %s", strings.Join(writers, ", ")) } -// Get a configuration value from the active providers. -// -// "value" must be a pointer to a Go type that can be unmarshalled from JSON. -func (m *Manager[R]) Get(ctx context.Context, ref Ref, value any) error { +// GetData returns a data value for a configuration from the active providers. +// The data can be unmarshalled from JSON. +func (m *Manager[R]) GetData(ctx context.Context, ref Ref) ([]byte, error) { key, err := m.resolver.Get(ctx, ref) // Try again at the global scope if the value is not found in module scope. if ref.Module.Ok() && errors.Is(err, ErrNotFound) { @@ -75,38 +74,56 @@ func (m *Manager[R]) Get(ctx context.Context, ref Ref, value any) error { gref.Module = optional.None[string]() key, err = m.resolver.Get(ctx, gref) if err != nil { - return err + return nil, err } } else if err != nil { - return err + return nil, err } provider, ok := m.providers[key.Scheme] if !ok { - return fmt.Errorf("no provider for scheme %q", key.Scheme) + return nil, fmt.Errorf("no provider for scheme %q", key.Scheme) } data, err := provider.Load(ctx, ref, key) if err != nil { - return fmt.Errorf("%s: %w", ref, err) + return nil, fmt.Errorf("%s: %w", ref, err) + } + return data, nil +} + +// Get a configuration value from the active providers. +// +// "value" must be a pointer to a Go type that can be unmarshalled from JSON. +func (m *Manager[R]) Get(ctx context.Context, ref Ref, value any) error { + data, err := m.GetData(ctx, ref) + if err != nil { + return err } return json.Unmarshal(data, value) } -// Set a configuration value in the active writing provider. +// SetData updates the configuration value in the active writing provider as raw bytes. // -// "value" must be a Go type that can be marshalled to JSON. -func (m *Manager[R]) Set(ctx context.Context, ref Ref, value any) error { +// "data" must be bytes that can be unmarshalled into a Go type. +func (m *Manager[R]) SetData(ctx context.Context, ref Ref, data []byte) error { if err := m.Mutable(); err != nil { return err } - data, err := json.Marshal(value) + key, err := m.writer.Store(ctx, ref, data) if err != nil { return err } - key, err := m.writer.Store(ctx, ref, data) + return m.resolver.Set(ctx, ref, key) +} + +// Set a configuration value in the active writing provider. +// +// "value" must be a Go type that can be marshalled to JSON. +func (m *Manager[R]) Set(ctx context.Context, ref Ref, value any) error { + data, err := json.Marshal(value) if err != nil { return err } - return m.resolver.Set(ctx, ref, key) + return m.SetData(ctx, ref, data) } // Unset a configuration value in all providers. diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go new file mode 100644 index 0000000000..bf3bce51b2 --- /dev/null +++ b/common/modulecontext/builder.go @@ -0,0 +1,254 @@ +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" +) + +type ref struct { + Module optional.Option[string] + Name string +} + +type refValuePair struct { + ref ref + resolver resolver +} + +func (r refValuePair) configOrSecretItem() {} + +type dsnEntry struct { + name string + dbType DBType + resolver stringResolver +} + +type resolver interface { + Resolve() (isData bool, value any, data []byte, err error) +} + +var _ resolver = valueResolver{} +var _ resolver = dataResolver{} + +type stringResolver interface { + ResolveString() (string, error) +} + +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 + dsns []dsnEntry +} + +func NewBuilder(moduleName string) Builder { + return Builder{ + moduleName: moduleName, + configs: []configOrSecretItem{}, + secrets: []configOrSecretItem{}, + dsns: []dsnEntry{}, + } +} + +func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) Builder { + return Builder{ + moduleName: moduleName, + configs: slices.Map(response.Configs, func(c *ftlv1.ModuleContextResponse_Config) configOrSecretItem { + return refValuePair{ref: refFromProto(c.Ref), resolver: dataResolver{data: c.Data}} + }), + secrets: slices.Map(response.Secrets, func(s *ftlv1.ModuleContextResponse_Secret) configOrSecretItem { + return refValuePair{ref: refFromProto(s.Ref), resolver: dataResolver{data: s.Data}} + }), + dsns: slices.Map(response.Databases, func(d *ftlv1.ModuleContextResponse_DSN) dsnEntry { + return dsnEntry{name: d.Name, dbType: DBType(d.Type), resolver: valueResolver{value: d.Dsn}} + }), + } +} + +func (b Builder) Build(ctx context.Context) (*ModuleContext, error) { + cm, err := newInMemoryConfigManager[cf.Configuration](ctx) + if err != nil { + return nil, err + } + sm, err := newInMemoryConfigManager[cf.Secrets](ctx) + if err != nil { + return nil, err + } + moduleCtx := &ModuleContext{ + configManager: cm, + secretsManager: sm, + dbProvider: NewDBProvider(), + } + + if err := buildConfigOrSecrets[cf.Configuration](ctx, b, *moduleCtx.configManager, b.configs); err != nil { + return nil, err + } + if err := buildConfigOrSecrets[cf.Secrets](ctx, b, *moduleCtx.secretsManager, b.secrets); err != nil { + return nil, err + } + + for _, entry := range b.dsns { + dsn, err := entry.resolver.ResolveString() + if err != nil { + return nil, err + } + if err = moduleCtx.dbProvider.AddDSN(entry.name, entry.dbType, dsn); err != nil { + return nil, err + } + } + + return moduleCtx, nil +} + +func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], error) { + provider := cf.NewInMemoryProvider[R]() + resolver := cf.NewInMemoryResolver[R]() + manager, err := cf.New(ctx, resolver, []cf.Provider[R]{provider}) + if err != nil { + return nil, err + } + return manager, nil +} + +func buildConfigOrSecrets[R cf.Role](ctx context.Context, b Builder, manager cf.Manager[R], items []configOrSecretItem) error { + for _, item := range items { + switch item := item.(type) { + case refValuePair: + 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 + } + } 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 { + 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), + Name: r.Name, + } +} + +type valueResolver struct { + value any +} + +func (r valueResolver) Resolve() (isData bool, value any, data []byte, err error) { + return false, r.value, nil, nil +} + +func (r valueResolver) ResolveString() (string, error) { + str, ok := r.value.(string) + if !ok { + return "", fmt.Errorf("value is not a string: %v", r.value) + } + return str, nil +} + +type dataResolver struct { + data []byte +} + +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 new file mode 100644 index 0000000000..9aa6da9dc0 --- /dev/null +++ b/common/modulecontext/builder_test.go @@ -0,0 +1 @@ +package modulecontext diff --git a/common/modulecontext/db_provider.go b/common/modulecontext/db_provider.go new file mode 100644 index 0000000000..d45eac0b7b --- /dev/null +++ b/common/modulecontext/db_provider.go @@ -0,0 +1,81 @@ +package modulecontext + +import ( + "context" + "database/sql" + "fmt" + "strconv" + + ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" + + _ "github.com/jackc/pgx/v5/stdlib" // SQL driver +) + +type DBType int32 + +const ( + DBTypePostgres = DBType(ftlv1.ModuleContextResponse_POSTGRES) +) + +func (x DBType) String() string { + switch x { + case DBTypePostgres: + return "Postgres" + default: + panic(fmt.Sprintf("unknown DB type: %s", strconv.Itoa(int(x)))) + } +} + +type dbEntry struct { + dsn string + dbType DBType + db *sql.DB +} + +// DBProvider takes in DSNs and holds a *sql.DB for each +// this allows us to: +// - pool db connections, rather than initializing anew each time +// - validate DSNs at startup, rather than returning errors or panicking at Database.Get() +type DBProvider struct { + entries map[string]dbEntry +} + +type contextKeyDSNProvider struct{} + +func NewDBProvider() *DBProvider { + return &DBProvider{ + entries: map[string]dbEntry{}, + } +} + +func ContextWithDBProvider(ctx context.Context, provider *DBProvider) context.Context { + return context.WithValue(ctx, contextKeyDSNProvider{}, provider) +} + +func DBProviderFromContext(ctx context.Context) *DBProvider { + m, ok := ctx.Value(contextKeyDSNProvider{}).(*DBProvider) + if !ok { + panic("no db provider in context") + } + return m +} + +func (d *DBProvider) AddDSN(name string, dbType DBType, dsn string) error { + db, err := sql.Open("pgx", dsn) + if err != nil { + return err + } + d.entries[name] = dbEntry{ + dsn: dsn, + db: db, + dbType: dbType, + } + return nil +} + +func (d *DBProvider) GetDB(name string) (*sql.DB, error) { + if entry, ok := d.entries[name]; ok { + return entry.db, nil + } + return nil, fmt.Errorf("missing DSN for database %s", name) +} diff --git a/common/modulecontext/db_provider_test.go b/common/modulecontext/db_provider_test.go new file mode 100644 index 0000000000..7f308cd241 --- /dev/null +++ b/common/modulecontext/db_provider_test.go @@ -0,0 +1,24 @@ +package modulecontext + +import ( + "context" + "testing" + + "github.com/TBD54566975/ftl/internal/log" + "github.com/alecthomas/assert/v2" +) + +func TestValidDSN(t *testing.T) { + dbProvider := NewDBProvider() + dsn := "postgres://localhost:54320/echo?sslmode=disable&user=postgres&password=secret" + err := dbProvider.AddDSN("test", DBTypePostgres, dsn) + assert.NoError(t, err, "expected no error for valid DSN") + assert.Equal(t, dbProvider.entries["test"].dsn, dsn, "expected DSN to be set and unmodified") +} + +func TestGettingAndSettingFromContext(t *testing.T) { + ctx := log.ContextWithNewDefaultLogger(context.Background()) + dbProvider := NewDBProvider() + ctx = ContextWithDBProvider(ctx, dbProvider) + assert.Equal(t, dbProvider, DBProviderFromContext(ctx), "expected dbProvider to be set and retrieved correctly") +} diff --git a/common/modulecontext/modulecontext.go b/common/modulecontext/modulecontext.go new file mode 100644 index 0000000000..c5a19aaa23 --- /dev/null +++ b/common/modulecontext/modulecontext.go @@ -0,0 +1,96 @@ +package modulecontext + +import ( + "context" + + ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" + cf "github.com/TBD54566975/ftl/common/configuration" +) + +type ModuleContext struct { + configManager *cf.Manager[cf.Configuration] + secretsManager *cf.Manager[cf.Secrets] + dbProvider *DBProvider +} + +func (m *ModuleContext) ApplyToContext(ctx context.Context) context.Context { + ctx = ContextWithDBProvider(ctx, m.dbProvider) + ctx = cf.ContextWithConfig(ctx, m.configManager) + 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 +} diff --git a/go-runtime/ftl/database.go b/go-runtime/ftl/database.go index efdef16420..516bd242f5 100644 --- a/go-runtime/ftl/database.go +++ b/go-runtime/ftl/database.go @@ -1,25 +1,34 @@ package ftl import ( + "context" "database/sql" "fmt" - "os" - "strings" + + "github.com/TBD54566975/ftl/common/modulecontext" _ "github.com/jackc/pgx/v5/stdlib" // Register Postgres driver ) -// PostgresDatabase returns a Postgres database connection for the named database. -func PostgresDatabase(name string) *sql.DB { - module := strings.ToUpper(callerModule()) - key := fmt.Sprintf("FTL_POSTGRES_DSN_%s_%s", module, strings.ToUpper(name)) - dsn, ok := os.LookupEnv(key) - if !ok { - panic(fmt.Sprintf("missing DSN environment variable %s", key)) +type Database struct { + Name string +} + +// PostgresDatabase returns a handler for the named database. +func PostgresDatabase(name string) Database { + return Database{ + Name: name, } - db, err := sql.Open("pgx", dsn) +} + +func (d Database) String() string { return fmt.Sprintf("database %q", d.Name) } + +// Get returns the sql db connection for the database. +func (d Database) Get(ctx context.Context) *sql.DB { + provider := modulecontext.DBProviderFromContext(ctx) + db, err := provider.GetDB(d.Name) if err != nil { - panic(fmt.Sprintf("failed to open database: %s", err)) + panic(err.Error()) } return db } diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index 34810fb947..4855ea45e9 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -10,7 +10,7 @@ import ( ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" - cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/TBD54566975/ftl/common/modulecontext" "github.com/TBD54566975/ftl/common/plugin" "github.com/TBD54566975/ftl/go-runtime/encoding" "github.com/TBD54566975/ftl/go-runtime/ftl" @@ -34,21 +34,17 @@ func NewUserVerbServer(moduleName string, handlers ...Handler) plugin.Constructo verbServiceClient := rpc.Dial(ftlv1connect.NewVerbServiceClient, uc.FTLEndpoint.String(), log.Error) ctx = rpc.ContextWithClient(ctx, verbServiceClient) - // Add config manager to context. - cr := &cf.ProjectConfigResolver[cf.Configuration]{Config: uc.Config} - cm, err := cf.NewConfigurationManager(ctx, cr) + resp, err := verbServiceClient.GetModuleContext(ctx, connect.NewRequest(&ftlv1.ModuleContextRequest{ + Module: moduleName, + })) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("could not get config: %w", err) } - ctx = cf.ContextWithConfig(ctx, cm) - - // Add secrets manager to context. - sr := &cf.ProjectConfigResolver[cf.Secrets]{Config: uc.Config} - sm, err := cf.NewSecretsManager(ctx, sr) + moduleCtx, err := modulecontext.NewBuilderFromProto(moduleName, resp.Msg).Build(ctx) if err != nil { return nil, nil, err } - ctx = cf.ContextWithSecrets(ctx, sm) + ctx = moduleCtx.ApplyToContext(ctx) err = observability.Init(ctx, moduleName, "HEAD", uc.ObservabilityConfig) if err != nil { From 8e24780b4dcf8f8b179d845ee0792c7c1b3e79e6 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 13:02:52 +1000 Subject: [PATCH 02/10] add builder test --- common/modulecontext/builder_test.go | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/common/modulecontext/builder_test.go b/common/modulecontext/builder_test.go index 9aa6da9dc0..343375855b 100644 --- a/common/modulecontext/builder_test.go +++ b/common/modulecontext/builder_test.go @@ -1 +1,48 @@ 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") +} From f06ef41b3fecdb46994d3dbdf060b5f3afcdca89 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 13:04:39 +1000 Subject: [PATCH 03/10] make builders mutable so chaining not required --- backend/controller/controller.go | 6 +++--- common/modulecontext/builder.go | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/controller/controller.go b/backend/controller/controller.go index 9c4e8fe922..52a56f6cb3 100644 --- a/backend/controller/controller.go +++ b/backend/controller/controller.go @@ -659,9 +659,9 @@ func (s *Service) GetModuleContext(ctx context.Context, req *connect.Request[ftl } b := modulecontext.NewBuilder(req.Msg.Module) - b = b.AddConfigFromManager(configuration.ConfigFromContext(ctx)) - b = b.AddSecretsFromManager(configuration.SecretsFromContext(ctx)) - b = b.AddDSNsFromEnvarsForModule(schemas[0]) + 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 diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go index bf3bce51b2..d5be67becd 100644 --- a/common/modulecontext/builder.go +++ b/common/modulecontext/builder.go @@ -66,8 +66,8 @@ type Builder struct { dsns []dsnEntry } -func NewBuilder(moduleName string) Builder { - return Builder{ +func NewBuilder(moduleName string) *Builder { + return &Builder{ moduleName: moduleName, configs: []configOrSecretItem{}, secrets: []configOrSecretItem{}, @@ -75,8 +75,8 @@ func NewBuilder(moduleName string) Builder { } } -func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) Builder { - return Builder{ +func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) *Builder { + return &Builder{ moduleName: moduleName, configs: slices.Map(response.Configs, func(c *ftlv1.ModuleContextResponse_Config) configOrSecretItem { return refValuePair{ref: refFromProto(c.Ref), resolver: dataResolver{data: c.Data}} @@ -90,7 +90,7 @@ func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextRespons } } -func (b Builder) Build(ctx context.Context) (*ModuleContext, error) { +func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { cm, err := newInMemoryConfigManager[cf.Configuration](ctx) if err != nil { return nil, err @@ -135,7 +135,7 @@ 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, b *Builder, manager cf.Manager[R], items []configOrSecretItem) error { for _, item := range items { switch item := item.(type) { case refValuePair: @@ -174,7 +174,7 @@ func buildConfigOrSecrets[R cf.Role](ctx context.Context, b Builder, manager cf. return nil } -func (b Builder) AddDSNsFromEnvarsForModule(module *schema.Module) Builder { +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) @@ -192,12 +192,12 @@ func (b Builder) AddDSNsFromEnvarsForModule(module *schema.Module) Builder { return b } -func (b Builder) AddConfigFromManager(cm *cf.Manager[cf.Configuration]) Builder { +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 { +func (b *Builder) AddSecretsFromManager(sm *cf.Manager[cf.Secrets]) *Builder { b.secrets = append(b.secrets, configManager[cf.Secrets]{manager: sm}) return b } From a5816163011579d17dd8ab0d5d8808174a54c719 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 13:42:41 +1000 Subject: [PATCH 04/10] serverside no longer uses modulecontext directly --- backend/controller/controller.go | 25 +----- backend/controller/modulecontext.go | 97 ++++++++++++++++++++ common/modulecontext/builder.go | 122 ++++---------------------- common/modulecontext/builder_test.go | 47 ---------- common/modulecontext/modulecontext.go | 76 ---------------- 5 files changed, 115 insertions(+), 252 deletions(-) create mode 100644 backend/controller/modulecontext.go 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 -} From 43f2fdd22777964a2103c0fa5afa4ccb6116938a Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:03:24 +1000 Subject: [PATCH 05/10] =?UTF-8?q?module=20context=20proto=20doesn=E2=80=99?= =?UTF-8?q?t=20need=20to=20care=20if=20global=20or=20not?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{modulecontext.go => module_context.go} | 70 +++++----- backend/controller/module_context_test.go | 55 ++++++++ common/modulecontext/builder.go | 128 ++++-------------- common/modulecontext/builder_test.go | 1 - 4 files changed, 120 insertions(+), 134 deletions(-) rename backend/controller/{modulecontext.go => module_context.go} (60%) create mode 100644 backend/controller/module_context_test.go delete mode 100644 common/modulecontext/builder_test.go diff --git a/backend/controller/modulecontext.go b/backend/controller/module_context.go similarity index 60% rename from backend/controller/modulecontext.go rename to backend/controller/module_context.go index 54d4c4afa3..1c41c9df9b 100644 --- a/backend/controller/modulecontext.go +++ b/backend/controller/module_context.go @@ -9,7 +9,6 @@ import ( "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" ) @@ -25,40 +24,18 @@ func moduleContextToProto(ctx context.Context, name string, schemas []*schema.Mo } // configs - configManager := configuration.ConfigFromContext(ctx) - configList, err := configManager.List(ctx) + configManager := cf.ConfigFromContext(ctx) + configMap, err := bytesMapFromConfigManager(ctx, configManager, name) 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) + secretsManager := cf.SecretsFromContext(ctx) + secretsMap, err := bytesMapFromConfigManager(ctx, secretsManager, name) 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{} @@ -80,18 +57,41 @@ func moduleContextToProto(ctx context.Context, name string, schemas []*schema.Mo } return connect.NewResponse(&ftlv1.ModuleContextResponse{ - Configs: configProtos, - Secrets: secretProtos, + Configs: configMap, + Secrets: secretsMap, Databases: dsnProtos, }), nil } -func configRefToProto(r cf.Ref) *ftlv1.ModuleContextResponse_Ref { - protoRef := &ftlv1.ModuleContextResponse_Ref{ - Name: r.Name, +func bytesMapFromConfigManager[R cf.Role](ctx context.Context, manager *cf.Manager[R], moduleName string) (map[string][]byte, error) { + configList, err := manager.List(ctx) + if err != nil { + return nil, err + } + + // module specific values must override global values + // put module specific values into moduleConfigMap, then merge with configMap + configMap := map[string][]byte{} + moduleConfigMap := map[string][]byte{} + + for _, entry := range configList { + refModule, isModuleSpecific := entry.Module.Get() + if isModuleSpecific && refModule != moduleName { + continue + } + data, err := manager.GetData(ctx, entry.Ref) + if err != nil { + return nil, err + } + if !isModuleSpecific { + configMap[entry.Ref.Name] = data + } else { + moduleConfigMap[entry.Ref.Name] = data + } } - if module, ok := r.Module.Get(); ok { - protoRef.Module = &module + + for name, data := range moduleConfigMap { + configMap[name] = data } - return protoRef + return configMap, nil } diff --git a/backend/controller/module_context_test.go b/backend/controller/module_context_test.go new file mode 100644 index 0000000000..0bf5fa3eb7 --- /dev/null +++ b/backend/controller/module_context_test.go @@ -0,0 +1,55 @@ +package controller + +import ( + "context" + "fmt" + "testing" + + "github.com/TBD54566975/ftl/backend/schema" + cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/TBD54566975/ftl/internal/log" + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/types/optional" +) + +func TestModuleContextProto(t *testing.T) { + ctx := log.ContextWithNewDefaultLogger(context.Background()) + + moduleName := "test" + + cp := cf.NewInMemoryProvider[cf.Configuration]() + cr := cf.NewInMemoryResolver[cf.Configuration]() + cm, err := cf.New(ctx, cr, []cf.Provider[cf.Configuration]{cp}) + assert.NoError(t, err) + ctx = cf.ContextWithConfig(ctx, cm) + + sp := cf.NewInMemoryProvider[cf.Secrets]() + sr := cf.NewInMemoryResolver[cf.Secrets]() + sm, err := cf.New(ctx, sr, []cf.Provider[cf.Secrets]{sp}) + assert.NoError(t, err) + ctx = cf.ContextWithSecrets(ctx, sm) + + // Set 50 configs and 50 global configs + // It's hard to tell if module config beats global configs because we are dealing with unordered maps, or because the logic is correct + // Repeating it 50 times hopefully gives us a good chance of catching inconsistencies + for i := range 50 { + key := fmt.Sprintf("key%d", i) + + strValue := "HelloWorld" + globalStrValue := "GlobalHelloWorld" + cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: key}, strValue) + cm.Set(ctx, cf.Ref{Module: optional.None[string](), Name: key}, globalStrValue) + } + + response, err := moduleContextToProto(ctx, moduleName, []*schema.Module{ + { + Name: moduleName, + }, + }) + assert.NoError(t, err) + + for i := range 50 { + key := fmt.Sprintf("key%d", i) + assert.Equal(t, "\"HelloWorld\"", string(response.Msg.Configs[key]), "module configs should beat global configs") + } +} diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go index 8a1b938ba5..3c18e1b85f 100644 --- a/common/modulecontext/builder.go +++ b/common/modulecontext/builder.go @@ -2,71 +2,50 @@ package modulecontext import ( "context" - "fmt" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" cf "github.com/TBD54566975/ftl/common/configuration" - "github.com/TBD54566975/ftl/internal/slices" - "github.com/alecthomas/types/optional" ) -type ref struct { - Module optional.Option[string] - Name string -} - -type refValuePair struct { - ref ref - resolver resolver -} - type dsnEntry struct { - name string - dbType DBType - resolver stringResolver -} - -type resolver interface { - Resolve() (isData bool, value any, data []byte, err error) -} - -var _ resolver = valueResolver{} -var _ resolver = dataResolver{} - -type stringResolver interface { - ResolveString() (string, error) + dbType DBType + dsn string } -var _ stringResolver = valueResolver{} - type Builder struct { moduleName string - configs []refValuePair - secrets []refValuePair - dsns []dsnEntry + configs map[string][]byte + secrets map[string][]byte + dsns map[string]dsnEntry } func NewBuilder(moduleName string) *Builder { return &Builder{ moduleName: moduleName, - configs: []refValuePair{}, - secrets: []refValuePair{}, - dsns: []dsnEntry{}, + configs: map[string][]byte{}, + secrets: map[string][]byte{}, + dsns: map[string]dsnEntry{}, } } func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) *Builder { + configs := map[string][]byte{} + for name, bytes := range response.Configs { + configs[name] = bytes + } + secrets := map[string][]byte{} + for name, bytes := range response.Secrets { + secrets[name] = bytes + } + dsns := map[string]dsnEntry{} + for _, d := range response.Databases { + dsns[d.Name] = dsnEntry{dbType: DBType(d.Type), dsn: d.Dsn} + } return &Builder{ moduleName: moduleName, - 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) refValuePair { - return refValuePair{ref: refFromProto(s.Ref), resolver: dataResolver{data: s.Data}} - }), - dsns: slices.Map(response.Databases, func(d *ftlv1.ModuleContextResponse_DSN) dsnEntry { - return dsnEntry{name: d.Name, dbType: DBType(d.Type), resolver: valueResolver{value: d.Dsn}} - }), + configs: configs, + secrets: secrets, + dsns: dsns, } } @@ -85,23 +64,17 @@ func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { dbProvider: NewDBProvider(), } - if err := buildConfigOrSecrets[cf.Configuration](ctx, *moduleCtx.configManager, b.configs); err != nil { + if err := buildConfigOrSecrets[cf.Configuration](ctx, *moduleCtx.configManager, b.configs, b.moduleName); err != nil { return nil, err } - if err := buildConfigOrSecrets[cf.Secrets](ctx, *moduleCtx.secretsManager, b.secrets); err != nil { + if err := buildConfigOrSecrets[cf.Secrets](ctx, *moduleCtx.secretsManager, b.secrets, b.moduleName); err != nil { return nil, err } - - for _, entry := range b.dsns { - dsn, err := entry.resolver.ResolveString() - if err != nil { - return nil, err - } - if err = moduleCtx.dbProvider.AddDSN(entry.name, entry.dbType, dsn); err != nil { + for name, entry := range b.dsns { + if err = moduleCtx.dbProvider.AddDSN(name, entry.dbType, entry.dsn); err != nil { return nil, err } } - return moduleCtx, nil } @@ -115,52 +88,11 @@ func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], e return manager, nil } -func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], items []refValuePair) error { - for _, item := range items { - isData, value, data, err := item.resolver.Resolve() - if err != nil { +func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string][]byte, moduleName string) error { + for name, data := range valueMap { + if err := manager.SetData(ctx, cf.Ref(cf.Ref{Name: name}), 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 - } - } } return nil } - -func refFromProto(r *ftlv1.ModuleContextResponse_Ref) ref { - return ref{ - Module: optional.Ptr(r.Module), - Name: r.Name, - } -} - -type valueResolver struct { - value any -} - -func (r valueResolver) Resolve() (isData bool, value any, data []byte, err error) { - return false, r.value, nil, nil -} - -func (r valueResolver) ResolveString() (string, error) { - str, ok := r.value.(string) - if !ok { - return "", fmt.Errorf("value is not a string: %v", r.value) - } - return str, nil -} - -type dataResolver struct { - data []byte -} - -func (r dataResolver) Resolve() (isData bool, value any, data []byte, err error) { - return true, nil, r.data, nil -} diff --git a/common/modulecontext/builder_test.go b/common/modulecontext/builder_test.go deleted file mode 100644 index 9aa6da9dc0..0000000000 --- a/common/modulecontext/builder_test.go +++ /dev/null @@ -1 +0,0 @@ -package modulecontext From 03f45136b6075b05443981cd33f02bb44ec78a84 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:23:51 +1000 Subject: [PATCH 06/10] add comments --- common/modulecontext/builder.go | 2 ++ common/modulecontext/modulecontext.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go index 3c18e1b85f..86be493d5a 100644 --- a/common/modulecontext/builder.go +++ b/common/modulecontext/builder.go @@ -12,6 +12,8 @@ type dsnEntry struct { dsn string } +// Builder is used to set up a ModuleContext with configs, secrets and DSNs +// It is able to parse a ModuleContextResponse type Builder struct { moduleName string configs map[string][]byte diff --git a/common/modulecontext/modulecontext.go b/common/modulecontext/modulecontext.go index 72e946c87a..64858bc2cc 100644 --- a/common/modulecontext/modulecontext.go +++ b/common/modulecontext/modulecontext.go @@ -6,12 +6,15 @@ import ( cf "github.com/TBD54566975/ftl/common/configuration" ) +// ModuleContext holds the context needed for a module, including configs, secrets and DSNs type ModuleContext struct { configManager *cf.Manager[cf.Configuration] secretsManager *cf.Manager[cf.Secrets] dbProvider *DBProvider } +// ApplyToContext sets up the context so that configurations, secrets and DSNs can be retreived +// Each of these components have accessors to get a manager back from the context func (m *ModuleContext) ApplyToContext(ctx context.Context) context.Context { ctx = ContextWithDBProvider(ctx, m.dbProvider) ctx = cf.ContextWithConfig(ctx, m.configManager) From 088acdcca2a1b181a994690a9b6c777d532479c8 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:28:48 +1000 Subject: [PATCH 07/10] simplify db provider func names --- common/modulecontext/builder.go | 2 +- common/modulecontext/db_provider.go | 4 ++-- common/modulecontext/db_provider_test.go | 2 +- go-runtime/ftl/database.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/modulecontext/builder.go b/common/modulecontext/builder.go index 86be493d5a..48fdb93f0c 100644 --- a/common/modulecontext/builder.go +++ b/common/modulecontext/builder.go @@ -73,7 +73,7 @@ func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { return nil, err } for name, entry := range b.dsns { - if err = moduleCtx.dbProvider.AddDSN(name, entry.dbType, entry.dsn); err != nil { + if err = moduleCtx.dbProvider.Add(name, entry.dbType, entry.dsn); err != nil { return nil, err } } diff --git a/common/modulecontext/db_provider.go b/common/modulecontext/db_provider.go index d45eac0b7b..a121ee6de1 100644 --- a/common/modulecontext/db_provider.go +++ b/common/modulecontext/db_provider.go @@ -60,7 +60,7 @@ func DBProviderFromContext(ctx context.Context) *DBProvider { return m } -func (d *DBProvider) AddDSN(name string, dbType DBType, dsn string) error { +func (d *DBProvider) Add(name string, dbType DBType, dsn string) error { db, err := sql.Open("pgx", dsn) if err != nil { return err @@ -73,7 +73,7 @@ func (d *DBProvider) AddDSN(name string, dbType DBType, dsn string) error { return nil } -func (d *DBProvider) GetDB(name string) (*sql.DB, error) { +func (d *DBProvider) Get(name string) (*sql.DB, error) { if entry, ok := d.entries[name]; ok { return entry.db, nil } diff --git a/common/modulecontext/db_provider_test.go b/common/modulecontext/db_provider_test.go index 7f308cd241..66cf14b157 100644 --- a/common/modulecontext/db_provider_test.go +++ b/common/modulecontext/db_provider_test.go @@ -11,7 +11,7 @@ import ( func TestValidDSN(t *testing.T) { dbProvider := NewDBProvider() dsn := "postgres://localhost:54320/echo?sslmode=disable&user=postgres&password=secret" - err := dbProvider.AddDSN("test", DBTypePostgres, dsn) + err := dbProvider.Add("test", DBTypePostgres, dsn) assert.NoError(t, err, "expected no error for valid DSN") assert.Equal(t, dbProvider.entries["test"].dsn, dsn, "expected DSN to be set and unmodified") } diff --git a/go-runtime/ftl/database.go b/go-runtime/ftl/database.go index 516bd242f5..3adc2a5f5b 100644 --- a/go-runtime/ftl/database.go +++ b/go-runtime/ftl/database.go @@ -26,7 +26,7 @@ func (d Database) String() string { return fmt.Sprintf("database %q", d.Name) } // Get returns the sql db connection for the database. func (d Database) Get(ctx context.Context) *sql.DB { provider := modulecontext.DBProviderFromContext(ctx) - db, err := provider.GetDB(d.Name) + db, err := provider.Get(d.Name) if err != nil { panic(err.Error()) } From 95f6017ef68419e862647223c88c44f42ad9b64b Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:30:27 +1000 Subject: [PATCH 08/10] move modulecontext to go-runtime --- {common => go-runtime}/modulecontext/builder.go | 0 {common => go-runtime}/modulecontext/db_provider.go | 0 {common => go-runtime}/modulecontext/db_provider_test.go | 0 {common => go-runtime}/modulecontext/modulecontext.go | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {common => go-runtime}/modulecontext/builder.go (100%) rename {common => go-runtime}/modulecontext/db_provider.go (100%) rename {common => go-runtime}/modulecontext/db_provider_test.go (100%) rename {common => go-runtime}/modulecontext/modulecontext.go (100%) diff --git a/common/modulecontext/builder.go b/go-runtime/modulecontext/builder.go similarity index 100% rename from common/modulecontext/builder.go rename to go-runtime/modulecontext/builder.go diff --git a/common/modulecontext/db_provider.go b/go-runtime/modulecontext/db_provider.go similarity index 100% rename from common/modulecontext/db_provider.go rename to go-runtime/modulecontext/db_provider.go diff --git a/common/modulecontext/db_provider_test.go b/go-runtime/modulecontext/db_provider_test.go similarity index 100% rename from common/modulecontext/db_provider_test.go rename to go-runtime/modulecontext/db_provider_test.go diff --git a/common/modulecontext/modulecontext.go b/go-runtime/modulecontext/modulecontext.go similarity index 100% rename from common/modulecontext/modulecontext.go rename to go-runtime/modulecontext/modulecontext.go From 3e761c3cd641ef97557a487f54819655b4bd7f8c Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:35:24 +1000 Subject: [PATCH 09/10] fix errors and lint --- backend/controller/module_context_test.go | 4 ++-- go-runtime/ftl/database.go | 2 +- go-runtime/modulecontext/builder.go | 3 ++- go-runtime/server/server.go | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/controller/module_context_test.go b/backend/controller/module_context_test.go index 0bf5fa3eb7..a53a092d6a 100644 --- a/backend/controller/module_context_test.go +++ b/backend/controller/module_context_test.go @@ -37,8 +37,8 @@ func TestModuleContextProto(t *testing.T) { strValue := "HelloWorld" globalStrValue := "GlobalHelloWorld" - cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: key}, strValue) - cm.Set(ctx, cf.Ref{Module: optional.None[string](), Name: key}, globalStrValue) + assert.NoError(t, cm.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: key}, strValue)) + assert.NoError(t, cm.Set(ctx, cf.Ref{Module: optional.None[string](), Name: key}, globalStrValue)) } response, err := moduleContextToProto(ctx, moduleName, []*schema.Module{ diff --git a/go-runtime/ftl/database.go b/go-runtime/ftl/database.go index 3adc2a5f5b..48f750d490 100644 --- a/go-runtime/ftl/database.go +++ b/go-runtime/ftl/database.go @@ -5,7 +5,7 @@ import ( "database/sql" "fmt" - "github.com/TBD54566975/ftl/common/modulecontext" + "github.com/TBD54566975/ftl/go-runtime/modulecontext" _ "github.com/jackc/pgx/v5/stdlib" // Register Postgres driver ) diff --git a/go-runtime/modulecontext/builder.go b/go-runtime/modulecontext/builder.go index 48fdb93f0c..e4ee42285f 100644 --- a/go-runtime/modulecontext/builder.go +++ b/go-runtime/modulecontext/builder.go @@ -5,6 +5,7 @@ import ( ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/alecthomas/types/optional" ) type dsnEntry struct { @@ -92,7 +93,7 @@ func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], e func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string][]byte, moduleName string) error { for name, data := range valueMap { - if err := manager.SetData(ctx, cf.Ref(cf.Ref{Name: name}), data); err != nil { + if err := manager.SetData(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, data); err != nil { return err } } diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index 4855ea45e9..f8f8f02ce5 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -10,10 +10,10 @@ 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/common/modulecontext" "github.com/TBD54566975/ftl/common/plugin" "github.com/TBD54566975/ftl/go-runtime/encoding" "github.com/TBD54566975/ftl/go-runtime/ftl" + "github.com/TBD54566975/ftl/go-runtime/modulecontext" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/maps" "github.com/TBD54566975/ftl/internal/observability" From 30da78551385f7e366e7a49384de75594b501e4f Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 16:00:11 +1000 Subject: [PATCH 10/10] simplify by removing builder --- go-runtime/modulecontext/builder.go | 101 ---------------------- go-runtime/modulecontext/modulecontext.go | 50 +++++++++++ go-runtime/server/server.go | 2 +- 3 files changed, 51 insertions(+), 102 deletions(-) delete mode 100644 go-runtime/modulecontext/builder.go diff --git a/go-runtime/modulecontext/builder.go b/go-runtime/modulecontext/builder.go deleted file mode 100644 index e4ee42285f..0000000000 --- a/go-runtime/modulecontext/builder.go +++ /dev/null @@ -1,101 +0,0 @@ -package modulecontext - -import ( - "context" - - ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" - cf "github.com/TBD54566975/ftl/common/configuration" - "github.com/alecthomas/types/optional" -) - -type dsnEntry struct { - dbType DBType - dsn string -} - -// Builder is used to set up a ModuleContext with configs, secrets and DSNs -// It is able to parse a ModuleContextResponse -type Builder struct { - moduleName string - configs map[string][]byte - secrets map[string][]byte - dsns map[string]dsnEntry -} - -func NewBuilder(moduleName string) *Builder { - return &Builder{ - moduleName: moduleName, - configs: map[string][]byte{}, - secrets: map[string][]byte{}, - dsns: map[string]dsnEntry{}, - } -} - -func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) *Builder { - configs := map[string][]byte{} - for name, bytes := range response.Configs { - configs[name] = bytes - } - secrets := map[string][]byte{} - for name, bytes := range response.Secrets { - secrets[name] = bytes - } - dsns := map[string]dsnEntry{} - for _, d := range response.Databases { - dsns[d.Name] = dsnEntry{dbType: DBType(d.Type), dsn: d.Dsn} - } - return &Builder{ - moduleName: moduleName, - configs: configs, - secrets: secrets, - dsns: dsns, - } -} - -func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { - cm, err := newInMemoryConfigManager[cf.Configuration](ctx) - if err != nil { - return nil, err - } - sm, err := newInMemoryConfigManager[cf.Secrets](ctx) - if err != nil { - return nil, err - } - moduleCtx := &ModuleContext{ - configManager: cm, - secretsManager: sm, - dbProvider: NewDBProvider(), - } - - if err := buildConfigOrSecrets[cf.Configuration](ctx, *moduleCtx.configManager, b.configs, b.moduleName); err != nil { - return nil, err - } - if err := buildConfigOrSecrets[cf.Secrets](ctx, *moduleCtx.secretsManager, b.secrets, b.moduleName); err != nil { - return nil, err - } - for name, entry := range b.dsns { - if err = moduleCtx.dbProvider.Add(name, entry.dbType, entry.dsn); err != nil { - return nil, err - } - } - return moduleCtx, nil -} - -func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], error) { - provider := cf.NewInMemoryProvider[R]() - resolver := cf.NewInMemoryResolver[R]() - manager, err := cf.New(ctx, resolver, []cf.Provider[R]{provider}) - if err != nil { - return nil, err - } - return manager, nil -} - -func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string][]byte, moduleName string) error { - for name, data := range valueMap { - if err := manager.SetData(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, data); err != nil { - return err - } - } - return nil -} diff --git a/go-runtime/modulecontext/modulecontext.go b/go-runtime/modulecontext/modulecontext.go index 64858bc2cc..0f9b2f2441 100644 --- a/go-runtime/modulecontext/modulecontext.go +++ b/go-runtime/modulecontext/modulecontext.go @@ -3,7 +3,9 @@ package modulecontext import ( "context" + ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" cf "github.com/TBD54566975/ftl/common/configuration" + "github.com/alecthomas/types/optional" ) // ModuleContext holds the context needed for a module, including configs, secrets and DSNs @@ -13,6 +15,54 @@ type ModuleContext struct { dbProvider *DBProvider } +func NewFromProto(ctx context.Context, moduleName string, response *ftlv1.ModuleContextResponse) (*ModuleContext, error) { + cm, err := newInMemoryConfigManager[cf.Configuration](ctx) + if err != nil { + return nil, err + } + sm, err := newInMemoryConfigManager[cf.Secrets](ctx) + if err != nil { + return nil, err + } + moduleCtx := &ModuleContext{ + configManager: cm, + secretsManager: sm, + dbProvider: NewDBProvider(), + } + + if err := addConfigOrSecrets[cf.Configuration](ctx, *moduleCtx.configManager, response.Configs, moduleName); err != nil { + return nil, err + } + if err := addConfigOrSecrets[cf.Secrets](ctx, *moduleCtx.secretsManager, response.Secrets, moduleName); err != nil { + return nil, err + } + for _, entry := range response.Databases { + if err = moduleCtx.dbProvider.Add(entry.Name, DBType(entry.Type), entry.Dsn); err != nil { + return nil, err + } + } + return moduleCtx, nil +} + +func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], error) { + provider := cf.NewInMemoryProvider[R]() + resolver := cf.NewInMemoryResolver[R]() + manager, err := cf.New(ctx, resolver, []cf.Provider[R]{provider}) + if err != nil { + return nil, err + } + return manager, nil +} + +func addConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string][]byte, moduleName string) error { + for name, data := range valueMap { + if err := manager.SetData(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, data); err != nil { + return err + } + } + return nil +} + // ApplyToContext sets up the context so that configurations, secrets and DSNs can be retreived // Each of these components have accessors to get a manager back from the context func (m *ModuleContext) ApplyToContext(ctx context.Context) context.Context { diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index f8f8f02ce5..b305d97645 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -40,7 +40,7 @@ func NewUserVerbServer(moduleName string, handlers ...Handler) plugin.Constructo if err != nil { return nil, nil, fmt.Errorf("could not get config: %w", err) } - moduleCtx, err := modulecontext.NewBuilderFromProto(moduleName, resp.Msg).Build(ctx) + moduleCtx, err := modulecontext.NewFromProto(ctx, moduleName, resp.Msg) if err != nil { return nil, nil, err }