Load ordinary overlays without a main source

This commit is contained in:
2026-10-24 15:59:39 +00:00
parent 53ce6c63d8
commit 8642337966
10 changed files with 125 additions and 21 deletions

View File

@@ -23,7 +23,8 @@ type Definition[T any] struct {
// Options contains the application definition and per-load input selections.
// Imports enables traversal of declarations in a selected main document; an
// absent source has no main declarations to traverse. Overlay selects one fixed
// overlay. Overrides contain only values explicitly supplied by an application CLI.
// overlay and requires filesystem context for NoSource. Overrides contain only
// values explicitly supplied by an application CLI.
type Options[T any] struct {
Definition Definition[T]
Imports bool

View File

@@ -31,8 +31,8 @@ func selectOptions[T any](source Source, supplied []Options[T]) (Options[T], []D
if options.Filesystem != nil {
diagnostics = append(diagnostics, filesystemOptionDiagnostics(source, options.Filesystem)...)
}
if source.kind == sourceNone && options.Overlay != "" && options.Overlay != "default" {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource cannot select an active overlay"))
if source.kind == sourceNone && activeOverlay(options.Overlay) && options.Filesystem == nil {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context"))
}
if options.Filesystem == nil && (source.kind == sourceBytes || source.kind == sourceReader || source.kind == sourceMapping) && (options.Imports || activeOverlay(options.Overlay)) {
diagnostics = append(diagnostics, optionDiagnostic(CodeOptionsInvalid, "synthetic-source imports or overlays require filesystem context"))

View File

@@ -59,9 +59,24 @@ func loadSelectedOverlayIntoWithVersion(source Source, context *FilesystemContex
}
return loadSelectedOverlayWithAccessAndVersion(access, base, sources, name, rule, limits, versionState, policy)
}
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
if diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
var rootPath string
if source.kind == sourceNone {
if context == nil {
value := optionDiagnostic(CodeOptionsInvalid, "NoSource overlays require explicit filesystem context")
return cloneSourceInfos(sources), []Diagnostic{value}
}
var err error
rootPath, err = canonicalExistingPath(context.Root)
if err != nil {
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
return cloneSourceInfos(sources), []Diagnostic{value}
}
} else {
var diagnostic *Diagnostic
rootPath, _, _, diagnostic = importFilesystem(source, context, main)
if diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
}
access, err := openHostAccess(rootPath)
if err != nil {

View File

@@ -2,11 +2,14 @@ package engine
import (
"errors"
"io/fs"
"os"
"path"
"path/filepath"
"reflect"
"runtime"
"testing"
"testing/fstest"
)
type overlayTestConfig struct {
@@ -21,6 +24,81 @@ type overlayTestConfig struct {
Schema map[string]string `yaml:"schema"`
}
func TestNoSourceSelectedOverlayUsesRootedHostContext(t *testing.T) {
t.Parallel()
type config struct {
Resource string `yaml:"resource"`
}
root := t.TempDir()
overlayPath := writeImportFile(t, root, "overlays/testing.yml", "resource: asset.txt\n")
writeImportFile(t, root, "overlays/asset.txt", "host asset\n")
loaded, err := testLoad[config](NoSource(), Options[config]{
Overlay: "testing", Filesystem: &FilesystemContext{Root: root},
})
if err != nil {
t.Fatal(err)
}
sources := loaded.Sources()
if len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Path != canonicalTestPath(t, overlayPath) || sources[0].Layer != LayerOverlay || sources[0].Kind != SourceKindFile {
t.Fatalf("NoSource overlay sources = %#v", sources)
}
context, ok := loaded.Inspection.SourceContext("source-1")
if !ok || context.BaseDir != "overlays" {
t.Fatalf("overlay resource context = %#v, %v", context, ok)
}
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, loaded.Config.Resource))
if err != nil || string(data) != "host asset\n" {
t.Fatalf("overlay resource = %q, %v", data, err)
}
}
func TestNoSourceSelectedOverlayRequiresContextAndSelection(t *testing.T) {
t.Parallel()
requireLoadCode(t, NoSource(), Options[importTestConfig]{Overlay: "testing"}, CodeOptionsInvalid)
root := t.TempDir()
requireLoadCode(t, NoSource(), Options[importTestConfig]{
Overlay: "testing", Filesystem: &FilesystemContext{Root: root},
}, CodeOverlayMissing)
outside := t.TempDir()
requireLoadCode(t, NoSource(), Options[importTestConfig]{
Overlay: "testing", Filesystem: &FilesystemContext{Root: root, BaseDir: outside},
}, CodeImportOutsideRoot)
}
func TestNoSourceSelectedOverlayUsesCompleteNativeContext(t *testing.T) {
t.Parallel()
type config struct {
Resource string `yaml:"resource"`
}
filesystem := NewFilesystem("embedded", fstest.MapFS{
"overlays/testing.yml": &fstest.MapFile{Data: []byte("resource: asset.txt\n")},
"overlays/asset.txt": &fstest.MapFile{Data: []byte("native asset\n")},
})
loaded, err := testLoad[config](NoSource(), Options[config]{
Overlay: "testing", Filesystem: &FilesystemContext{FS: &filesystem, BaseDir: ".", Root: "."},
})
if err != nil {
t.Fatal(err)
}
sources := loaded.Sources()
if len(sources) != 1 || sources[0].ID != "source-1" || sources[0].Path != "overlays/testing.yml" || sources[0].Layer != LayerOverlay || sources[0].Kind != SourceKindFilesystem {
t.Fatalf("native NoSource overlay sources = %#v", sources)
}
context, ok := loaded.Inspection.SourceContext("source-1")
if !ok || context.BaseDir != "overlays" {
t.Fatalf("native overlay resource context = %#v, %v", context, ok)
}
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, loaded.Config.Resource))
if err != nil || string(data) != "native asset\n" {
t.Fatalf("native overlay resource = %q, %v", data, err)
}
}
func TestLoadAppliesSelectedOverlayAfterBaseAndBeforeDefaults(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit:

View File

@@ -176,8 +176,7 @@ func Mapping(label string, values map[string]any) Source {
// NoSource selects an absent input for defaults and resolution. It creates no
// source record and has no main declarations to traverse when imports are enabled.
// An explicit filesystem context may be supplied; active overlays are not
// available for absent inputs.
// An active overlay requires explicit filesystem context.
func NoSource() Source {
return Source{kind: sourceNone}
}

View File

@@ -51,10 +51,14 @@ func contextsForAcquiredSources(source Source, context *FilesystemContext, baseS
value := importDiagnostic(CodeImportInvalid, "configuration root could not be canonicalized", nil)
return nil, &value
}
basePath, err = canonicalExistingPath(context.BaseDir)
if err != nil {
value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil)
return nil, &value
if source.kind == sourceNone && context.BaseDir == "" {
basePath = rootPath
} else {
basePath, err = canonicalExistingPath(context.BaseDir)
if err != nil {
value := importDiagnostic(CodeImportInvalid, "import base directory could not be canonicalized", nil)
return nil, &value
}
}
} else {
return nil, nil