Select configuration files through host rules

This commit is contained in:
2026-10-24 15:46:36 +00:00
parent 4748313105
commit 2f0900535d
3 changed files with 236 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
# Configuration Discovery Implementation Plan
Status: in progress. Stages 1 and 2 are complete.
Status: in progress. Stages 1, 2, and 3 are complete.
Implement the target behavior in [the discovery roadmap](discovery.md). That
document owns the selected conventions and OD-1 through OD-5; this plan owns the
@@ -246,7 +246,7 @@ Work is linear in description size plus candidates visited, excluding OS path
operations and selected-file processing. No benchmark gate is needed. Run focused
discovery tests and the everyday check; mark and commit.
## Stage 3 — Select A Concrete File Through Host Acquisition Rules
## Stage 3 — Select A Concrete File Through Host Acquisition Rules (Complete)
1. Add a private resolver taking an immutable search description and returning
the concrete source or safe diagnostics. Non-search sources pass through.

View File

@@ -2,6 +2,7 @@ package engine
import (
"errors"
"io/fs"
"os"
"path/filepath"
"runtime"
@@ -459,3 +460,60 @@ func filenameGroupAt(group FilenameGroup, index int, application string) string
return ""
}
}
type discoveryHostProbe struct {
evalSymlinks func(string) (string, error)
stat func(string) (fs.FileInfo, error)
}
func defaultDiscoveryHostProbe() discoveryHostProbe {
return discoveryHostProbe{evalSymlinks: filepath.EvalSymlinks, stat: os.Stat}
}
// resolveSearchSource selects one ordinary file source. A nil description leaves
// an existing source untouched; the selected file is read later by readSource.
func resolveSearchSource(source Source, description *Search, application string, required bool, helpers discoveryDirectoryHelpers, probe discoveryHostProbe) (Source, []Diagnostic) {
if description == nil {
return source, nil
}
iterator, diagnostics := newSearchCandidateIterator(*description, application, helpers)
if len(diagnostics) != 0 {
return Source{}, diagnostics
}
defaults := defaultDiscoveryHostProbe()
if probe.evalSymlinks == nil {
probe.evalSymlinks = defaults.evalSymlinks
}
if probe.stat == nil {
probe.stat = defaults.stat
}
for {
candidate, ok, err := iterator.next()
if err != nil {
if errors.Is(err, errUnixSystemPresetUnavailable) {
return Source{}, []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "Unix system directory search is unavailable on this platform")}
}
return Source{}, []Diagnostic{classifySourceError(err, "")}
}
if !ok {
if required {
return Source{}, []Diagnostic{sourceDiagnostic(CodeSourceMissing, "", "required configuration search found no file")}
}
return NoSource(), nil
}
canonical, err := probe.evalSymlinks(candidate)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
return Source{}, []Diagnostic{classifySourceError(err, "")}
}
if _, err := probe.stat(canonical); err != nil {
if errors.Is(err, fs.ErrNotExist) {
continue
}
return Source{}, []Diagnostic{classifySourceError(err, "")}
}
return File(candidate), nil
}
}

View File

@@ -2,8 +2,11 @@ package engine
import (
"errors"
"io/fs"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
@@ -333,3 +336,176 @@ func assertCandidatePaths(t *testing.T, got, want []string) {
}
}
}
func TestResolveSearchSelectsFirstExistingCandidate(t *testing.T) {
t.Parallel()
root := t.TempDir()
first := filepath.Join(root, "first.yml")
second := filepath.Join(root, "second.yml")
writeDiscoveryFile(t, first, "value: first\n")
writeDiscoveryFile(t, second, "value: second\n")
selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(filepath.Join(root, "missing.yml"), first, second)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.kind != sourceFile || selected.path != first {
t.Fatalf("selection = %#v, diagnostics %#v", selected, diagnostics)
}
read, diagnostics := readSource(selected, "source-1", defaultSourceLimits())
if len(diagnostics) != 0 || string(read.data) != "value: first\n" || read.info.Path != canonicalTestPath(t, first) {
t.Fatalf("selected source read = %#v, diagnostics %#v", read, diagnostics)
}
}
func TestResolveSearchNamedExtensionsAndNonregularSelection(t *testing.T) {
t.Parallel()
root := t.TempDir()
writeDiscoveryFile(t, filepath.Join(root, "app.yml"), "value: yml\n")
writeDiscoveryFile(t, filepath.Join(root, "app.yaml"), "value: yaml\n")
selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(SearchDirectory(root)), "app", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.path != filepath.Join(root, "app.yml") {
t.Fatalf("named extension selection = %#v, %#v", selected, diagnostics)
}
directory := filepath.Join(root, "subdirectory")
if err := os.Mkdir(directory, 0o700); err != nil {
t.Fatal(err)
}
good := filepath.Join(root, "good.yml")
writeDiscoveryFile(t, good, "value: later\n")
selected, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(directory, good)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.path != directory {
t.Fatalf("directory candidate selection = %#v, %#v", selected, diagnostics)
}
_, diagnostics = readSource(selected, "source-1", defaultSourceLimits())
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceNotRegular {
t.Fatalf("selected directory read diagnostics = %#v", diagnostics)
}
}
func TestResolveSearchContainedSymlinkAndNoFallbackAfterReadFailure(t *testing.T) {
t.Parallel()
root := t.TempDir()
target := filepath.Join(root, "actual.yml")
link := filepath.Join(root, "linked.yml")
writeDiscoveryFile(t, target, "value: linked\n")
if err := os.Symlink(target, link); err != nil {
t.Skipf("symlinks unavailable: %v", err)
}
writeDiscoveryFile(t, filepath.Join(root, "later.yml"), "value: later\n")
selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(link, filepath.Join(root, "later.yml"))), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.path != link {
t.Fatalf("symlink selection = %#v, %#v", selected, diagnostics)
}
read, diagnostics := readSource(selected, "source-1", defaultSourceLimits())
if len(diagnostics) != 0 || string(read.data) != "value: linked\n" || read.info.Path != canonicalTestPath(t, target) {
t.Fatalf("symlink read = %#v, %#v", read, diagnostics)
}
if err := os.Remove(target); err != nil {
t.Fatal(err)
}
_, diagnostics = readSource(selected, "source-1", defaultSourceLimits())
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing {
t.Fatalf("selected source read failure = %#v", diagnostics)
}
}
func TestResolveSearchRequiredAndOptionalExhaustion(t *testing.T) {
t.Parallel()
optional, diagnostics := resolveSearchSource(Source{}, ptrSearch(Search{}), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || optional.kind != sourceNone {
t.Fatalf("optional empty search = %#v, %#v", optional, diagnostics)
}
_, diagnostics = resolveSearchSource(Source{}, ptrSearch(Search{}), "", true, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing || diagnostics[0].Primary != nil || !strings.Contains(diagnostics[0].Message, "search") {
t.Fatalf("required empty search = %#v", diagnostics)
}
root := t.TempDir()
missing := filepath.Join(root, "missing.yml")
optional, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(missing)), "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || optional.kind != sourceNone {
t.Fatalf("optional missing search = %#v, %#v", optional, diagnostics)
}
_, diagnostics = resolveSearchSource(Source{}, ptrSearch(Paths(missing)), "", true, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourceMissing {
t.Fatalf("required missing search = %#v", diagnostics)
}
}
func TestResolveSearchLeavesNonSearchSourcesUntouched(t *testing.T) {
t.Parallel()
source := Bytes("memory", []byte("value: unchanged\n"))
resolved, diagnostics := resolveSearchSource(source, nil, "", false, discoveryDirectoryHelpers{}, discoveryHostProbe{})
if len(diagnostics) != 0 || resolved.kind != sourceBytes || string(resolved.data) != "value: unchanged\n" {
t.Fatalf("non-search source = %#v, diagnostics = %#v", resolved, diagnostics)
}
}
func TestResolveSearchStopsOnReachedProbeError(t *testing.T) {
t.Parallel()
root := t.TempDir()
first := filepath.Join(root, "first.yml")
second := filepath.Join(root, "second.yml")
writeDiscoveryFile(t, first, "value: first\n")
writeDiscoveryFile(t, second, "value: second\n")
statCalls := 0
probe := discoveryHostProbe{stat: func(path string) (fs.FileInfo, error) {
statCalls++
return nil, fs.ErrPermission
}}
_, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(first, second)), "", false, discoveryDirectoryHelpers{}, probe)
if len(diagnostics) != 1 || diagnostics[0].Code != CodeSourcePermission || diagnostics[0].Primary != nil || statCalls != 1 {
t.Fatalf("probe failure = %#v, stat calls = %d", diagnostics, statCalls)
}
}
func TestResolveSearchLeavesLaterDirectoryUnresolvedAfterSelection(t *testing.T) {
t.Parallel()
root := t.TempDir()
first := filepath.Join(root, "selected.yml")
writeDiscoveryFile(t, first, "value: selected\n")
baseCalls := 0
helpers := discoveryDirectoryHelpers{userConfigDir: func() (string, error) {
baseCalls++
return "", errors.New("later base should not be reached")
}}
search := ptrSearch(Searches(Paths(first), UserConfigDirectory()))
selected, diagnostics := resolveSearchSource(Source{}, search, "app", false, helpers, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.path != first || baseCalls != 0 {
t.Fatalf("selected = %#v, diagnostics = %#v, later base calls = %d", selected, diagnostics, baseCalls)
}
}
func TestResolveSearchTreatsShellLikeNamesLiterally(t *testing.T) {
t.Parallel()
root := t.TempDir()
paths := []string{"~/literal.yml", "$CONFIG/literal.yml", "*.yaml"}
for _, name := range paths {
writeDiscoveryFile(t, filepath.Join(root, name), "value: literal\n")
}
helper := discoveryDirectoryHelpers{workingDir: func() (string, error) { return root, nil }}
for _, name := range paths {
selected, diagnostics := resolveSearchSource(Source{}, ptrSearch(Paths(name)), "", false, helper, discoveryHostProbe{})
if len(diagnostics) != 0 || selected.path != filepath.Join(root, name) {
t.Fatalf("literal %q selection = %#v, %#v", name, selected, diagnostics)
}
}
}
func ptrSearch(search Search) *Search { return &search }
func writeDiscoveryFile(t *testing.T, path, contents string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}