package ckconfig import ( "bytes" "errors" "fmt" "io" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func newConfigExplainCommand[C any](prepared *preparedConfigSuite[C]) (Command, error) { flags := configInputFlags(prepared.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(prepared, 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, prepared, 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, prepared *preparedConfigSuite[C], input configInput[C]) Outcome { if err := invocation.Context.Err(); err != nil { return Failure(err) } scopeNames, err := selectConfigScopes(prepared, 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(prepared, input, scopeNames) scope, ok := findConfigScope(prepared, name) if !ok || scope.Acquire == nil { return Failure(fmt.Errorf("configuration scope %q has no acquisition callback", name)) } if err := invocation.Context.Err(); err != nil { return explanationFailure(err, nil, false) } 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 err := invocation.Context.Err(); err != nil { return explanationFailure(err, 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 err := invocation.Context.Err(); err != nil { return explanationFailure(err, 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 := invocation.Context.Err(); 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 }