Bind engine secret handling to facade types

This commit is contained in:
2026-09-12 16:04:31 +00:00
parent 9b4f9ac50e
commit da02a19f2c
28 changed files with 348 additions and 232 deletions

View File

@@ -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)
}
}
}