Complete facade inspection and secrets bootstrap
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 2,
|
"schema_version": 2,
|
||||||
"commit": "b8935e77bd5ac1dc347339966fbaba5de9af5348",
|
"commit": "da307c9f49550d789df4efe7310bb3d837e97df4",
|
||||||
"indexed_at": "2026-09-12T16:16:09Z",
|
"indexed_at": "2026-09-12T16:19:20Z",
|
||||||
"project": "home-eric-Workspace-ConfigKit",
|
"project": "home-eric-Workspace-ConfigKit",
|
||||||
"nodes": 2447,
|
"nodes": 2469,
|
||||||
"edges": 13587,
|
"edges": 13742,
|
||||||
"original_size": 11206656,
|
"original_size": 11206656,
|
||||||
"compressed_size": 1701304,
|
"compressed_size": 1715591,
|
||||||
"compression_level": 3
|
"compression_level": 3
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -423,7 +423,7 @@ candidate inspection and secrets-bootstrap surface.
|
|||||||
|
|
||||||
## Stage 7 — Complete candidate inspection and bootstrap surface
|
## Stage 7 — Complete candidate inspection and bootstrap surface
|
||||||
|
|
||||||
Status: not started.
|
Status: complete (2026-09-12).
|
||||||
|
|
||||||
1. Add the public explanation/value records and Result Explain, WriteEffective,
|
1. Add the public explanation/value records and Result Explain, WriteEffective,
|
||||||
and WriteExplanation delegation. Inspection uses the frozen engine state,
|
and WriteExplanation delegation. Inspection uses the frozen engine state,
|
||||||
@@ -444,6 +444,24 @@ integration evidence for inspection, credentials, bootstrap, and writer failures
|
|||||||
Run `go test ./internal/facade ./internal/engine`, race tests for both packages,
|
Run `go test ./internal/facade ./internal/engine`, race tests for both packages,
|
||||||
and the everyday check. Root remains unchanged until Stage 8.
|
and the everyday check. Root remains unchanged until Stage 8.
|
||||||
|
|
||||||
|
### Completion
|
||||||
|
|
||||||
|
Completed the candidate's inspection and bootstrap API with root-owned
|
||||||
|
explanation, snapshot, secret-reference, and bootstrap records. Result delegates
|
||||||
|
to the private frozen engine inspection for explanation and rendering, so public
|
||||||
|
configuration mutation cannot affect historical or effective output. Bootstrap
|
||||||
|
delegates to the engine while returning facade-owned report and error types with
|
||||||
|
detached converted entries and diagnostics.
|
||||||
|
|
||||||
|
The candidate's exported declarations and methods now match the root surface
|
||||||
|
after normalizing its temporary import path. External integration tests cover
|
||||||
|
frozen inspection, secret-safe explanation/rendering, malformed and nil result
|
||||||
|
handling, bootstrap application/skips, detached report entries, and typed
|
||||||
|
bootstrap failures. Linux validation with Go 1.26.7 passed
|
||||||
|
`go test ./internal/facade ./internal/engine`, `go test -race ./internal/facade ./internal/engine`,
|
||||||
|
and `./scripts/check.sh`. There were no deviations or remaining risks. The next
|
||||||
|
handoff moves the verified candidate to the root and removes migration copies.
|
||||||
|
|
||||||
## Stage 8 — Cut over the root and remove migration duplication
|
## Stage 8 — Cut over the root and remove migration duplication
|
||||||
|
|
||||||
Status: not started.
|
Status: not started.
|
||||||
|
|||||||
@@ -89,3 +89,45 @@ func fromEngineChange(value engine.Change) Change {
|
|||||||
}
|
}
|
||||||
return result
|
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
|
package configkit_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit/internal/facade"
|
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.FilesystemContext](), reflect.TypeFor[configkit.Override](),
|
||||||
reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](),
|
reflect.TypeFor[configkit.Definition[struct{}]](), reflect.TypeFor[configkit.Options[struct{}]](),
|
||||||
reflect.TypeFor[configkit.Result[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 {
|
if candidate.PkgPath() != packagePath {
|
||||||
t.Fatalf("type %s belongs to %q", candidate, candidate.PkgPath())
|
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) {
|
func TestFacadeLoadBridgesLifecycleCallbacks(t *testing.T) {
|
||||||
t.Setenv("FACADE_SECRET_NAME", "credential")
|
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