package engine import ( "cmp" "reflect" "slices" "go.maximumdirect.net/configkit/internal/document" ) 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 } type secretReferenceLocationResolver struct { input *document.Node lookup *documentLookup } func (resolver *secretReferenceLocationResolver) diagnostic(code Code, path Path, message string) Diagnostic { if resolver.lookup == nil { resolver.lookup = buildDocumentLookup(resolver.input) } diagnostic := Diagnostic{Code: code, Severity: SeverityError, Path: path.clone(), Message: message} if location, ok := nearestDocumentLocationWithLookup(resolver.input, path, resolver.lookup); ok { diagnostic.Primary = documentLocationPointer(location) } return 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 } } locations := &secretReferenceLocationResolver{input: input} walkSecretSlots(root, schema.root, secretSlotsWritable, func(value reflect.Value, path Path, optional bool) { resolveSecretReference(value, schema.secrets, path, optional, locations, lookup, &result) }) result.diagnostics = canonicalDiagnostics(result.diagnostics) return result } func resolveSecretReference(value reflect.Value, secrets SecretBinding, path Path, optional bool, locations *secretReferenceLocationResolver, lookup secretLookup, result *secretResolutionResult) { reference := secrets.Read(value) name := reference.Name 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, locations.diagnostic(code, path, message)) return } if !validEnvironmentName(name) { result.diagnostics = append(result.diagnostics, locations.diagnostic(CodeSecretInvalidName, path, "secret environment name is invalid")) 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, locations.diagnostic(CodeSecretUnset, path, "secret environment variable is not set")) return } if found.value == "" { result.diagnostics = append(result.diagnostics, locations.diagnostic(CodeSecretEmpty, path, "secret environment variable is empty")) return } value.Set(secrets.Make(SecretState{Name: name, Value: found.value, Specified: true, Resolved: true})) result.references[path.String()] = name } func changedSecretStates(root reflect.Value, schema *schemaDescriptor, lookups map[string]secretLookupResult) []Path { var changed []Path if schema == nil || schema.root == nil { return nil } walkSecretSlots(root, schema.root, secretSlotsReadOnly, func(value reflect.Value, path Path, _ bool) { reference := schema.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()) } }) slices.SortFunc(changed, func(left, right Path) int { return cmp.Compare(left.String(), right.String()) }) return 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 }