diff --git a/acceptance/bundle/validate/duplicate_yaml_merge_key/databricks.yml b/acceptance/bundle/validate/duplicate_yaml_merge_key/databricks.yml new file mode 100644 index 00000000000..630455bc24f --- /dev/null +++ b/acceptance/bundle/validate/duplicate_yaml_merge_key/databricks.yml @@ -0,0 +1,20 @@ +bundle: + name: test-bundle + +definitions: + cluster1: &cluster1 + num_workers: 1 + cluster2: &cluster2 + spark_version: "13.3.x-scala2.12" + +resources: + jobs: + my_job: + name: "test job" + tasks: + - task_key: "main" + new_cluster: + <<: *cluster1 + <<: *cluster2 + notebook_task: + notebook_path: "/notebook" diff --git a/acceptance/bundle/validate/duplicate_yaml_merge_key/out.test.toml b/acceptance/bundle/validate/duplicate_yaml_merge_key/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/validate/duplicate_yaml_merge_key/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/validate/duplicate_yaml_merge_key/output.txt b/acceptance/bundle/validate/duplicate_yaml_merge_key/output.txt new file mode 100644 index 00000000000..420ad818626 --- /dev/null +++ b/acceptance/bundle/validate/duplicate_yaml_merge_key/output.txt @@ -0,0 +1,20 @@ + +>>> [CLI] bundle validate +Error: duplicate YAML merge key ('<<') is not allowed; to merge multiple maps, use a sequence: '<<: [*anchor1, *anchor2]' + in databricks.yml:18:13 + + +Found 1 error + +>>> [CLI] bundle validate +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Validation OK! +{ + "num_workers": 1, + "spark_version": "13.3.x-scala2.12" +} diff --git a/acceptance/bundle/validate/duplicate_yaml_merge_key/script b/acceptance/bundle/validate/duplicate_yaml_merge_key/script new file mode 100644 index 00000000000..472434d5d5f --- /dev/null +++ b/acceptance/bundle/validate/duplicate_yaml_merge_key/script @@ -0,0 +1,8 @@ +musterr trace $CLI bundle validate + +update_file.py databricks.yml " <<: *cluster1 + <<: *cluster2" " <<: [*cluster1, *cluster2]" + +trace $CLI bundle validate + +$CLI bundle validate -o json | jq '.resources.jobs.my_job.tasks[0].new_cluster' diff --git a/bundle/config/root.go b/bundle/config/root.go index 764d801bc27..6d4697cc1ba 100644 --- a/bundle/config/root.go +++ b/bundle/config/root.go @@ -3,6 +3,7 @@ package config import ( "bytes" "context" + "errors" "fmt" "os" "reflect" @@ -106,6 +107,14 @@ func LoadFromBytes(path string, raw []byte) (*Root, diag.Diagnostics) { // Load configuration tree from YAML. v, err := yamlloader.LoadYAML(path, bytes.NewBuffer(raw)) if err != nil { + var le *yamlloader.LocationError + if errors.As(err, &le) { + return nil, diag.Diagnostics{{ + Severity: diag.Error, + Summary: le.Summary, + Locations: []dyn.Location{le.Loc}, + }} + } return nil, diag.Errorf("failed to load %s: %v", path, err) } diff --git a/libs/dyn/yamlloader/loader.go b/libs/dyn/yamlloader/loader.go index 79a4fb1d177..7ff4303d8ee 100644 --- a/libs/dyn/yamlloader/loader.go +++ b/libs/dyn/yamlloader/loader.go @@ -10,6 +10,17 @@ import ( "go.yaml.in/yaml/v3" ) +// LocationError is an error with a YAML source location that can be displayed +// to the user with a file path, line, and column number. +type LocationError struct { + Loc dyn.Location + Summary string +} + +func (e *LocationError) Error() string { + return fmt.Sprintf("yaml (%s): %s", e.Loc, e.Summary) +} + type loader struct { path string } @@ -110,7 +121,12 @@ func (d *loader) loadMapping(node *yaml.Node, loc dyn.Location) (dyn.Value, erro // However, when used as a key, it is treated as the string "null". case "!!merge": if merge != nil { - panic("merge node already set") + // The YAML merge key spec allows a single '<<' key per mapping. + // To merge multiple maps, use a sequence: '<<: [*anchor1, *anchor2]'. + return dyn.InvalidValue, &LocationError{ + Loc: d.location(key), + Summary: "duplicate YAML merge key ('<<') is not allowed; to merge multiple maps, use a sequence: '<<: [*anchor1, *anchor2]'", + } } merge = val continue