Skip to content

Add Cluster Policy support to bundle resources - #6225

Merged
Sankalp-Mittal merged 44 commits into
mainfrom
sankalp-mittal/cluster-policy-resource
Aug 24, 2026
Merged

Add Cluster Policy support to bundle resources#6225
Sankalp-Mittal merged 44 commits into
mainfrom
sankalp-mittal/cluster-policy-resource

Conversation

@Sankalp-Mittal

@Sankalp-Mittal Sankalp-Mittal commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add support for Cluster Policies resource in DABs. These can be defined as multiline json, inline json and yaml. Cluster Policies can be defined either as a definition or policy_family_definition_overrides

Changes

Add support for a new cluster_policies resource type in DABs. Cluster policies are admin-defined guardrails that constrain how clusters can be configured (allowed node types, fixed autotermination, required tags, cost limits). This lets users manage them declaratively alongside their other bundle resources. This includes:

  • New resources.ClusterPolicy config type, wrapping compute.CreatePolicy.
  • Direct-engine resource implementation (ResourceClusterPolicy) covering create/read/update/delete plus PrepareState/RemapState.
  • Wiring into resource enumerations: supported resources, direct-engine registration (all.go), dev-mode name prefixing (presets), and the workspace URL pattern.
  • Authoring the policy definition as native inline YAML, in addition to a JSON string or block scalar (see "Key points" below).
  • Regenerated JSON schema, reference schema, and annotations.
  • Testserver handlers for the /api/2.0/policies/clusters/* endpoints.

Cluster policies are only supported in direct deployment mode — they have no Terraform provider mapping here, so they are excluded from Terraform-mode conversion.

Why

Cluster policies were previously only referenceable from bundles via a variable lookup (resolving an existing policy name to its policy_id). Users want to define and own the policies themselves as code, instead of provisioning them out-of-band through the UI.

Example

The definition can be an inline JSON string:

resources:
  cluster_policies:
    team_policy:
      name: my_cluster_policy
      definition: '{"spark_version":{"type":"fixed","value":"13.3.x-scala2.12"}}'

…or native YAML, which is normalized to the same JSON string at deploy time:

resources:
  cluster_policies:
    team_policy:
      name: my_cluster_policy
      definition:
        spark_version:
          type: fixed
          value: 13.3.x-scala2.12

Key points to note

Update uses Edit, and it is a full replace. The SDK's update call is ClusterPolicies.Edit (not Update); DoUpdate sends the whole desired policy along with the existing policy_id.

Output-only fields are excluded from state. policy_id, created_at_timestamp, creator_user_name, and is_default are returned on read but never written back, so RemapState copies only the fields shared with CreatePolicy.

Policy-family definitions are treated as a backend default. When a policy is authored with policy_family_id and no definition, the backend computes the full definition from the policy family and returns it on read. Config leaves definition empty while remote holds a populated value, which would otherwise register as drift and re-issue an Edit on every deploy. cluster_policies.definition is therefore declared under backend_defaults in resources.yml: because definition is an omitempty field, an empty config value is diffed as nil, so the old == nil && new == nil && remote != nil rule skips the server-computed value. An explicitly authored definition keeps old/new non-nil, so it still participates in normal drift detection.

Dev-mode name prefixing. The policy name is a user-facing display name (unique, 1–100 chars), not the API identity (policy_id), so it is prefixed in development mode to avoid collisions between developers without changing identity.

Inline-YAML definition replicates the Genie serialized_space pattern. A top-level Definition any field shadows the embedded compute.CreatePolicy.Definition string, and a per-resource dyn-layer mutator (ConfigureClusterPolicyDefinition, registered right after ConfigureGenieSpaceSerializedSpace()) normalizes it before deploy: a string passes through unchanged, an inline map/sequence is marshaled to a compact JSON string (json.Marshal, sorted keys), and any other kind is rejected with a diagnostic. PrepareState then copies the normalized string into state; the state type stays compute.CreatePolicy, so there is no state-compatibility break.

The conversion is done at the config layer on purpose: authoring structured content inline in YAML decodes small integers as Go int, while the state JSON round-trip decodes them as float64, so structdiff reports false drift on every plan. Normalizing to a canonical JSON string makes config-side and state-side hold an identical string — the same reason Genie does it. The dashboard serialized_dashboard mutator is deliberately not the model here: it only reads a file_path into an opaque string and does no YAML→JSON conversion.

Tests

  • Acceptance group acceptance/bundle/resources/cluster_policies/:
    • basic/ — single-policy validate/summary/deploy plus create/edit/delete, pinned to the direct engine.
    • job_ref/ — a job task referencing the policy via ${resources.cluster_policies.pol.id}; asserts the direct engine orders policy create before job create (and job delete before policy delete on destroy) and resolves the policy id into the job body.
    • definition_multiline/ — a block-scalar JSON definition preserved as a \n-escaped string end to end.
    • definition_yaml/ — a native YAML mapping that serializes to the same compact JSON string the API receives as basic/.
    • direct-only/ — deploying in terraform mode fails with the actionable "only supported with direct deployment mode" error.
    • missing_ref/ — a job referencing an undeclared cluster_policies.* resource fails to plan with a dependency-resolution error.
    • cross_bundle_id/ — one bundle creates a policy; a separate bundle consumes it by hardcoding the generated policy_id (captured from the producer's state) in a job.
    • cross_bundle_lookup/ — a separate bundle resolves the policy created by the producer via a variable lookup: {cluster_policy: <name>} and uses the resolved id in a job.
  • Unit tests for ConfigureClusterPolicyDefinition (string passthrough, map/sequence → JSON, invalid-kind diagnostic, absent field) and for ResourceClusterPolicy.PrepareState.
  • Made the testserver's GET /api/2.0/policies/clusters/list stateful (returning created policies, seeded with the two legacy names the variable-lookup tests rely on) so the lookup-by-name flow is exercisable.
  • Added cluster_policies coverage to shared suites: bundle permissions, presets/target-mode prefixing, run-as, state-load round-tripping, and the Terraform lifecycle exclusion list.

This pull request and its description were written by Isaac.

@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 3cc0cb9

Run: 32743879730

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 2 3 300 1456 11:05
🔄​ aws windows 3 1 3 300 1454 14:12
🔄​ azure linux 2 2 3 297 1456 11:36
💚​ azure windows 2 3 301 1454 11:00
💚​ gcp linux 2 3 300 1456 12:58
🔄​ gcp windows 3 1 3 300 1454 13:19
11 interesting tests: 7 flaky, 3 SKIP, 1 RECOVERED
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
🔄​ TestAccept 💚​R 🔄​f 💚​R 💚​R 💚​R 🔄​f
💚​ TestAccept/bundle/invariant/no_drift 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🔄​ TestAccept/bundle/resources/cluster_policies/basic ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
🔄​ TestAccept/bundle/resources/cluster_policies/basic/DATABRICKS_BUNDLE_ENGINE=direct ✅​p ✅​p ✅​p ✅​p ✅​p 🔄​f
🔄​ TestAccept/bundle/resources/cluster_policies/out_of_band_change ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/cluster_policies/out_of_band_change/DATABRICKS_BUNDLE_ENGINE=direct ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestFsCpFileToFileFileNotOverwritten ✅​p ✅​p 🔄​f ✅​p ✅​p ✅​p
🔄​ TestFsCpFileToFileFileNotOverwritten/uc-volumes_to_uc-volumes ✅​p ✅​p 🔄​f ✅​p ✅​p ✅​p
Top 24 slowest tests (at least 2 minutes):
duration env testname
6:09 aws windows TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
3:41 gcp windows TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
3:39 gcp linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
3:38 azure windows TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=
3:28 aws linux TestFilerWorkspaceFilesExtensionsDelete
3:14 azure windows TestAccept
3:10 azure linux TestFilerWorkspaceFilesExtensionsStat
3:07 gcp windows TestAccept/bundle/resources/cluster_policies/basic/DATABRICKS_BUNDLE_ENGINE=direct
3:02 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=
2:59 gcp windows TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=
2:55 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
2:53 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
2:46 azure windows TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
2:30 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=
2:21 azure linux TestAccept/bundle/resources/cluster_policies/basic/DATABRICKS_BUNDLE_ENGINE=direct
2:18 gcp windows TestAccept/bundle/resources/cluster_policies/policy_family_definition/DATABRICKS_BUNDLE_ENGINE=direct
2:17 gcp linux TestFilerWorkspaceFilesExtensionsRead
2:17 aws windows TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
2:13 aws linux TestAccept/bundle/resources/cluster_policies/basic/DATABRICKS_BUNDLE_ENGINE=direct
2:12 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1
2:07 aws windows TestFilerWorkspaceFilesExtensionsStat
2:06 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=
2:04 gcp linux TestAccept/bundle/resources/cluster_policies/policy_family_definition/DATABRICKS_BUNDLE_ENGINE=direct
2:03 azure windows TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_policy.yml.tmpl/READPLAN=1

"vector_search_indexes",
"volumes",
},
require.Equal(

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is due to the linter

@Sankalp-Mittal
Sankalp-Mittal force-pushed the sankalp-mittal/cluster-policy-resource branch 3 times, most recently from 08272eb to a93ad20 Compare August 13, 2026 13:40
@Sankalp-Mittal
Sankalp-Mittal marked this pull request as ready for review August 13, 2026 13:58
case dyn.KindInvalid, dyn.KindNil, dyn.KindString:
// KindInvalid means definition is absent; leave it for backend validation.
return v, nil
case dyn.KindMap, dyn.KindSequence:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we allow KindSequence? yes, it's valid JSON but it's not valid policy language (or is it?)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was also present in Genie spaces in configure_genie_space_serialized_space.go:68 so I copied it here as well. Though I think it should be removed from both places.

Comment thread acceptance/bundle/resources/cluster_policies/missing_ref/out.plan.txt Outdated
Comment thread acceptance/bundle/resources/cluster_policies/direct-only/script Outdated

# catalogs: no api field behaviors

# cluster_policies: no api field behaviors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be great to add hashed_in_state behaviour for the policy field after #6105 lands & cover with acceptance test for a large policy doc

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That can be done in a follow up

Comment thread acceptance/bundle/resources/cluster_policies/cross_bundle_id/script Outdated
@@ -0,0 +1,6 @@
Cloud = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please enable cloud for some of the tests once you have run them and verified they pass

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do Cloud = true for all by default and only add Cloud = false on a per-test basis (if for some reason [state that reason as comment] Cloud isn't suitable for them)

Comment thread acceptance/bundle/resources/cluster_policies/basic/script
Comment thread bundle/config/mutator/resourcemutator/resource_mutator.go
The cluster_policies resource was added without regenerating derived files,
failing validate-generated and the refschema acceptance test.

Co-authored-by: Isaac
Adding cluster_policies with a workspace URL pattern extended the list of
openable resource types, but the workspace_open command tests hardcoded the
old list. Add cluster_policies to the expected completion, help text, and
unknown-type error assertions.

Co-authored-by: Isaac
The workspace open command's supported-resource-type list now includes
cluster_policies; regenerate the golden output.

Co-authored-by: Isaac
TestInvariantConfigsCoverage requires every resource type to have an
invariant config. Add a cluster_policy config and register it in the
invariant matrix. Exclude it from the migrate suite (terraform-seeded; the
resource is direct-only) and continue_293 (unsupported on the old CLI).
Regenerate the affected out.test.toml snapshots.

Co-authored-by: Isaac
Restructure the single cluster_policies acceptance test into a group:
- move the existing test unchanged into basic/
- add job_ref/: a job task referencing the policy via
  ${resources.cluster_policies.pol.id}, asserting the direct engine
  orders policy create before job create (and job delete before policy
  delete on destroy) and resolves the policy id into the job body
- add definition_multiline/: a block-scalar JSON definition, asserting
  it is preserved as a newline-escaped string end to end

No production code change.

Co-authored-by: Isaac
The cluster policy `definition` was a plain JSON string. Add a top-level
`Definition any` field that shadows the embedded compute.CreatePolicy
string so the definition can also be written as native YAML.

ConfigureClusterPolicyDefinition normalizes an inline map/sequence to a
JSON string at the dyn layer (same approach as genie serialized_space),
avoiding int/float structdiff drift; PrepareState copies the normalized
string into state. A string definition passes through unchanged.

Co-authored-by: Isaac
- unit test for ConfigureClusterPolicyDefinition (string passthrough,
  map/sequence -> JSON string, invalid-kind diagnostic, absent field)
- unit test for ResourceClusterPolicy.PrepareState copying the
  normalized string into state
- acceptance test definition_yaml/: a native YAML mapping serializes to
  the same compact JSON string the API receives as the basic test

Co-authored-by: Isaac
Instead of inline JSON everywhere, spread the three authoring forms
across the non-targeted tests for incidental coverage: job_ref and
cross_bundle_lookup use a multiline JSON block scalar, direct-only uses
native YAML, cross_bundle_id keeps inline JSON. basic and the two
targeted definition tests are unchanged.

Co-authored-by: Isaac
@Sankalp-Mittal
Sankalp-Mittal force-pushed the sankalp-mittal/cluster-policy-resource branch from a96139d to aef7b32 Compare August 17, 2026 12:28

@Sankalp-Mittal Sankalp-Mittal left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

replied to comments

# DataSecurityModeDiffSuppressFunc: suppress when old != "" && new == ""
#- field: data_security_mode

cluster_policies:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added because if policy_definition_overrides are used then the backend fills in the definition fields and false drift can be detected

Comment thread libs/testserver/cluster_policies.go
@@ -0,0 +1,6 @@
Cloud = false

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's do Cloud = true for all by default and only add Cloud = false on a per-test basis (if for some reason [state that reason as comment] Cloud isn't suitable for them)

@Sankalp-Mittal

Copy link
Copy Markdown
Collaborator Author

let's do Cloud = true for all by default and only add Cloud = false on a per-test basis (if for some reason [state that reason as comment] Cloud isn't suitable for them)

Done

Cluster policy names are workspace-global and the CI workspace is shared
across concurrent runs, so fixed policy names collided ("already exists").
Author names and the bundle root_path with $UNIQUE_NAME via databricks.yml.tmpl
+ envsubst so each run is isolated.

Co-authored-by: Isaac
@Sankalp-Mittal
Sankalp-Mittal added this pull request to the merge queue Aug 24, 2026
Merged via the queue into main with commit ce0ddfd Aug 24, 2026
24 checks passed
@Sankalp-Mittal
Sankalp-Mittal deleted the sankalp-mittal/cluster-policy-resource branch August 24, 2026 22:39
@eng-dev-ecosystem-bot

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: ce0ddfd

Run: 32785763717

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🔄​ aws linux 4 3 2 1205 966 155:42
🔄​ aws windows 6 3 2 1142 985 189:36
💚​ azure linux 3 2 1103 1003 93:18
🔄​ azure windows 1 3 2 1041 1022 116:01
🔄​ gcp linux 3 2 2 1091 1007 102:38
💚​ gcp windows 3 2 1032 1026 120:55
18 interesting tests: 14 flaky, 2 RECOVERED, 2 SKIP
Test Name aws linux aws windows azure linux azure windows gcp linux gcp windows
🔄​ TestAccept 💚​R 💚​R 💚​R 💚​R 🔄​f 💚​R
🔄​ TestAccept/bundle/debug/fetch-repository-info ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p
🔄​ TestAccept/bundle/debug/fetch-repository-info/DATABRICKS_BUNDLE_ENGINE=direct ✅​p ✅​p ✅​p ✅​p 🔄​f ✅​p
💚​ TestAccept/bundle/invariant/no_drift 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🔄​ TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/deploy/simple ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/deploy/simple/DATABRICKS_BUNDLE_ENGINE=direct ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/deploy/update-after-create 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=terraform 🔄​f ✅​p ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/lifecycle-started ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
💚​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestFsCpFileToFileWithOverwriteFlag ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestFsCpFileToFileWithOverwriteFlag/uc-volumes_to_uc-volumes ✅​p 🔄​f ✅​p ✅​p ✅​p ✅​p
🔄​ TestImportFileFormatSource ✅​p ✅​p ✅​p 🔄​f ✅​p ✅​p
Top 50 slowest tests (at least 2 minutes):
duration env testname
26:08 aws linux TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
25:16 aws windows TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
23:13 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
19:08 aws windows TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
15:59 aws windows TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
14:41 aws linux TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
14:08 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
13:57 gcp windows TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
13:56 aws linux TestAccept/bundle/resources/clusters/deploy/simple/DATABRICKS_BUNDLE_ENGINE=direct
13:47 gcp linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
12:09 aws windows TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
11:39 aws windows TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
11:32 aws windows TestAccept/bundle/resources/clusters/lifecycle-started-toggle/DATABRICKS_BUNDLE_ENGINE=direct
11:12 aws linux TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
10:47 gcp linux TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DATABRICKS_BUNDLE_ENGINE=direct
10:06 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
9:58 aws linux TestAccept/bundle/deploy/spark-jar-task/DATABRICKS_BUNDLE_ENGINE=direct
8:14 aws windows TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
8:12 gcp windows TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:59 aws linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:44 aws windows TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:27 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
7:26 aws windows TestAccept/bundle/resources/clusters/deploy/data_security_mode/DATABRICKS_BUNDLE_ENGINE=direct
7:26 aws linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=terraform
7:22 gcp linux TestAccept/bundle/resources/clusters/deploy/data_security_mode/DATABRICKS_BUNDLE_ENGINE=direct
7:18 gcp linux TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:14 aws linux TestAccept/bundle/resources/clusters/resize-terminated-fallback/DATABRICKS_BUNDLE_ENGINE=direct
7:13 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
7:10 azure windows TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
7:10 aws linux TestAccept/bundle/resources/clusters/deploy/data_security_mode/DATABRICKS_BUNDLE_ENGINE=direct
7:09 aws linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
7:07 aws linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
7:03 aws windows TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
7:03 aws windows TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
6:59 gcp windows TestAccept/bundle/resources/clusters/deploy/local_ssd_count/DATABRICKS_BUNDLE_ENGINE=direct
6:57 aws linux TestAccept/bundle/invariant/destroy_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
6:57 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
6:53 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
6:50 azure linux TestAccept/bundle/resources/apps/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
6:48 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
6:45 gcp linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
6:45 aws linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=
6:44 gcp linux TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=1
6:29 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster_apply_policy_default_values.yml.tmpl/READPLAN=
6:06 aws linux TestAccept/bundle/config-remote-sync/multiple_resources/DATABRICKS_BUNDLE_ENGINE=direct
6:04 azure linux TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
6:04 azure linux TestAccept/bundle/invariant/delete_idempotent/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=1
6:00 azure windows TestAccept/bundle/resources/clusters/lifecycle-started/DATABRICKS_BUNDLE_ENGINE=direct
5:49 azure windows TestAccept/bundle/resources/clusters/deploy/update-after-create/DATABRICKS_BUNDLE_ENGINE=direct
5:46 aws windows TestAccept/bundle/invariant/no_drift/DATABRICKS_BUNDLE_ENGINE=direct/INPUT_CONFIG=cluster.yml.tmpl/READPLAN=

jariaspdev-lab pushed a commit to jariaspdev-lab/cli that referenced this pull request Aug 25, 2026
…6304)

## Summary

Stacked on databricks#6225 (cluster policy resource). Adds the per-resource
`permissions` field to `cluster_policies` so a policy can grant access
via the direct engine's generic permissions applier.

Cluster policies support a **single** ACL level — `CAN_USE` (SDK
`compute.ClusterPolicyPermissionLevel`), meaning "may create clusters
governed by this policy." There is no `CAN_MANAGE`/`CAN_VIEW`/`CAN_RUN`;
policy administration is an admin-only function.

```yaml
resources:
  cluster_policies:
    my_policy:
      name: my-policy
      permissions:
        - level: CAN_USE
          group_name: users
```

## Changes

- **Resource wiring**: declare `ClusterPolicyPermission`, add the
`Permissions` field, register `cluster_policies.permissions` in the
direct engine, and map the permissions-API object type to
`/cluster-policies/`.
- **`FixPermissions`**: add `cluster_policies` to `ignoredResources` so
the deploying user is **not** auto-injected as `CAN_MANAGE`/`IS_OWNER` —
cluster policies can't accept it, and the real backend would reject the
deploy (same treatment as `secret_scopes`).
- **Testserver**: model the real API — a non-`CAN_USE` level on a
cluster policy returns `400`, so local and cloud runs agree.
- **Generated files**: regenerated schema, refschema field list, and
validation (`level` enum = `CAN_USE` only, `level` required).

## Design note

`cluster_policies` is intentionally **not** added to the top-level
`levelsMap`; it stays in `unsupportedResources`. The bundle-wide
`permissions:` block is expressed in `CAN_MANAGE`/`CAN_VIEW`/`CAN_RUN`
(and auto-grants the deployer `CAN_MANAGE`), none of which a cluster
policy can accept. Supporting only the explicit per-resource form avoids
silently dropping those grants. Two current-user injection paths had to
be handled for this: the top-level `levelsMap` and the `FixPermissions`
mutator.

## Testing

Acceptance tests under
`acceptance/bundle/resources/cluster_policies/permissions/`:

- `basic/` — deploy applies the `CAN_USE` grants, redeploy updates the
ACL; requests asserted with `print_requests.py`.
- `out_of_band_change/` — an out-of-band ACL change is detected by
`bundle plan` and reconciled on redeploy.
- `levels/` (`Cloud=true`) — `CAN_USE` deploys successfully; a direct
`CAN_MANAGE` `set-permissions` call is rejected (asserted with
`musterr`; the backend error differs between the fake and cloud, so it
is routed to a `LOG` file rather than pinned).

Plus the `cluster_policies.permissions` CRUD roundtrip fixture in
`dresources/all_test.go`. Unit + acceptance suites pass locally.

This pull request and its description were written by Isaac.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants