2026-09-12 15:58:06 +00:00
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
}
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 }
}
2026-09-12 16:04:31 +00:00
resolveSecretValue ( root , schema . root , schema . secrets , Path { } , false , input , lookup , & result )
2026-09-12 15:58:06 +00:00
result . diagnostics = canonicalDiagnostics ( result . diagnostics )
return result
}
2026-09-12 16:04:31 +00:00
func resolveSecretValue ( value reflect . Value , descriptor * typeDescriptor , secrets SecretBinding , path Path , optional bool , input * document . Node , lookup secretLookup , result * secretResolutionResult ) {
2026-09-12 15:58:06 +00:00
if descriptor . kind == descriptorPointer {
if value . IsNil ( ) {
return
}
2026-09-12 16:04:31 +00:00
resolveSecretValue ( value . Elem ( ) , descriptor . element , secrets , path , descriptor . element . kind == descriptorSecret , input , lookup , result )
2026-09-12 15:58:06 +00:00
return
}
switch descriptor . kind {
case descriptorSecret :
2026-09-12 16:04:31 +00:00
reference := secrets . Read ( value )
name := reference . Name
2026-09-12 15:58:06 +00:00
if name == "" {
code := CodeSecretRequired
message := "required secret reference is absent"
2026-09-12 16:04:31 +00:00
if optional || reference . Specified {
2026-09-12 15:58:06 +00:00
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
}
2026-09-12 16:04:31 +00:00
value . Set ( secrets . Make ( SecretState { Name : name , Value : found . value , Specified : true , Resolved : true } ) )
2026-09-12 15:58:06 +00:00
result . references [ path . String ( ) ] = name
case descriptorStruct :
for _ , member := range descriptor . members {
memberPath := path
if ! member . inline {
memberPath = path . Key ( member . name )
}
2026-09-12 16:04:31 +00:00
resolveSecretValue ( value . Field ( member . index ) , member . descriptor , secrets , memberPath , false , input , lookup , result )
2026-09-12 15:58:06 +00:00
}
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 ) )
2026-09-12 16:04:31 +00:00
resolveSecretValue ( entry , descriptor . element , secrets , path . Key ( key . String ( ) ) , false , input , lookup , result )
2026-09-12 15:58:06 +00:00
value . SetMapIndex ( key , entry )
}
case descriptorSlice , descriptorArray :
if descriptor . kind == descriptorSlice && value . IsNil ( ) {
return
}
for index := 0 ; index < value . Len ( ) ; index ++ {
2026-09-12 16:04:31 +00:00
resolveSecretValue ( value . Index ( index ) , descriptor . element , secrets , path . Index ( index ) , false , input , lookup , result )
2026-09-12 15:58:06 +00:00
}
}
}
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
}
2026-09-12 16:04:31 +00:00
func changedSecretStates ( root reflect . Value , schema * schemaDescriptor , lookups map [ string ] secretLookupResult ) [ ] Path {
2026-09-12 15:58:06 +00:00
var changed [ ] Path
2026-09-12 16:04:31 +00:00
if schema == nil || schema . root == nil {
return nil
}
collectChangedSecretStates ( root , schema . root , schema . secrets , Path { } , lookups , & changed )
2026-09-12 15:58:06 +00:00
slices . SortFunc ( changed , func ( left , right Path ) int { return cmp . Compare ( left . String ( ) , right . String ( ) ) } )
return changed
}
2026-09-12 16:04:31 +00:00
func collectChangedSecretStates ( value reflect . Value , descriptor * typeDescriptor , secrets SecretBinding , path Path , lookups map [ string ] secretLookupResult , changed * [ ] Path ) {
2026-09-12 15:58:06 +00:00
if descriptor . kind == descriptorPointer {
if ! value . IsNil ( ) {
2026-09-12 16:04:31 +00:00
collectChangedSecretStates ( value . Elem ( ) , descriptor . element , secrets , path , lookups , changed )
2026-09-12 15:58:06 +00:00
}
return
}
switch descriptor . kind {
case descriptorSecret :
2026-09-12 16:04:31 +00:00
reference := secrets . Read ( value )
expected , ok := lookups [ reference . Name ]
if ok && ( ! expected . present || expected . value == "" || ! reference . Resolved || reference . Value != expected . value ) {
2026-09-12 15:58:06 +00:00
* changed = append ( * changed , path . clone ( ) )
}
case descriptorStruct :
for _ , member := range descriptor . members {
memberPath := path
if ! member . inline {
memberPath = path . Key ( member . name )
}
2026-09-12 16:04:31 +00:00
collectChangedSecretStates ( value . Field ( member . index ) , member . descriptor , secrets , memberPath , lookups , changed )
2026-09-12 15:58:06 +00:00
}
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 {
2026-09-12 16:04:31 +00:00
collectChangedSecretStates ( value . MapIndex ( key ) , descriptor . element , secrets , path . Key ( key . String ( ) ) , lookups , changed )
2026-09-12 15:58:06 +00:00
}
case descriptorSlice , descriptorArray :
if descriptor . kind == descriptorSlice && value . IsNil ( ) {
return
}
for index := 0 ; index < value . Len ( ) ; index ++ {
2026-09-12 16:04:31 +00:00
collectChangedSecretStates ( value . Index ( index ) , descriptor . element , secrets , path . Index ( index ) , lookups , changed )
2026-09-12 15:58:06 +00:00
}
}
}
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
}