Add internal engine relocation candidate
This commit is contained in:
225
internal/engine/secret_reference.go
Normal file
225
internal/engine/secret_reference.go
Normal file
@@ -0,0 +1,225 @@
|
||||
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 {
|
||||
value string
|
||||
present bool
|
||||
}
|
||||
|
||||
type secretResolutionResult struct {
|
||||
lookups map[string]secretLookupResult
|
||||
references map[string]string
|
||||
diagnostics []Diagnostic
|
||||
}
|
||||
|
||||
func resolveSecretReferences(root reflect.Value, schema *schemaDescriptor, input *document.Node, lookup secretLookup) secretResolutionResult {
|
||||
result := secretResolutionResult{
|
||||
lookups: make(map[string]secretLookupResult), references: make(map[string]string),
|
||||
}
|
||||
if schema == nil || schema.root == nil || !root.IsValid() || root.Type() != schema.root.typeOf {
|
||||
result.diagnostics = []Diagnostic{{Code: CodeSecretRequired, Severity: SeverityError, Message: "configuration value does not match its secret-reference schema"}}
|
||||
return result
|
||||
}
|
||||
if lookup == nil {
|
||||
lookup = func(string) (string, bool) { return "", false }
|
||||
}
|
||||
resolveSecretValue(root, schema.root, 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) {
|
||||
if descriptor.kind == descriptorPointer {
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
resolveSecretValue(value.Elem(), descriptor.element, path, descriptor.element.kind == descriptorSecret, input, lookup, result)
|
||||
return
|
||||
}
|
||||
|
||||
switch descriptor.kind {
|
||||
case descriptorSecret:
|
||||
reference := value.Interface().(Secret)
|
||||
name := reference.EnvName()
|
||||
if name == "" {
|
||||
code := CodeSecretRequired
|
||||
message := "required secret reference is absent"
|
||||
if optional || reference.specified {
|
||||
code = CodeSecretInvalidName
|
||||
message = "secret environment name is empty"
|
||||
}
|
||||
result.diagnostics = append(result.diagnostics, secretDiagnostic(code, path, message, input))
|
||||
return
|
||||
}
|
||||
if !validEnvironmentName(name) {
|
||||
result.diagnostics = append(result.diagnostics, secretDiagnostic(CodeSecretInvalidName, path, "secret environment name is invalid", input))
|
||||
return
|
||||
}
|
||||
found, exists := result.lookups[name]
|
||||
if !exists {
|
||||
found.value, found.present = lookup(name)
|
||||
result.lookups[name] = found
|
||||
}
|
||||
if !found.present {
|
||||
result.diagnostics = append(result.diagnostics, secretDiagnostic(CodeSecretUnset, path, "secret environment variable is not set", input))
|
||||
return
|
||||
}
|
||||
if found.value == "" {
|
||||
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}))
|
||||
result.references[path.String()] = name
|
||||
case descriptorStruct:
|
||||
for _, member := range descriptor.members {
|
||||
memberPath := path
|
||||
if !member.inline {
|
||||
memberPath = path.Key(member.name)
|
||||
}
|
||||
resolveSecretValue(value.Field(member.index), member.descriptor, memberPath, false, input, lookup, result)
|
||||
}
|
||||
case descriptorMap:
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
keys := value.MapKeys()
|
||||
slices.SortFunc(keys, func(left, right reflect.Value) int { return cmp.Compare(left.String(), right.String()) })
|
||||
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)
|
||||
value.SetMapIndex(key, entry)
|
||||
}
|
||||
case descriptorSlice, descriptorArray:
|
||||
if descriptor.kind == descriptorSlice && value.IsNil() {
|
||||
return
|
||||
}
|
||||
for index := 0; index < value.Len(); index++ {
|
||||
resolveSecretValue(value.Index(index), descriptor.element, path.Index(index), false, input, lookup, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func secretDiagnostic(code Code, path Path, message string, input *document.Node) Diagnostic {
|
||||
diagnostic := Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message}
|
||||
if location, ok := nearestDocumentLocation(input, path); ok {
|
||||
diagnostic.Primary = documentLocationPointer(location)
|
||||
}
|
||||
return diagnostic
|
||||
}
|
||||
|
||||
func changedSecretStates(root reflect.Value, descriptor *typeDescriptor, lookups map[string]secretLookupResult) []Path {
|
||||
var changed []Path
|
||||
collectChangedSecretStates(root, descriptor, 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) {
|
||||
if descriptor.kind == descriptorPointer {
|
||||
if !value.IsNil() {
|
||||
collectChangedSecretStates(value.Elem(), descriptor.element, 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) {
|
||||
*changed = append(*changed, path.clone())
|
||||
}
|
||||
case descriptorStruct:
|
||||
for _, member := range descriptor.members {
|
||||
memberPath := path
|
||||
if !member.inline {
|
||||
memberPath = path.Key(member.name)
|
||||
}
|
||||
collectChangedSecretStates(value.Field(member.index), member.descriptor, memberPath, lookups, changed)
|
||||
}
|
||||
case descriptorMap:
|
||||
if value.IsNil() {
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cloneSecretReferences(values map[string]string) map[string]string {
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := make(map[string]string, len(values))
|
||||
for path, name := range values {
|
||||
cloned[path] = name
|
||||
}
|
||||
return cloned
|
||||
}
|
||||
Reference in New Issue
Block a user