package ckconfig import ( "bytes" "fmt" "strings" "text/template" commandkit "go.maximumdirect.net/commandkit" configkit "go.maximumdirect.net/configkit" ) type explanationReport struct { Scope string `json:"scope" yaml:"scope"` Path string `json:"path" yaml:"path"` Effective snapshotDTO `json:"effective" yaml:"effective"` Origin explanationOriginDTO `json:"origin" yaml:"origin"` History []explanationChangeDTO `json:"history" yaml:"history"` SecretReference *secretReferenceDTO `json:"secret_reference" yaml:"secret_reference"` Metadata *explanationMetadata `json:"metadata,omitempty"` } type explanationMetadata struct { Explicit *bool `json:"explicit,omitempty"` Null *bool `json:"null,omitempty"` History []metadataHistoryRecord `json:"history,omitempty"` } type metadataHistoryRecord struct { Path string `json:"path"` Previous []explanationOriginDTO `json:"previous,omitempty"` } type pathDiscoveryReport struct { Scope string `json:"scope"` Paths []pathDiscoveryEntry `json:"paths"` } type pathDiscoveryEntry struct { Path string `json:"path"` Present bool `json:"present"` Metadata *pathMetadata `json:"metadata,omitempty"` } type pathMetadata struct { Explicit bool `json:"explicit"` } var pathDiscoveryTemplate = template.Must(commandkit.ParseTextTemplate("paths", "{{range .Paths}}{{printf \"%q\" .Path}} {{if .Present}}{{if .Metadata.Explicit}}explicit{{else}}non-explicit{{end}}{{else}}historical{{end}}\n{{end}}", nil)) type snapshotDTO struct { Present bool `json:"present" yaml:"present"` Redacted bool `json:"redacted" yaml:"redacted"` YAML string `json:"yaml" yaml:"yaml"` } type explanationOriginDTO struct { Layer string `json:"layer" yaml:"layer"` Locations []locationDTO `json:"locations" yaml:"locations"` Operation string `json:"operation" yaml:"operation"` Inheritance []inheritanceStepDTO `json:"inheritance" yaml:"inheritance"` } type inheritanceStepDTO struct { From string `json:"from" yaml:"from"` To string `json:"to" yaml:"to"` Reference string `json:"reference" yaml:"reference"` Locations []locationDTO `json:"locations" yaml:"locations"` } type explanationChangeDTO struct { Before snapshotDTO `json:"before" yaml:"before"` After snapshotDTO `json:"after" yaml:"after"` Origin explanationOriginDTO `json:"origin" yaml:"origin"` Reasons []string `json:"reasons" yaml:"reasons"` Dependencies []string `json:"dependencies" yaml:"dependencies"` } type secretReferenceDTO struct { Name string `json:"name" 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: selected.selection, }, selected.input, state) queueConfigAcquisition(reporter, acquired) 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 plan.list { if acquired.Value.Metadata == nil { return configFailure("Configuration scope did not provide path metadata.", acquired.Explanation, fmt.Errorf("configuration scope %q declared path discovery without metadata", name)) } report := pathDiscoveryReport{Scope: name, Paths: []pathDiscoveryEntry{}} for _, path := range acquired.Value.Metadata.HistoricalPaths() { if plan.under != nil && !pathWithin(path, *plan.under) { continue } info, present := acquired.Value.Metadata.Lookup(path) if !present && (!plan.historical || plan.explicit.Set) { continue } if present && plan.explicit.Set && info.Explicit != plan.explicit.Value { continue } entry := pathDiscoveryEntry{Path: path.String(), Present: present} if present { entry.Metadata = &pathMetadata{Explicit: info.Explicit} } report.Paths = append(report.Paths, entry) } _ = reporter.Report(commandkit.Report{Value: report, TextTemplate: pathDiscoveryTemplate}) return commandkit.Success() } 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) } report := makeExplanationReport(name, explanation) if acquired.Value.Metadata != nil { metadata := &explanationMetadata{} if info, ok := acquired.Value.Metadata.Lookup(plan.path); ok { metadata.Explicit, metadata.Null = &info.Explicit, &info.Null } for _, change := range acquired.Value.Metadata.History(plan.path) { entry := metadataHistoryRecord{Path: change.Path.String()} for _, previous := range change.Previous { entry.Previous = append(entry.Previous, originReport(previous)) } metadata.History = append(metadata.History, entry) } if metadata.Explicit != nil || len(metadata.History) > 0 { report.Metadata = metadata } } text := template.Must(commandkit.ParseTextTemplate("explanation", "{{render .}}", template.FuncMap{ "render": func(report explanationReport) (string, error) { var output bytes.Buffer err := acquired.Value.Explanation.WriteExplanation(&output, plan.path) if err == nil && report.Metadata != nil && report.Metadata.Explicit != nil { _, err = fmt.Fprintf(&output, "Explicit: %t\nNull: %t\n", *report.Metadata.Explicit, *report.Metadata.Null) } return output.String(), err }, })) renderErr := reporter.Report(commandkit.Report{Value: report, TextTemplate: text}) if renderErr != nil && invocation.Context.Err() != nil { return configFailure("Configuration explanation was canceled.", acquired.Explanation, invocation.Context.Err()) } return commandkit.Success() } func pathWithin(path, parent configkit.Path) bool { tokens := func(value string) []string { if value == "" { return nil } return strings.Split(strings.TrimPrefix(value, "/"), "/") } child, prefix := tokens(path.String()), tokens(parent.String()) if len(prefix) > len(child) { return false } for index := range prefix { if prefix[index] != child[index] { return false } } return true } 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 }