package ckconfig import ( "bytes" "fmt" "io" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) 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[I, S any](invocation commandkit.Invocation, plan *configOperationPlan[I, S], state S) commandkit.Outcome { selected := plan.scopes[0] name := selected.scope.scope.Name reporter := invocation.Reporter() if err := invocation.Context.Err(); err != nil { return configFailure("Configuration explanation was canceled.", commandkit.Explanation{}, err) } if selected.scope.scope.Acquire == nil { cause := fmt.Errorf("configuration scope %q has no acquisition callback", name) return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), commandkit.Explanation{}, cause) } acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{ Operation: Explain, Selection: plan.selection, }, selected.input, state) queueConfigDiagnostics(reporter, acquired.Diagnostics) if acquireErr != nil { return configFailure(fmt.Sprintf("Unable to explain configuration scope %q.", name), acquired.Explanation, acquireErr) } if err := invocation.Context.Err(); err != nil { return configFailure("Configuration explanation was canceled.", acquired.Explanation, err) } 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) } explanation, found := acquired.Value.Explanation.Explain(plan.path) if !found { cause := fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name) return configFailure("The requested configuration path is not available.", acquired.Explanation, cause) } if err := invocation.Context.Err(); err != nil { return configFailure("Configuration explanation was canceled.", acquired.Explanation, err) } if plan.format != "yaml" { err := reporter.RenderOutput(func(writer io.Writer) error { return acquired.Value.Explanation.WriteExplanation(writer, plan.path) }) if err != nil && invocation.Context.Err() != nil { return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err()) } return commandkit.Success() } err := reporter.RenderOutput(func(writer io.Writer) error { var report bytes.Buffer if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil { return err } _, err := writer.Write(report.Bytes()) return err }) if err != nil && invocation.Context.Err() != nil { return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err()) } return commandkit.Success() } 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 }