diff --git a/.nextchanges/bundles/6999.md b/.nextchanges/bundles/6999.md new file mode 100644 index 00000000000..fd67b05254e --- /dev/null +++ b/.nextchanges/bundles/6999.md @@ -0,0 +1 @@ +* Diagnostics about configuration values now consistently show the configuration path (`at ...`) and its location. ([#6999](https://github.com/databricks/cli/pull/6999)) diff --git a/acceptance/bundle/deploy/immutable-permissions-change/output.txt b/acceptance/bundle/deploy/immutable-permissions-change/output.txt index 7e45089b269..59f94e0e194 100644 --- a/acceptance/bundle/deploy/immutable-permissions-change/output.txt +++ b/acceptance/bundle/deploy/immutable-permissions-change/output.txt @@ -19,6 +19,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:22:3 Recreated internal_immutable_snapshots.immutable @@ -42,6 +43,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:22:3 The following resources will be deleted: diff --git a/acceptance/bundle/migrate/auto/default/output.txt b/acceptance/bundle/migrate/auto/default/output.txt index 30a5dd774e6..c7da628a3f6 100644 --- a/acceptance/bundle/migrate/auto/default/output.txt +++ b/acceptance/bundle/migrate/auto/default/output.txt @@ -7,6 +7,7 @@ >>> [CLI] bundle summary Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -26,6 +27,7 @@ Resources: >>> [CLI] bundle summary -o json Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/migrate/auto/run-no-commit/output.txt b/acceptance/bundle/migrate/auto/run-no-commit/output.txt index 755265b41f1..fbb60477f67 100644 --- a/acceptance/bundle/migrate/auto/run-no-commit/output.txt +++ b/acceptance/bundle/migrate/auto/run-no-commit/output.txt @@ -24,6 +24,7 @@ ID Type Language Path >>> [CLI] bundle run test_job --no-wait Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:1:37 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/migrate/command/engine-config-terraform/output.txt b/acceptance/bundle/migrate/command/engine-config-terraform/output.txt index e2456dba2d6..46f57519fc5 100644 --- a/acceptance/bundle/migrate/command/engine-config-terraform/output.txt +++ b/acceptance/bundle/migrate/command/engine-config-terraform/output.txt @@ -1,6 +1,7 @@ >>> musterr [CLI] bundle deployment migrate Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/migrate/command/runas/output.txt b/acceptance/bundle/migrate/command/runas/output.txt index c17ffdec425..4de05f1f9ea 100644 --- a/acceptance/bundle/migrate/command/runas/output.txt +++ b/acceptance/bundle/migrate/command/runas/output.txt @@ -14,6 +14,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Notice: migrating your bundle to direct deployment engine (https://github.com/databricks/cli/issues/6765). @@ -31,6 +32,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Success! Migrated 2 resources to direct engine state file: [TEST_TMP_DIR]/.databricks/bundle/production/resources.json @@ -55,6 +57,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged @@ -70,6 +73,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 @@ -84,6 +88,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 The following resources will be deleted: diff --git a/acceptance/bundle/presets/preset_vs_dev_mode/output.txt b/acceptance/bundle/presets/preset_vs_dev_mode/output.txt index 9e4e62dc935..1f44f4743fb 100644 --- a/acceptance/bundle/presets/preset_vs_dev_mode/output.txt +++ b/acceptance/bundle/presets/preset_vs_dev_mode/output.txt @@ -1,4 +1,5 @@ Error: target with 'mode: development' cannot set trigger pause status to UNPAUSED by default + at presets.trigger_pause_status in databricks.yml:9:29 Name: preset diff --git a/acceptance/bundle/resources/job_runs/on_file_change/output.txt b/acceptance/bundle/resources/job_runs/on_file_change/output.txt index cd580a8efa4..6a8321afda9 100644 --- a/acceptance/bundle/resources/job_runs/on_file_change/output.txt +++ b/acceptance/bundle/resources/job_runs/on_file_change/output.txt @@ -115,6 +115,7 @@ Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged === deleting the file warns and re-fires >>> [CLI] bundle plan -o json Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 @@ -168,6 +169,7 @@ Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" } } Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/job-runs-on-file-change/default/files... @@ -189,6 +191,7 @@ Resources: 1 created, 0 changed, 1 deleted, 1 unchanged >>> [CLI] bundle plan Warning: lifecycle.triggers.on_file_change: no synced files match "seed.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged diff --git a/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt b/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt index 73f170fe8e9..cc48f824049 100644 --- a/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt +++ b/acceptance/bundle/resources/job_runs/on_file_change_errors/output.txt @@ -2,6 +2,7 @@ === a pattern that escapes the sync root is rejected >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: pattern "../shared/*.txt" is not under the sync root + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -15,6 +16,7 @@ Found 1 error === a glob that only matches directories warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "dirs/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -28,6 +30,7 @@ Found 1 warning === a glob matching a directory and an excluded file warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "mixed/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -41,6 +44,7 @@ Found 1 warning === a glob matching only excluded files warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "allexcluded/*" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -54,6 +58,7 @@ Found 1 warning === a pattern that matches nothing warns >>> [CLI] bundle validate Warning: lifecycle.triggers.on_file_change: no synced files match "missing/*.txt" + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -67,6 +72,7 @@ Found 1 warning === ** is not supported >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: ** in "src/**" is not supported; use * for a single directory level + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:24:29 Name: job-runs-on-file-change-errors @@ -80,6 +86,7 @@ Found 1 error === an absolute pattern is rejected >>> musterr [CLI] bundle validate Error: lifecycle.triggers.on_file_change: pattern "/tmp/seed.txt" must be relative to the defining YAML file + at resources.job_runs.my_run.lifecycle.triggers[0].on_file_change in databricks.yml:18:29 Name: job-runs-on-file-change-errors diff --git a/acceptance/bundle/resources/permissions/clusters/target/output.txt b/acceptance/bundle/resources/permissions/clusters/target/output.txt index 37ff60bd025..32f1a555758 100644 --- a/acceptance/bundle/resources/permissions/clusters/target/output.txt +++ b/acceptance/bundle/resources/permissions/clusters/target/output.txt @@ -10,6 +10,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:9:7 { @@ -56,6 +57,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:9:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/dev/files... diff --git a/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt b/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt index 07cfd2978b5..4c525a2b2bc 100644 --- a/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt +++ b/acceptance/bundle/resources/secrets/validate-secret-is-string/output.txt @@ -7,6 +7,7 @@ Warning: expected string, found sequence Error: Secret value must be a string at resources.secrets.secret1.value + in databricks.yml:7:7 The secret value for "secret1" must be a string. diff --git a/acceptance/bundle/run/scripts/no_content/output.txt b/acceptance/bundle/run/scripts/no_content/output.txt index f56be04cac9..8ade3094f0a 100644 --- a/acceptance/bundle/run/scripts/no_content/output.txt +++ b/acceptance/bundle/run/scripts/no_content/output.txt @@ -2,9 +2,11 @@ >>> [CLI] bundle run foo Error: Script bar has no content at scripts.bar.content + in databricks.yml:7:14 Error: Script foo has no content at scripts.foo.content + in databricks.yml:5:7 Exit code: 1 @@ -12,9 +14,11 @@ Exit code: 1 >>> [CLI] bundle run bar Error: Script bar has no content at scripts.bar.content + in databricks.yml:7:14 Error: Script foo has no content at scripts.foo.content + in databricks.yml:5:7 Exit code: 1 diff --git a/acceptance/bundle/run_as/empty_run_as_dict/output.txt b/acceptance/bundle/run_as/empty_run_as_dict/output.txt index 88363707b5c..4fc9f617b60 100644 --- a/acceptance/bundle/run_as/empty_run_as_dict/output.txt +++ b/acceptance/bundle/run_as/empty_run_as_dict/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:4:9 "run_as": {}, diff --git a/acceptance/bundle/run_as/empty_sp/output.txt b/acceptance/bundle/run_as/empty_sp/output.txt index 2664376966e..fb87bc34d73 100644 --- a/acceptance/bundle/run_as/empty_sp/output.txt +++ b/acceptance/bundle/run_as/empty_sp/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/empty_user/output.txt b/acceptance/bundle/run_as/empty_user/output.txt index 509819dff0a..9e333cd54bf 100644 --- a/acceptance/bundle/run_as/empty_user/output.txt +++ b/acceptance/bundle/run_as/empty_user/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/empty_user_and_sp/output.txt b/acceptance/bundle/run_as/empty_user_and_sp/output.txt index 0d330606fee..21da625f1fd 100644 --- a/acceptance/bundle/run_as/empty_user_and_sp/output.txt +++ b/acceptance/bundle/run_as/empty_user_and_sp/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt b/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt index 3634b730576..24d52c2a070 100644 --- a/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt +++ b/acceptance/bundle/run_as/invalid_both_sp_and_user/output.txt @@ -1,4 +1,5 @@ Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + at run_as in databricks.yml:6:3 Name: run_as diff --git a/acceptance/bundle/summary/engine-terraform/output.txt b/acceptance/bundle/summary/engine-terraform/output.txt index 815ccf11750..5a56fcf634a 100644 --- a/acceptance/bundle/summary/engine-terraform/output.txt +++ b/acceptance/bundle/summary/engine-terraform/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle summary Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -19,6 +20,7 @@ Resources: >>> [CLI] bundle summary -o json Warning: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -28,6 +30,7 @@ See https://docs.databricks.com/dev-tools/bundles/direct for details >>> musterr [CLI] bundle validate Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -40,6 +43,7 @@ Found 1 error >>> musterr [CLI] bundle validate -o json Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:3:11 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt index 32b813560a0..d494e84ba4c 100644 --- a/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt +++ b/acceptance/bundle/telemetry/deploy-workspace-folder-permissions/output.txt @@ -44,6 +44,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:23:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/user_not_declared/files... @@ -107,6 +108,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:47:7 Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/shared_users_can_manage/files... diff --git a/acceptance/bundle/validate/empty_resources/empty_dict/output.txt b/acceptance/bundle/validate/empty_resources/empty_dict/output.txt index 0ff8111602e..b58917eccad 100644 --- a/acceptance/bundle/validate/empty_resources/empty_dict/output.txt +++ b/acceptance/bundle/validate/empty_resources/empty_dict/output.txt @@ -148,6 +148,7 @@ Warning: required field "name" is not set in databricks.yml:6:12 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:6:12 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/empty_resources/with_grants/output.txt b/acceptance/bundle/validate/empty_resources/with_grants/output.txt index 38c4dc55d19..99ccf3de3ec 100644 --- a/acceptance/bundle/validate/empty_resources/with_grants/output.txt +++ b/acceptance/bundle/validate/empty_resources/with_grants/output.txt @@ -185,6 +185,7 @@ Warning: required field "name" is not set in databricks.yml:7:7 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:7:7 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/empty_resources/with_permissions/output.txt b/acceptance/bundle/validate/empty_resources/with_permissions/output.txt index cef0b18baa3..41b71086fdd 100644 --- a/acceptance/bundle/validate/empty_resources/with_permissions/output.txt +++ b/acceptance/bundle/validate/empty_resources/with_permissions/output.txt @@ -164,6 +164,7 @@ Warning: required field "name" is not set in databricks.yml:7:7 Error: Missing app source code path or git source + at resources.apps.rname in databricks.yml:7:7 app resource 'rname' should have either source_code_path or git_source field diff --git a/acceptance/bundle/validate/engine-config-valid/output.txt b/acceptance/bundle/validate/engine-config-valid/output.txt index e3b138ac1ce..125c9df8a90 100644 --- a/acceptance/bundle/validate/engine-config-valid/output.txt +++ b/acceptance/bundle/validate/engine-config-valid/output.txt @@ -10,6 +10,7 @@ Validation OK! >>> musterr [CLI] bundle validate -t terraform-target Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:10:15 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. @@ -22,6 +23,7 @@ Found 1 error >>> musterr [CLI] bundle validate -t terraform-target Error: the Terraform deployment engine has been removed in Databricks CLI v1.20.0 + at bundle.engine in databricks.yml:10:15 Remove the "bundle.engine" setting (or set it to "direct") to deploy with the direct engine; existing Terraform state is migrated automatically. To keep using Terraform, revert to Databricks CLI v1.19.x. diff --git a/acceptance/bundle/validate/invalid-engine-bundle/output.txt b/acceptance/bundle/validate/invalid-engine-bundle/output.txt index e76e18b693b..2df5b97577d 100644 --- a/acceptance/bundle/validate/invalid-engine-bundle/output.txt +++ b/acceptance/bundle/validate/invalid-engine-bundle/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle validate Error: invalid value "invalid_engine" for bundle.engine (expected "direct") + at bundle.engine in databricks.yml:3:11 Name: test-engine-config diff --git a/acceptance/bundle/validate/invalid-engine-target/output.txt b/acceptance/bundle/validate/invalid-engine-target/output.txt index c44d9dea7b6..af65b8f85ee 100644 --- a/acceptance/bundle/validate/invalid-engine-target/output.txt +++ b/acceptance/bundle/validate/invalid-engine-target/output.txt @@ -1,6 +1,7 @@ >>> [CLI] bundle validate Error: invalid value "invalid_engine" for bundle.engine (expected "direct") + at bundle.engine in databricks.yml:7:15 Name: test-engine-config diff --git a/acceptance/bundle/validate/permissions_overlap/output.txt b/acceptance/bundle/validate/permissions_overlap/output.txt index 43b95ddd440..c2086552f5d 100644 --- a/acceptance/bundle/validate/permissions_overlap/output.txt +++ b/acceptance/bundle/validate/permissions_overlap/output.txt @@ -10,6 +10,7 @@ Consider using a adding a top-level permissions section such as the following: level: CAN_MANAGE See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + at permissions in databricks.yml:5:3 { diff --git a/acceptance/bundle/validate/presets_name_prefix_dev/output.txt b/acceptance/bundle/validate/presets_name_prefix_dev/output.txt index c7d93c71444..76b53f1a7db 100644 --- a/acceptance/bundle/validate/presets_name_prefix_dev/output.txt +++ b/acceptance/bundle/validate/presets_name_prefix_dev/output.txt @@ -12,6 +12,7 @@ presets: # PRESETS >>> musterr [CLI] bundle validate -o json Error: prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development' + at presets.name_prefix in databricks.yml:25:16 diff --git a/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt b/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt index eec035d2671..bed48e44963 100644 --- a/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt +++ b/acceptance/bundle/validate/secret_scope_invalid_permission_level/output.txt @@ -2,9 +2,11 @@ >>> [CLI] bundle validate Warning: invalid value "12" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[0].level + in databricks.yml:7:7 Warning: invalid value "fuzz_badlevel" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[1].level + in databricks.yml:7:7 Name: test-bundle Target: default @@ -17,9 +19,11 @@ Found 2 warnings >>> [CLI] bundle validate --strict Warning: invalid value "12" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[0].level + in databricks.yml:7:7 Warning: invalid value "fuzz_badlevel" for enum field. Valid values are [READ WRITE MANAGE] at resources.secret_scopes.bad_levels.permissions[1].level + in databricks.yml:7:7 Name: test-bundle Target: default diff --git a/acceptance/bundle/validate/secret_scope_required_principal/output.txt b/acceptance/bundle/validate/secret_scope_required_principal/output.txt index 987ce601cb6..99d2359f04a 100644 --- a/acceptance/bundle/validate/secret_scope_required_principal/output.txt +++ b/acceptance/bundle/validate/secret_scope_required_principal/output.txt @@ -11,14 +11,14 @@ Error: secret scope permission principal is required Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.wrong_type_principal.permissions[0] - in databricks.yml:18:7 + at resources.secret_scopes.no_principal.permissions[0] + in databricks.yml:7:7 Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.no_principal.permissions[0] - in databricks.yml:7:7 + at resources.secret_scopes.wrong_type_principal.permissions[0] + in databricks.yml:18:7 Set one of user_name, group_name or service_principal_name @@ -42,14 +42,14 @@ Error: secret scope permission principal is required Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.wrong_type_principal.permissions[0] - in databricks.yml:18:7 + at resources.secret_scopes.no_principal.permissions[0] + in databricks.yml:7:7 Set one of user_name, group_name or service_principal_name Error: secret scope permission principal is required - at resources.secret_scopes.no_principal.permissions[0] - in databricks.yml:7:7 + at resources.secret_scopes.wrong_type_principal.permissions[0] + in databricks.yml:18:7 Set one of user_name, group_name or service_principal_name diff --git a/acceptance/bundle/validate/volume_defaults/output.txt b/acceptance/bundle/validate/volume_defaults/output.txt index ecdbd7ae7b7..8de98a74537 100644 --- a/acceptance/bundle/validate/volume_defaults/output.txt +++ b/acceptance/bundle/validate/volume_defaults/output.txt @@ -1,3 +1,7 @@ +Warning: required field "catalog_name" is not set + at resources.volumes.v1 + in databricks.yml:7:7 + Warning: required field "catalog_name" is not set at resources.volumes.v2 in databricks.yml:10:7 @@ -6,7 +10,7 @@ Warning: required field "catalog_name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "catalog_name" is not set +Warning: required field "name" is not set at resources.volumes.v1 in databricks.yml:7:7 @@ -18,7 +22,7 @@ Warning: required field "name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "name" is not set +Warning: required field "schema_name" is not set at resources.volumes.v1 in databricks.yml:7:7 @@ -30,10 +34,6 @@ Warning: required field "schema_name" is not set at resources.volumes.v3 in databricks.yml:13:7 -Warning: required field "schema_name" is not set - at resources.volumes.v1 - in databricks.yml:7:7 - Warning: invalid value "" for enum field. Valid values are [EXTERNAL MANAGED] at resources.volumes.v1.volume_type in databricks.yml:7:20 diff --git a/bundle/apps/validate.go b/bundle/apps/validate.go index be356a3d6d8..ad13574e0dc 100644 --- a/bundle/apps/validate.go +++ b/bundle/apps/validate.go @@ -25,30 +25,30 @@ func (v *validate) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics for key, app := range b.Config.Resources.Apps { if app.SourceCodePath == "" && app.GitSource == nil { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Missing app source code path or git source", - Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)), + Severity: diag.Error, + Summary: "Missing app source code path or git source", + Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field", key), + Paths: structpath.NewPathSlice("resources", "apps", key), }) continue } if app.SourceCodePath != "" && app.GitSource != nil { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Both source_code_path and git_source fields are set", - Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key)), + Severity: diag.Error, + Summary: "Both source_code_path and git_source fields are set", + Detail: fmt.Sprintf("app resource '%s' should have either source_code_path or git_source field, not both", key), + Paths: structpath.NewPathSlice("resources", "apps", key), }) continue } if _, ok := usedSourceCodePaths[app.SourceCodePath]; ok { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Duplicate app source code path", - Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "apps", key, "source_code_path")), + Severity: diag.Error, + Summary: "Duplicate app source code path", + Detail: fmt.Sprintf("app resource '%s' has the same source code path as app resource '%s', this will lead to the app configuration being overridden by each other", key, usedSourceCodePaths[app.SourceCodePath]), + Paths: structpath.NewPathSlice("resources", "apps", key, "source_code_path"), }) } usedSourceCodePaths[app.SourceCodePath] = key @@ -167,8 +167,7 @@ func warnForAppResourcePermissions(b *bundle.Bundle, appKey string, app *resourc ref.permission, appKey, ), - Paths: []*structpath.PathNode{appPath}, - Locations: b.Config.GetLocationsOf(appPath), + Paths: []*structpath.PathNode{appPath}, }) } diff --git a/bundle/bundle.go b/bundle/bundle.go index 68654bead7c..823c42a618f 100644 --- a/bundle/bundle.go +++ b/bundle/bundle.go @@ -22,10 +22,12 @@ import ( "github.com/databricks/cli/bundle/statemgmt/resourcestate" "github.com/databricks/cli/libs/auth" "github.com/databricks/cli/libs/cache" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/fileset" "github.com/databricks/cli/libs/locker" "github.com/databricks/cli/libs/log" "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structpath" libsync "github.com/databricks/cli/libs/sync" "github.com/databricks/cli/libs/tags" "github.com/databricks/cli/libs/telemetry/protos" @@ -236,6 +238,14 @@ func Load(ctx context.Context, path string) (*Bundle, error) { return b, nil } +// setLocationsOf lets logdiag fill diagnostic locations from the paths into the bundle configuration. +// The closure reads b.Config at log time, so later configuration changes are seen. +func setLocationsOf(ctx context.Context, b *Bundle) { + logdiag.SetLocationsOf(ctx, func(p *structpath.PathNode) []diag.Location { + return b.Config.GetLocationsOf(p) + }) +} + // MustLoad returns a bundle configuration. // The errors are recorded by logdiag, check with logdiag.HasError(). func MustLoad(ctx context.Context) *Bundle { @@ -254,6 +264,7 @@ func MustLoad(ctx context.Context) *Bundle { logdiag.LogError(ctx, err) return nil } + setLocationsOf(ctx, b) return b } @@ -281,6 +292,7 @@ func TryLoad(ctx context.Context) *Bundle { logdiag.LogError(ctx, err) return nil } + setLocationsOf(ctx, b) return b } diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset.go b/bundle/config/mutator/apply_source_linked_deployment_preset.go index 5c560db0116..fddd1a2e2f2 100644 --- a/bundle/config/mutator/apply_source_linked_deployment_preset.go +++ b/bundle/config/mutator/apply_source_linked_deployment_preset.go @@ -77,11 +77,10 @@ func (m *applySourceLinkedDeploymentPreset) Apply(ctx context.Context, b *bundle path := structpath.MustParsePath("workspace.file_path") diags = diags.Append( diag.Diagnostic{ - Severity: diag.Warning, - Summary: "workspace.file_path setting will be ignored in source-linked deployment mode", - Detail: "In source-linked deployment files are not copied to the destination and resources use source files instead", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.GetLocationsOf(path), + Severity: diag.Warning, + Summary: "workspace.file_path setting will be ignored in source-linked deployment mode", + Detail: "In source-linked deployment files are not copied to the destination and resources use source files instead", + Paths: []*structpath.PathNode{path}, }, ) } diff --git a/bundle/config/mutator/apply_source_linked_deployment_preset_test.go b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go index 8d50788a3b3..ece102fb358 100644 --- a/bundle/config/mutator/apply_source_linked_deployment_preset_test.go +++ b/bundle/config/mutator/apply_source_linked_deployment_preset_test.go @@ -138,12 +138,12 @@ func TestApplyPresetsSourceLinkedDeployment(t *testing.T) { if tt.expectedWarning != "" { require.Equal(t, tt.expectedWarning, diags[0].Summary) - require.NotEmpty(t, diags[0].Locations) + require.NotEmpty(t, diags[0].Paths) } if tt.expectedError != "" { require.Equal(t, tt.expectedError, diags[0].Summary) - require.NotEmpty(t, diags[0].Locations) + require.NotEmpty(t, diags[0].Paths) } require.Equal(t, tt.expectedValue, b.Config.Presets.SourceLinkedDeployment) diff --git a/bundle/config/mutator/compute_id_compat.go b/bundle/config/mutator/compute_id_compat.go index 6948ba3bf25..b14372ee49d 100644 --- a/bundle/config/mutator/compute_id_compat.go +++ b/bundle/config/mutator/compute_id_compat.go @@ -58,8 +58,9 @@ func rewriteComputeIdToClusterId(b *bundle.Bundle, p *structpath.PathNode, v str } diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: "compute_id is deprecated, please use cluster_id instead", + Severity: diag.Warning, + Summary: "compute_id is deprecated, please use cluster_id instead", + // Explicit: the key is deleted below, before the diagnostic is logged. Locations: computeId.Locations(), Paths: []*structpath.PathNode{structpath.NewStringKey(diagPath, "compute_id")}, }) diff --git a/bundle/config/mutator/compute_id_compate_test.go b/bundle/config/mutator/compute_id_compate_test.go index 728bae43191..0add944b167 100644 --- a/bundle/config/mutator/compute_id_compate_test.go +++ b/bundle/config/mutator/compute_id_compate_test.go @@ -8,6 +8,7 @@ import ( "github.com/databricks/cli/bundle/config/mutator" "github.com/databricks/cli/libs/diag" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestComputeIdToClusterId(t *testing.T) { @@ -54,3 +55,15 @@ func TestComputeIdToClusterIdInTargetOverride(t *testing.T) { assert.Equal(t, "compute_id is deprecated, please use cluster_id instead", diags[0].Summary) assert.Equal(t, diag.Warning, diags[0].Severity) } + +func TestComputeIdToClusterIdKeepsLocation(t *testing.T) { + root, diags := config.LoadFromBytes("databricks.yml", []byte("bundle:\n name: x\n compute_id: compute-id\n")) + require.NoError(t, diags.Error()) + b := &bundle.Bundle{Config: *root} + + diags = bundle.Apply(t.Context(), b, mutator.ComputeIdToClusterId()) + require.NoError(t, diags.Error()) + require.Len(t, diags, 1) + // The key is deleted before the diagnostic is logged, so its location is kept explicitly. + assert.Equal(t, []diag.Location{{File: "databricks.yml", Line: 3, Column: 15}}, diags[0].Locations) +} diff --git a/bundle/config/mutator/load_dbalert_files.go b/bundle/config/mutator/load_dbalert_files.go index 41bb604597c..c5e898b23f4 100644 --- a/bundle/config/mutator/load_dbalert_files.go +++ b/bundle/config/mutator/load_dbalert_files.go @@ -80,8 +80,8 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia Severity: diag.Error, Summary: fmt.Sprintf("field %s is not allowed in the bundle configuration.", k), Detail: "When a .dbalert.json is specified, only the following fields are allowed in the bundle configuration: " + strings.Join(allowedInYAML, ", "), + Locations: nil, Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, k)}, - Locations: v.Locations(), }, } } @@ -102,8 +102,8 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia Severity: diag.Error, Summary: fmt.Sprintf("failed to read .dbalert.json file %s: %s", alert.FilePath, err), Detail: "", + Locations: nil, Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), }, } } @@ -117,8 +117,8 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia Severity: diag.Error, Summary: fmt.Sprintf("failed to parse .dbalert.json file %s: %s", alert.FilePath, err), Detail: "", + Locations: nil, Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), }, } } @@ -131,8 +131,8 @@ func (m *loadDBAlertFiles) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia Severity: diag.Error, Summary: fmt.Sprintf(".alert file %s must not contain variable interpolations.", alert.FilePath), Detail: "Please inline the alert configuration in the bundle configuration to use variables", + Locations: nil, Paths: []*structpath.PathNode{structpath.NewStringKey(alertPath, "file_path")}, - Locations: alertV.Get("file_path").Locations(), }, } } diff --git a/bundle/config/mutator/reject_internal_resources.go b/bundle/config/mutator/reject_internal_resources.go index 0e4e28e53b5..071e7065acd 100644 --- a/bundle/config/mutator/reject_internal_resources.go +++ b/bundle/config/mutator/reject_internal_resources.go @@ -24,10 +24,9 @@ func (m *rejectInternalResources) Apply(ctx context.Context, b *bundle.Bundle) d var diags diag.Diagnostics if b.Config.Resources.HasInternalResources() { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "Internal resources cannot be set in bundle configuration", - Paths: structpath.NewPathSlice("resources"), - Locations: b.Config.GetLocations("resources"), + Severity: diag.Error, + Summary: "Internal resources cannot be set in bundle configuration", + Paths: structpath.NewPathSlice("resources"), }) } diff --git a/bundle/config/mutator/resolve_job_run_file_triggers.go b/bundle/config/mutator/resolve_job_run_file_triggers.go index 3e097afbb61..83fc50711d5 100644 --- a/bundle/config/mutator/resolve_job_run_file_triggers.go +++ b/bundle/config/mutator/resolve_job_run_file_triggers.go @@ -118,9 +118,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri matched, err := pathlib.Match(relPattern, rel) if err != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), + Paths: []*structpath.PathNode{loc}, }) continue } @@ -130,9 +130,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri hash, err := hashFile(b.SyncRoot, rel) if err != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("hash %q: %s", rel, err), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("hash %q: %s", rel, err), + Paths: []*structpath.PathNode{loc}, }) continue } @@ -144,9 +144,9 @@ func resolveFileTrigger(b *bundle.Bundle, loc *structpath.PathNode, pattern stri } if matches == 0 && !diags.HasError() { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fileTriggerPrefix + fmt.Sprintf("no synced files match %q", pattern), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Warning, + Summary: fileTriggerPrefix + fmt.Sprintf("no synced files match %q", pattern), + Paths: []*structpath.PathNode{loc}, }) } return relPattern, hex.EncodeToString(h.Sum(nil)), diags @@ -157,9 +157,9 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt // A double star looks recursive but path.Match treats it as two ordinary stars. if strings.Contains(pattern, "**") { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("** in %q is not supported; use * for a single directory level", pattern), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("** in %q is not supported; use * for a single directory level", pattern), + Paths: []*structpath.PathNode{loc}, }) } // Reject a genuinely absolute path; Join would otherwise silently reinterpret it @@ -169,9 +169,9 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt // through to the sync-root containment check below. if filepath.IsAbs(pattern) || pathlib.IsAbs(pattern) { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q must be relative to the defining YAML file", pattern), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q must be relative to the defining YAML file", pattern), + Paths: []*structpath.PathNode{loc}, }) } // NormalizePaths has already rewritten YAML-relative globs to be bundle-root @@ -181,18 +181,18 @@ func validateFileTriggerPattern(b *bundle.Bundle, loc *structpath.PathNode, patt relPattern, err := filepath.Rel(b.SyncRootPath, joined) if err != nil || !filepath.IsLocal(relPattern) { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q is not under the sync root", pattern), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("pattern %q is not under the sync root", pattern), + Paths: []*structpath.PathNode{loc}, }) } relPattern = filepath.ToSlash(relPattern) _, err = pathlib.Match(relPattern, "") if err != nil { return "", diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), - Locations: b.Config.GetLocationsOf(loc), + Severity: diag.Error, + Summary: fileTriggerPrefix + fmt.Sprintf("invalid pattern %q: %s", pattern, err), + Paths: []*structpath.PathNode{loc}, }) } return relPattern, diags diff --git a/bundle/config/mutator/resourcemutator/apply_presets.go b/bundle/config/mutator/resourcemutator/apply_presets.go index bb84240d7cc..15b92c1d4d4 100644 --- a/bundle/config/mutator/resourcemutator/apply_presets.go +++ b/bundle/config/mutator/resourcemutator/apply_presets.go @@ -11,6 +11,7 @@ import ( "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/metrics" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/structs/structpath" "github.com/databricks/cli/libs/textutil" "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/jobs" @@ -350,8 +351,10 @@ func validatePauseStatus(b *bundle.Bundle) diag.Diagnostics { return nil } return diag.Diagnostics{{ - Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED", - Severity: diag.Error, + Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED", + Severity: diag.Error, + Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), + // Only the effective location; the automatic lookup would also add overridden ones. Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, }} } diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index 21751e32813..4a184133bba 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -55,9 +55,9 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics { } if identityCount != 1 { return diag.Diagnostics{{ - Summary: "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name", - Locations: []diag.Location{b.Config.GetLocation("run_as")}, - Severity: diag.Error, + Summary: "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name", + Paths: structpath.NewPathSlice("run_as"), + Severity: diag.Error, }} } @@ -236,10 +236,9 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { setRunAsForJobs(b) return diag.Diagnostics{ { - Severity: diag.Warning, - Summary: "You are using the legacy mode of run_as. The support for this mode is experimental and might be removed in a future release of the CLI. In order to run the pipelines in your DABs project as the run_as user this mode changes the owners of the pipelines to the run_as identity, which requires the user deploying the bundle to be a workspace admin, and also a Metastore admin if the pipeline target is in UC.", - Paths: structpath.NewPathSlice("experimental", "use_legacy_run_as"), - Locations: b.Config.GetLocations("experimental.use_legacy_run_as"), + Severity: diag.Warning, + Summary: "You are using the legacy mode of run_as. The support for this mode is experimental and might be removed in a future release of the CLI. In order to run the pipelines in your DABs project as the run_as user this mode changes the owners of the pipelines to the run_as identity, which requires the user deploying the bundle to be a workspace admin, and also a Metastore admin if the pipeline target is in UC.", + Paths: structpath.NewPathSlice("experimental", "use_legacy_run_as"), }, } } diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index 1be8d37957c..2946762e8a0 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -10,7 +10,6 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/libs/diag" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" "github.com/databricks/databricks-sdk-go/service/pipelines" @@ -383,7 +382,7 @@ func TestRunAsIdentities(t *testing.T) { diags = bundle.Apply(t.Context(), b, SetRunAs()) if tc.wantError { require.ErrorContains(t, diags.Error(), "run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name") - assert.Equal(t, []diag.Location{r.GetLocation("run_as")}, diags[0].Locations) + assert.Equal(t, "run_as", diags[0].Paths[0].String()) } else { require.NoError(t, diags.Error()) } diff --git a/bundle/config/mutator/resourcemutator/validate_target_mode.go b/bundle/config/mutator/resourcemutator/validate_target_mode.go index 825d7267665..f5240f2bff5 100644 --- a/bundle/config/mutator/resourcemutator/validate_target_mode.go +++ b/bundle/config/mutator/resourcemutator/validate_target_mode.go @@ -10,6 +10,7 @@ import ( "github.com/databricks/cli/bundle" "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/structs/structpath" ) type validateTargetMode struct{} @@ -52,8 +53,10 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { // historically allowed.) if p.TriggerPauseStatus == config.Unpaused { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", + Severity: diag.Error, + Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default", + Paths: structpath.NewPathSlice("presets", "trigger_pause_status"), + // Only the effective location; the automatic lookup would also add overridden ones. Locations: []diag.Location{b.Config.GetLocation("presets.trigger_pause_status")}, }) } @@ -74,8 +77,10 @@ func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics { // it's a pitfall for users if they don't include it and later find out that // only a single user can do development deployments. diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", + Severity: diag.Error, + Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'", + Paths: structpath.NewPathSlice("presets", "name_prefix"), + // Only the effective location; the automatic lookup would also add overridden ones. Locations: []diag.Location{b.Config.GetLocation("presets.name_prefix")}, }) } diff --git a/bundle/config/mutator/rewrite_workspace_prefix.go b/bundle/config/mutator/rewrite_workspace_prefix.go index 390a13bb490..e194086fac0 100644 --- a/bundle/config/mutator/rewrite_workspace_prefix.go +++ b/bundle/config/mutator/rewrite_workspace_prefix.go @@ -61,11 +61,10 @@ func (m *rewriteWorkspacePrefix) Apply(ctx context.Context, b *bundle.Bundle) di newPath = strings.ReplaceAll(newPath, rewrite.pattern, rewrite.replacement) diags = append(diags, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", rewrite.pattern, vv, newPath), - Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths", - Locations: v.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Warning, + Summary: fmt.Sprintf("substring %q found in %q. Please update this to %q.", rewrite.pattern, vv, newPath), + Detail: "For more information, please refer to: https://docs.databricks.com/en/release-notes/dev-tools/bundles.html#workspace-paths", + Paths: []*structpath.PathNode{p}, }) } diff --git a/bundle/config/mutator/sync_infer_root.go b/bundle/config/mutator/sync_infer_root.go index b0cc9f0e8fb..2a4225675ab 100644 --- a/bundle/config/mutator/sync_infer_root.go +++ b/bundle/config/mutator/sync_infer_root.go @@ -91,10 +91,9 @@ func (m *syncInferRoot) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagno } diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("invalid sync path %q", path), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "sync", "paths", i)), - Paths: []*structpath.PathNode{structpath.NewIndex(structpath.MustParsePath("sync.paths"), i)}, + Severity: diag.Error, + Summary: fmt.Sprintf("invalid sync path %q", path), + Paths: []*structpath.PathNode{structpath.NewIndex(structpath.MustParsePath("sync.paths"), i)}, }) } diff --git a/bundle/config/mutator/sync_infer_root_test.go b/bundle/config/mutator/sync_infer_root_test.go index e34b4210bd6..e2c5f081432 100644 --- a/bundle/config/mutator/sync_infer_root_test.go +++ b/bundle/config/mutator/sync_infer_root_test.go @@ -190,9 +190,9 @@ func TestSyncInferRoot_Error(t *testing.T) { diags := bundle.Apply(ctx, b, mutator.SyncInferRoot()) require.Len(t, diags, 2) assert.Equal(t, `invalid sync path "../../../../error"`, diags[0].Summary) - assert.Equal(t, "databricks.yml:0:0", diags[0].Locations[0].String()) + assert.Equal(t, "databricks.yml:0:0", b.Config.GetLocationsOf(diags[0].Paths[0])[0].String()) assert.Equal(t, "sync.paths[0]", diags[0].Paths[0].String()) assert.Equal(t, `invalid sync path "../../../../../error"`, diags[1].Summary) - assert.Equal(t, "databricks.yml:0:0", diags[1].Locations[0].String()) + assert.Equal(t, "databricks.yml:0:0", b.Config.GetLocationsOf(diags[1].Paths[0])[0].String()) assert.Equal(t, "sync.paths[2]", diags[1].Paths[0].String()) } diff --git a/bundle/config/mutator/validate_job_run_triggers.go b/bundle/config/mutator/validate_job_run_triggers.go index 3d9df745261..3579d3f6281 100644 --- a/bundle/config/mutator/validate_job_run_triggers.go +++ b/bundle/config/mutator/validate_job_run_triggers.go @@ -30,34 +30,34 @@ func (*validateJobRunTriggers) Apply(_ context.Context, b *bundle.Bundle) diag.D path := structpath.NewPath(nil, "resources", "job_runs", name, "lifecycle", "triggers", i) if t.OnBundleDeploy == nil && t.OnFileChange == nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers entry must set on_bundle_deploy or on_file_change", - Locations: b.Config.GetLocationsOf(path), + Severity: diag.Error, + Summary: "lifecycle.triggers entry must set on_bundle_deploy or on_file_change", + Paths: []*structpath.PathNode{path}, }) continue } if t.OnBundleDeploy != nil && t.OnFileChange != nil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers entry must set only one of on_bundle_deploy or on_file_change", - Locations: b.Config.GetLocationsOf(path), + Severity: diag.Error, + Summary: "lifecycle.triggers entry must set only one of on_bundle_deploy or on_file_change", + Paths: []*structpath.PathNode{path}, }) continue } if t.OnBundleDeploy != nil && !*t.OnBundleDeploy { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers.on_bundle_deploy must be true when set", - Locations: b.Config.GetLocationsOf(structpath.NewPath(path, "on_bundle_deploy")), + Severity: diag.Error, + Summary: "lifecycle.triggers.on_bundle_deploy must be true when set", + Paths: []*structpath.PathNode{structpath.NewPath(path, "on_bundle_deploy")}, }) } if t.OnFileChange != nil { onFileChange := structpath.NewPath(path, "on_file_change") if strings.TrimSpace(*t.OnFileChange) == "" { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "lifecycle.triggers.on_file_change must be non-empty when set", - Locations: b.Config.GetLocationsOf(onFileChange), + Severity: diag.Error, + Summary: "lifecycle.triggers.on_file_change must be non-empty when set", + Paths: []*structpath.PathNode{onFileChange}, }) continue } diff --git a/bundle/config/mutator/validate_secret_value_is_variable.go b/bundle/config/mutator/validate_secret_value_is_variable.go index 467f8a6d2ae..bf65bbd9704 100644 --- a/bundle/config/mutator/validate_secret_value_is_variable.go +++ b/bundle/config/mutator/validate_secret_value_is_variable.go @@ -29,11 +29,10 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun val := b.Config.View().Lookup(p) if !val.IsValid() { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Secret value must be a string", - Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: "Secret value must be a string", + Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), + Paths: []*structpath.PathNode{p}, }) continue } @@ -41,11 +40,10 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun valueStr, ok := val.AsString() if !ok { diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "Secret value must be a string", - Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: "Secret value must be a string", + Detail: fmt.Sprintf(`The secret value for "%s" must be a string.`, key), + Paths: []*structpath.PathNode{p}, }) continue } @@ -58,8 +56,7 @@ func (v *validateSecretValueIsVariable) Apply(ctx context.Context, b *bundle.Bun Detail: fmt.Sprintf(`The secret value for "%s" must be a variable reference (e.g., ${var.my_secret}). Plain text secret values are not allowed to prevent leaking secrets in configuration files. Use bundle variables to pass secret values at deployment time.`, key), - Locations: val.Locations(), - Paths: []*structpath.PathNode{p}, + Paths: []*structpath.PathNode{p}, }) continue } diff --git a/bundle/config/validate/all_resources_have_values.go b/bundle/config/validate/all_resources_have_values.go index 9e3aecb9743..2ad149b9145 100644 --- a/bundle/config/validate/all_resources_have_values.go +++ b/bundle/config/validate/all_resources_have_values.go @@ -39,10 +39,9 @@ func (m *allResourcesHaveValues) Apply(ctx context.Context, b *bundle.Bundle) di p := structpath.NewPath(nil, "resources", group.Description.PluralName, rName) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("%s %s is not defined", rType, rName), - Locations: b.Config.LocationsAt(p), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: fmt.Sprintf("%s %s is not defined", rType, rName), + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/enum.go b/bundle/config/validate/enum.go index 392b431c50c..11fa2a22edf 100644 --- a/bundle/config/validate/enum.go +++ b/bundle/config/validate/enum.go @@ -1,7 +1,6 @@ package validate import ( - "cmp" "context" "fmt" "maps" @@ -61,10 +60,9 @@ func (f *enum) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { if !validValue { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("invalid value %q for enum field. Valid values are %v", strValue, validValues), - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Warning, + Summary: fmt.Sprintf("invalid value %q for enum field. Valid values are %v", strValue, validValues), + Paths: []*structpath.PathNode{np}, }) } @@ -74,16 +72,7 @@ func (f *enum) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { return diag.FromErr(err) } - // Sort diagnostics to make them deterministic - slices.SortFunc(diags, func(a, b diag.Diagnostic) int { - // First sort by summary - if n := cmp.Compare(a.Summary, b.Summary); n != 0 { - return n - } - - // Then sort by locations as a tie breaker if summaries are the same. - return cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)) - }) + sortDiagnostics(diags) return diags } diff --git a/bundle/config/validate/files_to_sync.go b/bundle/config/validate/files_to_sync.go index b2408bbf3ce..95f2781d2f1 100644 --- a/bundle/config/validate/files_to_sync.go +++ b/bundle/config/validate/files_to_sync.go @@ -61,8 +61,7 @@ func (v *filesToSync) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnost Summary: "There are no files to sync, please check your .gitignore and sync.exclude configuration", // Show all locations where sync.exclude is defined, since merging // sync.exclude is additive. - Locations: b.Config.GetLocations("sync.exclude"), - Paths: structpath.NewPathSlice("sync", "exclude"), + Paths: structpath.NewPathSlice("sync", "exclude"), }) } diff --git a/bundle/config/validate/interpolation_in_auth_config.go b/bundle/config/validate/interpolation_in_auth_config.go index a282b72790e..3e7999f0551 100644 --- a/bundle/config/validate/interpolation_in_auth_config.go +++ b/bundle/config/validate/interpolation_in_auth_config.go @@ -74,8 +74,7 @@ func (f *noInterpolationInAuthConfig) Apply(ctx context.Context, b *bundle.Bundl Summary: "Variable interpolation is not supported for fields that configure authentication", Detail: fmt.Sprintf(`Interpolation is not supported for the field %s. Please set the %s environment variable if you wish to configure this field at runtime.`, p.String(), envVar), - Locations: v.Locations(), - Paths: []*structpath.PathNode{p}, + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/job_task_cluster_spec.go b/bundle/config/validate/job_task_cluster_spec.go index 71bce7173d5..9ecaea9c908 100644 --- a/bundle/config/validate/job_task_cluster_spec.go +++ b/bundle/config/validate/job_task_cluster_spec.go @@ -34,14 +34,14 @@ func (v *jobTaskClusterSpec) Apply(ctx context.Context, b *bundle.Bundle) diag.D for taskIndex, task := range job.Tasks { taskPath := structpath.NewPath(resourcePath, "tasks", taskIndex) - diags = diags.Extend(validateJobTask(b, task, taskPath)) + diags = diags.Extend(validateJobTask(task, taskPath)) } } return diags } -func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.PathNode) diag.Diagnostics { +func validateJobTask(task jobs.Task, taskPath *structpath.PathNode) diag.Diagnostics { diags := diag.Diagnostics{} var specified []string @@ -74,7 +74,7 @@ func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.Path if task.ForEachTask != nil { forEachTaskPath := structpath.NewPath(taskPath, "for_each_task", "task") - diags = diags.Extend(validateJobTask(b, task.ForEachTask.Task, forEachTaskPath)) + diags = diags.Extend(validateJobTask(task.ForEachTask.Task, forEachTaskPath)) } if isComputeTask(task) && len(specified) == 0 { @@ -89,11 +89,10 @@ func validateJobTask(b *bundle.Bundle, task jobs.Task, taskPath *structpath.Path ) diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "Missing required cluster or environment settings", - Detail: detail, - Locations: b.Config.GetLocationsOf(taskPath), - Paths: []*structpath.PathNode{taskPath}, + Severity: diag.Error, + Summary: "Missing required cluster or environment settings", + Detail: detail, + Paths: []*structpath.PathNode{taskPath}, }) } } diff --git a/bundle/config/validate/no_interpolation_in_bundle_name.go b/bundle/config/validate/no_interpolation_in_bundle_name.go index 0850f27ac15..ef1ea23ed57 100644 --- a/bundle/config/validate/no_interpolation_in_bundle_name.go +++ b/bundle/config/validate/no_interpolation_in_bundle_name.go @@ -28,8 +28,7 @@ func (m *noInterpolationInBundleName) Apply(ctx context.Context, b *bundle.Bundl is a part of the path at which your bundle state is stored at by default. Parameterizing it at runtime can have unexpected consequences like duplicate deployments or resources not being cleaned up during bundle destroy.`, - Locations: b.Config.GetLocations("bundle.name"), - Paths: structpath.NewPathSlice("bundle", "name"), + Paths: structpath.NewPathSlice("bundle", "name"), }) } diff --git a/bundle/config/validate/no_variable_reference_in_resource_key.go b/bundle/config/validate/no_variable_reference_in_resource_key.go index a2db01ffa14..7000ecb60c4 100644 --- a/bundle/config/validate/no_variable_reference_in_resource_key.go +++ b/bundle/config/validate/no_variable_reference_in_resource_key.go @@ -36,10 +36,9 @@ func (m *noVariableReferenceInResourceKey) Apply(_ context.Context, b *bundle.Bu } p := structpath.NewPath(prefix, group.Description.PluralName, key) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("resource key %q must not contain variable references", key), - Locations: b.Config.LocationsAt(p), - Paths: []*structpath.PathNode{p}, + Severity: diag.Error, + Summary: fmt.Sprintf("resource key %q must not contain variable references", key), + Paths: []*structpath.PathNode{p}, }) } } diff --git a/bundle/config/validate/required.go b/bundle/config/validate/required.go index 2cad8a4d9be..6eb485f21ea 100644 --- a/bundle/config/validate/required.go +++ b/bundle/config/validate/required.go @@ -48,10 +48,9 @@ func warnForMissingFields(ctx context.Context, b *bundle.Bundle) diag.Diagnostic vv := v.Get(field) if vv.Kind() == structvar.KindInvalid || vv.Kind() == structvar.KindNil { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("required field %q is not set", field), - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Warning, + Summary: fmt.Sprintf("required field %q is not set", field), + Paths: []*structpath.PathNode{np}, }) } } @@ -75,13 +74,12 @@ func sortDiagnostics(diags diag.Diagnostics) { return n } - // Then sort by locations as a tie breaker if summaries are the same. - if n := cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)); n != 0 { + // Then by path: locations are filled in from paths only when the diagnostics are + // logged, after sorting. + if n := cmp.Compare(fmt.Sprintf("%v", a.Paths), fmt.Sprintf("%v", b.Paths)); n != 0 { return n } - - // Sibling entries can share a location; fall back to path for a stable order. - return cmp.Compare(fmt.Sprintf("%v", a.Paths), fmt.Sprintf("%v", b.Paths)) + return cmp.Compare(fmt.Sprintf("%v", a.Locations), fmt.Sprintf("%v", b.Locations)) }) } @@ -128,10 +126,9 @@ func errorForMissingFields(ctx context.Context, b *bundle.Bundle) diag.Diagnosti if strings.TrimSpace(warehouse.Name) == "" { path := structpath.NewPath(nil, "resources", "sql_warehouses", key) diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "sql_warehouse name is required", - Locations: b.Config.GetLocationsOf(path), - Paths: []*structpath.PathNode{path}, + Severity: diag.Error, + Summary: "sql_warehouse name is required", + Paths: []*structpath.PathNode{path}, }) } } @@ -155,18 +152,16 @@ func errorForInvalidGrants(ctx context.Context, b *bundle.Bundle) diag.Diagnosti func(np *structpath.PathNode, v structvar.View) error { if isMissingOrEmptyString(v.Get("principal")) { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "grant principal is required", - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Error, + Summary: "grant principal is required", + Paths: []*structpath.PathNode{np}, }) } if isMissingOrEmptySequence(v.Get("privileges")) { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "grant privileges is required", - Locations: v.Locations(), - Paths: []*structpath.PathNode{np}, + Severity: diag.Error, + Summary: "grant privileges is required", + Paths: []*structpath.PathNode{np}, }) } return nil diff --git a/bundle/config/validate/scripts.go b/bundle/config/validate/scripts.go index 328ba2404b6..73943afd289 100644 --- a/bundle/config/validate/scripts.go +++ b/bundle/config/validate/scripts.go @@ -78,8 +78,7 @@ from "content" with $NAME: env: MY_VAR: ${var.foo} content: echo "$MY_VAR"`, - Locations: b.Config.LocationsAt(p), - Paths: []*structpath.PathNode{p}, + Paths: []*structpath.PathNode{p}, }} } @@ -100,10 +99,9 @@ func validateScriptEnv(b *bundle.Bundle, key string, env map[string]string) diag continue } diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("${%s} cannot be used in scripts.%s.env.%s; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute", refPath, key, name), - Locations: b.Config.LocationsAt(envValuePath), - Paths: []*structpath.PathNode{envValuePath}, + Severity: diag.Error, + Summary: fmt.Sprintf("${%s} cannot be used in scripts.%s.env.%s; only ${bundle.*}, ${workspace.*}, and ${var.*} are resolved before scripts execute", refPath, key, name), + Paths: []*structpath.PathNode{envValuePath}, }) } } diff --git a/bundle/config/validate/validate_artifact_path.go b/bundle/config/validate/validate_artifact_path.go index 572c3a0de5a..ceb1b31ed22 100644 --- a/bundle/config/validate/validate_artifact_path.go +++ b/bundle/config/validate/validate_artifact_path.go @@ -83,10 +83,9 @@ func (v *validateArtifactPath) Apply(ctx context.Context, b *bundle.Bundle) diag wrapErrorMsg := func(s string) diag.Diagnostics { return diag.Diagnostics{ { - Summary: s, - Severity: diag.Error, - Locations: b.Config.GetLocations("workspace.artifact_path"), - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Summary: s, + Severity: diag.Error, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }, } } diff --git a/bundle/config/validate/validate_artifact_path_test.go b/bundle/config/validate/validate_artifact_path_test.go index 778326430e0..d4e30b2e708 100644 --- a/bundle/config/validate/validate_artifact_path_test.go +++ b/bundle/config/validate/validate_artifact_path_test.go @@ -80,10 +80,9 @@ func TestValidateArtifactPath(t *testing.T) { assertDiags := func(t *testing.T, diags diag.Diagnostics, expected string) { assert.Len(t, diags, 1) assert.Equal(t, diag.Diagnostics{{ - Severity: diag.Error, - Summary: expected, - Locations: []diag.Location{{File: "file", Line: 1, Column: 1}}, - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Severity: diag.Error, + Summary: expected, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }}, diags) } @@ -167,10 +166,9 @@ func TestValidateArtifactPathWithInvalidPaths(t *testing.T) { diags := ValidateArtifactPath().Apply(t.Context(), b) require.Equal(t, diag.Diagnostics{{ - Severity: diag.Error, - Summary: "expected UC volume path to be in the format /Volumes////..., got " + p, - Locations: []diag.Location{{File: "config.yml", Line: 1, Column: 2}}, - Paths: structpath.NewPathSlice("workspace", "artifact_path"), + Severity: diag.Error, + Summary: "expected UC volume path to be in the format /Volumes////..., got " + p, + Paths: structpath.NewPathSlice("workspace", "artifact_path"), }}, diags) } } diff --git a/bundle/config/validate/validate_dashboard_etags.go b/bundle/config/validate/validate_dashboard_etags.go index 1715f22cb84..853fecfe5fb 100644 --- a/bundle/config/validate/validate_dashboard_etags.go +++ b/bundle/config/validate/validate_dashboard_etags.go @@ -25,10 +25,9 @@ func (v *validateDashboardEtags) Apply(ctx context.Context, b *bundle.Bundle) di if dashboard.Etag != "" { return diag.Diagnostics{ { - Severity: diag.Error, - Summary: fmt.Sprintf("dashboard %q has an etag set. Etags must not be set in bundle configuration", dashboard.DisplayName), - Paths: structpath.NewPathSlice("resources", "dashboards", k), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "dashboards", k)), + Severity: diag.Error, + Summary: fmt.Sprintf("dashboard %q has an etag set. Etags must not be set in bundle configuration", dashboard.DisplayName), + Paths: structpath.NewPathSlice("resources", "dashboards", k), }, } } diff --git a/bundle/config/validate/validate_deployment_fields.go b/bundle/config/validate/validate_deployment_fields.go index 59868cb3fca..c89e5b9a07e 100644 --- a/bundle/config/validate/validate_deployment_fields.go +++ b/bundle/config/validate/validate_deployment_fields.go @@ -32,10 +32,9 @@ func (v *validateDeploymentFields) Apply(_ context.Context, b *bundle.Bundle) di } path := structpath.NewPath(resource, "deployment", field) diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: field + " must not be set in bundle configuration; it is managed by Declarative Automation Bundles", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.GetLocationsOf(path), + Severity: diag.Error, + Summary: field + " must not be set in bundle configuration; it is managed by Declarative Automation Bundles", + Paths: []*structpath.PathNode{path}, }) } diff --git a/bundle/config/validate/validate_engine.go b/bundle/config/validate/validate_engine.go index 11f7b106b87..5bdb5763fda 100644 --- a/bundle/config/validate/validate_engine.go +++ b/bundle/config/validate/validate_engine.go @@ -40,14 +40,14 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos return diags } - loc := b.Config.GetLocation("bundle.engine") - parsed, ok := engine.Parse(string(configEngine)) if !ok { return diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), - Locations: []diag.Location{loc}, + Severity: diag.Error, + Summary: fmt.Sprintf("invalid value %q for bundle.engine (expected %q)", configEngine, engine.EngineDirect), + Paths: structpath.NewPathSlice("bundle", "engine"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("bundle.engine")}, }) } @@ -57,10 +57,12 @@ func (v *validateEngine) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos severity = diag.Warning } return diags.Append(diag.Diagnostic{ - Severity: severity, - Summary: engine.TerraformRemovedSummary, - Detail: engine.TerraformRemovedConfigDetail, - Locations: []diag.Location{loc}, + Severity: severity, + Summary: engine.TerraformRemovedSummary, + Detail: engine.TerraformRemovedConfigDetail, + Paths: structpath.NewPathSlice("bundle", "engine"), + // Only the effective location; the automatic lookup would also add overridden ones. + Locations: []diag.Location{b.Config.GetLocation("bundle.engine")}, }) } diff --git a/bundle/config/validate/validate_engine_test.go b/bundle/config/validate/validate_engine_test.go index da1bd7bf12d..4e4c49c3ca7 100644 --- a/bundle/config/validate/validate_engine_test.go +++ b/bundle/config/validate/validate_engine_test.go @@ -38,7 +38,7 @@ func TestValidateEngineTerraformRemoved(t *testing.T) { assert.Len(t, diags, 1) assert.Equal(t, diag.Error, diags[0].Severity) assert.Contains(t, diags[0].Summary, "has been removed") - assert.Equal(t, []diag.Location{loc}, diags[0].Locations) + assert.Equal(t, "bundle.engine", diags[0].Paths[0].String()) } func TestValidateEngineNotSet(t *testing.T) { diff --git a/bundle/config/validate/validate_genie_space_etags.go b/bundle/config/validate/validate_genie_space_etags.go index 963e572346f..e39c2f00914 100644 --- a/bundle/config/validate/validate_genie_space_etags.go +++ b/bundle/config/validate/validate_genie_space_etags.go @@ -27,10 +27,9 @@ func (v *validateGenieSpaceEtags) Apply(ctx context.Context, b *bundle.Bundle) d if genieSpace.Etag != "" { return diag.Diagnostics{ { - Severity: diag.Error, - Summary: fmt.Sprintf("genie space %q has an etag set. Etags must not be set in bundle configuration", genieSpace.Title), - Paths: structpath.NewPathSlice("resources", "genie_spaces", k), - Locations: b.Config.GetLocationsOf(structpath.NewPath(nil, "resources", "genie_spaces", k)), + Severity: diag.Error, + Summary: fmt.Sprintf("genie space %q has an etag set. Etags must not be set in bundle configuration", genieSpace.Title), + Paths: structpath.NewPathSlice("resources", "genie_spaces", k), }, } } diff --git a/bundle/config/validate/validate_job_run_idempotency_token.go b/bundle/config/validate/validate_job_run_idempotency_token.go index 61e07e302ca..ff5dc7f4bd8 100644 --- a/bundle/config/validate/validate_job_run_idempotency_token.go +++ b/bundle/config/validate/validate_job_run_idempotency_token.go @@ -34,10 +34,9 @@ func (v *validateJobRunIdempotencyToken) Apply(_ context.Context, b *bundle.Bund // the run is deleted and break the next deploy. path := structpath.NewPath(nil, "resources", "job_runs", name, "idempotency_token") diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: "idempotency_token must not be set in bundle configuration; the CLI sets it on each run-now request", - Paths: []*structpath.PathNode{path}, - Locations: b.Config.GetLocationsOf(path), + Severity: diag.Error, + Summary: "idempotency_token must not be set in bundle configuration; the CLI sets it on each run-now request", + Paths: []*structpath.PathNode{path}, }) } diff --git a/bundle/config/validate/validate_sync_patterns.go b/bundle/config/validate/validate_sync_patterns.go index c888a05a7dd..35dce4ebcdd 100644 --- a/bundle/config/validate/validate_sync_patterns.go +++ b/bundle/config/validate/validate_sync_patterns.go @@ -59,10 +59,9 @@ func checkPatterns(ctx context.Context, patterns []string, path *structpath.Path if len(all) == 0 { path := structpath.NewPath(path, index) logdiag.LogDiag(ctx, diag.Diagnostic{ - Severity: diag.Warning, - Summary: fmt.Sprintf("Pattern %s does not match any files", pattern), - Locations: b.Config.GetLocationsOf(path), - Paths: []*structpath.PathNode{path}, + Severity: diag.Warning, + Summary: fmt.Sprintf("Pattern %s does not match any files", pattern), + Paths: []*structpath.PathNode{path}, }) } return nil diff --git a/bundle/config/validate/validate_volume_path.go b/bundle/config/validate/validate_volume_path.go index 802034bbbee..c1a6e9942d0 100644 --- a/bundle/config/validate/validate_volume_path.go +++ b/bundle/config/validate/validate_volume_path.go @@ -35,11 +35,10 @@ func (m *validateVolumePath) Apply(ctx context.Context, b *bundle.Bundle) diag.D for _, check := range pathChecks { if check.path != "" && strings.HasPrefix(check.path, "/Volumes/") { diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("%s %s starts with /Volumes. /Volumes can only be used with workspace.artifact_path.", check.configPath, check.path), - Detail: "For more information, see https://docs.databricks.com/aws/en/dev-tools/bundles/settings#workspace", - Locations: b.Config.GetLocationsOf(check.configPath), - Paths: []*structpath.PathNode{check.configPath}, + Severity: diag.Error, + Summary: fmt.Sprintf("%s %s starts with /Volumes. /Volumes can only be used with workspace.artifact_path.", check.configPath, check.path), + Detail: "For more information, see https://docs.databricks.com/aws/en/dev-tools/bundles/settings#workspace", + Paths: []*structpath.PathNode{check.configPath}, }) // Return early for root path validation diff --git a/bundle/permissions/permission_diagnostics.go b/bundle/permissions/permission_diagnostics.go index 0e01f72a0f4..55437b5b57d 100644 --- a/bundle/permissions/permission_diagnostics.go +++ b/bundle/permissions/permission_diagnostics.go @@ -10,6 +10,7 @@ import ( "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/iamutil" "github.com/databricks/cli/libs/set" + "github.com/databricks/cli/libs/structs/structpath" ) type permissionDiagnostics struct{} @@ -58,8 +59,8 @@ func (m *permissionDiagnostics) Apply(ctx context.Context, b *bundle.Bundle) dia identityType, b.Config.Workspace.CurrentUser.UserName, ), - Locations: []diag.Location{b.Config.GetLocation("permissions")}, - ID: diag.PermissionNotIncluded, + Paths: structpath.NewPathSlice("permissions"), + ID: diag.PermissionNotIncluded, }} } diff --git a/bundle/tests/sync_include_exclude_no_matches_test.go b/bundle/tests/sync_include_exclude_no_matches_test.go index 42938266c5d..a2d3496b668 100644 --- a/bundle/tests/sync_include_exclude_no_matches_test.go +++ b/bundle/tests/sync_include_exclude_no_matches_test.go @@ -3,6 +3,7 @@ package config_tests import ( "fmt" "path" + "path/filepath" "testing" "github.com/databricks/cli/bundle" @@ -25,10 +26,12 @@ func TestSyncIncludeExcludeNoMatchesTest(t *testing.T) { require.Len(t, diags[0].Paths, 1) require.Equal(t, "sync.exclude[0]", diags[0].Paths[0].String()) - assert.Len(t, diags[0].Locations, 1) - require.Equal(t, diags[0].Locations[0].File, path.Join("sync", "override", "databricks.yml")) - require.Equal(t, 17, diags[0].Locations[0].Line) - require.Equal(t, 11, diags[0].Locations[0].Column) + // Locations are filled from Paths by logdiag when the diagnostic is logged. + locations := b.Config.GetLocationsOf(diags[0].Paths[0]) + assert.Len(t, locations, 1) + require.Equal(t, path.Join("sync", "override", "databricks.yml"), filepath.ToSlash(locations[0].File)) + require.Equal(t, 17, locations[0].Line) + require.Equal(t, 11, locations[0].Column) summaries := []string{ fmt.Sprintf("Pattern %s does not match any files", path.Join("src", "*")), diff --git a/libs/logdiag/logdiag.go b/libs/logdiag/logdiag.go index 28ed3b5ba21..d74436f3455 100644 --- a/libs/logdiag/logdiag.go +++ b/libs/logdiag/logdiag.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/structs/structpath" ) type keyType int @@ -27,6 +28,10 @@ type LogDiagData struct { // Root to resolve location against Root string + // LocationsOf resolves the locations of a configuration path. + // It is used to fill Locations of diagnostics that only set Paths. Use SetLocationsOf() to set. + LocationsOf func(path *structpath.PathNode) []diag.Location + // If Collect is true, diagnostics are appended to Collected. Use SetCollected() to set. Collect bool Collected []diag.Diagnostic @@ -108,6 +113,14 @@ func SetRoot(ctx context.Context, root string) { read(ctx).Root = root } +func SetLocationsOf(ctx context.Context, fn func(path *structpath.PathNode) []diag.Location) { + val := read(ctx) + val.mu.Lock() + defer val.mu.Unlock() + + val.LocationsOf = fn +} + func SetCollect(ctx context.Context, collect bool) { val := read(ctx) val.mu.Lock() @@ -157,6 +170,14 @@ func LogDiag(ctx context.Context, d diag.Diagnostic) { return } + if len(d.Locations) == 0 && val.LocationsOf != nil { + var locations []diag.Location + for _, p := range d.Paths { + locations = append(locations, val.LocationsOf(p)...) + } + d.Locations = locations + } + for i := range d.Locations { // Make location relative to the root if d.Locations[i].File != "" { diff --git a/libs/logdiag/logdiag_test.go b/libs/logdiag/logdiag_test.go index ecf12ee78d1..21a036ab257 100644 --- a/libs/logdiag/logdiag_test.go +++ b/libs/logdiag/logdiag_test.go @@ -4,7 +4,9 @@ import ( "errors" "testing" + "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/structs/structpath" "github.com/stretchr/testify/assert" ) @@ -22,3 +24,48 @@ func TestIsolatedContext(t *testing.T) { assert.False(t, logdiag.HasError(ctx)) assert.Empty(t, logdiag.FlushCollected(ctx)) } + +func TestLocationsFilledFromPaths(t *testing.T) { + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + + var calls []string + logdiag.SetLocationsOf(ctx, func(p *structpath.PathNode) []diag.Location { + calls = append(calls, p.String()) + return []diag.Location{{File: p.String(), Line: 1}} + }) + + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "paths only", + Paths: structpath.MustParsePaths("a.b", "c"), + }) + explicit := []diag.Location{{File: "explicit", Line: 7}} + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "explicit", + Paths: structpath.MustParsePaths("a.b"), + Locations: explicit, + }) + + got := logdiag.FlushCollected(ctx) + assert.Len(t, got, 2) + assert.Equal(t, []string{"a.b", "c"}, calls) + assert.Equal(t, []diag.Location{{File: "a.b", Line: 1}, {File: "c", Line: 1}}, got[0].Locations) + assert.Equal(t, explicit, got[1].Locations) +} + +func TestLocationsNilResolver(t *testing.T) { + ctx := logdiag.InitContext(t.Context()) + logdiag.SetCollect(ctx, true) + + logdiag.LogDiag(ctx, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "no resolver", + Paths: structpath.MustParsePaths("a.b"), + }) + + got := logdiag.FlushCollected(ctx) + assert.Len(t, got, 1) + assert.Empty(t, got[0].Locations) +}