569 lines
21 KiB
Go
569 lines
21 KiB
Go
package engine
|
|
|
|
import (
|
|
"path/filepath"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
type importDestinationConfig struct {
|
|
Mounted map[string]string `yaml:"mounted"`
|
|
Collections map[string]map[string]string `yaml:"collections"`
|
|
}
|
|
|
|
func TestLayeredHostImportsInheritInputDestinationTransitively(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: first.yml
|
|
- path: second.yml
|
|
root: main
|
|
`)
|
|
firstPath := writeImportFile(t, directory, "first.yml", `configkit:
|
|
imports:
|
|
- path: nested/third.yml
|
|
first: one
|
|
`)
|
|
thirdPath := writeImportFile(t, directory, "nested/third.yml", "nested: three\n")
|
|
secondPath := writeImportFile(t, directory, "second.yml", "second: two\n")
|
|
|
|
loaded, err := LoadLayered(
|
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("mounted")},
|
|
testSecretBinding(),
|
|
LayeredOptions[importDestinationConfig]{},
|
|
)
|
|
if err != nil {
|
|
if loadError, ok := err.(*LoadError); ok {
|
|
t.Fatalf("LoadLayered diagnostics: %#v", loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
want := map[string]string{"root": "main", "first": "one", "nested": "three", "second": "two"}
|
|
if len(result.Config.Mounted) != len(want) {
|
|
t.Fatalf("mounted values = %#v", result.Config.Mounted)
|
|
}
|
|
for key, value := range want {
|
|
if result.Config.Mounted[key] != value {
|
|
t.Errorf("mounted[%q] = %q, want %q", key, result.Config.Mounted[key], value)
|
|
}
|
|
}
|
|
if _, ok := result.Metadata().Lookup(Keys("mounted", "configkit")); ok {
|
|
t.Fatal("source-root import directive leaked into application content")
|
|
}
|
|
sources := result.Sources()
|
|
wantPaths := []string{canonicalTestPath(t, mainPath), canonicalTestPath(t, firstPath), canonicalTestPath(t, thirdPath), canonicalTestPath(t, secondPath)}
|
|
if len(sources) != len(wantPaths) {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
for index, source := range sources {
|
|
if source.Destination.String() != "/mounted" {
|
|
t.Errorf("source %d destination = %q", index, source.Destination.String())
|
|
}
|
|
if source.Path != wantPaths[index] {
|
|
t.Errorf("source %d path = %q, want %q", index, source.Path, wantPaths[index])
|
|
}
|
|
if index > 0 && source.ImportedFrom == nil {
|
|
t.Errorf("imported source %d has no physical import location", index)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestImportTraversalUsesPerDeclarationDestinations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: parent.yml
|
|
- path: sibling.yml
|
|
- glob: fragments/*.yml
|
|
`)
|
|
parentPath := writeImportFile(t, directory, "parent.yml", `configkit:
|
|
imports:
|
|
- path: nested/child.yml
|
|
parent: true
|
|
`)
|
|
childPath := writeImportFile(t, directory, "nested/child.yml", "child: true\n")
|
|
siblingPath := writeImportFile(t, directory, "sibling.yml", "sibling: true\n")
|
|
alphaPath := writeImportFile(t, directory, "fragments/a.yml", "alpha: true\n")
|
|
betaPath := writeImportFile(t, directory, "fragments/b.yml", "beta: true\n")
|
|
|
|
main := parseImportMain(t, mainPath)
|
|
_, declarations, diagnostics := splitImportDirectives(main.document)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("main directive diagnostics = %#v", diagnostics)
|
|
}
|
|
declarations[0].destination = Keys("parent")
|
|
declarations[1].destination = Keys("sibling")
|
|
declarations[2].destination = Keys("collection")
|
|
|
|
access, err := openHostAccess(directory)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = access.close() }()
|
|
state := importTraversal{
|
|
access: access, accounting: &importAccounting{}, limits: defaultImportLimits(),
|
|
active: make(map[resourceIdentity]int), visited: make(map[resourceIdentity]importRoute),
|
|
}
|
|
if diagnostics := state.walkDeclarations(directory, declarations, 0, Keys("entry")); len(diagnostics) != 0 {
|
|
t.Fatalf("walk diagnostics = %#v", diagnostics)
|
|
}
|
|
|
|
wantSources := []struct {
|
|
path string
|
|
destination string
|
|
}{
|
|
{path: parentPath, destination: "/entry/parent"},
|
|
{path: childPath, destination: "/entry/parent"},
|
|
{path: siblingPath, destination: "/entry/sibling"},
|
|
{path: alphaPath, destination: "/entry/collection"},
|
|
{path: betaPath, destination: "/entry/collection"},
|
|
}
|
|
if len(state.sources) != len(wantSources) {
|
|
t.Fatalf("import sources = %#v, want %d", state.sources, len(wantSources))
|
|
}
|
|
for index, want := range wantSources {
|
|
got := state.sources[index]
|
|
if got.Path != canonicalTestPath(t, want.path) || got.Destination.String() != want.destination {
|
|
t.Errorf("source %d = (%q, %q), want (%q, %q)", index, got.Path, got.Destination.String(), canonicalTestPath(t, want.path), want.destination)
|
|
}
|
|
}
|
|
|
|
composed, diagnostics := composeDocuments(state.documents...)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("composition diagnostics = %#v", diagnostics)
|
|
}
|
|
for _, path := range []Path{
|
|
Keys("entry", "parent", "parent"), Keys("entry", "parent", "child"),
|
|
Keys("entry", "sibling", "sibling"), Keys("entry", "collection", "alpha"),
|
|
Keys("entry", "collection", "beta"),
|
|
} {
|
|
if _, present := documentAtPath(composed.root, path); !present {
|
|
t.Errorf("no composed value at %q", path.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryDestinationSurvivesImportSetupAndParseFailures(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("import context failure", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: child.yml}]}\n")
|
|
input, diagnostics := assembleBaseInputTracked(
|
|
File(mainPath),
|
|
Options[map[string]any]{Imports: true, Filesystem: &FilesystemContext{Root: t.TempDir()}},
|
|
Keys("mounted"), &importAccounting{}, nil, nil,
|
|
)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot {
|
|
t.Fatalf("context diagnostics = %#v", diagnostics)
|
|
}
|
|
if len(input.sources) != 1 || input.sources[0].Destination.String() != "/mounted" {
|
|
t.Fatalf("entry source fallback = %#v", input.sources)
|
|
}
|
|
})
|
|
|
|
t.Run("imported parse failure", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: broken.yml}]}\n")
|
|
writeImportFile(t, directory, "broken.yml", "broken: [\n")
|
|
input, diagnostics := assembleBaseInputTracked(
|
|
File(mainPath), Options[map[string]any]{Imports: true},
|
|
Keys("mounted"), &importAccounting{}, nil, nil,
|
|
)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeYAMLInvalid {
|
|
t.Fatalf("parse diagnostics = %#v", diagnostics)
|
|
}
|
|
if len(input.sources) != 2 {
|
|
t.Fatalf("parse-failure sources = %#v", input.sources)
|
|
}
|
|
for index, source := range input.sources {
|
|
if source.Destination.String() != "/mounted" {
|
|
t.Errorf("source %d destination = %q, want /mounted", index, source.Destination.String())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRelativeImportDestinationsKeepStrictCompositionBoundaries(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
main string
|
|
files map[string]string
|
|
want map[string]any
|
|
conflict bool
|
|
wantPath string
|
|
}{
|
|
{
|
|
name: "disjoint peers at one destination",
|
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
|
files: map[string]string{"left.yml": "left: one\n", "right.yml": "right: two\n"},
|
|
want: map[string]any{"/application/left": "one", "/application/right": "two"},
|
|
},
|
|
{
|
|
name: "different destinations",
|
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [left]\n - path: right.yml\n destination: [right]\n",
|
|
files: map[string]string{"left.yml": "value: shared\n", "right.yml": "value: shared\n"},
|
|
want: map[string]any{"/left/value": "shared", "/right/value": "shared"},
|
|
},
|
|
{
|
|
name: "ancestor and descendant overlap",
|
|
main: "configkit:\n imports:\n - path: parent.yml\n destination: [application]\n - path: child.yml\n destination: [application, branch]\n",
|
|
files: map[string]string{"parent.yml": "branch: {base: one}\n", "child.yml": "leaf: two\n"},
|
|
want: map[string]any{"/application/branch/base": "one", "/application/branch/leaf": "two"},
|
|
},
|
|
{
|
|
name: "collision with explicitly wrapped content",
|
|
main: "configkit: {imports: [{path: fragment.yml, destination: [application]}]}\napplication: {value: root}\n",
|
|
files: map[string]string{"fragment.yml": "value: imported\n"},
|
|
conflict: true,
|
|
wantPath: "/application/value",
|
|
},
|
|
{
|
|
name: "equal peer values conflict",
|
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
|
files: map[string]string{"left.yml": "value: same\n", "right.yml": "value: same\n"},
|
|
conflict: true,
|
|
wantPath: "/application/value",
|
|
},
|
|
{
|
|
name: "unequal peer values conflict",
|
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
|
files: map[string]string{"left.yml": "value: left\n", "right.yml": "value: right\n"},
|
|
conflict: true,
|
|
wantPath: "/application/value",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", test.main)
|
|
for name, body := range test.files {
|
|
writeImportFile(t, directory, name, body)
|
|
}
|
|
loaded, err := LoadLayered(
|
|
Input{Source: File(mainPath), Imports: true}, testSecretBinding(),
|
|
LayeredOptions[map[string]any]{},
|
|
)
|
|
if test.conflict {
|
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
|
t.Fatalf("relative import composition = %#v, %v", loaded, err)
|
|
}
|
|
loadError := err.(*LoadError)
|
|
if diagnostics := loadError.Diagnostics(); diagnostics[0].Path.String() != test.wantPath {
|
|
t.Fatalf("conflict path = %q, want %q", diagnostics[0].Path.String(), test.wantPath)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("relative import composition: %v", err)
|
|
}
|
|
configuration := testLoaded(loaded).Config
|
|
for path, want := range test.want {
|
|
parsed, err := ParsePath(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
keys := make([]string, len(parsed.segments))
|
|
for index, segment := range parsed.segments {
|
|
keys[index] = segment.key
|
|
}
|
|
if got := layeredMapValue(t, configuration, keys...); got != want {
|
|
t.Errorf("value at %s = %#v, want %#v", path, got, want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("upper tier cannot repair a lower import conflict", func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
lowerPath := writeImportFile(t, directory, "lower.yml", `configkit:
|
|
imports:
|
|
- path: left.yml
|
|
destination: [application]
|
|
- path: right.yml
|
|
destination: [application]
|
|
`)
|
|
writeImportFile(t, directory, "left.yml", "value: left\n")
|
|
writeImportFile(t, directory, "right.yml", "value: right\n")
|
|
loaded, err := LoadLayered(
|
|
Input{Source: Bytes("upper", []byte("application: {value: upper}\n"))},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{{Source: File(lowerPath), Imports: true}}}}},
|
|
)
|
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
|
t.Fatalf("upper tier repaired a lower conflict: %#v, %v", loaded, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestRelativeImportDestinationsUseEffectiveVersionOwnership(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
rulePath Path
|
|
fragment string
|
|
}{
|
|
{name: "direct claim", rulePath: Keys("collection", "group", "nested", "version"), fragment: "version: v2\n"},
|
|
{name: "ancestor claim", rulePath: Keys("collection", "group", "nested", "schema", "version"), fragment: "schema: null\n"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: parent.yml
|
|
destination: [group]
|
|
`)
|
|
writeImportFile(t, directory, "parent.yml", `configkit:
|
|
imports:
|
|
- path: fragment.yml
|
|
destination: [nested]
|
|
`)
|
|
writeImportFile(t, directory, "fragment.yml", test.fragment)
|
|
_, err := LoadLayered(
|
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("collection")},
|
|
testSecretBinding(),
|
|
LayeredOptions[map[string]any]{Definition: Definition[map[string]any]{Version: &VersionRule{
|
|
Path: test.rulePath, Check: func(Version, *Validation) error { return nil },
|
|
}}},
|
|
)
|
|
if groupedErrorCode(t, err) != CodeImportVersion {
|
|
t.Fatalf("effective imported claim error = %v", err)
|
|
}
|
|
loadError := err.(*LoadError)
|
|
diagnostic := loadError.Diagnostics()[0]
|
|
if diagnostic.Path.String() != test.rulePath.String() || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-3" {
|
|
t.Fatalf("effective version diagnostic = %#v", diagnostic)
|
|
}
|
|
sources := loadError.Sources()
|
|
wantDestinations := []string{"/collection", "/collection/group", "/collection/group/nested"}
|
|
if len(sources) != len(wantDestinations) {
|
|
t.Fatalf("version failure sources = %#v", sources)
|
|
}
|
|
for index, source := range sources {
|
|
if source.Destination.String() != wantDestinations[index] {
|
|
t.Errorf("source %d destination = %q, want %q", index, source.Destination.String(), wantDestinations[index])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRelativeImportDestinationsKeepMainVersionCheckScoped(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type config struct {
|
|
Collection struct {
|
|
SchemaVersion string `yaml:"schema_version"`
|
|
Group map[string]any `yaml:"group"`
|
|
} `yaml:"collection"`
|
|
}
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: parent.yml
|
|
destination: [group]
|
|
schema_version: 6
|
|
`)
|
|
writeImportFile(t, directory, "parent.yml", `configkit:
|
|
imports:
|
|
- path: fragment.yml
|
|
destination: [nested]
|
|
`)
|
|
writeImportFile(t, directory, "fragment.yml", "version: 7\n")
|
|
checks := 0
|
|
loaded, err := LoadLayered(
|
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("collection")},
|
|
testSecretBinding(),
|
|
LayeredOptions[config]{Definition: Definition[config]{Version: &VersionRule{
|
|
Path: Keys("collection", "schema_version"),
|
|
Check: func(version Version, _ *Validation) error {
|
|
checks++
|
|
if !version.Present || version.Text != "6" {
|
|
t.Errorf("main-owned version = %#v", version)
|
|
}
|
|
return nil
|
|
},
|
|
}}},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ordinary nested imported version was rejected: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if checks != 1 || result.Config.Collection.SchemaVersion != "6" {
|
|
t.Fatalf("main version callback/value = %d/%q", checks, result.Config.Collection.SchemaVersion)
|
|
}
|
|
nested, ok := result.Config.Collection.Group["nested"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("nested imported payload = %#v", result.Config.Collection.Group["nested"])
|
|
}
|
|
if got, ok := nested["version"].(int64); !ok || got != 7 {
|
|
t.Fatalf("ordinary nested version = %#v, want integer payload 7", nested["version"])
|
|
}
|
|
}
|
|
|
|
func TestLayeredNativeFilesystemImportsInheritInputDestination(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
native := fstest.MapFS{
|
|
"main.yml": &fstest.MapFile{Data: []byte("configkit:\n imports:\n - path: first.yml\nroot: main\n")},
|
|
"first.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [{path: nested.yml}]}\nfirst: imported\n")},
|
|
"nested.yml": &fstest.MapFile{Data: []byte("nested: imported\n")},
|
|
}
|
|
namespace := newNativeNamespace("bundle", native)
|
|
loaded, err := LoadLayered(
|
|
Input{Source: filesystemFile(namespace, "main.yml"), Imports: true, Destination: Keys("mounted")},
|
|
testSecretBinding(),
|
|
LayeredOptions[importDestinationConfig]{},
|
|
)
|
|
if err != nil {
|
|
if loadError, ok := err.(*LoadError); ok {
|
|
t.Fatalf("LoadLayered diagnostics: %#v", loadError.Diagnostics())
|
|
}
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if result.Config.Mounted["root"] != "main" || result.Config.Mounted["first"] != "imported" || result.Config.Mounted["nested"] != "imported" {
|
|
t.Fatalf("mounted native filesystem values = %#v", result.Config.Mounted)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 3 || sources[0].Destination.String() != "/mounted" || sources[1].Destination.String() != "/mounted" || sources[2].Destination.String() != "/mounted" {
|
|
t.Fatalf("native source destinations = %#v", sources)
|
|
}
|
|
}
|
|
|
|
func TestLayeredImportOnlyInputMaterializesDestination(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
|
imports:
|
|
- path: optional.yml
|
|
optional: true
|
|
`)
|
|
loaded, err := LoadLayered(
|
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("collections", "empty")},
|
|
testSecretBinding(),
|
|
LayeredOptions[importDestinationConfig]{},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadLayered: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
info, ok := result.Metadata().Lookup(Keys("collections", "empty"))
|
|
if !ok || !info.Explicit {
|
|
t.Fatalf("import-only body destination = %#v, %v", info, ok)
|
|
}
|
|
sources := result.Sources()
|
|
if len(sources) != 1 || sources[0].Destination.String() != "/collections/empty" {
|
|
t.Fatalf("sources = %#v", sources)
|
|
}
|
|
}
|
|
|
|
func TestLayeredImportedVersionClaimsUseEffectiveDestination(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
rulePath Path
|
|
destination Path
|
|
wantCode Code
|
|
}{
|
|
{name: "protected effective path", rulePath: Keys("mounted", "version"), destination: Keys("mounted"), wantCode: CodeImportVersion},
|
|
{name: "ordinary nested domain version", rulePath: Keys("version"), destination: Keys("collections", "entry")},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml}]}\n")
|
|
writeImportFile(t, directory, "fragment.yml", "version: v2\n")
|
|
rule := &VersionRule{Path: test.rulePath, Check: func(Version, *Validation) error { return nil }}
|
|
loaded, err := LoadLayered(
|
|
Input{Source: NoSource()},
|
|
testSecretBinding(),
|
|
LayeredOptions[importDestinationConfig]{
|
|
Definition: Definition[importDestinationConfig]{Version: rule},
|
|
Underlays: []Underlay{{Inputs: []Input{{
|
|
Source: File(filepath.Join(directory, "main.yml")), Imports: true, Destination: test.destination,
|
|
}}}},
|
|
},
|
|
)
|
|
if test.wantCode != "" {
|
|
if err == nil {
|
|
t.Fatal("protected imported version claim was accepted")
|
|
}
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != test.wantCode {
|
|
t.Fatalf("load error = %#v", err)
|
|
}
|
|
if test.name == "protected effective path" {
|
|
sources := loadError.Sources()
|
|
if len(sources) != 2 || sources[0].Destination.String() != "/mounted" || sources[1].Destination.String() != "/mounted" {
|
|
t.Fatalf("error source destinations = %#v", sources)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("ordinary nested version was rejected: %v", err)
|
|
}
|
|
result := testLoaded(loaded)
|
|
if got := result.Config.Collections["entry"]["version"]; got != "v2" {
|
|
t.Fatalf("nested version = %q", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
directory := t.TempDir()
|
|
writeImportFile(t, directory, "shared.yml", "value: shared\n")
|
|
importerPath := writeImportFile(t, directory, "importer.yml", "configkit: {imports: [{path: shared.yml, destination: [nested]}]}\n")
|
|
_, err := LoadLayered(
|
|
Input{Source: NoSource()},
|
|
testSecretBinding(),
|
|
LayeredOptions[importDestinationConfig]{Underlays: []Underlay{{Inputs: []Input{
|
|
{Source: File(importerPath), Imports: true, Destination: Keys("imported")},
|
|
{Source: File(filepath.Join(directory, "shared.yml")), Destination: Keys("direct")},
|
|
}}}},
|
|
)
|
|
if err == nil {
|
|
t.Fatal("the same physical source was accepted at two destinations in one group")
|
|
}
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportRepeated {
|
|
t.Fatalf("load error = %#v", err)
|
|
}
|
|
sources := loadError.Sources()
|
|
if len(sources) != 3 || sources[1].Destination.String() != "/imported/nested" || sources[2].Destination.String() != "/direct" {
|
|
t.Fatalf("repeated import sources = %#v", sources)
|
|
}
|
|
|
|
t.Run("placement does not break a physical cycle", func(t *testing.T) {
|
|
t.Parallel()
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: parent.yml, destination: [one]}]}\n")
|
|
writeImportFile(t, directory, "parent.yml", "configkit: {imports: [{path: main.yml, destination: [two]}]}\n")
|
|
_, err := LoadLayered(Input{Source: File(mainPath), Imports: true}, testSecretBinding(), LayeredOptions[map[string]any]{})
|
|
loadError, ok := err.(*LoadError)
|
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportCycle {
|
|
t.Fatalf("cycle load error = %#v", err)
|
|
}
|
|
diagnostic := loadError.Diagnostics()[0]
|
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" {
|
|
t.Fatalf("cycle primary location = %#v", diagnostic.Primary)
|
|
}
|
|
if sources := loadError.Sources(); len(sources) != 2 || sources[1].Destination.String() != "/one" {
|
|
t.Fatalf("cycle sources = %#v", sources)
|
|
}
|
|
})
|
|
}
|