Add configuration path discovery

This commit is contained in:
2026-11-07 04:44:01 +00:00
parent aeb70e5873
commit b38130548e
8 changed files with 307 additions and 20 deletions

View File

@@ -3,6 +3,7 @@ package ckconfig
import (
"bytes"
"fmt"
"strings"
"text/template"
commandkit "go.maximumdirect.net/commandkit"
@@ -16,8 +17,37 @@ type explanationReport struct {
Origin explanationOriginDTO `json:"origin" yaml:"origin"`
History []explanationChangeDTO `json:"history" yaml:"history"`
SecretReference *secretReferenceDTO `json:"secret_reference" yaml:"secret_reference"`
Metadata *explanationMetadata `json:"metadata,omitempty"`
}
type explanationMetadata struct {
Explicit *bool `json:"explicit,omitempty"`
Null *bool `json:"null,omitempty"`
History []metadataHistoryRecord `json:"history,omitempty"`
}
type metadataHistoryRecord struct {
Path string `json:"path"`
Previous []explanationOriginDTO `json:"previous,omitempty"`
}
type pathDiscoveryReport struct {
Scope string `json:"scope"`
Paths []pathDiscoveryEntry `json:"paths"`
}
type pathDiscoveryEntry struct {
Path string `json:"path"`
Present bool `json:"present"`
Metadata *pathMetadata `json:"metadata,omitempty"`
}
type pathMetadata struct {
Explicit bool `json:"explicit"`
}
var pathDiscoveryTemplate = template.Must(commandkit.ParseTextTemplate("paths", "{{range .Paths}}{{printf \"%q\" .Path}} {{if .Present}}{{if .Metadata.Explicit}}explicit{{else}}non-explicit{{end}}{{else}}historical{{end}}\n{{end}}", nil))
type snapshotDTO struct {
Present bool `json:"present" yaml:"present"`
Redacted bool `json:"redacted" yaml:"redacted"`
@@ -72,6 +102,31 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
}
if plan.list {
if acquired.Value.Metadata == nil {
return configFailure("Configuration scope did not provide path metadata.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path discovery without metadata", name))
}
report := pathDiscoveryReport{Scope: name, Paths: []pathDiscoveryEntry{}}
for _, path := range acquired.Value.Metadata.HistoricalPaths() {
if plan.under != nil && !pathWithin(path, *plan.under) {
continue
}
info, present := acquired.Value.Metadata.Lookup(path)
if !present && (!plan.historical || plan.explicit.Set) {
continue
}
if present && plan.explicit.Set && info.Explicit != plan.explicit.Value {
continue
}
entry := pathDiscoveryEntry{Path: path.String(), Present: present}
if present {
entry.Metadata = &pathMetadata{Explicit: info.Explicit}
}
report.Paths = append(report.Paths, entry)
}
_ = reporter.Report(commandkit.Report{Value: report, TextTemplate: pathDiscoveryTemplate})
return commandkit.Success()
}
if acquired.Value.Explanation == nil {
cause := fmt.Errorf("configuration scope %q did not provide an explanation reader", name)
return configFailure("Configuration scope did not provide explanation data.", acquired.Explanation, cause)
@@ -85,20 +140,59 @@ func runConfigExplanation[I, S any](invocation commandkit.Invocation, plan *conf
if err := invocation.Context.Err(); err != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, err)
}
report := makeExplanationReport(name, explanation)
if acquired.Value.Metadata != nil {
metadata := &explanationMetadata{}
if info, ok := acquired.Value.Metadata.Lookup(plan.path); ok {
metadata.Explicit, metadata.Null = &info.Explicit, &info.Null
}
for _, change := range acquired.Value.Metadata.History(plan.path) {
entry := metadataHistoryRecord{Path: change.Path.String()}
for _, previous := range change.Previous {
entry.Previous = append(entry.Previous, originReport(previous))
}
metadata.History = append(metadata.History, entry)
}
if metadata.Explicit != nil || len(metadata.History) > 0 {
report.Metadata = metadata
}
}
text := template.Must(commandkit.ParseTextTemplate("explanation", "{{render .}}", template.FuncMap{
"render": func(explanationReport) (string, error) {
"render": func(report explanationReport) (string, error) {
var output bytes.Buffer
err := acquired.Value.Explanation.WriteExplanation(&output, plan.path)
if err == nil && report.Metadata != nil && report.Metadata.Explicit != nil {
_, err = fmt.Fprintf(&output, "Explicit: %t\nNull: %t\n", *report.Metadata.Explicit, *report.Metadata.Null)
}
return output.String(), err
},
}))
renderErr := reporter.Report(commandkit.Report{Value: makeExplanationReport(name, explanation), TextTemplate: text})
renderErr := reporter.Report(commandkit.Report{Value: report, TextTemplate: text})
if renderErr != nil && invocation.Context.Err() != nil {
return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err())
}
return commandkit.Success()
}
func pathWithin(path, parent configkit.Path) bool {
tokens := func(value string) []string {
if value == "" {
return nil
}
return strings.Split(strings.TrimPrefix(value, "/"), "/")
}
child, prefix := tokens(path.String()), tokens(parent.String())
if len(prefix) > len(child) {
return false
}
for index := range prefix {
if prefix[index] != child[index] {
return false
}
}
return true
}
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
report := explanationReport{
Scope: scope,