From 477cabe6b14b7060d0d02cb9dda21c6172c7a661 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Mon, 14 Sep 2026 14:59:20 +0000 Subject: [PATCH 1/4] Support bundle-wide group run identities for jobs --- .nextchanges/bundles/run-as-group.md | 1 + .../run_as/empty_run_as_dict/output.txt | 2 +- acceptance/bundle/run_as/empty_sp/output.txt | 2 +- .../bundle/run_as/empty_user/output.txt | 2 +- .../run_as/empty_user_and_sp/output.txt | 2 +- acceptance/bundle/run_as/group/databricks.yml | 61 ++++++++++++ acceptance/bundle/run_as/group/out.test.toml | 3 + acceptance/bundle/run_as/group/output.txt | 55 +++++++++++ acceptance/bundle/run_as/group/script | 7 ++ .../run_as/group_invalid/databricks.yml | 45 +++++++++ .../bundle/run_as/group_invalid/out.test.toml | 3 + .../bundle/run_as/group_invalid/output.txt | 84 ++++++++++++++++ acceptance/bundle/run_as/group_invalid/script | 7 ++ .../run_as/group_terraform/databricks.yml | 34 +++++++ .../run_as/group_terraform/out.test.toml | 3 + .../bundle/run_as/group_terraform/output.txt | 40 ++++++++ .../bundle/run_as/group_terraform/script | 12 +++ .../bundle/run_as/group_terraform/test.toml | 2 + .../run_as/group_unsupported/databricks.yml | 51 ++++++++++ .../run_as/group_unsupported/out.test.toml | 3 + .../run_as/group_unsupported/output.txt | 95 +++++++++++++++++++ .../bundle/run_as/group_unsupported/script | 4 + .../invalid_both_sp_and_user/output.txt | 2 +- .../config/mutator/resourcemutator/run_as.go | 65 ++++++++----- .../config/mutator/validate_run_as_group.go | 51 ++++++++++ .../mutator/validate_run_as_group_test.go | 40 ++++++++ bundle/phases/plan.go | 1 + 27 files changed, 650 insertions(+), 27 deletions(-) create mode 100644 .nextchanges/bundles/run-as-group.md create mode 100644 acceptance/bundle/run_as/group/databricks.yml create mode 100644 acceptance/bundle/run_as/group/out.test.toml create mode 100644 acceptance/bundle/run_as/group/output.txt create mode 100644 acceptance/bundle/run_as/group/script create mode 100644 acceptance/bundle/run_as/group_invalid/databricks.yml create mode 100644 acceptance/bundle/run_as/group_invalid/out.test.toml create mode 100644 acceptance/bundle/run_as/group_invalid/output.txt create mode 100644 acceptance/bundle/run_as/group_invalid/script create mode 100644 acceptance/bundle/run_as/group_terraform/databricks.yml create mode 100644 acceptance/bundle/run_as/group_terraform/out.test.toml create mode 100644 acceptance/bundle/run_as/group_terraform/output.txt create mode 100644 acceptance/bundle/run_as/group_terraform/script create mode 100644 acceptance/bundle/run_as/group_terraform/test.toml create mode 100644 acceptance/bundle/run_as/group_unsupported/databricks.yml create mode 100644 acceptance/bundle/run_as/group_unsupported/out.test.toml create mode 100644 acceptance/bundle/run_as/group_unsupported/output.txt create mode 100644 acceptance/bundle/run_as/group_unsupported/script create mode 100644 bundle/config/mutator/validate_run_as_group.go create mode 100644 bundle/config/mutator/validate_run_as_group_test.go diff --git a/.nextchanges/bundles/run-as-group.md b/.nextchanges/bundles/run-as-group.md new file mode 100644 index 00000000000..065e15bdfde --- /dev/null +++ b/.nextchanges/bundles/run-as-group.md @@ -0,0 +1 @@ +* Add support for `run_as.group_name` at the bundle and target levels for jobs using the direct deployment engine. 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 63762765fb5..88363707b5c 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,4 @@ -Error: run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name 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 f5808a912e8..2664376966e 100644 --- a/acceptance/bundle/run_as/empty_sp/output.txt +++ b/acceptance/bundle/run_as/empty_sp/output.txt @@ -1,4 +1,4 @@ -Error: run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name 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 77aa2920022..509819dff0a 100644 --- a/acceptance/bundle/run_as/empty_user/output.txt +++ b/acceptance/bundle/run_as/empty_user/output.txt @@ -1,4 +1,4 @@ -Error: run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name 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 31712da26ba..0d330606fee 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,4 @@ -Error: run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name in databricks.yml:5:3 { diff --git a/acceptance/bundle/run_as/group/databricks.yml b/acceptance/bundle/run_as/group/databricks.yml new file mode 100644 index 00000000000..0a702055ae1 --- /dev/null +++ b/acceptance/bundle/run_as/group/databricks.yml @@ -0,0 +1,61 @@ +bundle: + name: run_as_group + +run_as: + group_name: bundle_group + +resources: + jobs: + inherited: + name: Inherited identity + user_override: + name: User override + run_as: + user_name: user_override + sp_override: + name: Service principal override + run_as: + service_principal_name: sp_override + group_override: + name: Group override + run_as: + group_name: resource_group + pipelines: + pipeline_override: + name: Pipeline override + run_as: + user_name: pipeline_user + alerts: + alert_override: + display_name: Alert override + query_text: SELECT 1 + warehouse_id: "1234567890" + evaluation: + comparison_operator: EQUAL + source: + name: "1" + threshold: + value: + double_value: 1 + schedule: + quartz_cron_schedule: "0 0 * * * ?" + timezone_id: UTC + run_as: + service_principal_name: alert_sp + +targets: + default: + default: true + group: + run_as: + group_name: target_group + user: + run_as: + user_name: target_user + sp: + run_as: + service_principal_name: target_sp + production: + mode: production + workspace: + root_path: /Workspace/Users/${workspace.current_user.userName}/run_as_group diff --git a/acceptance/bundle/run_as/group/out.test.toml b/acceptance/bundle/run_as/group/out.test.toml new file mode 100644 index 00000000000..e1af1a235ad --- /dev/null +++ b/acceptance/bundle/run_as/group/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group/output.txt b/acceptance/bundle/run_as/group/output.txt new file mode 100644 index 00000000000..684cbe684a8 --- /dev/null +++ b/acceptance/bundle/run_as/group/output.txt @@ -0,0 +1,55 @@ + +>>> [CLI] bundle validate -t default -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.group_name = "bundle_group"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.group_name = "bundle_group"; + +>>> [CLI] bundle validate -t group -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.group_name = "target_group"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.group_name = "target_group"; + +>>> [CLI] bundle validate -t user -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.user_name = "target_user"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.user_name = "target_user"; + +>>> [CLI] bundle validate -t sp -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.service_principal_name = "target_sp"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.service_principal_name = "target_sp"; + +>>> [CLI] bundle validate -t production -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.group_name = "bundle_group"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.group_name = "bundle_group"; + +=== Target group replaces a bundle user +>>> [CLI] bundle validate -t group -o json +json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; +json.resources.jobs.group_override.run_as.group_name = "resource_group"; +json.resources.jobs.inherited.run_as.group_name = "target_group"; +json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; +json.resources.jobs.user_override.run_as.user_name = "user_override"; +json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; +json.run_as.group_name = "target_group"; diff --git a/acceptance/bundle/run_as/group/script b/acceptance/bundle/run_as/group/script new file mode 100644 index 00000000000..28f80c7eedc --- /dev/null +++ b/acceptance/bundle/run_as/group/script @@ -0,0 +1,7 @@ +for target in default group user sp production; do + trace $CLI bundle validate -t "$target" -o json | gron.py | grep -F .run_as +done + +title "Target group replaces a bundle user" +update_file.py databricks.yml "group_name: bundle_group" "user_name: bundle_user" +trace $CLI bundle validate -t group -o json | gron.py | grep -F .run_as diff --git a/acceptance/bundle/run_as/group_invalid/databricks.yml b/acceptance/bundle/run_as/group_invalid/databricks.yml new file mode 100644 index 00000000000..20b98bd1cbb --- /dev/null +++ b/acceptance/bundle/run_as/group_invalid/databricks.yml @@ -0,0 +1,45 @@ +bundle: + name: run_as_group_invalid + +run_as: + group_name: "" + +experimental: + use_legacy_run_as: false + +resources: + jobs: + job: + name: Job + +targets: + empty: + run_as: + group_name: "" + user_group: + run_as: + user_name: user + group_name: group + sp_group: + run_as: + service_principal_name: sp + group_name: group + all: + run_as: + user_name: user + service_principal_name: sp + group_name: group + legacy: + run_as: + group_name: group + resources: + pipelines: + pipeline: + name: Pipeline + legacy_empty: + run_as: + group_name: "" + legacy_conflict: + run_as: + user_name: user + group_name: group diff --git a/acceptance/bundle/run_as/group_invalid/out.test.toml b/acceptance/bundle/run_as/group_invalid/out.test.toml new file mode 100644 index 00000000000..e1af1a235ad --- /dev/null +++ b/acceptance/bundle/run_as/group_invalid/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_invalid/output.txt b/acceptance/bundle/run_as/group_invalid/output.txt new file mode 100644 index 00000000000..49b0cbbc5a5 --- /dev/null +++ b/acceptance/bundle/run_as/group_invalid/output.txt @@ -0,0 +1,84 @@ + +>>> musterr [CLI] bundle validate -t empty +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + in databricks.yml:18:7 + +Name: run_as_group_invalid +Target: empty +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/empty + +Found 1 error + +>>> musterr [CLI] bundle validate -t user_group +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + in databricks.yml:21:7 + +Name: run_as_group_invalid +Target: user_group +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/user_group + +Found 1 error + +>>> musterr [CLI] bundle validate -t sp_group +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + in databricks.yml:25:7 + +Name: run_as_group_invalid +Target: sp_group +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/sp_group + +Found 1 error + +>>> musterr [CLI] bundle validate -t all +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name + in databricks.yml:29:7 + +Name: run_as_group_invalid +Target: all +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/all + +Found 1 error + +>>> musterr [CLI] bundle validate -t legacy +Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity + in databricks.yml:34:19 + +Name: run_as_group_invalid +Target: legacy +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy + +Found 1 error + +>>> musterr [CLI] bundle validate -t legacy_empty +Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity + in databricks.yml:41:19 + +Name: run_as_group_invalid +Target: legacy_empty +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy_empty + +Found 1 error + +>>> musterr [CLI] bundle validate -t legacy_conflict +Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity + in databricks.yml:45:19 + +Name: run_as_group_invalid +Target: legacy_conflict +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy_conflict + +Found 1 error diff --git a/acceptance/bundle/run_as/group_invalid/script b/acceptance/bundle/run_as/group_invalid/script new file mode 100644 index 00000000000..2bb48e5580f --- /dev/null +++ b/acceptance/bundle/run_as/group_invalid/script @@ -0,0 +1,7 @@ +for target in empty user_group sp_group all; do + trace musterr $CLI bundle validate -t "$target" +done +update_file.py databricks.yml "use_legacy_run_as: false" "use_legacy_run_as: true" +for target in legacy legacy_empty legacy_conflict; do + trace musterr $CLI bundle validate -t "$target" +done diff --git a/acceptance/bundle/run_as/group_terraform/databricks.yml b/acceptance/bundle/run_as/group_terraform/databricks.yml new file mode 100644 index 00000000000..a5582a477ad --- /dev/null +++ b/acceptance/bundle/run_as/group_terraform/databricks.yml @@ -0,0 +1,34 @@ +bundle: + name: run_as_group_terraform + +run_as: + group_name: bundle_group + +resources: + jobs: + inherited: + name: Inherited identity + user_override: + name: User override + run_as: + user_name: user@example.test + sp_override: + name: Service principal override + run_as: + service_principal_name: sp + +targets: + default: + default: true + resource_group: + resources: + jobs: + inherited: + run_as: + group_name: resource_group + user_override: + resources: + jobs: + inherited: + run_as: + user_name: user@example.test diff --git a/acceptance/bundle/run_as/group_terraform/out.test.toml b/acceptance/bundle/run_as/group_terraform/out.test.toml new file mode 100644 index 00000000000..5d0f3e4aac2 --- /dev/null +++ b/acceptance/bundle/run_as/group_terraform/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_terraform/output.txt b/acceptance/bundle/run_as/group_terraform/output.txt new file mode 100644 index 00000000000..c4608021d0d --- /dev/null +++ b/acceptance/bundle/run_as/group_terraform/output.txt @@ -0,0 +1,40 @@ + +=== Terraform rejects inherited group identities before planning or deploying +>>> musterr [CLI] bundle plan +Error: run_as.group_name is only supported in direct deployment mode + at resources.jobs.inherited.run_as.group_name + +Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job + + +>>> musterr [CLI] bundle deploy +Error: run_as.group_name is only supported in direct deployment mode + at resources.jobs.inherited.run_as.group_name + +Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job + + +=== Terraform rejects explicit job group identities +>>> musterr [CLI] bundle plan -t resource_group +Error: run_as.group_name is only supported in direct deployment mode + at resources.jobs.inherited.run_as.group_name + in databricks.yml:28:25 + +Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job + + +=== Terraform accepts explicit user and service principal overrides +>>> [CLI] bundle plan -t user_override +create jobs.inherited +create jobs.sp_override +create jobs.user_override + +Plan: 3 to add, 0 to change, 0 to delete, 0 unchanged + +=== Direct engine accepts group identities +>>> DATABRICKS_BUNDLE_ENGINE=direct [CLI] bundle plan +create jobs.inherited +create jobs.sp_override +create jobs.user_override + +Plan: 3 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/run_as/group_terraform/script b/acceptance/bundle/run_as/group_terraform/script new file mode 100644 index 00000000000..71fe26c4546 --- /dev/null +++ b/acceptance/bundle/run_as/group_terraform/script @@ -0,0 +1,12 @@ +title "Terraform rejects inherited group identities before planning or deploying" +trace musterr $CLI bundle plan +trace musterr $CLI bundle deploy + +title "Terraform rejects explicit job group identities" +trace musterr $CLI bundle plan -t resource_group + +title "Terraform accepts explicit user and service principal overrides" +trace $CLI bundle plan -t user_override + +title "Direct engine accepts group identities" +trace DATABRICKS_BUNDLE_ENGINE=direct $CLI bundle plan diff --git a/acceptance/bundle/run_as/group_terraform/test.toml b/acceptance/bundle/run_as/group_terraform/test.toml new file mode 100644 index 00000000000..e182aac29db --- /dev/null +++ b/acceptance/bundle/run_as/group_terraform/test.toml @@ -0,0 +1,2 @@ +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] +Ignore = [".databricks"] diff --git a/acceptance/bundle/run_as/group_unsupported/databricks.yml b/acceptance/bundle/run_as/group_unsupported/databricks.yml new file mode 100644 index 00000000000..825b5e8df8d --- /dev/null +++ b/acceptance/bundle/run_as/group_unsupported/databricks.yml @@ -0,0 +1,51 @@ +bundle: + name: run_as_group_unsupported + +run_as: + group_name: ${workspace.current_user.userName} + +targets: + pipeline: + resources: + pipelines: + pipeline: + name: Pipeline + second: + name: Second pipeline + alert: + resources: + alerts: + alert: + display_name: Alert + second: + display_name: Second alert + model_serving: + resources: + model_serving_endpoints: + endpoint: + name: Endpoint + quality_monitor: + resources: + quality_monitors: + monitor: + table_name: main.schema.table + output_schema_name: main.schema + app: + resources: + apps: + app: + name: app + embedded_dashboard: + resources: + dashboards: + dashboard: + display_name: Dashboard + embed_credentials: true + warehouse_id: "1234567890" + dashboard: + resources: + dashboards: + dashboard: + display_name: Dashboard + embed_credentials: false + warehouse_id: "1234567890" diff --git a/acceptance/bundle/run_as/group_unsupported/out.test.toml b/acceptance/bundle/run_as/group_unsupported/out.test.toml new file mode 100644 index 00000000000..e1af1a235ad --- /dev/null +++ b/acceptance/bundle/run_as/group_unsupported/out.test.toml @@ -0,0 +1,3 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_unsupported/output.txt b/acceptance/bundle/run_as/group_unsupported/output.txt new file mode 100644 index 00000000000..cb57d3fd905 --- /dev/null +++ b/acceptance/bundle/run_as/group_unsupported/output.txt @@ -0,0 +1,95 @@ + +>>> musterr [CLI] bundle validate -t pipeline +Error: pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as + in databricks.yml:12:11 + +Error: pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as + in databricks.yml:14:11 + +Name: run_as_group_unsupported +Target: pipeline +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/pipeline + +Found 2 errors + +>>> musterr [CLI] bundle validate -t alert +Error: alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as + in databricks.yml:19:11 + +Error: alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as + in databricks.yml:21:11 + +Name: run_as_group_unsupported +Target: alert +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/alert + +Found 2 errors + +>>> musterr [CLI] bundle validate -t model_serving +Error: model_serving_endpoints do not support a setting a run_as user that is different from the owner. +Current identity: [USERNAME]. Run as identity: group "[USERNAME]". +See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. + in databricks.yml:25:9 + +Name: run_as_group_unsupported +Target: model_serving +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/model_serving + +Found 1 error + +>>> musterr [CLI] bundle validate -t quality_monitor +Error: quality_monitors do not support a setting a run_as user that is different from the owner. +Current identity: [USERNAME]. Run as identity: group "[USERNAME]". +See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. + in databricks.yml:30:9 + +Name: run_as_group_unsupported +Target: quality_monitor +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/quality_monitor + +Found 1 error + +>>> musterr [CLI] bundle validate -t app +Error: apps do not support a setting a run_as user that is different from the owner. +Current identity: [USERNAME]. Run as identity: group "[USERNAME]". +See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. + in databricks.yml:36:9 + +Name: run_as_group_unsupported +Target: app +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/app + +Found 1 error + +>>> musterr [CLI] bundle validate -t embedded_dashboard +Error: dashboards with embed_credentials set to true do not support a setting a run_as user that is different from the owner. +Current identity: [USERNAME]. Run as identity: group "[USERNAME]". +See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. + in databricks.yml:42:11 + +Name: run_as_group_unsupported +Target: embedded_dashboard +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/embedded_dashboard + +Found 1 error + +>>> [CLI] bundle validate -t dashboard +Name: run_as_group_unsupported +Target: dashboard +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/dashboard + +Validation OK! diff --git a/acceptance/bundle/run_as/group_unsupported/script b/acceptance/bundle/run_as/group_unsupported/script new file mode 100644 index 00000000000..5d8354b65da --- /dev/null +++ b/acceptance/bundle/run_as/group_unsupported/script @@ -0,0 +1,4 @@ +for target in pipeline alert model_serving quality_monitor app embedded_dashboard; do + trace musterr $CLI bundle validate -t "$target" +done +trace $CLI bundle validate -t dashboard 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 78188b8260c..3634b730576 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,4 @@ -Error: run_as section cannot specify both user_name and service_principal_name +Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name in databricks.yml:6:3 Name: run_as diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index 4f5e3ce9036..869fc14cde7 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -3,6 +3,7 @@ package resourcemutator import ( "context" "fmt" + "maps" "slices" "github.com/databricks/cli/bundle" @@ -44,31 +45,21 @@ func reportRunAsNotSupported(resourceType string, location dyn.Location, current func validateRunAs(b *bundle.Bundle) diag.Diagnostics { diags := diag.Diagnostics{} - neitherSpecifiedErr := diag.Diagnostics{{ - Summary: "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified", - Locations: []dyn.Location{b.Config.GetLocation("run_as")}, - Severity: diag.Error, - }} - - // Fail fast if neither service_principal_name nor user_name are specified, but the - // run_as section is present. - if b.Config.Value().Get("run_as").Kind() == dyn.KindNil { - return neitherSpecifiedErr - } - - // Fail fast if one or both of service_principal_name and user_name are specified, - // but with empty values. + identityCount := 0 runAs := b.Config.RunAs - if runAs.ServicePrincipalName == "" && runAs.UserName == "" { - return neitherSpecifiedErr + if runAs != nil { + for _, identity := range []string{runAs.UserName, runAs.ServicePrincipalName, runAs.GroupName} { + if identity != "" { + identityCount++ + } + } } - - if runAs.UserName != "" && runAs.ServicePrincipalName != "" { - diags = diags.Extend(diag.Diagnostics{{ - Summary: "run_as section cannot specify both user_name and service_principal_name", + 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: []dyn.Location{b.Config.GetLocation("run_as")}, Severity: diag.Error, - }}) + }} } identity := runAs.ServicePrincipalName @@ -77,10 +68,32 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics { } // All resources are supported if the run_as identity is the same as the current deployment identity. - if identity == b.Config.Workspace.CurrentUser.UserName { + if runAs.GroupName == "" && identity == b.Config.Workspace.CurrentUser.UserName { return diags } + if runAs.GroupName != "" { + identity = fmt.Sprintf("group %q", runAs.GroupName) + for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Pipelines)) { + if b.Config.Resources.Pipelines[key].RunAs == nil { + diags = diags.Extend(diag.Diagnostics{{ + Summary: "pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as", + Locations: []dyn.Location{b.Config.GetLocation("resources.pipelines." + key)}, + Severity: diag.Error, + }}) + } + } + for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Alerts)) { + if b.Config.Resources.Alerts[key].RunAs == nil { + diags = diags.Extend(diag.Diagnostics{{ + Summary: "alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as", + Locations: []dyn.Location{b.Config.GetLocation("resources.alerts." + key)}, + Severity: diag.Error, + }}) + } + } + } + // Model serving endpoints do not support run_as in the API. if len(b.Config.Resources.ModelServingEndpoints) > 0 { diags = diags.Extend(reportRunAsNotSupported( @@ -141,6 +154,7 @@ func setRunAsForJobs(b *bundle.Bundle) { continue } job.RunAs = &jobs.JobRunAs{ + GroupName: runAs.GroupName, ServicePrincipalName: runAs.ServicePrincipalName, UserName: runAs.UserName, } @@ -228,6 +242,13 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { // User has opted to use the legacy behavior of run_as with the // experimental.use_legacy_run_as flag. if b.Config.Experimental != nil && b.Config.Experimental.UseLegacyRunAs { + if b.Config.Value().Get("run_as").Get("group_name").Kind() != dyn.KindInvalid { + return diag.Diagnostics{{ + Summary: "run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity", + Locations: b.Config.GetLocations("run_as.group_name"), + Severity: diag.Error, + }} + } setPipelineOwnersToRunAsIdentity(b) setRunAsForJobs(b) return diag.Diagnostics{ diff --git a/bundle/config/mutator/validate_run_as_group.go b/bundle/config/mutator/validate_run_as_group.go new file mode 100644 index 00000000000..49ac0d34db6 --- /dev/null +++ b/bundle/config/mutator/validate_run_as_group.go @@ -0,0 +1,51 @@ +package mutator + +import ( + "context" + "maps" + "slices" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/libs/diag" + "github.com/databricks/cli/libs/dyn" +) + +type validateRunAsGroup struct { + engine engine.EngineType +} + +// ValidateRunAsGroup rejects group identities unsupported by the Terraform provider. +// See https://github.com/databricks/terraform-provider-databricks/blob/v1.131.0/jobs/resource_job.go#L629. +func ValidateRunAsGroup(e engine.EngineType) bundle.Mutator { + return &validateRunAsGroup{engine: e} +} + +func (m *validateRunAsGroup) Name() string { + return "ValidateRunAsGroup" +} + +func (m *validateRunAsGroup) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { + if m.engine.IsDirect() { + return nil + } + + var diags diag.Diagnostics + for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Jobs)) { + runAs := b.Config.Resources.Jobs[key].RunAs + if runAs == nil || runAs.GroupName == "" { + continue + } + path := dyn.NewPath(dyn.Key("resources"), dyn.Key("jobs"), dyn.Key(key), dyn.Key("run_as"), dyn.Key("group_name")) + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: "run_as.group_name is only supported in direct deployment mode", + Detail: "Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. " + + "For an existing Terraform deployment, run 'databricks bundle migrate' first. " + + "Alternatively, set run_as.user_name or run_as.service_principal_name on this job", + Locations: b.Config.GetLocations(path.String()), + Paths: []dyn.Path{path}, + }) + } + return diags +} diff --git a/bundle/config/mutator/validate_run_as_group_test.go b/bundle/config/mutator/validate_run_as_group_test.go new file mode 100644 index 00000000000..9ae053ca5fd --- /dev/null +++ b/bundle/config/mutator/validate_run_as_group_test.go @@ -0,0 +1,40 @@ +package mutator_test + +import ( + "testing" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" + "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/bundle/config/mutator" + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateRunAsGroup(t *testing.T) { + for _, tc := range []struct { + engine engine.EngineType + paths []string + }{ + {engine: engine.EngineDirect}, + {engine: engine.EngineTerraform, paths: []string{"resources.jobs.a.run_as.group_name", "resources.jobs.z.run_as.group_name"}}, + } { + t.Run(string(tc.engine), func(t *testing.T) { + b := &bundle.Bundle{Config: config.Root{Resources: config.Resources{Jobs: map[string]*resources.Job{ + "z": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{GroupName: "group"}}}, + "a": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{GroupName: "group"}}}, + "user": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{UserName: "user@example.test"}}}, + "sp": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{ServicePrincipalName: "sp"}}}, + "none": {}, + }}}} + diags := bundle.Apply(t.Context(), b, mutator.ValidateRunAsGroup(tc.engine)) + require.Len(t, diags, len(tc.paths)) + for i, path := range tc.paths { + assert.Equal(t, []dyn.Path{dyn.MustPathFromString(path)}, diags[i].Paths) + } + }) + } +} diff --git a/bundle/phases/plan.go b/bundle/phases/plan.go index aee481b2b14..ae83459aa2b 100644 --- a/bundle/phases/plan.go +++ b/bundle/phases/plan.go @@ -27,6 +27,7 @@ func PreDeployChecks(ctx context.Context, b *bundle.Bundle, isPlan bool, engine deploy.StatePull(), mutator.ValidateGitDetails(), mutator.ValidateDirectOnlyResources(engine), + mutator.ValidateRunAsGroup(engine), mutator.ValidateLifecycleStarted(engine), mutator.ValidateCascadeOnDestroy(engine), statemgmt.CheckRunningResource(engine), From f2c257cf667c50a3bf223ea684f2be520850d0c6 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Mon, 14 Sep 2026 15:03:49 +0000 Subject: [PATCH 2/4] Link group run identity changelog to PR --- .nextchanges/bundles/run-as-group.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/run-as-group.md b/.nextchanges/bundles/run-as-group.md index 065e15bdfde..a6a39a92c5f 100644 --- a/.nextchanges/bundles/run-as-group.md +++ b/.nextchanges/bundles/run-as-group.md @@ -1 +1 @@ -* Add support for `run_as.group_name` at the bundle and target levels for jobs using the direct deployment engine. +* Add support for `run_as.group_name` at the bundle and target levels for jobs using the direct deployment engine. ([#6676](https://github.com/databricks/cli/pull/6676)) From fb182c7ae428db6ea0f6727fd4dfa26d5484540e Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Wed, 16 Sep 2026 10:50:51 +0000 Subject: [PATCH 3/4] Simplify group run identity test coverage --- acceptance/bundle/run_as/group/databricks.yml | 40 -------- acceptance/bundle/run_as/group/out.test.toml | 2 +- acceptance/bundle/run_as/group/output.txt | 48 +--------- acceptance/bundle/run_as/group/script | 6 +- acceptance/bundle/run_as/group/test.toml | 2 + .../run_as/group_invalid/databricks.yml | 45 --------- .../bundle/run_as/group_invalid/out.test.toml | 3 - .../bundle/run_as/group_invalid/output.txt | 84 ---------------- acceptance/bundle/run_as/group_invalid/script | 7 -- .../run_as/group_terraform/databricks.yml | 34 ------- .../run_as/group_terraform/out.test.toml | 3 - .../bundle/run_as/group_terraform/output.txt | 40 -------- .../bundle/run_as/group_terraform/script | 12 --- .../bundle/run_as/group_terraform/test.toml | 2 - .../run_as/group_unsupported/databricks.yml | 51 ---------- .../run_as/group_unsupported/out.test.toml | 3 - .../run_as/group_unsupported/output.txt | 95 ------------------- .../bundle/run_as/group_unsupported/script | 4 - .../config/mutator/resourcemutator/run_as.go | 2 +- .../mutator/resourcemutator/run_as_test.go | 58 +++++++++++ 20 files changed, 69 insertions(+), 472 deletions(-) create mode 100644 acceptance/bundle/run_as/group/test.toml delete mode 100644 acceptance/bundle/run_as/group_invalid/databricks.yml delete mode 100644 acceptance/bundle/run_as/group_invalid/out.test.toml delete mode 100644 acceptance/bundle/run_as/group_invalid/output.txt delete mode 100644 acceptance/bundle/run_as/group_invalid/script delete mode 100644 acceptance/bundle/run_as/group_terraform/databricks.yml delete mode 100644 acceptance/bundle/run_as/group_terraform/out.test.toml delete mode 100644 acceptance/bundle/run_as/group_terraform/output.txt delete mode 100644 acceptance/bundle/run_as/group_terraform/script delete mode 100644 acceptance/bundle/run_as/group_terraform/test.toml delete mode 100644 acceptance/bundle/run_as/group_unsupported/databricks.yml delete mode 100644 acceptance/bundle/run_as/group_unsupported/out.test.toml delete mode 100644 acceptance/bundle/run_as/group_unsupported/output.txt delete mode 100644 acceptance/bundle/run_as/group_unsupported/script diff --git a/acceptance/bundle/run_as/group/databricks.yml b/acceptance/bundle/run_as/group/databricks.yml index 0a702055ae1..6c23ad2b469 100644 --- a/acceptance/bundle/run_as/group/databricks.yml +++ b/acceptance/bundle/run_as/group/databricks.yml @@ -12,36 +12,6 @@ resources: name: User override run_as: user_name: user_override - sp_override: - name: Service principal override - run_as: - service_principal_name: sp_override - group_override: - name: Group override - run_as: - group_name: resource_group - pipelines: - pipeline_override: - name: Pipeline override - run_as: - user_name: pipeline_user - alerts: - alert_override: - display_name: Alert override - query_text: SELECT 1 - warehouse_id: "1234567890" - evaluation: - comparison_operator: EQUAL - source: - name: "1" - threshold: - value: - double_value: 1 - schedule: - quartz_cron_schedule: "0 0 * * * ?" - timezone_id: UTC - run_as: - service_principal_name: alert_sp targets: default: @@ -49,13 +19,3 @@ targets: group: run_as: group_name: target_group - user: - run_as: - user_name: target_user - sp: - run_as: - service_principal_name: target_sp - production: - mode: production - workspace: - root_path: /Workspace/Users/${workspace.current_user.userName}/run_as_group diff --git a/acceptance/bundle/run_as/group/out.test.toml b/acceptance/bundle/run_as/group/out.test.toml index e1af1a235ad..59b56a2037c 100644 --- a/acceptance/bundle/run_as/group/out.test.toml +++ b/acceptance/bundle/run_as/group/out.test.toml @@ -1,3 +1,3 @@ Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group/output.txt b/acceptance/bundle/run_as/group/output.txt index 684cbe684a8..c524b530b17 100644 --- a/acceptance/bundle/run_as/group/output.txt +++ b/acceptance/bundle/run_as/group/output.txt @@ -1,55 +1,17 @@ ->>> [CLI] bundle validate -t default -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; +>>> [CLI] bundle validate -o json json.resources.jobs.inherited.run_as.group_name = "bundle_group"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; json.run_as.group_name = "bundle_group"; ->>> [CLI] bundle validate -t group -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; -json.resources.jobs.inherited.run_as.group_name = "target_group"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; -json.run_as.group_name = "target_group"; - ->>> [CLI] bundle validate -t user -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; -json.resources.jobs.inherited.run_as.user_name = "target_user"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; -json.run_as.user_name = "target_user"; +>>> DATABRICKS_BUNDLE_ENGINE=terraform musterr [CLI] bundle plan +Error: run_as.group_name is only supported in direct deployment mode + at resources.jobs.inherited.run_as.group_name ->>> [CLI] bundle validate -t sp -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; -json.resources.jobs.inherited.run_as.service_principal_name = "target_sp"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; -json.run_as.service_principal_name = "target_sp"; +Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job ->>> [CLI] bundle validate -t production -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; -json.resources.jobs.inherited.run_as.group_name = "bundle_group"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; -json.run_as.group_name = "bundle_group"; -=== Target group replaces a bundle user >>> [CLI] bundle validate -t group -o json -json.resources.alerts.alert_override.run_as.service_principal_name = "alert_sp"; -json.resources.jobs.group_override.run_as.group_name = "resource_group"; json.resources.jobs.inherited.run_as.group_name = "target_group"; -json.resources.jobs.sp_override.run_as.service_principal_name = "sp_override"; json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.resources.pipelines.pipeline_override.run_as.user_name = "pipeline_user"; json.run_as.group_name = "target_group"; diff --git a/acceptance/bundle/run_as/group/script b/acceptance/bundle/run_as/group/script index 28f80c7eedc..c71789f6a46 100644 --- a/acceptance/bundle/run_as/group/script +++ b/acceptance/bundle/run_as/group/script @@ -1,7 +1,5 @@ -for target in default group user sp production; do - trace $CLI bundle validate -t "$target" -o json | gron.py | grep -F .run_as -done +trace $CLI bundle validate -o json | gron.py | grep -F .run_as +trace DATABRICKS_BUNDLE_ENGINE=terraform musterr $CLI bundle plan -title "Target group replaces a bundle user" update_file.py databricks.yml "group_name: bundle_group" "user_name: bundle_user" trace $CLI bundle validate -t group -o json | gron.py | grep -F .run_as diff --git a/acceptance/bundle/run_as/group/test.toml b/acceptance/bundle/run_as/group/test.toml new file mode 100644 index 00000000000..de8f3c05172 --- /dev/null +++ b/acceptance/bundle/run_as/group/test.toml @@ -0,0 +1,2 @@ +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] +Ignore = [".databricks"] diff --git a/acceptance/bundle/run_as/group_invalid/databricks.yml b/acceptance/bundle/run_as/group_invalid/databricks.yml deleted file mode 100644 index 20b98bd1cbb..00000000000 --- a/acceptance/bundle/run_as/group_invalid/databricks.yml +++ /dev/null @@ -1,45 +0,0 @@ -bundle: - name: run_as_group_invalid - -run_as: - group_name: "" - -experimental: - use_legacy_run_as: false - -resources: - jobs: - job: - name: Job - -targets: - empty: - run_as: - group_name: "" - user_group: - run_as: - user_name: user - group_name: group - sp_group: - run_as: - service_principal_name: sp - group_name: group - all: - run_as: - user_name: user - service_principal_name: sp - group_name: group - legacy: - run_as: - group_name: group - resources: - pipelines: - pipeline: - name: Pipeline - legacy_empty: - run_as: - group_name: "" - legacy_conflict: - run_as: - user_name: user - group_name: group diff --git a/acceptance/bundle/run_as/group_invalid/out.test.toml b/acceptance/bundle/run_as/group_invalid/out.test.toml deleted file mode 100644 index e1af1a235ad..00000000000 --- a/acceptance/bundle/run_as/group_invalid/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_invalid/output.txt b/acceptance/bundle/run_as/group_invalid/output.txt deleted file mode 100644 index 49b0cbbc5a5..00000000000 --- a/acceptance/bundle/run_as/group_invalid/output.txt +++ /dev/null @@ -1,84 +0,0 @@ - ->>> musterr [CLI] bundle validate -t empty -Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name - in databricks.yml:18:7 - -Name: run_as_group_invalid -Target: empty -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/empty - -Found 1 error - ->>> musterr [CLI] bundle validate -t user_group -Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name - in databricks.yml:21:7 - -Name: run_as_group_invalid -Target: user_group -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/user_group - -Found 1 error - ->>> musterr [CLI] bundle validate -t sp_group -Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name - in databricks.yml:25:7 - -Name: run_as_group_invalid -Target: sp_group -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/sp_group - -Found 1 error - ->>> musterr [CLI] bundle validate -t all -Error: run_as section must specify exactly one non-empty identity: user_name, service_principal_name, or group_name - in databricks.yml:29:7 - -Name: run_as_group_invalid -Target: all -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/all - -Found 1 error - ->>> musterr [CLI] bundle validate -t legacy -Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity - in databricks.yml:34:19 - -Name: run_as_group_invalid -Target: legacy -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy - -Found 1 error - ->>> musterr [CLI] bundle validate -t legacy_empty -Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity - in databricks.yml:41:19 - -Name: run_as_group_invalid -Target: legacy_empty -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy_empty - -Found 1 error - ->>> musterr [CLI] bundle validate -t legacy_conflict -Error: run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity - in databricks.yml:45:19 - -Name: run_as_group_invalid -Target: legacy_conflict -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_invalid/legacy_conflict - -Found 1 error diff --git a/acceptance/bundle/run_as/group_invalid/script b/acceptance/bundle/run_as/group_invalid/script deleted file mode 100644 index 2bb48e5580f..00000000000 --- a/acceptance/bundle/run_as/group_invalid/script +++ /dev/null @@ -1,7 +0,0 @@ -for target in empty user_group sp_group all; do - trace musterr $CLI bundle validate -t "$target" -done -update_file.py databricks.yml "use_legacy_run_as: false" "use_legacy_run_as: true" -for target in legacy legacy_empty legacy_conflict; do - trace musterr $CLI bundle validate -t "$target" -done diff --git a/acceptance/bundle/run_as/group_terraform/databricks.yml b/acceptance/bundle/run_as/group_terraform/databricks.yml deleted file mode 100644 index a5582a477ad..00000000000 --- a/acceptance/bundle/run_as/group_terraform/databricks.yml +++ /dev/null @@ -1,34 +0,0 @@ -bundle: - name: run_as_group_terraform - -run_as: - group_name: bundle_group - -resources: - jobs: - inherited: - name: Inherited identity - user_override: - name: User override - run_as: - user_name: user@example.test - sp_override: - name: Service principal override - run_as: - service_principal_name: sp - -targets: - default: - default: true - resource_group: - resources: - jobs: - inherited: - run_as: - group_name: resource_group - user_override: - resources: - jobs: - inherited: - run_as: - user_name: user@example.test diff --git a/acceptance/bundle/run_as/group_terraform/out.test.toml b/acceptance/bundle/run_as/group_terraform/out.test.toml deleted file mode 100644 index 5d0f3e4aac2..00000000000 --- a/acceptance/bundle/run_as/group_terraform/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_terraform/output.txt b/acceptance/bundle/run_as/group_terraform/output.txt deleted file mode 100644 index c4608021d0d..00000000000 --- a/acceptance/bundle/run_as/group_terraform/output.txt +++ /dev/null @@ -1,40 +0,0 @@ - -=== Terraform rejects inherited group identities before planning or deploying ->>> musterr [CLI] bundle plan -Error: run_as.group_name is only supported in direct deployment mode - at resources.jobs.inherited.run_as.group_name - -Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job - - ->>> musterr [CLI] bundle deploy -Error: run_as.group_name is only supported in direct deployment mode - at resources.jobs.inherited.run_as.group_name - -Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job - - -=== Terraform rejects explicit job group identities ->>> musterr [CLI] bundle plan -t resource_group -Error: run_as.group_name is only supported in direct deployment mode - at resources.jobs.inherited.run_as.group_name - in databricks.yml:28:25 - -Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job - - -=== Terraform accepts explicit user and service principal overrides ->>> [CLI] bundle plan -t user_override -create jobs.inherited -create jobs.sp_override -create jobs.user_override - -Plan: 3 to add, 0 to change, 0 to delete, 0 unchanged - -=== Direct engine accepts group identities ->>> DATABRICKS_BUNDLE_ENGINE=direct [CLI] bundle plan -create jobs.inherited -create jobs.sp_override -create jobs.user_override - -Plan: 3 to add, 0 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/run_as/group_terraform/script b/acceptance/bundle/run_as/group_terraform/script deleted file mode 100644 index 71fe26c4546..00000000000 --- a/acceptance/bundle/run_as/group_terraform/script +++ /dev/null @@ -1,12 +0,0 @@ -title "Terraform rejects inherited group identities before planning or deploying" -trace musterr $CLI bundle plan -trace musterr $CLI bundle deploy - -title "Terraform rejects explicit job group identities" -trace musterr $CLI bundle plan -t resource_group - -title "Terraform accepts explicit user and service principal overrides" -trace $CLI bundle plan -t user_override - -title "Direct engine accepts group identities" -trace DATABRICKS_BUNDLE_ENGINE=direct $CLI bundle plan diff --git a/acceptance/bundle/run_as/group_terraform/test.toml b/acceptance/bundle/run_as/group_terraform/test.toml deleted file mode 100644 index e182aac29db..00000000000 --- a/acceptance/bundle/run_as/group_terraform/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] -Ignore = [".databricks"] diff --git a/acceptance/bundle/run_as/group_unsupported/databricks.yml b/acceptance/bundle/run_as/group_unsupported/databricks.yml deleted file mode 100644 index 825b5e8df8d..00000000000 --- a/acceptance/bundle/run_as/group_unsupported/databricks.yml +++ /dev/null @@ -1,51 +0,0 @@ -bundle: - name: run_as_group_unsupported - -run_as: - group_name: ${workspace.current_user.userName} - -targets: - pipeline: - resources: - pipelines: - pipeline: - name: Pipeline - second: - name: Second pipeline - alert: - resources: - alerts: - alert: - display_name: Alert - second: - display_name: Second alert - model_serving: - resources: - model_serving_endpoints: - endpoint: - name: Endpoint - quality_monitor: - resources: - quality_monitors: - monitor: - table_name: main.schema.table - output_schema_name: main.schema - app: - resources: - apps: - app: - name: app - embedded_dashboard: - resources: - dashboards: - dashboard: - display_name: Dashboard - embed_credentials: true - warehouse_id: "1234567890" - dashboard: - resources: - dashboards: - dashboard: - display_name: Dashboard - embed_credentials: false - warehouse_id: "1234567890" diff --git a/acceptance/bundle/run_as/group_unsupported/out.test.toml b/acceptance/bundle/run_as/group_unsupported/out.test.toml deleted file mode 100644 index e1af1a235ad..00000000000 --- a/acceptance/bundle/run_as/group_unsupported/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group_unsupported/output.txt b/acceptance/bundle/run_as/group_unsupported/output.txt deleted file mode 100644 index cb57d3fd905..00000000000 --- a/acceptance/bundle/run_as/group_unsupported/output.txt +++ /dev/null @@ -1,95 +0,0 @@ - ->>> musterr [CLI] bundle validate -t pipeline -Error: pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as - in databricks.yml:12:11 - -Error: pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as - in databricks.yml:14:11 - -Name: run_as_group_unsupported -Target: pipeline -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/pipeline - -Found 2 errors - ->>> musterr [CLI] bundle validate -t alert -Error: alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as - in databricks.yml:19:11 - -Error: alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as - in databricks.yml:21:11 - -Name: run_as_group_unsupported -Target: alert -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/alert - -Found 2 errors - ->>> musterr [CLI] bundle validate -t model_serving -Error: model_serving_endpoints do not support a setting a run_as user that is different from the owner. -Current identity: [USERNAME]. Run as identity: group "[USERNAME]". -See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. - in databricks.yml:25:9 - -Name: run_as_group_unsupported -Target: model_serving -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/model_serving - -Found 1 error - ->>> musterr [CLI] bundle validate -t quality_monitor -Error: quality_monitors do not support a setting a run_as user that is different from the owner. -Current identity: [USERNAME]. Run as identity: group "[USERNAME]". -See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. - in databricks.yml:30:9 - -Name: run_as_group_unsupported -Target: quality_monitor -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/quality_monitor - -Found 1 error - ->>> musterr [CLI] bundle validate -t app -Error: apps do not support a setting a run_as user that is different from the owner. -Current identity: [USERNAME]. Run as identity: group "[USERNAME]". -See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. - in databricks.yml:36:9 - -Name: run_as_group_unsupported -Target: app -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/app - -Found 1 error - ->>> musterr [CLI] bundle validate -t embedded_dashboard -Error: dashboards with embed_credentials set to true do not support a setting a run_as user that is different from the owner. -Current identity: [USERNAME]. Run as identity: group "[USERNAME]". -See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property. - in databricks.yml:42:11 - -Name: run_as_group_unsupported -Target: embedded_dashboard -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/embedded_dashboard - -Found 1 error - ->>> [CLI] bundle validate -t dashboard -Name: run_as_group_unsupported -Target: dashboard -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/run_as_group_unsupported/dashboard - -Validation OK! diff --git a/acceptance/bundle/run_as/group_unsupported/script b/acceptance/bundle/run_as/group_unsupported/script deleted file mode 100644 index 5d8354b65da..00000000000 --- a/acceptance/bundle/run_as/group_unsupported/script +++ /dev/null @@ -1,4 +0,0 @@ -for target in pipeline alert model_serving quality_monitor app embedded_dashboard; do - trace musterr $CLI bundle validate -t "$target" -done -trace $CLI bundle validate -t dashboard diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index 869fc14cde7..3a9543e5523 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -22,7 +22,7 @@ type setRunAs struct{} // 1. Sets the run_as field for jobs to the value of the run_as field in the bundle. // // 2. Validates that the bundle run_as configuration is valid in the context of the bundle. -// If the run_as user is different from the current deployment user, DABs only +// If the run_as identity differs from the deployment identity, DABs only // supports a subset of resources. func SetRunAs() bundle.Mutator { return &setRunAs{} diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index 13a3491a9f3..47967a060f7 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -1,6 +1,7 @@ package resourcemutator import ( + "fmt" "slices" "testing" @@ -316,3 +317,60 @@ func TestRunAsNoErrorForSupportedResources(t *testing.T) { require.NoError(t, diags.Error()) } } + +func TestRunAsGroupInvalidIdentity(t *testing.T) { + for _, tc := range []struct { + name string + runAs string + legacy bool + }{ + {name: "empty", runAs: `{group_name: ""}`}, + {name: "user and group", runAs: `{user_name: user, group_name: group}`}, + {name: "legacy", runAs: `{group_name: group}`, legacy: true}, + {name: "legacy empty", runAs: `{group_name: ""}`, legacy: true}, + } { + t.Run(tc.name, func(t *testing.T) { + yaml := fmt.Sprintf("run_as: %s\nexperimental: {use_legacy_run_as: %t}", tc.runAs, tc.legacy) + r, diags := config.LoadFromBytes("databricks.yml", []byte(yaml)) + require.NoError(t, diags.Error()) + b := &bundle.Bundle{Config: *r} + diags = bundle.Apply(t.Context(), b, SetRunAs()) + require.Error(t, diags.Error()) + if tc.legacy { + assert.Contains(t, diags.Error().Error(), "run_as.group_name is not supported with experimental.use_legacy_run_as") + } else { + assert.Contains(t, diags.Error().Error(), "run_as section must specify exactly one non-empty identity") + } + }) + } +} + +func TestRunAsGroupResources(t *testing.T) { + for _, tc := range []struct { + name string + resource string + wantError string + }{ + {name: "pipeline", resource: `pipelines: {test: {}}`, wantError: "pipelines do not support run_as.group_name"}, + {name: "alert", resource: `alerts: {test: {}}`, wantError: "alerts do not support run_as.group_name"}, + {name: "model serving", resource: `model_serving_endpoints: {test: {}}`, wantError: "Run as identity: group \"group\""}, + {name: "pipeline user override", resource: `pipelines: {test: {run_as: {user_name: user}}}`}, + {name: "alert sp override", resource: `alerts: {test: {run_as: {service_principal_name: sp}}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + yaml := "run_as: {group_name: group}\nworkspace: {current_user: {userName: group}}\nresources:\n " + tc.resource + r, diags := config.LoadFromBytes("databricks.yml", []byte(yaml)) + require.NoError(t, diags.Error()) + b := &bundle.Bundle{Config: *r} + before := b.Config.Value().Get("resources") + diags = bundle.Apply(t.Context(), b, SetRunAs()) + if tc.wantError != "" { + require.Error(t, diags.Error()) + assert.Contains(t, diags.Error().Error(), tc.wantError) + } else { + require.NoError(t, diags.Error()) + assert.Equal(t, before, b.Config.Value().Get("resources")) + } + }) + } +} From f8a28839218351b9168d95554fef7ed8b893f70a Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Fri, 18 Sep 2026 11:56:56 +0000 Subject: [PATCH 4/4] Simplify bundle group run-as validation --- acceptance/bundle/run_as/group/databricks.yml | 21 ----- acceptance/bundle/run_as/group/out.test.toml | 3 - acceptance/bundle/run_as/group/output.txt | 17 ---- acceptance/bundle/run_as/group/script | 5 - acceptance/bundle/run_as/group/test.toml | 2 - .../config/mutator/resourcemutator/run_as.go | 29 ++---- .../mutator/resourcemutator/run_as_test.go | 91 ++++++++++++++++--- .../config/mutator/validate_run_as_group.go | 51 ----------- .../mutator/validate_run_as_group_test.go | 40 -------- bundle/phases/plan.go | 1 - 10 files changed, 84 insertions(+), 176 deletions(-) delete mode 100644 acceptance/bundle/run_as/group/databricks.yml delete mode 100644 acceptance/bundle/run_as/group/out.test.toml delete mode 100644 acceptance/bundle/run_as/group/output.txt delete mode 100644 acceptance/bundle/run_as/group/script delete mode 100644 acceptance/bundle/run_as/group/test.toml delete mode 100644 bundle/config/mutator/validate_run_as_group.go delete mode 100644 bundle/config/mutator/validate_run_as_group_test.go diff --git a/acceptance/bundle/run_as/group/databricks.yml b/acceptance/bundle/run_as/group/databricks.yml deleted file mode 100644 index 6c23ad2b469..00000000000 --- a/acceptance/bundle/run_as/group/databricks.yml +++ /dev/null @@ -1,21 +0,0 @@ -bundle: - name: run_as_group - -run_as: - group_name: bundle_group - -resources: - jobs: - inherited: - name: Inherited identity - user_override: - name: User override - run_as: - user_name: user_override - -targets: - default: - default: true - group: - run_as: - group_name: target_group diff --git a/acceptance/bundle/run_as/group/out.test.toml b/acceptance/bundle/run_as/group/out.test.toml deleted file mode 100644 index 59b56a2037c..00000000000 --- a/acceptance/bundle/run_as/group/out.test.toml +++ /dev/null @@ -1,3 +0,0 @@ -Cloud = false -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] -EnvMatrix.DMS = ["", "true"] diff --git a/acceptance/bundle/run_as/group/output.txt b/acceptance/bundle/run_as/group/output.txt deleted file mode 100644 index c524b530b17..00000000000 --- a/acceptance/bundle/run_as/group/output.txt +++ /dev/null @@ -1,17 +0,0 @@ - ->>> [CLI] bundle validate -o json -json.resources.jobs.inherited.run_as.group_name = "bundle_group"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.run_as.group_name = "bundle_group"; - ->>> DATABRICKS_BUNDLE_ENGINE=terraform musterr [CLI] bundle plan -Error: run_as.group_name is only supported in direct deployment mode - at resources.jobs.inherited.run_as.group_name - -Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. For an existing Terraform deployment, run 'databricks bundle migrate' first. Alternatively, set run_as.user_name or run_as.service_principal_name on this job - - ->>> [CLI] bundle validate -t group -o json -json.resources.jobs.inherited.run_as.group_name = "target_group"; -json.resources.jobs.user_override.run_as.user_name = "user_override"; -json.run_as.group_name = "target_group"; diff --git a/acceptance/bundle/run_as/group/script b/acceptance/bundle/run_as/group/script deleted file mode 100644 index c71789f6a46..00000000000 --- a/acceptance/bundle/run_as/group/script +++ /dev/null @@ -1,5 +0,0 @@ -trace $CLI bundle validate -o json | gron.py | grep -F .run_as -trace DATABRICKS_BUNDLE_ENGINE=terraform musterr $CLI bundle plan - -update_file.py databricks.yml "group_name: bundle_group" "user_name: bundle_user" -trace $CLI bundle validate -t group -o json | gron.py | grep -F .run_as diff --git a/acceptance/bundle/run_as/group/test.toml b/acceptance/bundle/run_as/group/test.toml deleted file mode 100644 index de8f3c05172..00000000000 --- a/acceptance/bundle/run_as/group/test.toml +++ /dev/null @@ -1,2 +0,0 @@ -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] -Ignore = [".databricks"] diff --git a/bundle/config/mutator/resourcemutator/run_as.go b/bundle/config/mutator/resourcemutator/run_as.go index 3a9543e5523..48f69c9ce14 100644 --- a/bundle/config/mutator/resourcemutator/run_as.go +++ b/bundle/config/mutator/resourcemutator/run_as.go @@ -3,7 +3,6 @@ package resourcemutator import ( "context" "fmt" - "maps" "slices" "github.com/databricks/cli/bundle" @@ -22,7 +21,7 @@ type setRunAs struct{} // 1. Sets the run_as field for jobs to the value of the run_as field in the bundle. // // 2. Validates that the bundle run_as configuration is valid in the context of the bundle. -// If the run_as identity differs from the deployment identity, DABs only +// If the run_as user is different from the current deployment user, DABs only // supports a subset of resources. func SetRunAs() bundle.Mutator { return &setRunAs{} @@ -74,22 +73,14 @@ func validateRunAs(b *bundle.Bundle) diag.Diagnostics { if runAs.GroupName != "" { identity = fmt.Sprintf("group %q", runAs.GroupName) - for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Pipelines)) { - if b.Config.Resources.Pipelines[key].RunAs == nil { - diags = diags.Extend(diag.Diagnostics{{ - Summary: "pipelines do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this pipeline to override the bundle run_as", - Locations: []dyn.Location{b.Config.GetLocation("resources.pipelines." + key)}, - Severity: diag.Error, - }}) + for _, pipeline := range b.Config.Resources.Pipelines { + if pipeline.RunAs == nil { + return diag.Errorf("this CLI version cannot configure run_as.group_name for pipelines; set run_as.user_name or run_as.service_principal_name on each pipeline") } } - for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Alerts)) { - if b.Config.Resources.Alerts[key].RunAs == nil { - diags = diags.Extend(diag.Diagnostics{{ - Summary: "alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on this alert to override the bundle run_as", - Locations: []dyn.Location{b.Config.GetLocation("resources.alerts." + key)}, - Severity: diag.Error, - }}) + for _, alert := range b.Config.Resources.Alerts { + if alert.RunAs == nil { + return diag.Errorf("alerts do not support run_as.group_name; set run_as.user_name or run_as.service_principal_name on each alert") } } } @@ -243,11 +234,7 @@ func (m *setRunAs) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { // experimental.use_legacy_run_as flag. if b.Config.Experimental != nil && b.Config.Experimental.UseLegacyRunAs { if b.Config.Value().Get("run_as").Get("group_name").Kind() != dyn.KindInvalid { - return diag.Diagnostics{{ - Summary: "run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity", - Locations: b.Config.GetLocations("run_as.group_name"), - Severity: diag.Error, - }} + return diag.Errorf("run_as.group_name is not supported with experimental.use_legacy_run_as; disable experimental.use_legacy_run_as to use a group identity") } setPipelineOwnersToRunAsIdentity(b) setRunAsForJobs(b) diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index 47967a060f7..dfd64eec079 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -318,40 +318,62 @@ func TestRunAsNoErrorForSupportedResources(t *testing.T) { } } -func TestRunAsGroupInvalidIdentity(t *testing.T) { +func TestRunAsIdentities(t *testing.T) { for _, tc := range []struct { - name string - runAs string - legacy bool + runAs string + wantError bool }{ - {name: "empty", runAs: `{group_name: ""}`}, - {name: "user and group", runAs: `{user_name: user, group_name: group}`}, - {name: "legacy", runAs: `{group_name: group}`, legacy: true}, - {name: "legacy empty", runAs: `{group_name: ""}`, legacy: true}, + {`null`, true}, + {`{}`, true}, + {`{user_name: ""}`, true}, + {`{service_principal_name: ""}`, true}, + {`{group_name: ""}`, true}, + {`{user_name: user}`, false}, + {`{service_principal_name: sp}`, false}, + {`{group_name: group}`, false}, + {`{user_name: "", service_principal_name: "", group_name: ""}`, true}, + {`{user_name: "", service_principal_name: "", group_name: group}`, false}, + {`{user_name: user, service_principal_name: sp}`, true}, + {`{user_name: user, group_name: group}`, true}, + {`{service_principal_name: sp, group_name: group}`, true}, + {`{user_name: user, service_principal_name: sp, group_name: group}`, true}, } { - t.Run(tc.name, func(t *testing.T) { - yaml := fmt.Sprintf("run_as: %s\nexperimental: {use_legacy_run_as: %t}", tc.runAs, tc.legacy) + t.Run(tc.runAs, func(t *testing.T) { + yaml := "workspace: {current_user: {userName: deployer}}\nrun_as: " + tc.runAs r, diags := config.LoadFromBytes("databricks.yml", []byte(yaml)) require.NoError(t, diags.Error()) b := &bundle.Bundle{Config: *r} diags = bundle.Apply(t.Context(), b, SetRunAs()) - require.Error(t, diags.Error()) - if tc.legacy { - assert.Contains(t, diags.Error().Error(), "run_as.group_name is not supported with experimental.use_legacy_run_as") + 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, []dyn.Location{r.GetLocation("run_as")}, diags[0].Locations) } else { - assert.Contains(t, diags.Error().Error(), "run_as section must specify exactly one non-empty identity") + require.NoError(t, diags.Error()) } }) } } +func TestRunAsLegacyGroup(t *testing.T) { + for _, runAs := range []string{`{group_name: group}`, `{group_name: ""}`} { + t.Run(runAs, func(t *testing.T) { + yaml := "workspace: {current_user: {userName: deployer}}\nexperimental: {use_legacy_run_as: true}\nrun_as: " + runAs + r, diags := config.LoadFromBytes("databricks.yml", []byte(yaml)) + require.NoError(t, diags.Error()) + b := &bundle.Bundle{Config: *r} + diags = bundle.Apply(t.Context(), b, SetRunAs()) + require.ErrorContains(t, diags.Error(), "run_as.group_name is not supported with experimental.use_legacy_run_as") + }) + } +} + func TestRunAsGroupResources(t *testing.T) { for _, tc := range []struct { name string resource string wantError string }{ - {name: "pipeline", resource: `pipelines: {test: {}}`, wantError: "pipelines do not support run_as.group_name"}, + {name: "pipeline", resource: `pipelines: {test: {}}`, wantError: "this CLI version cannot configure run_as.group_name for pipelines"}, {name: "alert", resource: `alerts: {test: {}}`, wantError: "alerts do not support run_as.group_name"}, {name: "model serving", resource: `model_serving_endpoints: {test: {}}`, wantError: "Run as identity: group \"group\""}, {name: "pipeline user override", resource: `pipelines: {test: {run_as: {user_name: user}}}`}, @@ -374,3 +396,42 @@ func TestRunAsGroupResources(t *testing.T) { }) } } + +func TestRunAsGroupInheritance(t *testing.T) { + for _, tc := range []struct { + name string + root string + target string + want jobs.JobRunAs + }{ + {name: "root group", root: `{group_name: group}`, target: `{}`, want: jobs.JobRunAs{GroupName: "group"}}, + {name: "target group replaces user", root: `{user_name: user}`, target: `{run_as: {group_name: group}}`, want: jobs.JobRunAs{GroupName: "group"}}, + {name: "target user replaces group", root: `{group_name: group}`, target: `{run_as: {user_name: user}}`, want: jobs.JobRunAs{UserName: "user"}}, + } { + t.Run(tc.name, func(t *testing.T) { + yaml := fmt.Sprintf(` +workspace: {current_user: {userName: deployer}} +run_as: %s +targets: + test: %s +resources: + jobs: + inherited: {} + user: {run_as: {user_name: other_user}} + sp: {run_as: {service_principal_name: other_sp}} + group: {run_as: {group_name: other_group}} +`, tc.root, tc.target) + r, diags := config.LoadFromBytes("databricks.yml", []byte(yaml)) + require.NoError(t, diags.Error()) + require.NoError(t, r.MergeTargetOverrides("test")) + b := &bundle.Bundle{Config: *r} + diags = bundle.Apply(t.Context(), b, SetRunAs()) + require.NoError(t, diags.Error()) + assert.Equal(t, &tc.want, b.Config.RunAs) + assert.Equal(t, &tc.want, b.Config.Resources.Jobs["inherited"].RunAs) + assert.Equal(t, &jobs.JobRunAs{UserName: "other_user"}, b.Config.Resources.Jobs["user"].RunAs) + assert.Equal(t, &jobs.JobRunAs{ServicePrincipalName: "other_sp"}, b.Config.Resources.Jobs["sp"].RunAs) + assert.Equal(t, &jobs.JobRunAs{GroupName: "other_group"}, b.Config.Resources.Jobs["group"].RunAs) + }) + } +} diff --git a/bundle/config/mutator/validate_run_as_group.go b/bundle/config/mutator/validate_run_as_group.go deleted file mode 100644 index 49ac0d34db6..00000000000 --- a/bundle/config/mutator/validate_run_as_group.go +++ /dev/null @@ -1,51 +0,0 @@ -package mutator - -import ( - "context" - "maps" - "slices" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config/engine" - "github.com/databricks/cli/libs/diag" - "github.com/databricks/cli/libs/dyn" -) - -type validateRunAsGroup struct { - engine engine.EngineType -} - -// ValidateRunAsGroup rejects group identities unsupported by the Terraform provider. -// See https://github.com/databricks/terraform-provider-databricks/blob/v1.131.0/jobs/resource_job.go#L629. -func ValidateRunAsGroup(e engine.EngineType) bundle.Mutator { - return &validateRunAsGroup{engine: e} -} - -func (m *validateRunAsGroup) Name() string { - return "ValidateRunAsGroup" -} - -func (m *validateRunAsGroup) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { - if m.engine.IsDirect() { - return nil - } - - var diags diag.Diagnostics - for _, key := range slices.Sorted(maps.Keys(b.Config.Resources.Jobs)) { - runAs := b.Config.Resources.Jobs[key].RunAs - if runAs == nil || runAs.GroupName == "" { - continue - } - path := dyn.NewPath(dyn.Key("resources"), dyn.Key("jobs"), dyn.Key(key), dyn.Key("run_as"), dyn.Key("group_name")) - diags = diags.Append(diag.Diagnostic{ - Severity: diag.Error, - Summary: "run_as.group_name is only supported in direct deployment mode", - Detail: "Set 'bundle.engine: direct' in your databricks.yml or set DATABRICKS_BUNDLE_ENGINE=direct. " + - "For an existing Terraform deployment, run 'databricks bundle migrate' first. " + - "Alternatively, set run_as.user_name or run_as.service_principal_name on this job", - Locations: b.Config.GetLocations(path.String()), - Paths: []dyn.Path{path}, - }) - } - return diags -} diff --git a/bundle/config/mutator/validate_run_as_group_test.go b/bundle/config/mutator/validate_run_as_group_test.go deleted file mode 100644 index 9ae053ca5fd..00000000000 --- a/bundle/config/mutator/validate_run_as_group_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package mutator_test - -import ( - "testing" - - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config" - "github.com/databricks/cli/bundle/config/engine" - "github.com/databricks/cli/bundle/config/mutator" - "github.com/databricks/cli/bundle/config/resources" - "github.com/databricks/cli/libs/dyn" - "github.com/databricks/databricks-sdk-go/service/jobs" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestValidateRunAsGroup(t *testing.T) { - for _, tc := range []struct { - engine engine.EngineType - paths []string - }{ - {engine: engine.EngineDirect}, - {engine: engine.EngineTerraform, paths: []string{"resources.jobs.a.run_as.group_name", "resources.jobs.z.run_as.group_name"}}, - } { - t.Run(string(tc.engine), func(t *testing.T) { - b := &bundle.Bundle{Config: config.Root{Resources: config.Resources{Jobs: map[string]*resources.Job{ - "z": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{GroupName: "group"}}}, - "a": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{GroupName: "group"}}}, - "user": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{UserName: "user@example.test"}}}, - "sp": {JobSettings: jobs.JobSettings{RunAs: &jobs.JobRunAs{ServicePrincipalName: "sp"}}}, - "none": {}, - }}}} - diags := bundle.Apply(t.Context(), b, mutator.ValidateRunAsGroup(tc.engine)) - require.Len(t, diags, len(tc.paths)) - for i, path := range tc.paths { - assert.Equal(t, []dyn.Path{dyn.MustPathFromString(path)}, diags[i].Paths) - } - }) - } -} diff --git a/bundle/phases/plan.go b/bundle/phases/plan.go index ae83459aa2b..aee481b2b14 100644 --- a/bundle/phases/plan.go +++ b/bundle/phases/plan.go @@ -27,7 +27,6 @@ func PreDeployChecks(ctx context.Context, b *bundle.Bundle, isPlan bool, engine deploy.StatePull(), mutator.ValidateGitDetails(), mutator.ValidateDirectOnlyResources(engine), - mutator.ValidateRunAsGroup(engine), mutator.ValidateLifecycleStarted(engine), mutator.ValidateCascadeOnDestroy(engine), statemgmt.CheckRunningResource(engine),