feat(cli): add multi-source resolve lookup helper

This commit is contained in:
thibaud-lclr 2026-04-14 16:48:26 +02:00
parent 26238eb31b
commit ea1768982e
3 changed files with 244 additions and 24 deletions

View file

@ -439,32 +439,20 @@ Les validations sont appliquées de manière cohérente en TTY et en stdin non i
Pour standardiser la résolution `flag > env > config > secret` avec provenance :
```go
lookup := func(source cli.ValueSource, key string) (string, bool, error) {
switch source {
case cli.SourceFlag:
value, ok := flagValues[key]
return value, ok, nil
case cli.SourceEnv:
value, ok := os.LookupEnv(key)
return value, ok, nil
case cli.SourceConfig:
value, ok := configValues[key]
return value, ok, nil
case cli.SourceSecret:
value, err := store.GetSecret(key)
switch {
case err == nil:
return value, true, nil
case errors.Is(err, secretstore.ErrNotFound):
return "", false, nil
default:
return "", false, err
}
default:
return "", false, nil
}
store, err := secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
ServiceName: "my-mcp",
})
if err != nil {
return err
}
lookup := cli.ResolveLookup(cli.ResolveLookupOptions{
Flag: cli.MapLookup(flagValues),
Env: cli.EnvLookup(os.LookupEnv),
Config: cli.ConfigMap(configValues),
Secret: cli.SecretStore(store), // ErrNotFound => valeur absente, pas une erreur
})
resolution, err := cli.ResolveFields(cli.ResolveOptions{
Fields: []cli.FieldSpec{
{Name: "base_url", Required: true, FlagKey: "base-url", EnvKey: "MY_MCP_BASE_URL"},

87
cli/resolve_lookup.go Normal file
View file

@ -0,0 +1,87 @@
package cli
import (
"errors"
"os"
"gitea.lclr.dev/AI/mcp-framework/secretstore"
)
type KeyLookupFunc func(key string) (string, bool, error)
type ResolveLookupOptions struct {
Flag KeyLookupFunc
Env KeyLookupFunc
Config KeyLookupFunc
Secret KeyLookupFunc
}
func ResolveLookup(options ResolveLookupOptions) LookupFunc {
return func(source ValueSource, key string) (string, bool, error) {
switch source {
case SourceFlag:
return runKeyLookup(options.Flag, key)
case SourceEnv:
return runKeyLookup(options.Env, key)
case SourceConfig:
return runKeyLookup(options.Config, key)
case SourceSecret:
return runKeyLookup(options.Secret, key)
case SourceDefault:
return "", false, nil
default:
return "", false, nil
}
}
}
func runKeyLookup(lookup KeyLookupFunc, key string) (string, bool, error) {
if lookup == nil {
return "", false, nil
}
if key == "" {
return "", false, nil
}
return lookup(key)
}
func MapLookup(values map[string]string) KeyLookupFunc {
return func(key string) (string, bool, error) {
value, ok := values[key]
return value, ok, nil
}
}
func EnvLookup(lookup func(string) (string, bool)) KeyLookupFunc {
reader := lookup
if reader == nil {
reader = os.LookupEnv
}
return func(key string) (string, bool, error) {
value, ok := reader(key)
return value, ok, nil
}
}
func ConfigMap(values map[string]string) KeyLookupFunc {
return MapLookup(values)
}
func SecretStore(store secretstore.Store) KeyLookupFunc {
if store == nil {
return nil
}
return func(key string) (string, bool, error) {
value, err := store.GetSecret(key)
switch {
case err == nil:
return value, true, nil
case errors.Is(err, secretstore.ErrNotFound):
return "", false, nil
default:
return "", false, err
}
}
}

145
cli/resolve_lookup_test.go Normal file
View file

@ -0,0 +1,145 @@
package cli
import (
"errors"
"testing"
"gitea.lclr.dev/AI/mcp-framework/secretstore"
)
type testSecretStore struct {
values map[string]string
errs map[string]error
}
func (s testSecretStore) SetSecret(name, label, secret string) error {
return nil
}
func (s testSecretStore) GetSecret(name string) (string, error) {
if err, ok := s.errs[name]; ok {
return "", err
}
value, ok := s.values[name]
if !ok {
return "", secretstore.ErrNotFound
}
return value, nil
}
func (s testSecretStore) DeleteSecret(name string) error {
return nil
}
func TestResolveLookupWithStandardProviders(t *testing.T) {
t.Setenv("MCP_PASSWORD", "")
lookup := ResolveLookup(ResolveLookupOptions{
Flag: MapLookup(map[string]string{"host": "https://flag.example.com"}),
Env: EnvLookup(nil),
Config: ConfigMap(map[string]string{"username": "config-user"}),
Secret: SecretStore(testSecretStore{
values: map[string]string{"smtp-password": "secret-password"},
}),
})
resolution, err := ResolveFields(ResolveOptions{
Fields: []FieldSpec{
{
Name: "host",
Required: true,
FlagKey: "host",
},
{
Name: "username",
Required: true,
EnvKey: "MCP_USERNAME",
ConfigKey: "username",
},
{
Name: "password",
Required: true,
EnvKey: "MCP_PASSWORD",
SecretKey: "smtp-password",
},
},
Lookup: lookup,
})
if err != nil {
t.Fatalf("ResolveFields returned error: %v", err)
}
host, _ := resolution.Get("host")
if host.Source != SourceFlag || host.Value != "https://flag.example.com" {
t.Fatalf("host = %+v", host)
}
username, _ := resolution.Get("username")
if username.Source != SourceConfig || username.Value != "config-user" {
t.Fatalf("username = %+v", username)
}
password, _ := resolution.Get("password")
if password.Source != SourceSecret || password.Value != "secret-password" {
t.Fatalf("password = %+v", password)
}
}
func TestSecretStoreProviderTreatsErrNotFoundAsMissing(t *testing.T) {
lookup := ResolveLookup(ResolveLookupOptions{
Secret: SecretStore(testSecretStore{
errs: map[string]error{"smtp-password": secretstore.ErrNotFound},
}),
})
resolution, err := ResolveFields(ResolveOptions{
Fields: []FieldSpec{
{
Name: "password",
Required: true,
SecretKey: "smtp-password",
DefaultValue: "fallback-password",
},
},
Lookup: lookup,
})
if err != nil {
t.Fatalf("ResolveFields returned error: %v", err)
}
password, _ := resolution.Get("password")
if password.Source != SourceDefault || password.Value != "fallback-password" {
t.Fatalf("password = %+v", password)
}
}
func TestSecretStoreProviderPropagatesBackendErrors(t *testing.T) {
backendErr := errors.New("backend unavailable")
lookup := ResolveLookup(ResolveLookupOptions{
Secret: SecretStore(testSecretStore{
errs: map[string]error{"smtp-password": backendErr},
}),
})
_, err := ResolveFields(ResolveOptions{
Fields: []FieldSpec{
{
Name: "password",
Required: true,
SecretKey: "smtp-password",
},
},
Lookup: lookup,
})
var sourceErr *SourceLookupError
if !errors.As(err, &sourceErr) {
t.Fatalf("ResolveFields error = %v, want SourceLookupError", err)
}
if sourceErr.Source != SourceSecret {
t.Fatalf("sourceErr.Source = %q, want %q", sourceErr.Source, SourceSecret)
}
if !errors.Is(err, backendErr) {
t.Fatalf("ResolveFields error should wrap backend error")
}
}