generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: module context over gRPC #1311
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
a464fdc
feat: module context over gRPC
matt2e 8e24780
add builder test
matt2e f06ef41
make builders mutable so chaining not required
matt2e a581616
serverside no longer uses modulecontext directly
matt2e 43f2fdd
module context proto doesn’t need to care if global or not
matt2e 03f4513
add comments
matt2e 088acdc
simplify db provider func names
matt2e 95f6017
move modulecontext to go-runtime
matt2e 3e761c3
fix errors and lint
matt2e 30da785
simplify by removing builder
matt2e File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
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 := cf.ConfigFromContext(ctx) | ||
configMap, err := bytesMapFromConfigManager(ctx, configManager, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// secrets | ||
secretsManager := cf.SecretsFromContext(ctx) | ||
secretsMap, err := bytesMapFromConfigManager(ctx, secretsManager, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// 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: configMap, | ||
Secrets: secretsMap, | ||
Databases: dsnProtos, | ||
}), nil | ||
} | ||
|
||
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 | ||
} | ||
} | ||
|
||
for name, data := range moduleConfigMap { | ||
configMap[name] = data | ||
} | ||
return configMap, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
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{ | ||
{ | ||
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") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noice!