2026-10-07 14:17:46 +00:00
|
|
|
package commandkit
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
|
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-09 01:46:19 +00:00
|
|
|
func newConfigExplainCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) {
|
|
|
|
|
flags := configInputFlags(prepared.suite, "text")
|
2026-10-07 14:17:46 +00:00
|
|
|
flags = append(flags, ValueFlag[configInput[C]](FlagMeta{
|
|
|
|
|
Name: "path",
|
|
|
|
|
Usage: "Configuration path as a JSON Pointer; an empty value selects the root.",
|
|
|
|
|
Required: true,
|
|
|
|
|
}, nil, parseString, func(input *configInput[C], value Supplied[string]) {
|
|
|
|
|
input.Path = value
|
|
|
|
|
}))
|
|
|
|
|
spec := CommandSpec[configInput[C]]{
|
|
|
|
|
Name: string(ConfigExplain),
|
|
|
|
|
Summary: "Explain the effective and historical value at a configuration path.",
|
|
|
|
|
Flags: flags,
|
|
|
|
|
CheckInput: func(input configInput[C]) error {
|
2026-10-09 01:46:19 +00:00
|
|
|
if _, err := preflightConfig(prepared, input, ConfigExplain); err != nil {
|
2026-10-07 14:17:46 +00:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !input.Path.Set {
|
|
|
|
|
return fmt.Errorf("--path is required")
|
|
|
|
|
}
|
|
|
|
|
if _, err := configkit.ParsePath(input.Path.Value); err != nil {
|
|
|
|
|
return fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
Run: func(invocation Invocation, input configInput[C]) Outcome {
|
2026-10-09 01:46:19 +00:00
|
|
|
return runConfigExplanation(invocation, prepared, input)
|
2026-10-07 14:17:46 +00:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
return NewCommand(spec)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type explanationReport struct {
|
|
|
|
|
Scope string `yaml:"scope"`
|
|
|
|
|
Path string `yaml:"path"`
|
|
|
|
|
Effective snapshotDTO `yaml:"effective"`
|
|
|
|
|
Origin explanationOriginDTO `yaml:"origin"`
|
|
|
|
|
History []explanationChangeDTO `yaml:"history"`
|
|
|
|
|
SecretReference *secretReferenceDTO `yaml:"secret_reference"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type snapshotDTO struct {
|
|
|
|
|
Present bool `yaml:"present"`
|
|
|
|
|
Redacted bool `yaml:"redacted"`
|
|
|
|
|
YAML string `yaml:"yaml"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type explanationOriginDTO struct {
|
|
|
|
|
Layer string `yaml:"layer"`
|
|
|
|
|
Locations []locationDTO `yaml:"locations"`
|
|
|
|
|
Operation string `yaml:"operation"`
|
|
|
|
|
Inheritance []inheritanceStepDTO `yaml:"inheritance"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type inheritanceStepDTO struct {
|
|
|
|
|
From string `yaml:"from"`
|
|
|
|
|
To string `yaml:"to"`
|
|
|
|
|
Reference string `yaml:"reference"`
|
|
|
|
|
Locations []locationDTO `yaml:"locations"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type explanationChangeDTO struct {
|
|
|
|
|
Before snapshotDTO `yaml:"before"`
|
|
|
|
|
After snapshotDTO `yaml:"after"`
|
|
|
|
|
Origin explanationOriginDTO `yaml:"origin"`
|
|
|
|
|
Reasons []string `yaml:"reasons"`
|
|
|
|
|
Dependencies []string `yaml:"dependencies"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type secretReferenceDTO struct {
|
|
|
|
|
Name string `yaml:"name"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-09 01:46:19 +00:00
|
|
|
func runConfigExplanation[C any](invocation Invocation, prepared *preparedConfigSuite[C], input configInput[C]) Outcome {
|
2026-10-07 16:47:28 +00:00
|
|
|
if err := invocation.Context.Err(); err != nil {
|
|
|
|
|
return Failure(err)
|
|
|
|
|
}
|
2026-10-09 01:46:19 +00:00
|
|
|
scopeNames, err := selectConfigScopes(prepared, input, ConfigExplain)
|
2026-10-07 14:17:46 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return Failure(err)
|
|
|
|
|
}
|
|
|
|
|
if !input.Path.Set {
|
|
|
|
|
return Failure(fmt.Errorf("--path is required"))
|
|
|
|
|
}
|
|
|
|
|
path, err := configkit.ParsePath(input.Path.Value)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return Usage(fmt.Errorf("invalid configuration path %q: %w", input.Path.Value, err))
|
|
|
|
|
}
|
|
|
|
|
name := scopeNames[0]
|
2026-10-09 01:46:19 +00:00
|
|
|
contexts := contextsForScopes(prepared, input, scopeNames)
|
|
|
|
|
scope, ok := findConfigScope(prepared, name)
|
2026-10-07 14:17:46 +00:00
|
|
|
if !ok || scope.Acquire == nil {
|
|
|
|
|
return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name))
|
|
|
|
|
}
|
2026-10-07 16:47:28 +00:00
|
|
|
if err := invocation.Context.Err(); err != nil {
|
|
|
|
|
return explanationFailure(err, nil, false)
|
|
|
|
|
}
|
2026-10-07 14:17:46 +00:00
|
|
|
acquired, acquireErr := scope.Acquire(invocation.Context, ConfigRequest{Operation: ConfigExplain, Selection: input.Selection}, contexts[name])
|
|
|
|
|
diagnostics := append([]Diagnostic(nil), acquired.Diagnostics...)
|
|
|
|
|
if acquireErr != nil {
|
|
|
|
|
return explanationFailure(acquireErr, diagnostics, false)
|
|
|
|
|
}
|
2026-10-07 16:47:28 +00:00
|
|
|
if err := invocation.Context.Err(); err != nil {
|
|
|
|
|
return explanationFailure(err, diagnostics, false)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-07 14:17:46 +00:00
|
|
|
if acquired.Value.Explanation == nil {
|
|
|
|
|
return explanationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false)
|
|
|
|
|
}
|
|
|
|
|
explanation, found := acquired.Value.Explanation.Explain(path)
|
|
|
|
|
if !found {
|
|
|
|
|
return explanationFailure(fmt.Errorf("configuration path %q is not known in scope %q", path.String(), name), diagnostics, false)
|
|
|
|
|
}
|
2026-10-07 16:47:28 +00:00
|
|
|
if err := invocation.Context.Err(); err != nil {
|
|
|
|
|
return explanationFailure(err, diagnostics, false)
|
|
|
|
|
}
|
2026-10-07 14:17:46 +00:00
|
|
|
if input.Format != "yaml" {
|
|
|
|
|
return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, path, diagnostics)
|
|
|
|
|
}
|
|
|
|
|
var report bytes.Buffer
|
|
|
|
|
if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil {
|
|
|
|
|
return explanationFailure(err, diagnostics, false)
|
|
|
|
|
}
|
2026-10-07 16:47:28 +00:00
|
|
|
if err := invocation.Context.Err(); err != nil {
|
|
|
|
|
return explanationFailure(err, diagnostics, false)
|
|
|
|
|
}
|
2026-10-07 14:17:46 +00:00
|
|
|
if err := writeReport(invocation.Streams.Out, report.Bytes()); err != nil {
|
|
|
|
|
outcome := explanationFailure(err, diagnostics, true)
|
|
|
|
|
outcome.DeliveryError = err
|
|
|
|
|
return outcome
|
|
|
|
|
}
|
|
|
|
|
outcome := Success()
|
|
|
|
|
outcome.Diagnostics = diagnostics
|
|
|
|
|
return outcome
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []Diagnostic) Outcome {
|
|
|
|
|
checked := &checkedWriter{writer: writer}
|
|
|
|
|
renderErr := reader.WriteExplanation(checked, path)
|
|
|
|
|
if checked.failure != nil {
|
|
|
|
|
if renderErr == nil {
|
|
|
|
|
renderErr = checked.failure
|
|
|
|
|
} else {
|
|
|
|
|
renderErr = errors.Join(renderErr, checked.failure)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if renderErr != nil {
|
|
|
|
|
outcome := explanationFailure(renderErr, diagnostics, true)
|
|
|
|
|
outcome.DeliveryError = checked.failure
|
|
|
|
|
return outcome
|
|
|
|
|
}
|
|
|
|
|
outcome := Success()
|
|
|
|
|
outcome.Diagnostics = diagnostics
|
|
|
|
|
return outcome
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeExplanationReport(scope string, explanation configkit.Explanation) explanationReport {
|
|
|
|
|
report := explanationReport{
|
|
|
|
|
Scope: scope,
|
|
|
|
|
Path: explanation.Path.String(),
|
|
|
|
|
Effective: snapshotReport(explanation.Effective),
|
|
|
|
|
Origin: originReport(explanation.Origin),
|
|
|
|
|
History: make([]explanationChangeDTO, 0, len(explanation.History)),
|
|
|
|
|
}
|
|
|
|
|
for _, change := range explanation.History {
|
|
|
|
|
entry := explanationChangeDTO{
|
|
|
|
|
Before: snapshotReport(change.Before),
|
|
|
|
|
After: snapshotReport(change.After),
|
|
|
|
|
Origin: originReport(change.Origin),
|
|
|
|
|
Reasons: append([]string{}, change.Reasons...),
|
|
|
|
|
Dependencies: make([]string, 0, len(change.Dependencies)),
|
|
|
|
|
}
|
|
|
|
|
for _, dependency := range change.Dependencies {
|
|
|
|
|
entry.Dependencies = append(entry.Dependencies, dependency.String())
|
|
|
|
|
}
|
|
|
|
|
report.History = append(report.History, entry)
|
|
|
|
|
}
|
|
|
|
|
if explanation.SecretReference != nil {
|
|
|
|
|
report.SecretReference = &secretReferenceDTO{Name: explanation.SecretReference.Name}
|
|
|
|
|
}
|
|
|
|
|
return report
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func snapshotReport(snapshot configkit.ValueSnapshot) snapshotDTO {
|
|
|
|
|
return snapshotDTO{Present: snapshot.Present, Redacted: snapshot.Redacted, YAML: snapshot.YAML}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func originReport(origin configkit.Origin) explanationOriginDTO {
|
|
|
|
|
report := explanationOriginDTO{
|
|
|
|
|
Layer: string(origin.Layer),
|
|
|
|
|
Locations: locationReports(origin.Locations),
|
|
|
|
|
Operation: string(origin.Operation),
|
|
|
|
|
Inheritance: make([]inheritanceStepDTO, 0, len(origin.Inheritance)),
|
|
|
|
|
}
|
|
|
|
|
for _, step := range origin.Inheritance {
|
|
|
|
|
report.Inheritance = append(report.Inheritance, inheritanceStepDTO{
|
|
|
|
|
From: step.From.String(),
|
|
|
|
|
To: step.To.String(),
|
|
|
|
|
Reference: step.Reference.String(),
|
|
|
|
|
Locations: locationReports(step.Locations),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return report
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func locationReports(locations []configkit.Location) []locationDTO {
|
|
|
|
|
report := make([]locationDTO, 0, len(locations))
|
|
|
|
|
for _, location := range locations {
|
|
|
|
|
report = append(report, locationDTO{SourceID: location.SourceID, Line: location.Line, Column: location.Column})
|
|
|
|
|
}
|
|
|
|
|
return report
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func explanationFailure(cause error, diagnostics []Diagnostic, reportStarted bool) Outcome {
|
|
|
|
|
outcome := Failure(cause)
|
|
|
|
|
outcome.Diagnostics = diagnostics
|
|
|
|
|
outcome.ReportIncomplete = reportStarted
|
|
|
|
|
return outcome
|
|
|
|
|
}
|