From d6142f15ebb95c4dc0557cf7c763734b120bb755 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sat, 8 Jun 2024 13:18:34 +1000 Subject: [PATCH] refactor: pull secrets from FTL runtime abstraction Complement to #1708 --- go-runtime/ftl/ftltest/fake.go | 19 +++++++++++ go-runtime/ftl/ftltest/ftltest.go | 33 ++++++++++++++----- .../ftl/ftltest/ftltest_integration_test.go | 2 +- go-runtime/ftl/map.go | 11 ++----- go-runtime/ftl/secrets.go | 4 +-- go-runtime/internal/api.go | 3 ++ go-runtime/internal/impl.go | 4 +++ internal/modulecontext/from_secrets.go | 18 ---------- 8 files changed, 57 insertions(+), 37 deletions(-) diff --git a/go-runtime/ftl/ftltest/fake.go b/go-runtime/ftl/ftltest/fake.go index 52754f9e10..35e9724392 100644 --- a/go-runtime/ftl/ftltest/fake.go +++ b/go-runtime/ftl/ftltest/fake.go @@ -16,6 +16,7 @@ type fakeFTL struct { mockMaps map[uintptr]mapImpl allowMapCalls bool configValues map[string][]byte + secretValues map[string][]byte } // mapImpl is a function that takes an object and returns an object of a potentially different @@ -28,6 +29,7 @@ func newFakeFTL() *fakeFTL { mockMaps: map[uintptr]mapImpl{}, allowMapCalls: false, configValues: map[string][]byte{}, + secretValues: map[string][]byte{}, } } @@ -50,6 +52,23 @@ func (f *fakeFTL) GetConfig(ctx context.Context, name string, dest any) error { return json.Unmarshal(data, dest) } +func (f *fakeFTL) setSecret(name string, value any) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + f.secretValues[name] = data + return nil +} + +func (f *fakeFTL) GetSecret(ctx context.Context, name string, dest any) error { + data, ok := f.secretValues[name] + if !ok { + return fmt.Errorf("config value %q not found: %w", name, configuration.ErrNotFound) + } + return json.Unmarshal(data, dest) +} + func (f *fakeFTL) FSMSend(ctx context.Context, fsm string, instance string, event any) error { return f.fsm.SendEvent(ctx, fsm, instance, event) } diff --git a/go-runtime/ftl/ftltest/ftltest.go b/go-runtime/ftl/ftltest/ftltest.go index 0b392d561e..3a6761017d 100644 --- a/go-runtime/ftl/ftltest/ftltest.go +++ b/go-runtime/ftl/ftltest/ftltest.go @@ -26,7 +26,6 @@ import ( ) type OptionsState struct { - secrets map[string][]byte databases map[string]modulecontext.Database mockVerbs map[schema.RefKey]modulecontext.Verb allowDirectVerbBehavior bool @@ -37,7 +36,6 @@ type Option func(context.Context, *OptionsState) error // Context suitable for use in testing FTL verbs with provided options func Context(options ...Option) context.Context { state := &OptionsState{ - secrets: make(map[string][]byte), databases: make(map[string]modulecontext.Database), mockVerbs: make(map[schema.RefKey]modulecontext.Verb), } @@ -53,7 +51,7 @@ func Context(options ...Option) context.Context { } } - builder := modulecontext.NewBuilder(name).AddSecrets(state.secrets).AddDatabases(state.databases) + builder := modulecontext.NewBuilder(name).AddDatabases(state.databases) builder = builder.UpdateForTesting(state.mockVerbs, state.allowDirectVerbBehavior, newFakeLeaseClient()) return builder.Build().ApplyToContext(ctx) } @@ -123,7 +121,9 @@ func WithProjectFiles(paths ...string) Option { return fmt.Errorf("could not read secrets: %w", err) } for name, data := range secrets { - state.secrets[name] = data + if err := fftl.setSecret(name, json.RawMessage(data)); err != nil { + return err + } } return nil } @@ -163,11 +163,10 @@ func WithSecret[T ftl.SecretType](secret ftl.SecretValue[T], value T) Option { if secret.Module != reflection.Module() { return fmt.Errorf("secret %v does not match current module %s", secret.Module, reflection.Module()) } - data, err := json.Marshal(value) - if err != nil { + fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert + if err := fftl.setSecret(secret.Name, value); err != nil { return err } - state.secrets[secret.Name] = data return nil } } @@ -182,7 +181,8 @@ func WithSecret[T ftl.SecretType](secret ftl.SecretValue[T], value T) Option { // ) func WithDatabase(dbHandle ftl.Database) Option { return func(ctx context.Context, state *OptionsState) error { - originalDSN, err := modulecontext.GetDSNFromSecret(reflection.Module(), dbHandle.Name, state.secrets) + fftl := internal.FromContext(ctx) + originalDSN, err := getDSNFromSecret(fftl, reflection.Module(), dbHandle.Name) if err != nil { return err } @@ -354,3 +354,20 @@ func WithMapsAllowed() Option { return nil } } + +// dsnSecretKey returns the key for the secret that is expected to hold the DSN for a database. +// +// The format is FTL_DSN__ +func dsnSecretKey(module, name string) string { + return fmt.Sprintf("FTL_DSN_%s_%s", strings.ToUpper(module), strings.ToUpper(name)) +} + +// getDSNFromSecret returns the DSN for a database from the relevant secret +func getDSNFromSecret(ftl internal.FTL, module, name string) (string, error) { + key := dsnSecretKey(module, name) + var dsn string + if err := ftl.GetSecret(context.Background(), key, &dsn); err != nil { + return "", fmt.Errorf("could not get DSN for database %q from secret %q: %w", name, key, err) + } + return dsn, nil +} diff --git a/go-runtime/ftl/ftltest/ftltest_integration_test.go b/go-runtime/ftl/ftltest/ftltest_integration_test.go index 63d6983b94..0918c1a514 100644 --- a/go-runtime/ftl/ftltest/ftltest_integration_test.go +++ b/go-runtime/ftl/ftltest/ftltest_integration_test.go @@ -9,7 +9,7 @@ import ( ) func TestModuleUnitTests(t *testing.T) { - in.Run(t, "wrapped/ftl-project.toml", + in.RunWithoutController(t, "wrapped/ftl-project.toml", in.GitInit(), in.CopyModule("time"), in.CopyModule("wrapped"), diff --git a/go-runtime/ftl/map.go b/go-runtime/ftl/map.go index 3367fb2aa7..fe1de88a78 100644 --- a/go-runtime/ftl/map.go +++ b/go-runtime/ftl/map.go @@ -9,17 +9,12 @@ import ( type MapHandle[T, U any] struct { fn func(context.Context, T) (U, error) - getter Handle[T] -} - -// Underlying returns the underlying value of the handle being mapped. -func (mh *MapHandle[T, U]) Underlying(ctx context.Context) T { - return mh.getter.Get(ctx) + handle Handle[T] } // Get the mapped value. func (mh *MapHandle[T, U]) Get(ctx context.Context) U { - value := mh.getter.Get(ctx) + value := mh.handle.Get(ctx) out := internal.FromContext(ctx).CallMap(ctx, mh, value, func(ctx context.Context) (any, error) { return mh.fn(ctx, value) }) @@ -34,6 +29,6 @@ func (mh *MapHandle[T, U]) Get(ctx context.Context) U { func Map[T, U any](getter Handle[T], fn func(context.Context, T) (U, error)) MapHandle[T, U] { return MapHandle[T, U]{ fn: fn, - getter: getter, + handle: getter, } } diff --git a/go-runtime/ftl/secrets.go b/go-runtime/ftl/secrets.go index 8ed4eccef0..2e20d622e7 100644 --- a/go-runtime/ftl/secrets.go +++ b/go-runtime/ftl/secrets.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/TBD54566975/ftl/go-runtime/ftl/reflection" - "github.com/TBD54566975/ftl/internal/modulecontext" + "github.com/TBD54566975/ftl/go-runtime/internal" ) // SecretType is a type that can be used as a secret value. @@ -31,7 +31,7 @@ func (s SecretValue[T]) GoString() string { // Get returns the value of the secret from FTL. func (s SecretValue[T]) Get(ctx context.Context) (out T) { - if err := modulecontext.FromContext(ctx).GetSecret(s.Name, &out); err != nil { + if err := internal.FromContext(ctx).GetSecret(ctx, s.Name, &out); err != nil { panic(fmt.Errorf("failed to get %s: %w", s, err)) } return diff --git a/go-runtime/internal/api.go b/go-runtime/internal/api.go index f86de391b0..fc75482001 100644 --- a/go-runtime/internal/api.go +++ b/go-runtime/internal/api.go @@ -27,6 +27,9 @@ type FTL interface { // GetConfig unmarshals a configuration value into dest. GetConfig(ctx context.Context, name string, dest any) error + + // GetSecret unmarshals a secret value into dest. + GetSecret(ctx context.Context, name string, dest any) error } type ftlContextKey struct{} diff --git a/go-runtime/internal/impl.go b/go-runtime/internal/impl.go index 61a29e6a7e..6dffdb9cd6 100644 --- a/go-runtime/internal/impl.go +++ b/go-runtime/internal/impl.go @@ -47,6 +47,10 @@ func (r *RealFTL) GetConfig(ctx context.Context, name string, dest any) error { return r.mctx.GetConfig(name, dest) } +func (r *RealFTL) GetSecret(ctx context.Context, name string, dest any) error { + return r.mctx.GetSecret(name, dest) +} + func (r *RealFTL) FSMSend(ctx context.Context, fsm, instance string, event any) error { client := rpc.ClientFromContext[ftlv1connect.VerbServiceClient](ctx) body, err := encoding.Marshal(event) diff --git a/internal/modulecontext/from_secrets.go b/internal/modulecontext/from_secrets.go index ed30109e5d..8bcb474848 100644 --- a/internal/modulecontext/from_secrets.go +++ b/internal/modulecontext/from_secrets.go @@ -35,21 +35,3 @@ func DatabasesFromSecrets(ctx context.Context, module string, secrets map[string } return databases, nil } - -// DSNSecretKey returns the key for the secret that is expected to hold the DSN for a database. -// -// The format is FTL_DSN__ -func DSNSecretKey(module, name string) string { - return fmt.Sprintf("FTL_DSN_%s_%s", strings.ToUpper(module), strings.ToUpper(name)) -} - -// GetDSNFromSecret returns the DSN for a database from the relevant secret -func GetDSNFromSecret(module, name string, secrets map[string][]byte) (string, error) { - key := DSNSecretKey(module, name) - dsn, ok := secrets[key] - if !ok { - return "", fmt.Errorf("secrets map %v is missing DSN with key %q", secrets, key) - } - dsnStr := string(dsn) - return dsnStr[1 : len(dsnStr)-1], nil // chop leading + trailing quotes -}