package ckconfig import ( "bytes" "errors" "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 if err := invocation.Context.Err(); err != nil { return configOperationFailure(err, nil, false) } if selected.scope.scope.Acquire == nil { return configOperationFailure(fmt.Errorf("configuration scope %q has no acquisition callback", name), nil, false) } acquired, acquireErr := selected.scope.scope.Acquire(invocation.Context, Request{ Operation: Explain, Selection: plan.selection, }, selected.input, state) diagnostics := append([]commandkit.Diagnostic(nil), acquired.Diagnostics...) if acquireErr != nil { return configOperationFailure(acquireErr, diagnostics, false) } if err := invocation.Context.Err(); err != nil { return configOperationFailure(err, diagnostics, false) } if acquired.Value.Explanation == nil { return configOperationFailure(fmt.Errorf("configuration scope %q did not provide an explanation reader", name), diagnostics, false) } explanation, found := acquired.Value.Explanation.Explain(plan.path) if !found { return configOperationFailure(fmt.Errorf("configuration path %q is not known in scope %q", plan.path.String(), name), diagnostics, false) } if err := invocation.Context.Err(); err != nil { return configOperationFailure(err, diagnostics, false) } if plan.format != "yaml" { return writeNativeExplanation(invocation.Streams.Out, acquired.Value.Explanation, plan.path, diagnostics) } var report bytes.Buffer if err := encodeYAML(&report, makeExplanationReport(name, explanation)); err != nil { return configOperationFailure(err, diagnostics, false) } if err := invocation.Context.Err(); err != nil { return configOperationFailure(err, diagnostics, false) } if err := writeConfigReport(invocation.Streams.Out, report.Bytes()); err != nil { return configDeliveryFailure(err, diagnostics, true) } outcome := commandkit.Success() outcome.Diagnostics = diagnostics return outcome } func writeNativeExplanation(writer io.Writer, reader ExplanationReader, path configkit.Path, diagnostics []commandkit.Diagnostic) commandkit.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 := configOperationFailure(renderErr, diagnostics, true) outcome.DeliveryError = checked.failure return outcome } outcome := commandkit.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 }