package engine import ( "cmp" "reflect" "slices" "gitea.maximumdirect.net/eric/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} resolveSecretValue(root, schema.root, schema.secrets, Path{}, false, locations, lookup, &result) result.diagnostics = canonicalDiagnostics(result.diagnostics) return result } func resolveSecretValue(value reflect.Value, descriptor *typeDescriptor, secrets SecretBinding, path Path, optional bool, locations *secretReferenceLocationResolver, lookup secretLookup, result *secretResolutionResult) { if descriptor.kind == descriptorPointer { if value.IsNil() { return } resolveSecretValue(value.Elem(), descriptor.element, secrets, path, descriptor.element.kind == descriptorSecret, locations, lookup, result) return } switch descriptor.kind { case descriptorSecret: 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 case descriptorStruct: for _, member := range descriptor.members { memberPath := path if !member.inline { memberPath = path.Key(member.name) } resolveSecretValue(value.Field(member.index), member.descriptor, secrets, memberPath, false, locations, 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, secrets, path.Key(key.String()), false, locations, 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, secrets, path.Index(index), false, locations, lookup, result) } } } func changedSecretStates(root reflect.Value, schema *schemaDescriptor, lookups map[string]secretLookupResult) []Path { var changed []Path 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, secrets SecretBinding, path Path, lookups map[string]secretLookupResult, changed *[]Path) { if descriptor.kind == descriptorPointer { if !value.IsNil() { collectChangedSecretStates(value.Elem(), descriptor.element, secrets, path, lookups, changed) } return } switch descriptor.kind { case descriptorSecret: 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: for _, member := range descriptor.members { memberPath := path if !member.inline { memberPath = path.Key(member.name) } collectChangedSecretStates(value.Field(member.index), member.descriptor, secrets, 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, 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, secrets, 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 }