-
Couldn't load subscription status.
- Fork 24
feat(core): introduce typed Secret with inline env/file/literal, BindServiceConfig; integrate in KAS #2799
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
base: main
Are you sure you want to change the base?
feat(core): introduce typed Secret with inline env/file/literal, BindServiceConfig; integrate in KAS #2799
Changes from all commits
29b4d1b
2248256
5861113
79d4320
5a4f902
a2473ba
5d9b126
3fefb48
4275942
867b0d6
a3df734
126f006
d445f8e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -40,7 +40,7 @@ type KASConfig struct { | |||||||||
| // Deprecated | ||||||||||
| RSACertID string `mapstructure:"rsacertid" json:"rsacertid"` | ||||||||||
|
|
||||||||||
| RootKey string `mapstructure:"root_key" json:"root_key"` | ||||||||||
| RootKey config.Secret `mapstructure:"root_key" json:"root_key"` | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even changes to 'experimental' features shouldn't break builds downstream. Deprecate instead of removing to avoid blocking the next release of downstream integrations.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There shouldn't be downstream impact. This should still support a string literal in yaml without any prefix. |
||||||||||
|
|
||||||||||
| KeyCacheExpiration time.Duration `mapstructure:"key_cache_expiration" json:"key_cache_expiration"` | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
|
|
||
| "github.com/go-playground/validator/v10" | ||
| "github.com/go-viper/mapstructure/v2" | ||
| ) | ||
|
|
||
| // BindOptions controls how service config binding behaves. | ||
| type BindOptions struct { | ||
| // Eagerly resolve secrets during binding; otherwise they resolve lazily. | ||
| EagerResolve bool | ||
| } | ||
|
|
||
| // BindOption functional option. | ||
| type BindOption func(*BindOptions) | ||
|
|
||
| // WithEagerSecretResolution enables eager secret resolution during bind. | ||
| func WithEagerSecretResolution() BindOption { return func(o *BindOptions) { o.EagerResolve = true } } | ||
|
|
||
| // BindServiceConfig decodes a ServiceConfig map into a typed struct target using | ||
| // mapstructure with custom decode hooks (e.g., Secret). It optionally validates | ||
| // the result and can eagerly resolve secret values. | ||
| func BindServiceConfig[T any](ctx context.Context, svcCfg ServiceConfig, out *T, opts ...BindOption) error { | ||
| var options BindOptions | ||
| for _, o := range opts { | ||
| o(&options) | ||
| } | ||
|
|
||
| if out == nil { | ||
| return errors.New("nil output target") | ||
| } | ||
|
|
||
| dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ | ||
| DecodeHook: mapstructure.ComposeDecodeHookFunc(secretDecodeHook), | ||
| Result: out, | ||
| TagName: "mapstructure", | ||
| Squash: true, | ||
| ZeroFields: true, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("bind decoder: %w", err) | ||
| } | ||
| if err := dec.Decode(svcCfg); err != nil { | ||
| return fmt.Errorf("bind decode: %w", err) | ||
| } | ||
|
|
||
| // Eager secret resolution | ||
| if options.EagerResolve { | ||
| if err := resolveSecrets(ctx, out); err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| // Validate struct using go-playground/validator tags, if present | ||
| if err := validator.New().Struct(out); err != nil { | ||
| var verrs validator.ValidationErrors | ||
| if errors.As(err, &verrs) { | ||
| return fmt.Errorf("validation failed: %s", verrs.Error()) | ||
| } | ||
| return fmt.Errorf("validation failed: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // resolveSecrets walks the struct and resolves any Secret fields. | ||
| func resolveSecrets(ctx context.Context, v any) error { | ||
| // Reflectively find Secret fields; keep it minimal and safe. | ||
| // We only walk exported fields of structs and slices/maps. | ||
| return walk(v, func(s *Secret) error { | ||
| // Skip zero-value secrets (unset optional fields) | ||
| if s.IsZero() { | ||
| return nil | ||
| } | ||
| _, err := s.Resolve(ctx) | ||
| return err | ||
| }) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| type demoCfg struct { | ||
| User string `mapstructure:"user" validate:"required"` | ||
| Pass Secret `mapstructure:"pass"` | ||
| Nested struct { | ||
| Token Secret `mapstructure:"token"` | ||
| } `mapstructure:"nested"` | ||
| } | ||
|
|
||
| func TestBindServiceConfig_LiteralAndEnv(t *testing.T) { | ||
| t.Setenv("OPENTDF_DEMO_TOKEN", "tok") | ||
|
|
||
| in := ServiceConfig{ | ||
| "user": "alice", | ||
| "pass": "p@ss", | ||
| "nested": map[string]any{ | ||
| "token": map[string]any{"fromEnv": "OPENTDF_DEMO_TOKEN"}, | ||
| }, | ||
| } | ||
|
|
||
| var out demoCfg | ||
| err := BindServiceConfig(t.Context(), in, &out, WithEagerSecretResolution()) | ||
| require.NoError(t, err) | ||
|
|
||
| assert.Equal(t, "alice", out.User) | ||
|
|
||
| pass, err := out.Pass.Resolve(t.Context()) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "p@ss", pass) | ||
|
|
||
| tok, err := out.Nested.Token.Resolve(t.Context()) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "tok", tok) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.