Complete facade inspection and secrets bootstrap
This commit is contained in:
@@ -89,3 +89,45 @@ func fromEngineChange(value engine.Change) Change {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fromEngineValueSnapshot(value engine.ValueSnapshot) ValueSnapshot {
|
||||
return ValueSnapshot{Present: value.Present, Redacted: value.Redacted, YAML: value.YAML}
|
||||
}
|
||||
|
||||
func fromEngineValueChange(value engine.ValueChange) ValueChange {
|
||||
result := ValueChange{
|
||||
Before: fromEngineValueSnapshot(value.Before),
|
||||
After: fromEngineValueSnapshot(value.After),
|
||||
Origin: fromEngineOrigin(value.Origin),
|
||||
Reasons: append([]string(nil), value.Reasons...),
|
||||
}
|
||||
if value.Dependencies != nil {
|
||||
result.Dependencies = make([]Path, len(value.Dependencies))
|
||||
for index := range value.Dependencies {
|
||||
result.Dependencies[index] = fromEnginePath(value.Dependencies[index])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fromEngineExplanation(value engine.Explanation) Explanation {
|
||||
result := Explanation{
|
||||
Path: fromEnginePath(value.Path),
|
||||
Effective: fromEngineValueSnapshot(value.Effective),
|
||||
Origin: fromEngineOrigin(value.Origin),
|
||||
}
|
||||
if value.History != nil {
|
||||
result.History = make([]ValueChange, len(value.History))
|
||||
for index := range value.History {
|
||||
result.History[index] = fromEngineValueChange(value.History[index])
|
||||
}
|
||||
}
|
||||
if value.SecretReference != nil {
|
||||
result.SecretReference = &SecretReferenceInfo{Name: value.SecretReference.Name}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func fromEngineSecretFileResult(value engine.SecretFileResult) SecretFileResult {
|
||||
return SecretFileResult{Name: value.Name, Path: value.Path, Status: SecretFileStatus(value.Status), ExistingEmpty: value.ExistingEmpty}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package configkit_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit/internal/facade"
|
||||
@@ -29,6 +33,10 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||
reflect.TypeFor[configkit.FilesystemContext](), reflect.TypeFor[configkit.Override](),
|
||||
reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](),
|
||||
reflect.TypeFor[configkit.Result[struct{}]](),
|
||||
reflect.TypeFor[configkit.ValueSnapshot](), reflect.TypeFor[configkit.ValueChange](),
|
||||
reflect.TypeFor[configkit.SecretReferenceInfo](), reflect.TypeFor[configkit.Explanation](),
|
||||
reflect.TypeFor[configkit.SecretFileResult](), reflect.TypeFor[configkit.SecretsReport](),
|
||||
reflect.TypeFor[configkit.SecretsError](),
|
||||
} {
|
||||
if candidate.PkgPath() != packagePath {
|
||||
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
|
||||
@@ -48,6 +56,118 @@ func TestFacadeValuesRemainRootOwned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeInspectionUsesFrozenState(t *testing.T) {
|
||||
t.Setenv("FACADE_INSPECTION_SECRET", "credential")
|
||||
|
||||
type configuration struct {
|
||||
Value string `yaml:"value"`
|
||||
Secret configkit.Secret `yaml:"secret"`
|
||||
}
|
||||
result, err := configkit.Load[configuration](configkit.Bytes("inspection.yml", []byte("value: original\nsecret: FACADE_INSPECTION_SECRET\n")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.Config.Secret.IsResolved() || result.Config.Secret.Value() != "credential" {
|
||||
t.Fatalf("secret = %#v", result.Config.Secret)
|
||||
}
|
||||
explanation, ok := result.Explain(configkit.Keys("value"))
|
||||
if !ok || explanation.Effective.YAML != "original\n" {
|
||||
t.Fatalf("explanation = %#v, %v", explanation, ok)
|
||||
}
|
||||
explanation.Effective.YAML = "mutated\n"
|
||||
result.Config.Value = "mutated"
|
||||
var effective bytes.Buffer
|
||||
if err := result.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "value: original\n") || strings.Contains(effective.String(), "credential") {
|
||||
t.Fatalf("effective = %q, %v", effective.String(), err)
|
||||
}
|
||||
again, ok := result.Explain(configkit.Keys("value"))
|
||||
if !ok || again.Effective.YAML != "original\n" {
|
||||
t.Fatalf("inspection changed = %#v, %v", again, ok)
|
||||
}
|
||||
secret, ok := result.Explain(configkit.Keys("secret"))
|
||||
if !ok || secret.SecretReference == nil || secret.SecretReference.Name != "FACADE_INSPECTION_SECRET" {
|
||||
t.Fatalf("secret explanation = %#v, %v", secret, ok)
|
||||
}
|
||||
var explanationOutput bytes.Buffer
|
||||
if err := result.WriteExplanation(&explanationOutput, configkit.Keys("value")); err != nil || !strings.Contains(explanationOutput.String(), `path: "/value"`) {
|
||||
t.Fatalf("explanation output = %q, %v", explanationOutput.String(), err)
|
||||
}
|
||||
|
||||
unknown := bytes.NewBufferString("unchanged")
|
||||
if err := result.WriteExplanation(unknown, configkit.Keys("unknown")); err == nil || unknown.String() != "unchanged" {
|
||||
t.Fatalf("unknown explanation = %q, %v", unknown.String(), err)
|
||||
}
|
||||
var nilResult *configkit.Result[configuration]
|
||||
if _, ok := nilResult.Explain(configkit.Keys("value")); ok {
|
||||
t.Fatal("nil result explained a path")
|
||||
}
|
||||
if err := nilResult.WriteEffective(io.Discard); err == nil {
|
||||
t.Fatal("nil result rendered effective configuration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeSecretsBootstrapUsesFacadeRecords(t *testing.T) {
|
||||
const (
|
||||
loadedName = "FACADE_BOOTSTRAP_LOADED"
|
||||
existingName = "FACADE_BOOTSTRAP_EXISTING"
|
||||
)
|
||||
restore := func(name string) {
|
||||
value, present := os.LookupEnv(name)
|
||||
t.Cleanup(func() {
|
||||
if present {
|
||||
_ = os.Setenv(name, value)
|
||||
} else {
|
||||
_ = os.Unsetenv(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
restore(loadedName)
|
||||
restore(existingName)
|
||||
if err := os.Unsetenv(loadedName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Setenv(existingName, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
directory := t.TempDir()
|
||||
if err := os.WriteFile(directory+"/"+loadedName, []byte("bootstrap\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(directory+"/"+existingName, []byte("ignored\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
report, err := configkit.LoadSecretsDir(directory)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
entries := report.Entries()
|
||||
if len(entries) != 2 || os.Getenv(loadedName) != "bootstrap" {
|
||||
t.Fatalf("entries/environment = %#v / %q", entries, os.Getenv(loadedName))
|
||||
}
|
||||
status := map[string]configkit.SecretFileResult{}
|
||||
for _, entry := range entries {
|
||||
status[entry.Name] = entry
|
||||
}
|
||||
if status[loadedName].Status != configkit.SecretFileLoaded || status[existingName].Status != configkit.SecretFileSkippedExisting || !status[existingName].ExistingEmpty {
|
||||
t.Fatalf("statuses = %#v", status)
|
||||
}
|
||||
entries[0].Name = "mutated"
|
||||
if report.Entries()[0].Name == "mutated" {
|
||||
t.Fatal("report entries share mutable state")
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if _, err := report.WriteTo(&rendered); err != nil || strings.Contains(rendered.String(), "bootstrap") || strings.Contains(rendered.String(), "ignored") {
|
||||
t.Fatalf("report rendering = %q, %v", rendered.String(), err)
|
||||
}
|
||||
|
||||
failedReport, err := configkit.LoadSecretsDir("")
|
||||
var secretsError *configkit.SecretsError
|
||||
if failedReport == nil || !errors.As(err, &secretsError) || len(secretsError.Diagnostics()) != 1 || secretsError.Diagnostics()[0].Code != configkit.CodeSecretsInvalidPath {
|
||||
t.Fatalf("bootstrap failure = %#v / %#v", failedReport, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||
t.Setenv("FACADE_SECRET_NAME", "credential")
|
||||
|
||||
|
||||
70
internal/facade/inspection.go
Normal file
70
internal/facade/inspection.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// ValueSnapshot is a detached, deterministic YAML representation of one value.
|
||||
// Present distinguishes absence or deletion from an explicit null value.
|
||||
type ValueSnapshot struct {
|
||||
Present bool
|
||||
Redacted bool
|
||||
YAML string
|
||||
}
|
||||
|
||||
// ValueChange describes one explicit definition or audited domain operation.
|
||||
type ValueChange struct {
|
||||
Before ValueSnapshot
|
||||
After ValueSnapshot
|
||||
Origin Origin
|
||||
Reasons []string
|
||||
Dependencies []Path
|
||||
}
|
||||
|
||||
// SecretReferenceInfo describes a successfully resolved effective reference.
|
||||
// Name is the environment-variable name, never its value.
|
||||
type SecretReferenceInfo struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// Explanation is a detached safe account of one effective or historical path.
|
||||
type Explanation struct {
|
||||
Path Path
|
||||
Effective ValueSnapshot
|
||||
Origin Origin
|
||||
History []ValueChange
|
||||
SecretReference *SecretReferenceInfo
|
||||
}
|
||||
|
||||
// Explain returns safe effective and chronological values for a known final or
|
||||
// historical path. Unknown and malformed paths return false.
|
||||
func (r *Result[T]) Explain(path Path) (Explanation, bool) {
|
||||
if r == nil {
|
||||
return Explanation{}, false
|
||||
}
|
||||
value, ok := r.inspection.Explain(toEnginePath(path))
|
||||
return fromEngineExplanation(value), ok
|
||||
}
|
||||
|
||||
// WriteEffective writes deterministic redacted YAML from the frozen resolved
|
||||
// snapshot. The output is intended for inspection and is not promised reloadable.
|
||||
// Writer errors, including short writes, are returned and may leave partial output.
|
||||
func (r *Result[T]) WriteEffective(output io.Writer) error {
|
||||
if r == nil {
|
||||
var inspection *engine.Inspection
|
||||
return inspection.WriteEffective(output)
|
||||
}
|
||||
return r.inspection.WriteEffective(output)
|
||||
}
|
||||
|
||||
// WriteExplanation writes a deterministic human-readable rendering of Explain.
|
||||
// Unknown paths produce no output. Writer failures may leave partial output.
|
||||
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
|
||||
if r == nil {
|
||||
var inspection *engine.Inspection
|
||||
return inspection.WriteExplanation(output, toEnginePath(path))
|
||||
}
|
||||
return r.inspection.WriteExplanation(output, toEnginePath(path))
|
||||
}
|
||||
102
internal/facade/secrets.go
Normal file
102
internal/facade/secrets.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// SecretFileStatus describes the outcome for one file considered by
|
||||
// LoadSecretsDir.
|
||||
type SecretFileStatus string
|
||||
|
||||
const (
|
||||
SecretFileLoaded SecretFileStatus = "loaded"
|
||||
SecretFileSkippedExisting SecretFileStatus = "skipped_existing"
|
||||
SecretFileNotApplied SecretFileStatus = "not_applied"
|
||||
SecretFileFailed SecretFileStatus = "failed"
|
||||
)
|
||||
|
||||
// SecretFileResult is the safe, value-free bootstrap result for one directory
|
||||
// entry. ExistingEmpty is meaningful when Status is SecretFileSkippedExisting.
|
||||
type SecretFileResult struct {
|
||||
Name string
|
||||
Path string
|
||||
Status SecretFileStatus
|
||||
ExistingEmpty bool
|
||||
}
|
||||
|
||||
// SecretsReport is a detached snapshot of an explicit secrets-directory
|
||||
// bootstrap. It does not retain file contents or environment values.
|
||||
type SecretsReport struct{ value *engine.SecretsReport }
|
||||
|
||||
// Entries returns a copy of the report's filename-ordered entries.
|
||||
func (r *SecretsReport) Entries() []SecretFileResult {
|
||||
if r == nil || r.value == nil {
|
||||
return nil
|
||||
}
|
||||
values := r.value.Entries()
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]SecretFileResult, len(values))
|
||||
for index := range values {
|
||||
result[index] = fromEngineSecretFileResult(values[index])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WriteTo renders one safe, deterministic line per entry. Names and paths are
|
||||
// quoted so control characters cannot affect terminal output.
|
||||
func (r *SecretsReport) WriteTo(output io.Writer) (int64, error) {
|
||||
if r == nil || r.value == nil {
|
||||
return (*engine.SecretsReport)(nil).WriteTo(output)
|
||||
}
|
||||
return r.value.WriteTo(output)
|
||||
}
|
||||
|
||||
// SecretsError reports safe structured diagnostics for a failed explicit
|
||||
// secrets-directory bootstrap. It intentionally does not unwrap filesystem or
|
||||
// environment errors.
|
||||
type SecretsError struct{ value *engine.SecretsError }
|
||||
|
||||
// Error implements error without rendering filenames, paths, or file contents.
|
||||
func (e *SecretsError) Error() string {
|
||||
if e == nil || e.value == nil {
|
||||
return "configkit: secrets bootstrap failed"
|
||||
}
|
||||
return e.value.Error()
|
||||
}
|
||||
|
||||
// Diagnostics returns a deep copy of the bootstrap diagnostics.
|
||||
func (e *SecretsError) Diagnostics() []Diagnostic {
|
||||
if e == nil || e.value == nil {
|
||||
return nil
|
||||
}
|
||||
values := e.value.Diagnostics()
|
||||
if values == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]Diagnostic, len(values))
|
||||
for index := range values {
|
||||
result[index] = fromEngineDiagnostic(values[index])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LoadSecretsDir explicitly loads immediate regular files from directory into
|
||||
// previously unset process environment variables. Applications should call it
|
||||
// once during startup, before concurrent work; Load never calls it.
|
||||
func LoadSecretsDir(directory string) (*SecretsReport, error) {
|
||||
report, err := engine.LoadSecretsDir(directory)
|
||||
result := &SecretsReport{value: report}
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
var secretsError *engine.SecretsError
|
||||
if errors.As(err, &secretsError) {
|
||||
return result, &SecretsError{value: secretsError}
|
||||
}
|
||||
return result, &SecretsError{}
|
||||
}
|
||||
Reference in New Issue
Block a user