Add configuration path explanations
This commit is contained in:
@@ -6,8 +6,8 @@ and shared command infrastructure across related applications.
|
||||
The current Go library provides an instance-owned command registry, typed flag
|
||||
and operand bindings, Cobra-backed routing, invocation outcomes, generated help,
|
||||
application or CommandKit version reporting, configuration scope discovery, and
|
||||
ConfigKit-backed validation, native effective YAML display, and source inventory
|
||||
reports. Explanation reports and PromptKit commands remain planned.
|
||||
ConfigKit-backed validation, native effective YAML display, source inventory,
|
||||
and path explanation reports. PromptKit commands remain planned.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
13
config.go
13
config.go
@@ -111,6 +111,7 @@ type configInput[C any] struct {
|
||||
Selection Selection
|
||||
Scope string
|
||||
Format string
|
||||
Path Supplied[string]
|
||||
SelectorContext C
|
||||
selectors []selectorState[C]
|
||||
}
|
||||
@@ -131,8 +132,8 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||
return fmt.Errorf("command path %q is already registered", "config")
|
||||
}
|
||||
|
||||
commands := make([]Command, 0, 4)
|
||||
installed := make([]ConfigOperation, 0, 3)
|
||||
commands := make([]Command, 0, 5)
|
||||
installed := make([]ConfigOperation, 0, 4)
|
||||
if configOperationDeclared(normalized.Scopes, ConfigValidate) {
|
||||
validate, err := newConfigValidateCommand(normalized)
|
||||
if err != nil {
|
||||
@@ -157,6 +158,14 @@ func RegisterConfig[C any](app *App, suite ConfigSuite[C]) error {
|
||||
commands = append(commands, sources)
|
||||
installed = append(installed, ConfigSources)
|
||||
}
|
||||
if configOperationDeclared(normalized.Scopes, ConfigExplain) {
|
||||
explain, err := newConfigExplainCommand(normalized)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
commands = append(commands, explain)
|
||||
installed = append(installed, ConfigExplain)
|
||||
}
|
||||
command, err := newConfigScopesCommandWithScopes(normalized.Scopes, installed)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestConfigScopeDiscoveryOrdersAndDeduplicatesCapabilities(t *testing.T) {
|
||||
if outcome.Kind != OutcomeSuccess {
|
||||
t.Fatalf("text inventory outcome = %#v", outcome)
|
||||
}
|
||||
wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show\n"
|
||||
wantText := "app: validate, show\npromptkit: sources\nmetadata:\nzeta: show, explain\n"
|
||||
if textOutput.String() != wantText {
|
||||
t.Fatalf("text inventory = %q, want %q", textOutput.String(), wantText)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,153 @@ type capturedSourceReader struct {
|
||||
calls int
|
||||
}
|
||||
|
||||
func TestConfigExplainPreservesConfigKitProvenanceAndRedaction(t *testing.T) {
|
||||
type entry struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Credential configkit.Secret `yaml:"credential"`
|
||||
}
|
||||
type configuration struct {
|
||||
Entries map[string]entry `yaml:"entries"`
|
||||
Odd map[string]string `yaml:"odd"`
|
||||
Value string `yaml:"value"`
|
||||
Dependency string `yaml:"dependency"`
|
||||
Secret configkit.Secret `yaml:"secret"`
|
||||
}
|
||||
const secretValue = "EXPLAIN_TEST_SECRET_VALUE"
|
||||
t.Setenv("EXPLAIN_TEST_SECRET", secretValue)
|
||||
input := []byte("entries:\n parent:\n enabled: false\n credential: EXPLAIN_TEST_SECRET\n child:\n extends: parent\nodd:\n \"key/name~\": escaped\nvalue: original\ndependency: source\nsecret: EXPLAIN_TEST_SECRET\n")
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("explanation.yml", input), configkit.Options[configuration]{Definition: configkit.Definition[configuration]{
|
||||
Secret: func(path configkit.Path) bool { return path.String() == "/entries/parent/credential" },
|
||||
Resolve: func(value *configuration, resolution *configkit.Resolution) error {
|
||||
resolution.Record(configkit.Keys("value"), configkit.ChangeNormalization, "upper case", configkit.Keys("dependency"))
|
||||
value.Value = strings.ToUpper(value.Value)
|
||||
return nil
|
||||
},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loads, contexts := 0, 0
|
||||
scope, err := commandkit.NewConfigKitScope[struct{}, configuration]("app", "", func(context.Context, commandkit.Selection, struct{}) (*configkit.Result[configuration], error) {
|
||||
loads++
|
||||
return result, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := commandkit.RegisterConfig(app, commandkit.ConfigSuite[struct{}]{NewContext: func() struct{} { contexts++; return struct{}{} }, Scopes: []commandkit.ConfigScope[struct{}]{scope}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runYAML := func(pointer string) (map[string]any, commandkit.Outcome, string) {
|
||||
var output bytes.Buffer
|
||||
argv := []string{"config", "explain", "--path", pointer, "--format", "yaml"}
|
||||
if pointer == "" {
|
||||
argv = []string{"config", "explain", "--path=", "--format", "yaml"}
|
||||
}
|
||||
outcome := app.Execute(context.Background(), argv, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
var report map[string]any
|
||||
if outcome.Kind == commandkit.OutcomeSuccess {
|
||||
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil {
|
||||
t.Fatalf("decode explanation YAML: %v; output=%q", err, output.String())
|
||||
}
|
||||
}
|
||||
return report, outcome, output.String()
|
||||
}
|
||||
|
||||
path := "/entries/child/enabled"
|
||||
var native bytes.Buffer
|
||||
if err := result.WriteExplanation(&native, configkit.Keys("entries", "child", "enabled")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var textOutput bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: &textOutput, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || textOutput.String() != native.String() {
|
||||
t.Fatalf("native text outcome=%#v output=%q want=%q", outcome, textOutput.String(), native.String())
|
||||
}
|
||||
var inherited map[string]any
|
||||
inherited, outcome, _ = runYAML(path)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || inherited["path"] != path {
|
||||
t.Fatalf("inherited explanation outcome=%#v report=%#v", outcome, inherited)
|
||||
}
|
||||
effective := inherited["effective"].(map[string]any)
|
||||
if effective["present"] != true || effective["yaml"] != "false\n" {
|
||||
t.Fatalf("inherited effective snapshot = %#v", effective)
|
||||
}
|
||||
origin := inherited["origin"].(map[string]any)
|
||||
steps := origin["inheritance"].([]any)
|
||||
step := steps[0].(map[string]any)
|
||||
if step["from"] != "/entries/parent/enabled" || step["to"] != path || step["reference"] != "/entries/child/extends" {
|
||||
t.Fatalf("inheritance paths = %#v", step)
|
||||
}
|
||||
|
||||
escaped, outcome, _ := runYAML("/odd/key~1name~0")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || escaped["path"] != "/odd/key~1name~0" || escaped["effective"].(map[string]any)["yaml"] != "escaped\n" {
|
||||
t.Fatalf("escaped path outcome=%#v report=%#v", outcome, escaped)
|
||||
}
|
||||
historical, outcome, _ := runYAML("/entries/child/extends")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || historical["effective"].(map[string]any)["present"] != false || len(historical["history"].([]any)) == 0 {
|
||||
t.Fatalf("historical path outcome=%#v report=%#v", outcome, historical)
|
||||
}
|
||||
resolved, outcome, _ := runYAML("/value")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || resolved["effective"].(map[string]any)["yaml"] != "ORIGINAL\n" {
|
||||
t.Fatalf("resolved snapshot outcome=%#v report=%#v", outcome, resolved)
|
||||
}
|
||||
changes := resolved["history"].([]any)
|
||||
last := changes[len(changes)-1].(map[string]any)
|
||||
if last["reasons"].([]any)[0] != "upper case" || last["dependencies"].([]any)[0] != "/dependency" {
|
||||
t.Fatalf("resolution change = %#v", last)
|
||||
}
|
||||
secretReport, outcome, secretText := runYAML("/entries/child/credential")
|
||||
secretEffective := secretReport["effective"].(map[string]any)
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretEffective["redacted"] != true || strings.Contains(secretText, secretValue) {
|
||||
t.Fatalf("secret report outcome=%#v report=%#v", outcome, secretReport)
|
||||
}
|
||||
for _, change := range secretReport["history"].([]any) {
|
||||
entry := change.(map[string]any)
|
||||
for _, key := range []string{"before", "after"} {
|
||||
snapshot := entry[key].(map[string]any)
|
||||
if snapshot["present"] == true && snapshot["redacted"] != true {
|
||||
t.Fatalf("secret history was not redacted: %#v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
directSecret, outcome, directSecretText := runYAML("/secret")
|
||||
secretRef, refPresent := directSecret["secret_reference"].(map[string]any)
|
||||
if !refPresent {
|
||||
t.Fatalf("secret reference was absent: %#v", directSecret)
|
||||
}
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || secretRef["name"] != "EXPLAIN_TEST_SECRET" || strings.Contains(directSecretText, secretValue) {
|
||||
t.Fatalf("direct secret reference outcome=%#v report=%#v", outcome, directSecret)
|
||||
}
|
||||
root, outcome, _ := runYAML("")
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || root["path"] != "" {
|
||||
t.Fatalf("root path outcome=%#v report=%#v", outcome, root)
|
||||
}
|
||||
|
||||
loadsBeforeInvalid, contextsBeforeInvalid := loads, contexts
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("missing path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/bad~2path"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || loads != loadsBeforeInvalid || contexts != contextsBeforeInvalid {
|
||||
t.Fatalf("malformed path outcome=%#v loads=%d contexts=%d", outcome, loads, contexts)
|
||||
}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", "/does-not-exist"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 {
|
||||
t.Fatalf("unknown path outcome = %#v", outcome)
|
||||
}
|
||||
partial := &configKitShowPartialWriter{failure: io.ErrClosedPipe}
|
||||
outcome = app.Execute(context.Background(), []string{"config", "explain", "--path", path}, commandkit.Streams{Out: partial, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !outcome.ReportIncomplete || !errors.Is(outcome.DeliveryError, io.ErrClosedPipe) {
|
||||
t.Fatalf("explanation writer failure = %#v", outcome)
|
||||
}
|
||||
}
|
||||
|
||||
func (reader *capturedSourceReader) Sources() []configkit.SourceInfo {
|
||||
reader.calls++
|
||||
return reader.entries
|
||||
|
||||
47
docs/cli.md
47
docs/cli.md
@@ -173,6 +173,53 @@ including empty fields, zero coordinates, and synthetic sources without paths.
|
||||
An empty inventory is `sources: []`. Text output begins with the scope heading
|
||||
and then prints the same source fields, quoting labels and paths.
|
||||
|
||||
### Explain a configuration path
|
||||
|
||||
`config explain --path POINTER` explains the effective and historical value at a
|
||||
JSON Pointer path. `--path` is required; an explicitly empty value selects the
|
||||
root. The default scope is `app`; `--format yaml` selects structured output, and
|
||||
text is the default. `--scope all` is unsupported.
|
||||
|
||||
```text
|
||||
example config explain --path /server/addr
|
||||
example config explain --path /profiles/team~1summary --format yaml
|
||||
example config explain --path= --format yaml
|
||||
```
|
||||
|
||||
Pointer tokens use JSON Pointer escaping: `~1` represents `/` and `~0`
|
||||
represents `~`. A malformed pointer is a usage error with status `2` and is
|
||||
rejected before acquisition. A well-formed path that is unknown in the selected
|
||||
configuration is an operational error with status `1`. Text output comes from
|
||||
the scope's native explanation writer.
|
||||
|
||||
YAML output uses these root fields:
|
||||
|
||||
```yaml
|
||||
scope: app
|
||||
path: /server/addr
|
||||
effective:
|
||||
present: true
|
||||
redacted: false
|
||||
yaml: "localhost:8080\n"
|
||||
origin:
|
||||
layer: base
|
||||
locations:
|
||||
- source_id: source-1
|
||||
line: 4
|
||||
column: 9
|
||||
operation: ""
|
||||
inheritance: []
|
||||
history: []
|
||||
secret_reference: null
|
||||
```
|
||||
|
||||
Snapshot `yaml` values remain strings, including null and empty fragments.
|
||||
Origin and history retain source locations, operation names, inheritance paths,
|
||||
ordered reasons and dependencies. Secret references contain only the owning
|
||||
environment-variable name; effective and historical secret fragments remain
|
||||
redacted. Empty location, inheritance, history, reason, and dependency lists are
|
||||
serialized as `[]`.
|
||||
|
||||
## Outcome status
|
||||
|
||||
CommandKit usage failures use status `2`; operational failures use status `1`.
|
||||
|
||||
@@ -5,8 +5,9 @@ decisions, implementation plans, identity and outcome primitives, typed command
|
||||
declarations, an instance-owned registry, a Cobra-backed routing and parsing
|
||||
adapter, public `App.Execute` coordination, and generated help/version commands.
|
||||
Configuration suite registration, scope discovery, ConfigKit integration,
|
||||
validation, native effective YAML display, and source inventory reports are
|
||||
implemented; other configuration and PromptKit operations remain pending. The
|
||||
validation, native effective YAML display, source inventory, and path
|
||||
explanation reports are implemented; other configuration and PromptKit
|
||||
operations remain pending. The
|
||||
[README](../README.md) provides project orientation; the
|
||||
[documentation policy](policy/documentation.md#canonical-ownership) assigns
|
||||
canonical documentation owners.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Initial CommandKit implementation plan
|
||||
|
||||
Status: implementation underway; Stages 1-10 complete and Stages 11-14 pending.
|
||||
Status: implementation underway; Stages 1-11 complete and Stages 12-14 pending.
|
||||
|
||||
Date: 2026-10-07.
|
||||
|
||||
@@ -676,7 +676,7 @@ describe the report.
|
||||
|
||||
## Stage 11: implement path explanation reports
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 10.
|
||||
**Status:** complete. **Prerequisite:** Stage 10.
|
||||
|
||||
1. Implement single-scope `config explain --path POINTER`. Presence is required;
|
||||
explicitly empty is the root. Parse with public `configkit.ParsePath` before
|
||||
@@ -704,6 +704,16 @@ inherited values; malformed versus unknown outcomes; redacted before/after and
|
||||
reference-name preservation; YAML snapshot strings and inheritance pointers;
|
||||
native text equivalence; writer failure. Update explanation CLI documentation.
|
||||
|
||||
**Completion evidence:** installed single-scope `config explain` with required
|
||||
JSON Pointer `--path`, text as the native writer output, and explicit YAML DTOs
|
||||
for snapshots, origins, history, inheritance, and secret references. Malformed
|
||||
pointers return usage before context creation or acquisition; unknown paths are
|
||||
operational failures checked before native writing. Checks cover root and escaped
|
||||
paths, inherited and historical values, ordered resolution dependencies, secret
|
||||
redaction and name-only references, exact native text, missing/invalid/unknown
|
||||
paths, and writer failure. `go test ./...` passes; README, development orientation,
|
||||
and CLI reference describe the feature.
|
||||
|
||||
## Stage 12: integrate the PromptKit configuration scope
|
||||
|
||||
**Status:** pending. **Prerequisite:** Stage 11.
|
||||
|
||||
214
explain.go
Normal file
214
explain.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
)
|
||||
|
||||
func newConfigExplainCommand[C any](suite ConfigSuite[C]) (Command, error) {
|
||||
flags := configInputFlags(suite, "text")
|
||||
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 {
|
||||
if _, err := preflightConfig(suite, input, ConfigExplain); err != nil {
|
||||
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 {
|
||||
return runConfigExplanation(invocation, suite, input)
|
||||
},
|
||||
}
|
||||
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"`
|
||||
}
|
||||
|
||||
func runConfigExplanation[C any](invocation Invocation, suite ConfigSuite[C], input configInput[C]) Outcome {
|
||||
scopeNames, err := selectConfigScopes(suite, input, ConfigExplain)
|
||||
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]
|
||||
contexts := contextsForScopes(suite, input, scopeNames)
|
||||
scope, ok := findConfigScope(suite.Scopes, name)
|
||||
if !ok || scope.Acquire == nil {
|
||||
return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name))
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user