Bind engine secret handling to facade types
This commit is contained in:
@@ -276,6 +276,30 @@ tests pass; binding instances cannot affect each other. Run
|
||||
`go test ./internal/engine -run 'Secret|Schema|Decode|Clone|Projection|Validation|Callback'`,
|
||||
`go test -race ./internal/engine`, and the everyday check.
|
||||
|
||||
### Completion
|
||||
|
||||
Status: complete (2026-09-12).
|
||||
|
||||
`internal/engine` now receives a per-operation `SecretBinding` at schema and
|
||||
load construction. Its exact `reflect.Type`, `Read`, and `Make` functions carry
|
||||
the facade-owned `SecretState` through decoding, projection, unresolved and
|
||||
validated cloning, dynamic-value rejection, secret lookup, resolution helpers,
|
||||
and validator mutation auditing. The binding is retained by each schema only;
|
||||
there is no package-global registration or engine-owned replacement secret.
|
||||
|
||||
Removed the candidate's production `Secret` and `SecretEnv` declarations.
|
||||
`secret_binding_test.go` provides a local test secret type, fresh binding for
|
||||
every test load, and exact-type/isolation coverage using a second local secret
|
||||
type. Existing secret tests continue to cover defaults, pointers and containers,
|
||||
resolution-selected names, lookup-once behavior, borrowed callback values,
|
||||
redaction, and validator replacement without credential disclosure. The root
|
||||
implementation remains unchanged for this migration handoff.
|
||||
|
||||
Linux validation with Go 1.26.7 passed the focused secret/schema/decode/clone/
|
||||
projection/validation/callback suite, `go test -race ./internal/engine`, and
|
||||
`./scripts/check.sh`. There were no deviations. The next handoff creates the
|
||||
temporary facade's root-owned value, source, diagnostic, and secret wrappers.
|
||||
|
||||
## Stage 4 — Build candidate value, source, and diagnostic wrappers
|
||||
|
||||
Status: not started.
|
||||
|
||||
@@ -30,11 +30,11 @@ func TestCallbackAssignmentsAreDetachedAcrossLoads(t *testing.T) {
|
||||
}
|
||||
calls := 0
|
||||
lookup := func(string) (string, bool) { calls++; return "credential", true }
|
||||
first, err := loadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
|
||||
first, err := testLoadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := loadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
|
||||
second, err := testLoadWithSecretLookup(NoSource(), lookup, Options[cfg]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -128,11 +128,11 @@ func (o *valueOperation) clone(value reflect.Value, descriptor *typeDescriptor,
|
||||
return result, nil
|
||||
case descriptorSecret:
|
||||
result := reflect.New(value.Type()).Elem()
|
||||
reference := value.Interface().(Secret)
|
||||
reference := o.schema.secrets.Read(value)
|
||||
if o.cloneMode == cloneValidatedSecrets {
|
||||
result.Set(value)
|
||||
} else if reference.specified {
|
||||
result.Set(reflect.ValueOf(SecretEnv(reference.EnvName())))
|
||||
} else if reference.Specified {
|
||||
result.Set(o.schema.secrets.Make(SecretState{Name: reference.Name, Specified: true}))
|
||||
}
|
||||
return result, nil
|
||||
case descriptorText:
|
||||
|
||||
@@ -179,7 +179,7 @@ func decodeValue(node *document.Node, base reflect.Value, descriptor *typeDescri
|
||||
if node.Kind != document.String {
|
||||
return typeMismatch(path, node, "secret environment name")
|
||||
}
|
||||
return reflect.ValueOf(SecretEnv(node.Text)), nil
|
||||
return schema.secrets.Make(SecretState{Name: node.Text, Specified: true}), nil
|
||||
case descriptorText:
|
||||
if node.Kind != document.String {
|
||||
return typeMismatch(path, node, "text string")
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestExplainShowsDefaultBaseAndEffectiveProfileValue(t *testing.T) {
|
||||
mainPath := writeImportFile(t, directory, "config.yml", "configkit:\n imports: [{path: transcription.yml}]\n")
|
||||
writeImportFile(t, directory, "transcription.yml", "transcription:\n concurrency: 8\n")
|
||||
writeImportFile(t, directory, filepath.Join("profiles", "testing.yml"), "transcription:\n concurrency: 1\n")
|
||||
result, err := Load[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{
|
||||
result, err := testLoad[explanationConcurrency](File(mainPath), Options[explanationConcurrency]{
|
||||
Imports: true, Profile: "testing",
|
||||
Definition: Definition[explanationConcurrency]{Defaults: func() explanationConcurrency {
|
||||
value := explanationConcurrency{}
|
||||
@@ -84,7 +84,7 @@ items: [profile]
|
||||
labels: {profile: retained}
|
||||
dynamic: selected
|
||||
`)
|
||||
result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
|
||||
if name != "APP_SECRET" {
|
||||
t.Fatalf("secret lookup = %q", name)
|
||||
}
|
||||
@@ -285,7 +285,7 @@ func TestSecretsBootstrapLoadAndExplanationRemainSeparateAndSafe(t *testing.T) {
|
||||
type config struct {
|
||||
Credential Secret `yaml:"credential"`
|
||||
}
|
||||
result, err := Load[config](Bytes("config.yml", []byte("credential: "+name+"\n")))
|
||||
result, err := testLoad[config](Bytes("config.yml", []byte("credential: "+name+"\n")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -343,7 +343,7 @@ func TestExplanationReconcilesDefaultsAfterInputReplacement(t *testing.T) {
|
||||
writeImportFile(t, root, "profiles/p.yml", "m: {b: 2}\n")
|
||||
opts.Profile = "p"
|
||||
}
|
||||
result, err := Load(File(main), opts)
|
||||
result, err := testLoad(File(main), opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -380,11 +380,11 @@ func TestIntegerVersionInspectionRetainsInputAndReconcilesTypedValue(t *testing.
|
||||
Defaults: func() config { return config{Version: "4"} },
|
||||
Version: rule,
|
||||
}}
|
||||
first, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
|
||||
first, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := Load(Bytes("version.yml", []byte("version: 4\n")), options)
|
||||
second, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func TestSensitiveIntegerVersionStaysRedactedWithoutSecretLookup(t *testing.T) {
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
lookups := 0
|
||||
result, err := loadWithSecretLookup(Bytes("version.yml", []byte("version: 987654321\n")), func(string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(Bytes("version.yml", []byte("version: 987654321\n")), func(string) (string, bool) {
|
||||
lookups++
|
||||
return "CREDENTIAL_CANARY", true
|
||||
}, Options[config]{Definition: Definition[config]{
|
||||
@@ -476,7 +476,7 @@ func TestExplanationReplaysResolutionAdditionsAndOverlappingIntent(t *testing.T)
|
||||
}
|
||||
for _, overlap := range []bool{false, true} {
|
||||
t.Run(fmt.Sprint(overlap), func(t *testing.T) {
|
||||
result, err := Load(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
|
||||
result, err := testLoad(Bytes("base", []byte("m: {old: 1}\n")), Options[cfg]{Definition: Definition[cfg]{Resolve: func(c *cfg, r *Resolution) error {
|
||||
c.M["new"] = map[string]any{"child": int64(7)}
|
||||
delete(c.M, "old")
|
||||
r.Record(Keys("m", "new"), ChangeDerivedDefault, "add nested", Keys("m", "old"))
|
||||
|
||||
@@ -26,7 +26,7 @@ items: {alpha: alpha}
|
||||
|
||||
var previous []SourceInfo
|
||||
for run := 0; run < 2; run++ {
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -68,7 +68,7 @@ items: {root: root}
|
||||
writeImportFile(t, directory, "conf.d/item-a.yml", "items: {a: a}\n")
|
||||
writeImportFile(t, directory, "conf.d/question1.yml", "items: {question: question}\n")
|
||||
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -79,7 +79,7 @@ items: {root: root}
|
||||
ignoredPath := writeImportFile(t, directory, "conf.d/not-discovered.yml", "unknown: value\n")
|
||||
_ = ignoredPath
|
||||
plainPath := writeImportFile(t, directory, "plain.yml", "items: {root: only}\n")
|
||||
plain, err := Load[importTestConfig](File(plainPath), Options[importTestConfig]{Imports: true})
|
||||
plain, err := testLoad[importTestConfig](File(plainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || len(plain.Sources()) != 1 || plain.Config.Items["root"] != "only" {
|
||||
t.Fatalf("implicit discovery result = %#v, %v", plain, err)
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func TestLoadGlobImportsRejectInvalidAndUnmatchedPatterns(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mainPath := writeGlobRoot(t, directory, "empty/*.yml", true)
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || len(result.Sources()) != 1 {
|
||||
t.Fatalf("optional unmatched glob = %#v, %v", result, err)
|
||||
}
|
||||
@@ -166,7 +166,7 @@ func TestLoadGlobImportsConfineSymlinkDirectories(t *testing.T) {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeGlobRoot(t, root, "linked/*.yml", false)
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || result.Config.Items["linked"] != "linked" {
|
||||
t.Fatalf("contained directory symlink = %#v, %v", result, err)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func TestGlobTraversalBoundsNonmatchesAndAcceptedCandidates(t *testing.T) {
|
||||
func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "items: {root: root}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
Profile: "default", Filesystem: &FilesystemContext{Root: directory},
|
||||
})
|
||||
if err != nil || result.Config.Items["root"] != "root" {
|
||||
@@ -242,12 +242,12 @@ func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T)
|
||||
requireLoadCode(t, File(mainPath), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: filepath.Join(directory, "missing")}}, CodeImportInvalid)
|
||||
|
||||
input := Bytes("memory.yml", []byte("items: {root: memory}\n"))
|
||||
result, err = Load[importTestConfig](input, Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}})
|
||||
result, err = testLoad[importTestConfig](input, Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory, Root: directory}})
|
||||
if err != nil || result.Config.Items["root"] != "memory" {
|
||||
t.Fatalf("byte filesystem context = %#v, %v", result, err)
|
||||
}
|
||||
requireLoadCode(t, Bytes("memory.yml", []byte("{}\n")), Options[importTestConfig]{Filesystem: &FilesystemContext{BaseDir: directory}}, CodeOptionsInvalid)
|
||||
defaultsOnly, err := Load[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"})
|
||||
defaultsOnly, err := testLoad[importTestConfig](NoSource(), Options[importTestConfig]{Profile: "default"})
|
||||
if err != nil || defaultsOnly == nil {
|
||||
t.Fatalf("default profile with NoSource = %#v, %v", defaultsOnly, err)
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func TestFilesystemContextIsValidatedWithoutImportsOrActiveProfile(t *testing.T)
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" {
|
||||
selectedLink := filepath.Join(directory, "selected-link.yml")
|
||||
if err := os.Symlink(mainPath, selectedLink); err == nil {
|
||||
linked, err := Load[importTestConfig](File(selectedLink), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: directory}})
|
||||
linked, err := testLoad[importTestConfig](File(selectedLink), Options[importTestConfig]{Filesystem: &FilesystemContext{Root: directory}})
|
||||
if err != nil || linked.Sources()[0].Path != canonicalTestPath(t, mainPath) {
|
||||
t.Fatalf("selected symlink identity = %#v, %v", linked, err)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func TestLoadFormerDirectiveKeyIsApplicationData(t *testing.T) {
|
||||
for _, imports := range []bool{false, true} {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "_configkit:\n imports:\n - path: nonexistent.yml\n")
|
||||
result, err := Load[config](File(mainPath), Options[config]{Imports: imports})
|
||||
result, err := testLoad[config](File(mainPath), Options[config]{Imports: imports})
|
||||
if err != nil {
|
||||
t.Fatalf("Imports=%v: %v", imports, err)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ settings:
|
||||
thirdPath := writeImportFile(t, directory, "nested/third.yml", "settings:\n nested: nested\n")
|
||||
secondPath := writeImportFile(t, directory, "second.yml", "settings:\n second: second\n")
|
||||
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -163,7 +163,7 @@ func TestLoadImportMissingContainmentAndRegularFileRules(t *testing.T) {
|
||||
optional: true
|
||||
settings: {root: retained}
|
||||
`)
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || result.Config.Settings.Root != "retained" || len(result.Sources()) != 1 {
|
||||
t.Fatalf("optional result = %#v, %v", result, err)
|
||||
}
|
||||
@@ -253,7 +253,7 @@ func TestLoadImportsCanonicalSymlinksAndKeepsHardLinksDistinct(t *testing.T) {
|
||||
t.Skipf("symlinks unavailable: %v", err)
|
||||
}
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: link.yml}]}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{Imports: true})
|
||||
if err != nil || result.Config.Settings.First != "linked" || result.Sources()[1].Path != canonicalTestPath(t, target) {
|
||||
t.Fatalf("symlink result = %#v, %v", result, err)
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func TestLoadByteAndReaderImportsRequireExplicitFilesystemContext(t *testing.T)
|
||||
{name: "reader URI label", source: func() Source { return Reader("https://example.invalid/main.yml", strings.NewReader(string(input))) }},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := Load[importTestConfig](test.source(), Options[importTestConfig]{Imports: true, Filesystem: context})
|
||||
result, err := testLoad[importTestConfig](test.source(), Options[importTestConfig]{Imports: true, Filesystem: context})
|
||||
if err != nil || result.Config.Settings.First != "imported" {
|
||||
t.Fatalf("explicit filesystem result = %#v, %v", result, err)
|
||||
}
|
||||
@@ -318,7 +318,7 @@ func TestFileImportRootCanContainTheSelectedFileDirectory(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
mainPath := writeImportFile(t, root, "configs/main.yml", "configkit: {imports: [{path: ../shared.yml}]}\n")
|
||||
writeImportFile(t, root, "shared.yml", "settings: {root: shared}\n")
|
||||
result, err := Load[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
result, err := testLoad[importTestConfig](File(mainPath), Options[importTestConfig]{
|
||||
Imports: true, Filesystem: &FilesystemContext{Root: root},
|
||||
})
|
||||
if err != nil || result.Config.Settings.Root != "shared" {
|
||||
@@ -455,7 +455,7 @@ func requireLoadCode(t *testing.T, source Source, options Options[importTestConf
|
||||
|
||||
func requireAnyLoadError(t *testing.T, source Source, options Options[importTestConfig]) *LoadError {
|
||||
t.Helper()
|
||||
result, err := Load[importTestConfig](source, options)
|
||||
result, err := testLoad[importTestConfig](source, options)
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, error = %#v, %v", result, err)
|
||||
}
|
||||
|
||||
@@ -46,14 +46,14 @@ func TestParallelLoadsShareRulesButNotMutableValues(t *testing.T) {
|
||||
go func(index int) {
|
||||
defer group.Done()
|
||||
input := fmt.Sprintf("custom: value-%d\n", index)
|
||||
results[index], errorsByIndex[index] = Load(Bytes("parallel.yml", []byte(input)), Options[independentConfig]{Definition: definition})
|
||||
results[index], errorsByIndex[index] = testLoad(Bytes("parallel.yml", []byte(input)), Options[independentConfig]{Definition: definition})
|
||||
}(index)
|
||||
}
|
||||
group.Wait()
|
||||
|
||||
for index, err := range errorsByIndex {
|
||||
if err != nil {
|
||||
t.Fatalf("Load(%d) failed: %v", index, err)
|
||||
t.Fatalf("testLoad(%d) failed: %v", index, err)
|
||||
}
|
||||
}
|
||||
results[0].Config.Labels["default"] = "mutated"
|
||||
@@ -88,7 +88,7 @@ func TestConcurrentLoadsOfCopiedReaderSourceHaveOneClaim(t *testing.T) {
|
||||
go func(candidate Source) {
|
||||
defer group.Done()
|
||||
<-start
|
||||
_, err := Load[config](candidate)
|
||||
_, err := testLoad[config](candidate)
|
||||
errorsFound <- err
|
||||
}(candidate)
|
||||
}
|
||||
|
||||
@@ -110,11 +110,11 @@ func (r *Result[T]) Diagnostics() []Diagnostic {
|
||||
// Load parses and strictly decodes zero or one selected source, explicitly enabled
|
||||
// imports, and one selected profile. It accepts at most one Options value and
|
||||
// returns no result when any error is reported.
|
||||
func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
|
||||
return loadWithSecretLookup(source, os.LookupEnv, supplied...)
|
||||
func Load[T any](source Source, secrets SecretBinding, supplied ...Options[T]) (*Result[T], error) {
|
||||
return loadWithSecretLookup(source, secrets, os.LookupEnv, supplied...)
|
||||
}
|
||||
|
||||
func loadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
||||
func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
||||
accumulator := diagnosticAccumulator{}
|
||||
options, diagnostics := selectOptions(source, supplied)
|
||||
accumulator.add(diagnostics...)
|
||||
@@ -128,7 +128,7 @@ func loadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ..
|
||||
if len(diagnostics) != 0 {
|
||||
return nil, accumulator.loadError()
|
||||
}
|
||||
schema, schemaIssue := buildSchema(reflect.TypeFor[T]())
|
||||
schema, schemaIssue := buildSchema(reflect.TypeFor[T](), secrets)
|
||||
if schemaIssue != nil {
|
||||
accumulator.add(diagnosticFromSchemaIssue(schemaIssue))
|
||||
return nil, accumulator.loadError()
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestLoadSingleSourceKindsAndDefaultsOnly(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var defaultsCalls atomic.Int32
|
||||
result, err := Load(test.source(), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(test.source(), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config {
|
||||
defaultsCalls.Add(1)
|
||||
return config{Name: "default"}
|
||||
@@ -92,7 +92,7 @@ func TestLoadVersionStatesWarningsAndSafeFailures(t *testing.T) {
|
||||
validation.Warning(versionPath, "review schema compatibility", Keys("missing_related"))
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(test.source, Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
result, err := testLoad(test.source, Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
if test.wantErr != "" {
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, err = %#v, %v", result, err)
|
||||
@@ -144,21 +144,21 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
|
||||
load func(Source, *VersionRule) (string, error)
|
||||
}{
|
||||
{name: "integer into string", source: "version: 4\n", load: func(source Source, rule *VersionRule) (string, error) {
|
||||
result, err := Load(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
|
||||
result, err := testLoad(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Config.Version, nil
|
||||
}},
|
||||
{name: "string into string", source: "version: \"4\"\n", load: func(source Source, rule *VersionRule) (string, error) {
|
||||
result, err := Load(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
|
||||
result, err := testLoad(source, Options[stringConfig]{Definition: Definition[stringConfig]{Version: rule}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.Config.Version, nil
|
||||
}},
|
||||
{name: "integer into pointer", source: "version: 4\n", load: func(source Source, rule *VersionRule) (string, error) {
|
||||
result, err := Load(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
|
||||
result, err := testLoad(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
|
||||
return *result.Config.Version, nil
|
||||
}},
|
||||
{name: "string into pointer", source: "version: \"4\"\n", load: func(source Source, rule *VersionRule) (string, error) {
|
||||
result, err := Load(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
|
||||
result, err := testLoad(source, Options[pointerConfig]{Definition: Definition[pointerConfig]{Version: rule}})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
if result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[stringConfig]{}); result != nil {
|
||||
if result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[stringConfig]{}); result != nil {
|
||||
t.Fatalf("load without rule = %#v, %v", result, err)
|
||||
} else {
|
||||
var loadError *LoadError
|
||||
@@ -207,7 +207,7 @@ func TestLoadDecodesIntegerVersionOnlyAtConfiguredStringPath(t *testing.T) {
|
||||
Label string `yaml:"label"`
|
||||
}
|
||||
rule := &VersionRule{Path: versionPath, Check: func(Version, *Validation) error { return nil }}
|
||||
if result, err := Load(Bytes("version.yml", []byte("version: 4\nlabel: 5\n")), Options[unrelatedConfig]{Definition: Definition[unrelatedConfig]{Version: rule}}); result != nil {
|
||||
if result, err := testLoad(Bytes("version.yml", []byte("version: 4\nlabel: 5\n")), Options[unrelatedConfig]{Definition: Definition[unrelatedConfig]{Version: rule}}); result != nil {
|
||||
t.Fatalf("unrelated coercion = %#v, %v", result, err)
|
||||
} else {
|
||||
var loadError *LoadError
|
||||
@@ -240,7 +240,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
checked = version
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
result, err := testLoad(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -266,7 +266,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
called = true
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
result, err := testLoad(Bytes("version.yml", []byte("version: "+test.value+"\n")), Options[config]{Definition: Definition[config]{Version: rule}})
|
||||
if result != nil || called {
|
||||
t.Fatalf("result = %#v, checker called = %v", result, called)
|
||||
}
|
||||
@@ -289,7 +289,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}})
|
||||
result, err := testLoad(Bytes("nested.yml", []byte("versions:\n \"0\": 4\nsibling: quoted\n")), Options[nested]{Definition: Definition[nested]{Version: rule}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -299,7 +299,7 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
type sequenceConfig struct {
|
||||
Versions []namedVersion `yaml:"versions"`
|
||||
}
|
||||
sequenceResult, err := Load(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}})
|
||||
sequenceResult, err := testLoad(Bytes("sequence.yml", []byte("versions: [6]\n")), Options[sequenceConfig]{Definition: Definition[sequenceConfig]{Version: &VersionRule{Path: parsed, Check: func(Version, *Validation) error { return nil }}}})
|
||||
if err != nil || len(sequenceResult.Config.Versions) != 1 || sequenceResult.Config.Versions[0] != "6" {
|
||||
t.Fatalf("sequence configuration = %#v, %v", sequenceResult, err)
|
||||
}
|
||||
@@ -311,13 +311,13 @@ func TestLoadVersionScalarAndPathBoundaries(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
escapedResult, err := Load(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}})
|
||||
escapedResult, err := testLoad(Bytes("escaped.yml", []byte("versions:\n a/b: 9\n")), Options[escaped]{Definition: Definition[escaped]{Version: &VersionRule{Path: escapedPath, Check: func(Version, *Validation) error { return nil }}}})
|
||||
if err != nil || escapedResult.Config.Versions["a/b"] != "9" {
|
||||
t.Fatalf("escaped configuration = %#v, %v", escapedResult, err)
|
||||
}
|
||||
|
||||
siblingRule := &VersionRule{Path: Keys("versions", "target"), Check: func(Version, *Validation) error { return nil }}
|
||||
if result, err := Load(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil {
|
||||
if result, err := testLoad(Bytes("sibling.yml", []byte("versions:\n target: 4\n other: 5\n")), Options[escaped]{Definition: Definition[escaped]{Version: siblingRule}}); result != nil {
|
||||
t.Fatalf("same-type sibling result = %#v, %v", result, err)
|
||||
} else {
|
||||
requireDiagnostic(t, err, CodeDecodeType, "/versions/other")
|
||||
@@ -332,16 +332,16 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
|
||||
type config struct {
|
||||
Version int8 `yaml:"version"`
|
||||
}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if err != nil || result.Config.Version != 4 {
|
||||
t.Fatalf("result = %#v, %v", result, err)
|
||||
}
|
||||
result, err = Load(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
result, err = testLoad(Bytes("version.yml", []byte("version: 999\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("range result = %#v", result)
|
||||
}
|
||||
requireDiagnostic(t, err, CodeDecodeRange, "/version")
|
||||
result, err = Load(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
result, err = testLoad(Bytes("version.yml", []byte("version: \"4\"\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("quoted integer result = %#v", result)
|
||||
}
|
||||
@@ -352,11 +352,11 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
|
||||
type config struct {
|
||||
Version any `yaml:"version"`
|
||||
}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if err != nil || result.Config.Version != int64(4) {
|
||||
t.Fatalf("result = %#v, %v", result, err)
|
||||
}
|
||||
result, err = Load(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
result, err = testLoad(Bytes("version.yml", []byte("version: 123456789012345678901234567890\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
if result != nil {
|
||||
t.Fatalf("range result = %#v", result)
|
||||
}
|
||||
@@ -371,21 +371,21 @@ func TestLoadVersionDestinationTypesKeepOrdinaryDecoding(t *testing.T) {
|
||||
type config struct {
|
||||
Version validatedText `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
{name: "duration", load: func() error {
|
||||
type config struct {
|
||||
Version time.Duration `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
{name: "secret", load: func() error {
|
||||
type config struct {
|
||||
Version Secret `yaml:"version"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
_, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: Keys("version"), Check: check}}})
|
||||
return err
|
||||
}},
|
||||
} {
|
||||
@@ -409,7 +409,7 @@ func TestLoadVersionPolicyStopsLaterHooks(t *testing.T) {
|
||||
validation.Error(Keys("version"), "unsupported configuration version")
|
||||
return nil
|
||||
}}
|
||||
result, err := Load(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("version.yml", []byte("version: 4\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { defaultsCalled = true; return config{} },
|
||||
Version: rule,
|
||||
Resolve: func(*config, *Resolution) error { resolutionCalled = true; return nil },
|
||||
@@ -440,7 +440,7 @@ func TestLoadVersionReportSuppressesReturnedErrorTextAndDefaults(t *testing.T) {
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
var defaultsCalls atomic.Int32
|
||||
result, err := Load(Bytes("version.yml", []byte("name: value\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("version.yml", []byte("name: value\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config {
|
||||
defaultsCalls.Add(1)
|
||||
return config{Name: "default"}
|
||||
@@ -474,7 +474,7 @@ func TestLoadMetadataReflectsDefaultsInputAndDetachedRecords(t *testing.T) {
|
||||
Nested nested `yaml:"nested"`
|
||||
Nullable map[string]string `yaml:"nullable"`
|
||||
}
|
||||
result, err := Load(Bytes("metadata.yml", []byte("nested: {set: configured}\nnullable: null\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("metadata.yml", []byte("nested: {set: configured}\nnullable: null\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config {
|
||||
return config{Nested: nested{Kept: "default", Set: "default"}, Nullable: map[string]string{"key": "value"}}
|
||||
},
|
||||
@@ -538,7 +538,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := Load(test.source, test.options...)
|
||||
result, err := testLoad(test.source, test.options...)
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, err = %#v, %v", result, err)
|
||||
}
|
||||
@@ -549,7 +549,7 @@ func TestLoadRejectsUnavailableOptionsAndDisabledDirectives(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
result, err := Load[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
|
||||
result, err := testLoad[config](Bytes("directive.yml", []byte("nested:\n configkit: {}\n")))
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, err = %#v, %v", result, err)
|
||||
}
|
||||
@@ -577,9 +577,9 @@ func TestLoadReturnsNilForSourceSchemaAndDecodeFailures(t *testing.T) {
|
||||
run func() (any, error)
|
||||
code Code
|
||||
}{
|
||||
{name: "source", run: func() (any, error) { return Load[validConfig](File(filepath.Join(t.TempDir(), "missing.yml"))) }, code: CodeSourceMissing},
|
||||
{name: "schema", run: func() (any, error) { return Load[invalidSchema](Bytes("schema.yml", []byte("{}\n"))) }, code: CodeSchemaUnsupportedType},
|
||||
{name: "decode", run: func() (any, error) { return Load[validConfig](Bytes("decode.yml", []byte("unknown: 1\n"))) }, code: CodeDecodeUnknownField},
|
||||
{name: "source", run: func() (any, error) { return testLoad[validConfig](File(filepath.Join(t.TempDir(), "missing.yml"))) }, code: CodeSourceMissing},
|
||||
{name: "schema", run: func() (any, error) { return testLoad[invalidSchema](Bytes("schema.yml", []byte("{}\n"))) }, code: CodeSchemaUnsupportedType},
|
||||
{name: "decode", run: func() (any, error) { return testLoad[validConfig](Bytes("decode.yml", []byte("unknown: 1\n"))) }, code: CodeDecodeUnknownField},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestMetadataHistoricalPaths(t *testing.T) {
|
||||
t.Parallel()
|
||||
result, err := Load[map[string]any](Bytes("base", []byte("retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n")), Options[map[string]any]{
|
||||
result, err := testLoad[map[string]any](Bytes("base", []byte("retired: {child: 1}\nshape: {'0': base}\n'a/b~': {'': 2}\n")), Options[map[string]any]{
|
||||
Overrides: []Override{
|
||||
{Path: Keys("retired"), Value: nil, Flag: "--retired"},
|
||||
{Path: Keys("shape"), Value: []any{"replacement"}, Flag: "--shape"},
|
||||
@@ -61,7 +61,7 @@ func TestHistoricalPathsExposeHiddenBaseOwnershipWithoutResolvingBaseSecrets(t *
|
||||
writeImportFile(t, dir, "first.yml", "definitions: {recap: {prompt: recap}}\n")
|
||||
writeImportFile(t, dir, "second.yml", "definitions: {recap: {output: recap.md}}\n")
|
||||
writeImportFile(t, dir, "profiles/testing.yml", "definitions: null\ncredential: "+credential+"\n")
|
||||
result, err := Load[config](File(root), Options[config]{Imports: true, Profile: "testing"})
|
||||
result, err := testLoad[config](File(root), Options[config]{Imports: true, Profile: "testing"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -232,7 +232,7 @@ func TestResolutionInputKeepsDetachedEntryMetadata(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestQueriesRespectActualHistoricalAndEffectiveContainers(t *testing.T) {
|
||||
result, err := Load[map[string]any](Bytes("base", []byte("items: [1]\nm: {'0': 2}\nx: 3\nretired: {child: 4}\n")), Options[map[string]any]{Overrides: []Override{{Path: Keys("retired"), Value: "removed", Flag: "--retired"}}})
|
||||
result, err := testLoad[map[string]any](Bytes("base", []byte("items: [1]\nm: {'0': 2}\nx: 3\nretired: {child: 4}\n")), Options[map[string]any]{Overrides: []Override{{Path: Keys("retired"), Value: "removed", Flag: "--retired"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te
|
||||
{Path: Keys("settings", "labels"), Value: map[string]any{"cli": "selected"}, Flag: "--label"},
|
||||
{Path: Keys("settings", "api_key"), Value: "CLI_SECRET", Flag: "--api-key-env"},
|
||||
}
|
||||
result, err := loadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(File(mainPath), func(name string) (string, bool) {
|
||||
if name != "CLI_SECRET" {
|
||||
t.Fatalf("looked up superseded secret name %q", name)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ func TestOverridesRunAtHighestInputPrecedenceBeforeResolutionAndValidation(t *te
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("loadWithSecretLookup() error = %v", err)
|
||||
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
|
||||
}
|
||||
want := overrideLifecycleSettings{
|
||||
Target: "cli", Labels: map[string]string{"base": "retained", "profile": "retained", "cli": "selected"},
|
||||
@@ -123,7 +123,7 @@ func TestOverridesSupportDefaultsOnlyMapMergeListReplacementAndNumericKeys(t *te
|
||||
"int": int8(1), "uint": uint16(2), "float": float32(3.5), "bool": false,
|
||||
"null": nil, "list": []any{"value"}, "map": map[string]any{"key": "value"},
|
||||
}
|
||||
result, err := Load(NoSource(), Options[config]{
|
||||
result, err := testLoad(NoSource(), Options[config]{
|
||||
Definition: Definition[config]{Defaults: func() config {
|
||||
return config{Labels: map[string]string{"kept": "default"}, Items: []int{9}}
|
||||
}},
|
||||
@@ -194,7 +194,7 @@ func TestOverrideValidationRejectsPathsOverlapValuesAndProtectedTargets(t *testi
|
||||
{name: "label", overrides: []Override{{Path: Keys("scalar"), Value: "x", Flag: "\nCANARY"}}, code: CodeOverrideInvalidLabel},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := Load(base, Options[config]{Definition: Definition[config]{Version: version}, Overrides: test.overrides})
|
||||
result, err := testLoad(base, Options[config]{Definition: Definition[config]{Version: version}, Overrides: test.overrides})
|
||||
if result != nil {
|
||||
t.Fatalf("result = %#v, want nil", result)
|
||||
}
|
||||
@@ -213,7 +213,7 @@ func TestOverrideCannotReplaceSchemaVersionAncestor(t *testing.T) {
|
||||
type config struct {
|
||||
Schema schemaConfig `yaml:"schema"`
|
||||
}
|
||||
_, err := Load(Bytes("version.yml", []byte("schema: {version: 4}\n")), Options[config]{
|
||||
_, err := testLoad(Bytes("version.yml", []byte("schema: {version: 4}\n")), Options[config]{
|
||||
Definition: Definition[config]{Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }}},
|
||||
Overrides: []Override{{Path: Keys("schema"), Value: map[string]any{"other": "value"}, Flag: "--schema"}},
|
||||
})
|
||||
@@ -224,7 +224,7 @@ func TestOverrideDecodeFailureUsesFlagSourceWithoutRawValue(t *testing.T) {
|
||||
type config struct {
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
result, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
|
||||
result, err := testLoad(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
|
||||
Path: Keys("missing"), Value: "RAW-ARG-CANARY", Flag: "--missing",
|
||||
}}})
|
||||
if result != nil {
|
||||
@@ -249,7 +249,7 @@ func TestOverrideTypeMismatchUsesNormalStrictDecode(t *testing.T) {
|
||||
type config struct {
|
||||
Count int `yaml:"count"`
|
||||
}
|
||||
_, err := Load(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
|
||||
_, err := testLoad(Bytes("base.yml", []byte("count: 1\n")), Options[config]{Overrides: []Override{{
|
||||
Path: Keys("count"), Value: "not-an-integer", Flag: "--count",
|
||||
}}})
|
||||
loadError := requireSecretLoadCode(t, err, CodeDecodeType)
|
||||
@@ -262,7 +262,7 @@ func TestOverrideEqualReplacementStillAppearsInHistory(t *testing.T) {
|
||||
type config struct {
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
result, err := Load(Bytes("equal.yml", []byte("value: same\n")), Options[config]{Overrides: []Override{{Path: Keys("value"), Value: "same", Flag: "--value"}}})
|
||||
result, err := testLoad(Bytes("equal.yml", []byte("value: same\n")), Options[config]{Overrides: []Override{{Path: Keys("value"), Value: "same", Flag: "--value"}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ nullable: null
|
||||
value: scalar
|
||||
`)
|
||||
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{
|
||||
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{
|
||||
Imports: true,
|
||||
Profile: "testing",
|
||||
Definition: Definition[profileTestConfig]{Defaults: func() profileTestConfig {
|
||||
@@ -101,12 +101,12 @@ func TestLoadProfileSelectionUsesOneFixedYAMLFile(t *testing.T) {
|
||||
writeImportFile(t, directory, "profiles/default.yml", "settings: {changed: forbidden}\n")
|
||||
|
||||
for _, name := range []string{"", "default"} {
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name})
|
||||
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: name})
|
||||
if err != nil || result.Config.Settings.Changed != "" || len(result.Sources()) != 1 {
|
||||
t.Fatalf("inactive profile %q = %#v, %v", name, result, err)
|
||||
}
|
||||
}
|
||||
selected, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"})
|
||||
selected, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing"})
|
||||
if err != nil || selected.Config.Settings.Changed != "selected" || len(selected.Sources()) != 2 {
|
||||
t.Fatalf("selected profile = %#v, %v", selected, err)
|
||||
}
|
||||
@@ -150,7 +150,7 @@ func TestLoadProfileRejectsDirectivesVersionsAndBaseAliases(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
mainPath := writeImportFile(t, directory, "main.yml", "schema: {version: 4}\n")
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "schema: {other: allowed}\n")
|
||||
result, err := Load[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}})
|
||||
result, err := testLoad[profileTestConfig](File(mainPath), Options[profileTestConfig]{Profile: "testing", Definition: Definition[profileTestConfig]{Version: versionRule}})
|
||||
if err != nil || result.Config.Schema["other"] != "allowed" {
|
||||
t.Fatalf("mapping sibling = %#v, %v", result, err)
|
||||
}
|
||||
@@ -201,7 +201,7 @@ func TestLoadProfileKeepsBaseFailuresAndFilesystemContainment(t *testing.T) {
|
||||
t.Run("byte source context", func(t *testing.T) {
|
||||
directory := t.TempDir()
|
||||
writeImportFile(t, directory, "profiles/testing.yml", "settings: {changed: selected}\n")
|
||||
result, err := Load[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{
|
||||
result, err := testLoad[profileTestConfig](Bytes("s3://bucket/main.yml", []byte("{}\n")), Options[profileTestConfig]{
|
||||
Profile: "testing", Filesystem: &FilesystemContext{BaseDir: directory, Root: directory},
|
||||
})
|
||||
if err != nil || result.Config.Settings.Changed != "selected" {
|
||||
@@ -251,7 +251,7 @@ func TestLoadProfileCountsAgainstAggregateBudgets(t *testing.T) {
|
||||
|
||||
func requireProfileLoadCode(t *testing.T, source Source, options Options[profileTestConfig], code Code) *LoadError {
|
||||
t.Helper()
|
||||
result, err := Load[profileTestConfig](source, options)
|
||||
result, err := testLoad[profileTestConfig](source, options)
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, error = %#v, %v", result, err)
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func (o *valueOperation) project(value reflect.Value, descriptor *typeDescriptor
|
||||
case descriptorSecret:
|
||||
node, issue := o.node(document.String, path, depth)
|
||||
if issue == nil {
|
||||
node.Text = value.Interface().(Secret).EnvName()
|
||||
node.Text = o.schema.secrets.Read(value).Name
|
||||
}
|
||||
return node, issue
|
||||
case descriptorText:
|
||||
|
||||
@@ -359,9 +359,9 @@ func TestCompareDocumentTreesTracksSemanticChanges(t *testing.T) {
|
||||
|
||||
func mustSchemaFor[T any](t *testing.T) *schemaDescriptor {
|
||||
t.Helper()
|
||||
schema, issue := buildSchema(reflect.TypeFor[T]())
|
||||
schema, issue := testBuildSchema(reflect.TypeFor[T]())
|
||||
if issue != nil {
|
||||
t.Fatalf("buildSchema() issue = %#v", issue)
|
||||
t.Fatalf("testBuildSchema() issue = %#v", issue)
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ empty: {}
|
||||
},
|
||||
}
|
||||
|
||||
result, err := Load[layeredConfig](File(mainPath), Options[layeredConfig]{
|
||||
result, err := testLoad[layeredConfig](File(mainPath), Options[layeredConfig]{
|
||||
Imports: true, Profile: "testing", Definition: definition,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -134,7 +134,7 @@ empty: {}
|
||||
t.Fatalf("canonical resolution history = %#v", lastValue)
|
||||
}
|
||||
|
||||
schema, issue := buildSchema(reflect.TypeFor[layeredConfig]())
|
||||
schema, issue := testBuildSchema(reflect.TypeFor[layeredConfig]())
|
||||
if issue != nil {
|
||||
t.Fatal(issue)
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
result, err := Load[layeredConfig](File(mainPath), options)
|
||||
result, err := testLoad[layeredConfig](File(mainPath), options)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
@@ -236,7 +236,7 @@ composite: {first: first}
|
||||
CLIEqual string `yaml:"cli_equal"`
|
||||
}
|
||||
var callbackErr error
|
||||
result, err := Load[config](File(mainPath), Options[config]{
|
||||
result, err := testLoad[config](File(mainPath), Options[config]{
|
||||
Imports: true, Profile: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}},
|
||||
Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
check := func(ok bool, message string) {
|
||||
@@ -322,7 +322,7 @@ func TestResolutionInputUsesEntryShapesAcrossTypedAndDynamicValues(t *testing.T)
|
||||
Dynamic any `yaml:"dynamic"`
|
||||
}
|
||||
var callbackErr error
|
||||
_, err := Load[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key, 'a/b~': escaped}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
_, err := testLoad[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key, 'a/b~': escaped}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
check := func(path Path, want bool) {
|
||||
_, found := resolution.InputInfo(path)
|
||||
if found != want && callbackErr == nil {
|
||||
@@ -371,7 +371,7 @@ func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T)
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var callbackErr error
|
||||
_, err := Load[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{
|
||||
_, err := testLoad[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Default: "retained"} },
|
||||
Resolve: func(_ *config, resolution *Resolution) error {
|
||||
root, _ := resolution.InputInfo(Keys("root"))
|
||||
@@ -398,7 +398,7 @@ func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T)
|
||||
}
|
||||
|
||||
var callbackErr error
|
||||
_, err := Load[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{
|
||||
_, err := testLoad[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Default: "retained"} },
|
||||
Resolve: func(_ *config, resolution *Resolution) error {
|
||||
root, rootFound := resolution.InputInfo(Keys("root"))
|
||||
@@ -426,7 +426,7 @@ func TestResolutionInputRemainsFixedAcrossRecordedMutations(t *testing.T) {
|
||||
Values map[string]string `yaml:"values"`
|
||||
}
|
||||
var callbackErr error
|
||||
result, err := Load[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
||||
result, err := testLoad[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
||||
original, originalFound := resolution.InputInfo(Keys("values", "original"))
|
||||
_, destinationFound := resolution.InputInfo(Keys("values", "destination"))
|
||||
existing, existingFound := resolution.InputInfo(Keys("existing"))
|
||||
@@ -475,7 +475,7 @@ func TestResolutionInputQueriesRemainIndependentAcrossLoads(t *testing.T) {
|
||||
group.Add(1)
|
||||
go func() {
|
||||
defer group.Done()
|
||||
_, err := Load[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
_, err := testLoad[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
info, found := resolution.InputInfo(Keys("value"))
|
||||
source, sourceFound := resolution.Source(info.Origin.Locations[0].SourceID)
|
||||
if !found || !sourceFound || source.ID != "source-1" {
|
||||
|
||||
@@ -85,7 +85,7 @@ sequence: [second, first]
|
||||
return nil
|
||||
},
|
||||
}
|
||||
result, err := Load[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"})
|
||||
result, err := testLoad[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -160,7 +160,7 @@ func TestSecretValuesStayOutOfLoadFailuresAndWarnings(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
test.definition.Secret = func(path Path) bool { return path.String() == "/credentials" }
|
||||
result, err := Load[renderConfig](Bytes("secret-failure.yml", []byte(test.input)), Options[renderConfig]{Definition: test.definition})
|
||||
result, err := testLoad[renderConfig](Bytes("secret-failure.yml", []byte(test.input)), Options[renderConfig]{Definition: test.definition})
|
||||
if result != nil || err == nil {
|
||||
t.Fatalf("result, error = %#v, %v", result, err)
|
||||
}
|
||||
@@ -205,7 +205,7 @@ func TestDynamicConcreteTagsRemainSecretThroughHistory(t *testing.T) {
|
||||
Normalize(r, Keys("derived"), &c.Derived, canary, "derive", path)
|
||||
return nil
|
||||
}
|
||||
result, err := Load(NoSource(), Options[cfg]{Definition: definition})
|
||||
result, err := testLoad(NoSource(), Options[cfg]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -234,7 +234,7 @@ func TestValidatorCannotChangeDynamicSensitivityWithoutChangingText(t *testing.T
|
||||
type cfg struct {
|
||||
Payload any `yaml:"payload"`
|
||||
}
|
||||
_, err := Load(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) {
|
||||
_, err := testLoad(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) {
|
||||
c.Payload = sensitive{Token: c.Payload.(map[string]any)["token"].(string)}
|
||||
}}})
|
||||
var failure *LoadError
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestLoadRunsRecordedResolutionWithMapWriteback(t *testing.T) {
|
||||
},
|
||||
}
|
||||
input := "poll_interval: 1m\nartifacts:\n second: {output: ' second '}\n first: {output: ' first '}\n"
|
||||
result, err := Load(Bytes("resolution.yml", []byte(input)), Options[config]{Definition: definition})
|
||||
result, err := testLoad(Bytes("resolution.yml", []byte(input)), Options[config]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestResolutionPreservesExplicitNullAndZeroWhileReplacingAbsentBaseline(t *t
|
||||
Absent int `yaml:"absent"`
|
||||
}
|
||||
five := 5
|
||||
result, err := Load(Bytes("explicit.yml", []byte("optional: null\ncount: 0\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("explicit.yml", []byte("optional: null\ncount: 0\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Optional: &five, Count: 5, Absent: 5} },
|
||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||
if !resolution.IsExplicit(Keys("optional")) || !resolution.IsExplicit(Keys("count")) || resolution.IsExplicit(Keys("absent")) {
|
||||
@@ -126,7 +126,7 @@ func TestResolutionCoalescesEqualAndRepeatedHelperIntent(t *testing.T) {
|
||||
Value int `yaml:"value"`
|
||||
Other int `yaml:"other"`
|
||||
}
|
||||
result, err := Load(NoSource(), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Value: 5} },
|
||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||
Default(resolution, Keys("value"), &configuration.Value, 5, "equal derived intent", Keys("other"))
|
||||
@@ -157,7 +157,7 @@ func TestResolutionContainerRecordCoversSubtreeRemovalAndSnapshotsAreDetached(t
|
||||
type config struct {
|
||||
Items map[string]item `yaml:"items"`
|
||||
}
|
||||
result, err := Load(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("remove.yml", []byte("items:\n keep: {value: 'yes'}\n remove: {value: 'no'}\n")), Options[config]{Definition: Definition[config]{
|
||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||
delete(configuration.Items, "remove")
|
||||
resolution.Record(Keys("items"), ChangeNormalization, "removed disabled entries")
|
||||
@@ -188,7 +188,7 @@ func TestResolutionSequenceReplacementUpdatesDescendantOrigins(t *testing.T) {
|
||||
type config struct {
|
||||
Values []int `yaml:"values"`
|
||||
}
|
||||
result, err := Load(Bytes("sequence.yml", []byte("values: [1, 2]\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("sequence.yml", []byte("values: [1, 2]\n")), Options[config]{Definition: Definition[config]{
|
||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||
Normalize(resolution, Keys("values"), &configuration.Values, []int{3}, "replaced values")
|
||||
return nil
|
||||
@@ -237,7 +237,7 @@ func TestResolutionRejectsUnrecordedAndMismatchedMutations(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := Load(Bytes("invalid-resolution.yml", []byte("value: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
|
||||
result, err := testLoad(Bytes("invalid-resolution.yml", []byte("value: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
|
||||
assertLoadDiagnostic(t, result, err, test.code)
|
||||
})
|
||||
}
|
||||
@@ -269,7 +269,7 @@ func TestResolutionRejectsInvalidProjectedValuesAndSuppressesReturnedErrors(t *t
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result, err := Load(Bytes("resolution-errors.yml", []byte("value: 1\nnumber: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
|
||||
result, err := testLoad(Bytes("resolution-errors.yml", []byte("value: 1\nnumber: 1\n")), Options[config]{Definition: Definition[config]{Resolve: test.resolve}})
|
||||
loadError := assertLoadDiagnostic(t, result, err, test.code)
|
||||
for _, diagnostic := range loadError.Diagnostics() {
|
||||
if strings.Contains(diagnostic.Message, "CANARY") {
|
||||
@@ -291,7 +291,7 @@ func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
||||
filename := writeImportFile(t, t.TempDir(), "config.yml", "declared: file\noptional: null\n")
|
||||
var savedField FieldInfo
|
||||
var savedSource SourceInfo
|
||||
result, err := Load[config](File(filename), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad[config](File(filename), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Default: "retained"} },
|
||||
Resolve: func(_ *config, resolution *Resolution) error {
|
||||
for _, path := range []Path{Keys("declared"), Keys("default"), Keys("optional")} {
|
||||
@@ -339,7 +339,7 @@ func TestResolutionEntryQueriesExposeDetachedLoadInput(t *testing.T) {
|
||||
t.Fatal("saved resolution records alias a completed result")
|
||||
}
|
||||
|
||||
noSource, err := Load[config](NoSource(), Options[config]{Definition: Definition[config]{
|
||||
noSource, err := testLoad[config](NoSource(), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Default: "retained"} },
|
||||
Resolve: func(_ *config, resolution *Resolution) error {
|
||||
info, found := resolution.InputInfo(Keys("default"))
|
||||
@@ -364,7 +364,7 @@ func TestResolutionContextPanicsAfterCallback(t *testing.T) {
|
||||
Value int `yaml:"value"`
|
||||
}
|
||||
var retained *Resolution
|
||||
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
retained = resolution
|
||||
return nil
|
||||
}}}); err != nil {
|
||||
@@ -397,7 +397,7 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
|
||||
requireClosed(t, func() { (&Resolution{}).Source("source-1") })
|
||||
|
||||
var successful *Resolution
|
||||
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
successful = resolution
|
||||
return nil
|
||||
}}}); err != nil {
|
||||
@@ -407,7 +407,7 @@ func TestResolutionEntryQueriesRequireAnActiveCallback(t *testing.T) {
|
||||
requireClosed(t, func() { successful.Source("source-1") })
|
||||
|
||||
var failed *Resolution
|
||||
if _, err := Load(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
if _, err := testLoad(NoSource(), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||
failed = resolution
|
||||
return errors.New("resolver failed")
|
||||
}}}); err == nil {
|
||||
@@ -424,7 +424,7 @@ func TestValidationContextPanicsAfterVersionCallback(t *testing.T) {
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
var retained *Validation
|
||||
if _, err := Load(Bytes("version-context.yml", []byte("version: one\n")), Options[config]{Definition: Definition[config]{
|
||||
if _, err := testLoad(Bytes("version-context.yml", []byte("version: one\n")), Options[config]{Definition: Definition[config]{
|
||||
Version: &VersionRule{Path: Keys("version"), Check: func(_ Version, validation *Validation) error {
|
||||
retained = validation
|
||||
return nil
|
||||
|
||||
@@ -29,8 +29,9 @@ const (
|
||||
)
|
||||
|
||||
type schemaDescriptor struct {
|
||||
root *typeDescriptor
|
||||
types map[reflect.Type]*typeDescriptor
|
||||
root *typeDescriptor
|
||||
types map[reflect.Type]*typeDescriptor
|
||||
secrets SecretBinding
|
||||
}
|
||||
|
||||
type typeDescriptor struct {
|
||||
@@ -65,20 +66,39 @@ type schemaIssue struct {
|
||||
message string
|
||||
}
|
||||
|
||||
// SecretState is the private operation state for one exact-type secret value.
|
||||
// It is supplied by the facade and never enters inspection records.
|
||||
type SecretState struct {
|
||||
Name string
|
||||
Value string
|
||||
Specified bool
|
||||
Resolved bool
|
||||
}
|
||||
|
||||
// SecretBinding adapts the facade-owned exact secret type for one load.
|
||||
// Bindings are operation data: callers must supply the type and conversions
|
||||
// together, and a binding never becomes package-global state.
|
||||
type SecretBinding struct {
|
||||
Type reflect.Type
|
||||
Read func(reflect.Value) SecretState
|
||||
Make func(SecretState) reflect.Value
|
||||
}
|
||||
|
||||
var (
|
||||
durationType = reflect.TypeFor[time.Duration]()
|
||||
secretType = reflect.TypeFor[Secret]()
|
||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||
textUnmarshalerType = reflect.TypeFor[encoding.TextUnmarshaler]()
|
||||
)
|
||||
|
||||
func buildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
||||
func buildSchema(root reflect.Type, secrets SecretBinding) (*schemaDescriptor, *schemaIssue) {
|
||||
secrets.validate()
|
||||
if root == nil || (root.Kind() != reflect.Struct && !isStringMap(root)) {
|
||||
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a struct or string-keyed map"}
|
||||
}
|
||||
builder := schemaBuilder{
|
||||
cache: make(map[reflect.Type]*typeDescriptor),
|
||||
building: make(map[reflect.Type]bool),
|
||||
secrets: secrets,
|
||||
}
|
||||
descriptor, issue := builder.describe(root, Path{})
|
||||
if issue != nil {
|
||||
@@ -87,12 +107,13 @@ func buildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
||||
if descriptor.kind != descriptorStruct && descriptor.kind != descriptorMap {
|
||||
return nil, &schemaIssue{code: CodeSchemaRoot, message: "configuration root must be a structural struct or string-keyed map"}
|
||||
}
|
||||
return &schemaDescriptor{root: descriptor, types: builder.cache}, nil
|
||||
return &schemaDescriptor{root: descriptor, types: builder.cache, secrets: secrets}, nil
|
||||
}
|
||||
|
||||
type schemaBuilder struct {
|
||||
cache map[reflect.Type]*typeDescriptor
|
||||
building map[reflect.Type]bool
|
||||
secrets SecretBinding
|
||||
}
|
||||
|
||||
func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescriptor, *schemaIssue) {
|
||||
@@ -122,7 +143,7 @@ func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescript
|
||||
descriptor.kind = descriptorDuration
|
||||
return descriptor, nil
|
||||
}
|
||||
if current == secretType {
|
||||
if b.secrets.Type != nil && current == b.secrets.Type {
|
||||
descriptor.kind = descriptorSecret
|
||||
return descriptor, nil
|
||||
}
|
||||
@@ -189,6 +210,15 @@ func (b *schemaBuilder) describe(current reflect.Type, path Path) (*typeDescript
|
||||
return descriptor, nil
|
||||
}
|
||||
|
||||
func (binding SecretBinding) validate() {
|
||||
if binding.Type == nil {
|
||||
return
|
||||
}
|
||||
if binding.Read == nil || binding.Make == nil {
|
||||
panic("configkit engine: incomplete secret binding")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *schemaBuilder) describeFields(descriptor *typeDescriptor, current reflect.Type, path Path) *schemaIssue {
|
||||
for i := 0; i < current.NumField(); i++ {
|
||||
field := current.Field(i)
|
||||
|
||||
@@ -68,9 +68,9 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
|
||||
private chan int
|
||||
}
|
||||
|
||||
schema, issue := buildSchema(reflect.TypeFor[config]())
|
||||
schema, issue := testBuildSchema(reflect.TypeFor[config]())
|
||||
if issue != nil {
|
||||
t.Fatalf("buildSchema() issue = %#v", issue)
|
||||
t.Fatalf("testBuildSchema() issue = %#v", issue)
|
||||
}
|
||||
root := schema.root
|
||||
checks := map[string]descriptorKind{
|
||||
@@ -105,7 +105,7 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
|
||||
if root.inlineMap == nil || root.inlineMap.descriptor.kind != descriptorMap {
|
||||
t.Fatalf("inline map = %#v", root.inlineMap)
|
||||
}
|
||||
if mapped, mapIssue := buildSchema(reflect.TypeFor[map[string]nested]()); mapIssue != nil || mapped.root.kind != descriptorMap {
|
||||
if mapped, mapIssue := testBuildSchema(reflect.TypeFor[map[string]nested]()); mapIssue != nil || mapped.root.kind != descriptorMap {
|
||||
t.Fatalf("string-keyed root map = %#v, issue=%#v", mapped, mapIssue)
|
||||
}
|
||||
if _, exists := root.fieldByName["ignored"]; exists {
|
||||
@@ -116,7 +116,7 @@ func TestBuildSchemaDescribesSupportedTypes(t *testing.T) {
|
||||
func TestBuildSchemaSupportsRecursiveDescriptorsWithoutSharingLoads(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
first, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
|
||||
first, issue := testBuildSchema(reflect.TypeFor[recursiveConfig]())
|
||||
if issue != nil {
|
||||
t.Fatal(issue.message)
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func TestBuildSchemaSupportsRecursiveDescriptorsWithoutSharingLoads(t *testing.T
|
||||
if children.kind != descriptorMap || children.element != first.root {
|
||||
t.Fatal("recursive map did not refer to the existing descriptor")
|
||||
}
|
||||
second, issue := buildSchema(reflect.TypeFor[recursiveConfig]())
|
||||
second, issue := testBuildSchema(reflect.TypeFor[recursiveConfig]())
|
||||
if issue != nil {
|
||||
t.Fatal(issue.message)
|
||||
}
|
||||
@@ -207,7 +207,7 @@ func TestBuildSchemaRejectsInvalidRootsTypesTagsAndBindings(t *testing.T) {
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, issue := buildSchema(test.typeOf); issue == nil || issue.code != test.code {
|
||||
if _, issue := testBuildSchema(test.typeOf); issue == nil || issue.code != test.code {
|
||||
t.Fatalf("issue = %#v, want %s", issue, test.code)
|
||||
}
|
||||
})
|
||||
@@ -221,7 +221,7 @@ func TestBuildSchemaDoesNotExecuteApplicationMethods(t *testing.T) {
|
||||
Value panicText `yaml:"value"`
|
||||
Structural yamlHookStruct `yaml:"structural"`
|
||||
}
|
||||
if _, issue := buildSchema(reflect.TypeFor[config]()); issue != nil {
|
||||
if _, issue := testBuildSchema(reflect.TypeFor[config]()); issue != nil {
|
||||
t.Fatal(issue.message)
|
||||
}
|
||||
}
|
||||
@@ -261,7 +261,7 @@ func TestResolveSchemaPathUsesContainerSemantics(t *testing.T) {
|
||||
Dynamic any `yaml:"dynamic"`
|
||||
Extras map[string]string `yaml:",inline"`
|
||||
}
|
||||
schema, issue := buildSchema(reflect.TypeFor[config]())
|
||||
schema, issue := testBuildSchema(reflect.TypeFor[config]())
|
||||
if issue != nil {
|
||||
t.Fatal(issue.message)
|
||||
}
|
||||
@@ -310,7 +310,7 @@ func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
|
||||
}
|
||||
for _, nonNil := range []bool{false, true} {
|
||||
called := false
|
||||
_, err := Load(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
|
||||
_, err := testLoad(NoSource(), Options[cfg]{Definition: Definition[cfg]{Defaults: func() cfg {
|
||||
called = true
|
||||
if nonNil {
|
||||
m := map[string]int{"value": 1}
|
||||
@@ -330,13 +330,13 @@ func TestInlineMapPointersFailBeforeDefaults(t *testing.T) {
|
||||
type recursive struct {
|
||||
Extra pointer `yaml:",inline"`
|
||||
}
|
||||
if _, err := Load[recursive](NoSource()); err == nil {
|
||||
if _, err := testLoad[recursive](NoSource()); err == nil {
|
||||
t.Fatal("recursive inline pointer was accepted")
|
||||
}
|
||||
type double struct {
|
||||
Extra **map[string]int `yaml:",inline"`
|
||||
}
|
||||
if _, err := Load[double](NoSource()); err == nil {
|
||||
if _, err := testLoad[double](NoSource()); err == nil {
|
||||
t.Fatal("double inline map pointer was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
107
internal/engine/secret_binding_test.go
Normal file
107
internal/engine/secret_binding_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Secret is a test-local facade secret. Production engine code must not depend
|
||||
// on this type; each test load supplies the binding below explicitly.
|
||||
type Secret struct {
|
||||
name string
|
||||
value string
|
||||
specified bool
|
||||
resolved bool
|
||||
}
|
||||
|
||||
func SecretEnv(name string) Secret {
|
||||
return Secret{name: name, specified: true}
|
||||
}
|
||||
|
||||
func (s Secret) EnvName() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
func (s Secret) Value() string {
|
||||
if !s.resolved {
|
||||
return ""
|
||||
}
|
||||
return s.value
|
||||
}
|
||||
|
||||
func (s Secret) IsResolved() bool {
|
||||
return s.resolved
|
||||
}
|
||||
|
||||
func (s Secret) Format(state fmt.State, _ rune) {
|
||||
_, _ = io.WriteString(state, "configkit.SecretEnv("+strconv.Quote(s.name)+")")
|
||||
}
|
||||
|
||||
func (s Secret) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.name)
|
||||
}
|
||||
|
||||
func testSecretBinding() SecretBinding {
|
||||
return SecretBinding{
|
||||
Type: reflect.TypeFor[Secret](),
|
||||
Read: func(value reflect.Value) SecretState {
|
||||
secret := value.Interface().(Secret)
|
||||
return SecretState{Name: secret.name, Value: secret.value, Specified: secret.specified, Resolved: secret.resolved}
|
||||
},
|
||||
Make: func(state SecretState) reflect.Value {
|
||||
return reflect.ValueOf(Secret{name: state.Name, value: state.Value, specified: state.Specified, resolved: state.Resolved})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testLoad[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
|
||||
return Load(source, testSecretBinding(), supplied...)
|
||||
}
|
||||
|
||||
func testLoadWithSecretLookup[T any](source Source, lookup secretLookup, supplied ...Options[T]) (*Result[T], error) {
|
||||
return loadWithSecretLookup(source, testSecretBinding(), lookup, supplied...)
|
||||
}
|
||||
|
||||
func testBuildSchema(root reflect.Type) (*schemaDescriptor, *schemaIssue) {
|
||||
return buildSchema(root, testSecretBinding())
|
||||
}
|
||||
|
||||
func TestSecretBindingsRemainExactAndIndependent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type otherSecret struct {
|
||||
name string
|
||||
}
|
||||
other := SecretBinding{
|
||||
Type: reflect.TypeFor[otherSecret](),
|
||||
Read: func(value reflect.Value) SecretState {
|
||||
return SecretState{Name: value.Interface().(otherSecret).name}
|
||||
},
|
||||
Make: func(state SecretState) reflect.Value {
|
||||
return reflect.ValueOf(otherSecret{name: state.Name})
|
||||
},
|
||||
}
|
||||
type config struct {
|
||||
First Secret `yaml:"first"`
|
||||
Second otherSecret `yaml:"second"`
|
||||
}
|
||||
firstSchema, firstIssue := buildSchema(reflect.TypeFor[config](), testSecretBinding())
|
||||
if firstIssue != nil {
|
||||
t.Fatal(firstIssue)
|
||||
}
|
||||
secondSchema, secondIssue := buildSchema(reflect.TypeFor[config](), other)
|
||||
if secondIssue != nil {
|
||||
t.Fatal(secondIssue)
|
||||
}
|
||||
firstField := firstSchema.root.fields[firstSchema.root.fieldByName["first"]].descriptor
|
||||
firstOther := firstSchema.root.fields[firstSchema.root.fieldByName["second"]].descriptor
|
||||
secondField := secondSchema.root.fields[secondSchema.root.fieldByName["first"]].descriptor
|
||||
secondOther := secondSchema.root.fields[secondSchema.root.fieldByName["second"]].descriptor
|
||||
if firstField.kind != descriptorSecret || firstOther.kind == descriptorSecret || secondField.kind == descriptorSecret || secondOther.kind != descriptorSecret {
|
||||
t.Fatalf("secret descriptor kinds = %v, %v, %v, %v", firstField.kind, firstOther.kind, secondField.kind, secondOther.kind)
|
||||
}
|
||||
}
|
||||
@@ -2,60 +2,12 @@ package engine
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
||||
)
|
||||
|
||||
// Secret is an opaque reference to an environment variable. Its name is public
|
||||
// configuration; its resolved value is available only through Value.
|
||||
type Secret struct {
|
||||
name string
|
||||
value string
|
||||
specified bool
|
||||
resolved bool
|
||||
}
|
||||
|
||||
// SecretEnv constructs an unresolved environment-backed secret reference.
|
||||
// Invalid names are reported if the reference survives a configuration load.
|
||||
func SecretEnv(name string) Secret {
|
||||
return Secret{name: name, specified: true}
|
||||
}
|
||||
|
||||
// EnvName returns the public environment-variable name.
|
||||
func (s Secret) EnvName() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// Value returns the resolved credential, or an empty string when unresolved.
|
||||
func (s Secret) Value() string {
|
||||
if !s.resolved {
|
||||
return ""
|
||||
}
|
||||
return s.value
|
||||
}
|
||||
|
||||
// IsResolved reports whether this reference resolved to a nonempty value during
|
||||
// its configuration load.
|
||||
func (s Secret) IsResolved() bool {
|
||||
return s.resolved
|
||||
}
|
||||
|
||||
// Format renders only the public reference name for every formatting verb.
|
||||
func (s Secret) Format(state fmt.State, _ rune) {
|
||||
_, _ = io.WriteString(state, "configkit.SecretEnv("+strconv.Quote(s.name)+")")
|
||||
}
|
||||
|
||||
// MarshalJSON emits only the public environment-variable name.
|
||||
func (s Secret) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.name)
|
||||
}
|
||||
|
||||
type secretLookup func(string) (string, bool)
|
||||
|
||||
type secretLookupResult struct {
|
||||
@@ -80,28 +32,28 @@ func resolveSecretReferences(root reflect.Value, schema *schemaDescriptor, input
|
||||
if lookup == nil {
|
||||
lookup = func(string) (string, bool) { return "", false }
|
||||
}
|
||||
resolveSecretValue(root, schema.root, Path{}, false, input, lookup, &result)
|
||||
resolveSecretValue(root, schema.root, schema.secrets, Path{}, false, input, lookup, &result)
|
||||
result.diagnostics = canonicalDiagnostics(result.diagnostics)
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Path, optional bool, input *document.Node, lookup secretLookup, result *secretResolutionResult) {
|
||||
func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, secrets SecretBinding, path Path, optional bool, input *document.Node, lookup secretLookup, result *secretResolutionResult) {
|
||||
if descriptor.kind == descriptorPointer {
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
resolveSecretValue(value.Elem(), descriptor.element, path, descriptor.element.kind == descriptorSecret, input, lookup, result)
|
||||
resolveSecretValue(value.Elem(), descriptor.element, secrets, path, descriptor.element.kind == descriptorSecret, input, lookup, result)
|
||||
return
|
||||
}
|
||||
|
||||
switch descriptor.kind {
|
||||
case descriptorSecret:
|
||||
reference := value.Interface().(Secret)
|
||||
name := reference.EnvName()
|
||||
reference := secrets.Read(value)
|
||||
name := reference.Name
|
||||
if name == "" {
|
||||
code := CodeSecretRequired
|
||||
message := "required secret reference is absent"
|
||||
if optional || reference.specified {
|
||||
if optional || reference.Specified {
|
||||
code = CodeSecretInvalidName
|
||||
message = "secret environment name is empty"
|
||||
}
|
||||
@@ -125,7 +77,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
|
||||
result.diagnostics = append(result.diagnostics, secretDiagnostic(CodeSecretEmpty, path, "secret environment variable is empty", input))
|
||||
return
|
||||
}
|
||||
value.Set(reflect.ValueOf(Secret{name: name, value: found.value, specified: true, resolved: true}))
|
||||
value.Set(secrets.Make(SecretState{Name: name, Value: found.value, Specified: true, Resolved: true}))
|
||||
result.references[path.String()] = name
|
||||
case descriptorStruct:
|
||||
for _, member := range descriptor.members {
|
||||
@@ -133,7 +85,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
|
||||
if !member.inline {
|
||||
memberPath = path.Key(member.name)
|
||||
}
|
||||
resolveSecretValue(value.Field(member.index), member.descriptor, memberPath, false, input, lookup, result)
|
||||
resolveSecretValue(value.Field(member.index), member.descriptor, secrets, memberPath, false, input, lookup, result)
|
||||
}
|
||||
case descriptorMap:
|
||||
if value.IsNil() {
|
||||
@@ -144,7 +96,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
|
||||
for _, key := range keys {
|
||||
entry := reflect.New(value.Type().Elem()).Elem()
|
||||
entry.Set(value.MapIndex(key))
|
||||
resolveSecretValue(entry, descriptor.element, path.Key(key.String()), false, input, lookup, result)
|
||||
resolveSecretValue(entry, descriptor.element, secrets, path.Key(key.String()), false, input, lookup, result)
|
||||
value.SetMapIndex(key, entry)
|
||||
}
|
||||
case descriptorSlice, descriptorArray:
|
||||
@@ -152,7 +104,7 @@ func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, path Pa
|
||||
return
|
||||
}
|
||||
for index := 0; index < value.Len(); index++ {
|
||||
resolveSecretValue(value.Index(index), descriptor.element, path.Index(index), false, input, lookup, result)
|
||||
resolveSecretValue(value.Index(index), descriptor.element, secrets, path.Index(index), false, input, lookup, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,25 +117,28 @@ func secretDiagnostic(code Code, path Path, message string, input *document.Node
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
func changedSecretStates(root reflect.Value, descriptor *typeDescriptor, lookups map[string]secretLookupResult) []Path {
|
||||
func changedSecretStates(root reflect.Value, schema *schemaDescriptor, lookups map[string]secretLookupResult) []Path {
|
||||
var changed []Path
|
||||
collectChangedSecretStates(root, descriptor, Path{}, lookups, &changed)
|
||||
if schema == nil || schema.root == nil {
|
||||
return nil
|
||||
}
|
||||
collectChangedSecretStates(root, schema.root, schema.secrets, Path{}, lookups, &changed)
|
||||
slices.SortFunc(changed, func(left, right Path) int { return cmp.Compare(left.String(), right.String()) })
|
||||
return changed
|
||||
}
|
||||
|
||||
func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor, path Path, lookups map[string]secretLookupResult, changed *[]Path) {
|
||||
func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor, secrets SecretBinding, path Path, lookups map[string]secretLookupResult, changed *[]Path) {
|
||||
if descriptor.kind == descriptorPointer {
|
||||
if !value.IsNil() {
|
||||
collectChangedSecretStates(value.Elem(), descriptor.element, path, lookups, changed)
|
||||
collectChangedSecretStates(value.Elem(), descriptor.element, secrets, path, lookups, changed)
|
||||
}
|
||||
return
|
||||
}
|
||||
switch descriptor.kind {
|
||||
case descriptorSecret:
|
||||
reference := value.Interface().(Secret)
|
||||
expected, ok := lookups[reference.EnvName()]
|
||||
if ok && (!expected.present || expected.value == "" || !reference.resolved || reference.value != expected.value) {
|
||||
reference := secrets.Read(value)
|
||||
expected, ok := lookups[reference.Name]
|
||||
if ok && (!expected.present || expected.value == "" || !reference.Resolved || reference.Value != expected.value) {
|
||||
*changed = append(*changed, path.clone())
|
||||
}
|
||||
case descriptorStruct:
|
||||
@@ -192,7 +147,7 @@ func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor,
|
||||
if !member.inline {
|
||||
memberPath = path.Key(member.name)
|
||||
}
|
||||
collectChangedSecretStates(value.Field(member.index), member.descriptor, memberPath, lookups, changed)
|
||||
collectChangedSecretStates(value.Field(member.index), member.descriptor, secrets, memberPath, lookups, changed)
|
||||
}
|
||||
case descriptorMap:
|
||||
if value.IsNil() {
|
||||
@@ -201,14 +156,14 @@ func collectChangedSecretStates(value reflect.Value, descriptor *typeDescriptor,
|
||||
keys := value.MapKeys()
|
||||
slices.SortFunc(keys, func(left, right reflect.Value) int { return cmp.Compare(left.String(), right.String()) })
|
||||
for _, key := range keys {
|
||||
collectChangedSecretStates(value.MapIndex(key), descriptor.element, path.Key(key.String()), lookups, changed)
|
||||
collectChangedSecretStates(value.MapIndex(key), descriptor.element, secrets, path.Key(key.String()), lookups, changed)
|
||||
}
|
||||
case descriptorSlice, descriptorArray:
|
||||
if descriptor.kind == descriptorSlice && value.IsNil() {
|
||||
return
|
||||
}
|
||||
for index := 0; index < value.Len(); index++ {
|
||||
collectChangedSecretStates(value.Index(index), descriptor.element, path.Index(index), lookups, changed)
|
||||
collectChangedSecretStates(value.Index(index), descriptor.element, secrets, path.Index(index), lookups, changed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ sequence: [SHARED_SECRET, SEQUENCE_SECRET]
|
||||
return value, present
|
||||
}
|
||||
validationCalled := false
|
||||
result, err := loadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{
|
||||
result, err := testLoadWithSecretLookup(Bytes("secrets.yml", []byte(input)), lookup, Options[secretReferenceConfig]{
|
||||
Definition: Definition[secretReferenceConfig]{Validate: func(configuration *secretReferenceConfig, validation *Validation) {
|
||||
validationCalled = true
|
||||
if configuration.Required.Value() != "shared-value" || !configuration.Required.IsResolved() ||
|
||||
@@ -62,7 +62,7 @@ sequence: [SHARED_SECRET, SEQUENCE_SECRET]
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("loadWithSecretLookup() error = %v", err)
|
||||
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
|
||||
}
|
||||
if !validationCalled || result.Config.Optional == nil || result.Config.Pointers["nil"] != nil {
|
||||
t.Fatalf("resolved config = %#v", result.Config)
|
||||
@@ -94,7 +94,7 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
|
||||
if lookup == nil {
|
||||
lookup = func(string) (string, bool) { return "", false }
|
||||
}
|
||||
_, err := loadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{})
|
||||
_, err := testLoadWithSecretLookup(Bytes("secret-errors.yml", []byte(test.input)), lookup, Options[secretReferenceConfig]{})
|
||||
loadError := requireSecretLoadCode(t, err, test.code)
|
||||
diagnostic := loadError.Diagnostics()[0]
|
||||
if diagnostic.Path.String() != "/required" || strings.Contains(fmt.Sprintf("%v %#v", err, err), "CREDENTIAL-CANARY") {
|
||||
@@ -107,9 +107,9 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
|
||||
type config struct {
|
||||
Optional *Secret `yaml:"optional"`
|
||||
}
|
||||
_, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
||||
_, err := testLoadWithSecretLookup(Bytes("optional.yml", []byte("optional: ''\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
||||
requireSecretLoadCode(t, err, CodeSecretInvalidName)
|
||||
result, err := loadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
||||
result, err := testLoadWithSecretLookup(Bytes("optional.yml", []byte("optional: null\n")), func(string) (string, bool) { return "", false }, Options[config]{})
|
||||
if err != nil || result.Config.Optional != nil {
|
||||
t.Fatalf("nil optional result = %#v, %v", result, err)
|
||||
}
|
||||
@@ -120,7 +120,7 @@ func TestSecretReferenceErrorsAreSafeAndPathSpecific(t *testing.T) {
|
||||
Secrets []Secret `yaml:"secrets"`
|
||||
}
|
||||
lookups := 0
|
||||
_, err := loadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) {
|
||||
_, err := testLoadWithSecretLookup(Bytes("missing-twice.yml", []byte("secrets: [MISSING_SHARED, MISSING_SHARED]\n")), func(string) (string, bool) {
|
||||
lookups++
|
||||
return "", false
|
||||
}, Options[config]{})
|
||||
@@ -138,7 +138,7 @@ func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) {
|
||||
}
|
||||
lookedUp := []string{}
|
||||
validated := ""
|
||||
result, err := loadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(Bytes("resolved-secret.yml", []byte("name: SELECTED_SECRET\nsecret: OLD_SECRET\n")), func(name string) (string, bool) {
|
||||
lookedUp = append(lookedUp, name)
|
||||
return "resolved-value", true
|
||||
}, Options[config]{Definition: Definition[config]{
|
||||
@@ -151,7 +151,7 @@ func TestResolutionSelectsEffectiveSecretNameBeforeLookup(t *testing.T) {
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("loadWithSecretLookup() error = %v", err)
|
||||
t.Fatalf("testLoadWithSecretLookup() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(lookedUp, []string{"SELECTED_SECRET"}) || validated != "resolved-value" || result.Config.Secret.EnvName() != "SELECTED_SECRET" {
|
||||
t.Fatalf("lookups = %#v, validated = %q, config = %#v", lookedUp, validated, result.Config)
|
||||
@@ -172,7 +172,7 @@ func TestResolutionInputQueriesDoNotReadOrResolveSecrets(t *testing.T) {
|
||||
reader := &resolutionQueryReader{input: strings.NewReader("value: source\nsecret: QUERY_SECRET\n")}
|
||||
lookups := 0
|
||||
var callbackErr error
|
||||
result, err := loadWithSecretLookup(Reader("query.yml", reader), func(name string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(Reader("query.yml", reader), func(name string) (string, bool) {
|
||||
lookups++
|
||||
if name != "QUERY_SECRET" {
|
||||
t.Fatalf("secret lookup name = %q", name)
|
||||
@@ -210,7 +210,7 @@ func TestValidationCannotReplaceResolvedSecretWithSameName(t *testing.T) {
|
||||
type config struct {
|
||||
Secret Secret `yaml:"secret"`
|
||||
}
|
||||
_, err := loadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) {
|
||||
_, err := testLoadWithSecretLookup(Bytes("validator.yml", []byte("secret: VALIDATOR_SECRET\n")), func(string) (string, bool) {
|
||||
return "CREDENTIAL-CANARY", true
|
||||
}, Options[config]{Definition: Definition[config]{Validate: func(configuration *config, validation *Validation) {
|
||||
configuration.Secret = SecretEnv(configuration.Secret.EnvName())
|
||||
@@ -225,13 +225,13 @@ func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) {
|
||||
type config struct {
|
||||
Secret Secret `yaml:"secret"`
|
||||
}
|
||||
first, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{
|
||||
first, err := testLoadWithSecretLookup(NoSource(), func(string) (string, bool) { return "first-value", true }, Options[config]{
|
||||
Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("DEFAULT_SECRET")} }},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := loadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{
|
||||
second, err := testLoadWithSecretLookup(NoSource(), func(string) (string, bool) { return "second-value", true }, Options[config]{
|
||||
Definition: Definition[config]{Defaults: func() config { return first.Config }},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -243,14 +243,14 @@ func TestResolvedDefaultsAreRebuiltAndLaterLoadsRelookup(t *testing.T) {
|
||||
|
||||
t.Setenv("CONFIGKIT_RELOOKUP_SECRET", "one")
|
||||
options := Options[config]{Definition: Definition[config]{Defaults: func() config { return config{Secret: SecretEnv("CONFIGKIT_RELOOKUP_SECRET")} }}}
|
||||
realFirst, err := Load(NoSource(), options)
|
||||
realFirst, err := testLoad(NoSource(), options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Setenv("CONFIGKIT_RELOOKUP_SECRET", "two"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
realSecond, err := Load(NoSource(), options)
|
||||
realSecond, err := testLoad(NoSource(), options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -276,7 +276,7 @@ func TestSecretSafeFormattingJSONProjectionAndHistory(t *testing.T) {
|
||||
type config struct {
|
||||
Secret Secret `yaml:"secret"`
|
||||
}
|
||||
result, err := loadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(Bytes("safe-secret.yml", []byte("secret: SAFE_SECRET_NAME\n")), func(string) (string, bool) {
|
||||
return credential, true
|
||||
}, Options[config]{})
|
||||
if err != nil {
|
||||
@@ -317,7 +317,7 @@ func TestBootstrapExistingEmptyWinsAndReferenceFails(t *testing.T) {
|
||||
type config struct {
|
||||
Secret Secret `yaml:"secret"`
|
||||
}
|
||||
_, err = Load[config](Bytes("blocked.yml", []byte("secret: "+name+"\n")))
|
||||
_, err = testLoad[config](Bytes("blocked.yml", []byte("secret: "+name+"\n")))
|
||||
requireSecretLoadCode(t, err, CodeSecretEmpty)
|
||||
if value, present := os.LookupEnv(name); !present || value != "" {
|
||||
t.Fatalf("environment changed to %q, %v", value, present)
|
||||
@@ -339,7 +339,7 @@ func TestDynamicValuesDoNotEstablishSecretReferences(t *testing.T) {
|
||||
}
|
||||
}
|
||||
lookups := 0
|
||||
result, err := loadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) {
|
||||
result, err := testLoadWithSecretLookup(Bytes("dynamic.yml", []byte("dynamic: ORDINARY_STRING\n")), func(string) (string, bool) {
|
||||
lookups++
|
||||
return "unexpected", true
|
||||
}, Options[config]{})
|
||||
@@ -354,9 +354,9 @@ func TestLoadDoesNotMutateSecretEnvironment(t *testing.T) {
|
||||
type config struct {
|
||||
Secret Secret `yaml:"secret"`
|
||||
}
|
||||
result, err := Load[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n")))
|
||||
result, err := testLoad[config](Bytes("unchanged.yml", []byte("secret: "+name+"\n")))
|
||||
if err != nil || result.Config.Secret.Value() != "original" {
|
||||
t.Fatalf("Load() = %#v, %v", result, err)
|
||||
t.Fatalf("testLoad() = %#v, %v", result, err)
|
||||
}
|
||||
if got := os.Getenv(name); got != "original" {
|
||||
t.Fatalf("environment = %q", got)
|
||||
|
||||
@@ -26,7 +26,7 @@ func TestNonregularSourcesDoNotBlock(t *testing.T) {
|
||||
case "profile":
|
||||
opts.Profile = "fifo"
|
||||
}
|
||||
_, err := Load(source, opts)
|
||||
_, err := testLoad(source, opts)
|
||||
var loadErr *LoadError
|
||||
if !errors.As(err, &loadErr) {
|
||||
t.Fatalf("expected safe load error, got %v", err)
|
||||
|
||||
@@ -110,7 +110,7 @@ func runValidation[T any](configuration *T, validator func(*T, *Validation), sch
|
||||
return canonicalDiagnostics(state.diagnostics)
|
||||
}
|
||||
state.diagnostics = append(state.diagnostics, validationValueMutations(before, after)...)
|
||||
for _, path := range changedSecretStates(reflect.ValueOf(*configuration), schema.root, lookups) {
|
||||
for _, path := range changedSecretStates(reflect.ValueOf(*configuration), schema, lookups) {
|
||||
state.diagnostics = append(state.diagnostics, validationMutationDiagnostic(path))
|
||||
}
|
||||
return canonicalDiagnostics(state.diagnostics)
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestValidationCollectsSchemaAwareReportsAndNearestLocations(t *testing.T) {
|
||||
Missing string `yaml:"missing"`
|
||||
Dynamic map[string]any `yaml:"dynamic"`
|
||||
}
|
||||
result, err := Load(Bytes("validation.yml", []byte("primary: configured\ndynamic: {present: 1}\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("validation.yml", []byte("primary: configured\ndynamic: {present: 1}\n")), Options[config]{Definition: Definition[config]{
|
||||
Validate: func(_ *config, validation *Validation) {
|
||||
validation.Error(Keys("missing"), "is required", Keys("primary"))
|
||||
validation.Error(Keys("dynamic", "missing"), "dynamic entry is required")
|
||||
@@ -58,7 +58,7 @@ func TestValidationRejectsUnknownStructPaths(t *testing.T) {
|
||||
type config struct {
|
||||
Known string `yaml:"known"`
|
||||
}
|
||||
result, err := Load(Bytes("validation-path.yml", []byte("known: value\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("validation-path.yml", []byte("known: value\n")), Options[config]{Definition: Definition[config]{
|
||||
Validate: func(_ *config, validation *Validation) {
|
||||
validation.Error(Keys("unknown"), "must be configured")
|
||||
},
|
||||
@@ -73,7 +73,7 @@ func TestValidationMutationFailsWithoutPublishingResult(t *testing.T) {
|
||||
Value int `yaml:"value"`
|
||||
Items map[string]int `yaml:"items"`
|
||||
}
|
||||
result, err := Load(Bytes("mutation.yml", []byte("value: 1\nitems: {one: 1}\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("mutation.yml", []byte("value: 1\nitems: {one: 1}\n")), Options[config]{Definition: Definition[config]{
|
||||
Validate: func(configuration *config, _ *Validation) {
|
||||
configuration.Value = 2
|
||||
configuration.Items["one"] = 2
|
||||
@@ -127,7 +127,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
|
||||
validation.Warning(Keys("value"), "validation warning")
|
||||
},
|
||||
}
|
||||
result, err := Load(Bytes("order.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
||||
result, err := testLoad(Bytes("order.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
|
||||
Resolve: func(_ *config, _ *Resolution) error { events = append(events, "resolve"); return nil },
|
||||
Validate: func(_ *config, _ *Validation) { events = append(events, "validate") },
|
||||
}
|
||||
result, err := Load(Bytes("order-error.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
||||
result, err := testLoad(Bytes("order-error.yml", []byte("version: one\n")), Options[config]{Definition: definition})
|
||||
assertLoadDiagnostic(t, result, err, CodeVersionError)
|
||||
if !reflect.DeepEqual(events, []string{"version"}) {
|
||||
t.Fatalf("events = %#v", events)
|
||||
@@ -157,7 +157,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
|
||||
|
||||
t.Run("resolution error stops validation", func(t *testing.T) {
|
||||
var validated bool
|
||||
result, err := Load(Bytes("resolution-order.yml", []byte("value: 1\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("resolution-order.yml", []byte("value: 1\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
||||
Resolve: func(_ *config, resolution *Resolution) error {
|
||||
resolution.Error(Keys("value"), "cannot resolve")
|
||||
return nil
|
||||
@@ -172,7 +172,7 @@ func TestHookOrderStopsAfterErrorsAndContinuesAfterWarnings(t *testing.T) {
|
||||
|
||||
t.Run("decode error stops application hooks", func(t *testing.T) {
|
||||
var resolved, validated bool
|
||||
result, err := Load(Bytes("decode-order.yml", []byte("value: wrong\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("decode-order.yml", []byte("value: wrong\nversion: one\n")), Options[config]{Definition: Definition[config]{
|
||||
Resolve: func(_ *config, _ *Resolution) error { resolved = true; return nil },
|
||||
Validate: func(_ *config, _ *Validation) { validated = true },
|
||||
}})
|
||||
@@ -190,7 +190,7 @@ func TestCompleteSingleSourceLifecycleKeepsMetadataAgreement(t *testing.T) {
|
||||
Input int `yaml:"input"`
|
||||
Derived int `yaml:"derived"`
|
||||
}
|
||||
result, err := Load(Bytes("complete.yml", []byte("input: 3\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad(Bytes("complete.yml", []byte("input: 3\n")), Options[config]{Definition: Definition[config]{
|
||||
Defaults: func() config { return config{Derived: 1} },
|
||||
Resolve: func(configuration *config, resolution *Resolution) error {
|
||||
Default(resolution, Keys("derived"), &configuration.Derived, configuration.Input*2, "derived from input", Keys("input"))
|
||||
|
||||
@@ -34,10 +34,10 @@ type valueOperation struct {
|
||||
}
|
||||
|
||||
func (o *valueOperation) dynamicDescriptor(current reflect.Type, path Path) (*typeDescriptor, *projectionIssue) {
|
||||
if current == secretType {
|
||||
if o.schema.secrets.Type != nil && current == o.schema.secrets.Type {
|
||||
return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration values cannot establish secret-reference semantics"}
|
||||
}
|
||||
builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool)}
|
||||
builder := schemaBuilder{cache: o.schema.types, building: make(map[reflect.Type]bool), secrets: o.schema.secrets}
|
||||
descriptor, issue := builder.describe(current, path)
|
||||
if issue != nil {
|
||||
return nil, &projectionIssue{code: CodeSchemaDynamicType, path: path.clone(), message: "dynamic configuration value has an unsupported type"}
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestVersionPathsShareCanonicalSequenceSemantics(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var checked Version
|
||||
result, err := Load[config](Bytes("version.yml", []byte("versions: ["+value+"]\n")), Options[config]{Definition: Definition[config]{
|
||||
result, err := testLoad[config](Bytes("version.yml", []byte("versions: ["+value+"]\n")), Options[config]{Definition: Definition[config]{
|
||||
Version: &VersionRule{Path: path, Check: func(v Version, _ *Validation) error { checked = v; return nil }},
|
||||
}})
|
||||
want := Version{}
|
||||
@@ -49,7 +49,7 @@ func TestVersionPathsPreserveLiteralMappingKeysAndSegmentIntent(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := Load[map[string]string](Bytes("version.yml", []byte("'"+key+"': 4\n")), Options[map[string]string]{Definition: Definition[map[string]string]{
|
||||
result, err := testLoad[map[string]string](Bytes("version.yml", []byte("'"+key+"': 4\n")), Options[map[string]string]{Definition: Definition[map[string]string]{
|
||||
Version: &VersionRule{Path: path, Check: func(v Version, _ *Validation) error {
|
||||
if !v.Present || v.Text != "4" {
|
||||
t.Errorf("checked = %#v", v)
|
||||
@@ -77,7 +77,7 @@ func TestVersionPathsPreserveLiteralMappingKeysAndSegmentIntent(t *testing.T) {
|
||||
type config struct {
|
||||
Versions any `yaml:"versions"`
|
||||
}
|
||||
_, err := Load[config](Bytes("version.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: test.path, Check: func(v Version, validation *Validation) error {
|
||||
_, err := testLoad[config](Bytes("version.yml", []byte(test.input)), Options[config]{Definition: Definition[config]{Version: &VersionRule{Path: test.path, Check: func(v Version, validation *Validation) error {
|
||||
if v.Present {
|
||||
t.Errorf("wrong segment kind found version: %#v", v)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user