Skip to content

direct: Fix permissions state path to match input config schema#4703

Open
denik wants to merge 73 commits intomainfrom
denik/permissions-reference
Open

direct: Fix permissions state path to match input config schema#4703
denik wants to merge 73 commits intomainfrom
denik/permissions-reference

Conversation

@denik
Copy link
Contributor

@denik denik commented Mar 11, 2026

Changes

  • Add EmbeddedSlice field name convention to struct walkers in libs/structs/ — when a struct field is named EmbeddedSlice, walkers treat it as transparent (no path segment added), so its elements appear directly at the parent path
  • Apply this to PermissionsState: rename Permissions field to EmbeddedSlice, making state paths like resources.jobs.foo.permissions[0] match input config paths (previously resources.jobs.foo.permissions.permissions[0])
  • Fix refschema output: permissions.[*]permissions[*] (remove spurious dot before bracket)
  • Enable job_permissions acceptance test for direct engine

Why

The direct deployment engine's permissions state used a wrapper struct that added an extra permissions segment to paths. This caused a mismatch with input config paths, preventing dependency tracking between permissions and their parent resources. With this fix, state and config paths are consistent.

@eng-dev-ecosystem-bot
Copy link
Collaborator

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

Commit: 67b41b9

Run: 23060807809

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 8 7 268 790 5:46
💚​ aws windows 8 7 270 788 5:33
🔄​ aws-ucws linux 2 7 7 364 705 8:16
🔄​ aws-ucws windows 2 7 7 366 703 6:43
💚​ azure linux 2 9 271 788 5:47
💚​ azure windows 2 9 273 786 4:54
🔄​ azure-ucws linux 2 1 9 369 701 8:33
🔄​ azure-ucws windows 2 1 9 371 699 7:52
💚​ gcp linux 2 9 267 791 5:43
💚​ gcp windows 2 9 269 789 5:56
16 interesting tests: 7 SKIP, 7 RECOVERED, 2 flaky
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
🔄​ TestAccept 💚​R 💚​R 🔄​f 🔄​f 💚​R 💚​R 🔄​f 🔄​f 💚​R 💚​R
🙈​ TestAccept/bundle/resources/permissions 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 💚​R 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/postgres_branches/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/update_protected 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/without_branch_id 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_endpoints/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/synced_database_tables/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestAccept/ssh/connect-serverless-gpu 🙈​s 🙈​s 🔄​f 🔄​f 🙈​s 🙈​s 🔄​f 🔄​f 🙈​s 🙈​s
💚​ TestAccept/ssh/connection 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R 💚​R
Top 20 slowest tests (at least 2 minutes):
duration env testname
4:18 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:22 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:19 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:15 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:13 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:08 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:07 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:06 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:03 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:54 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:53 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:51 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:46 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:25 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:23 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:22 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:16 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:13 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:10 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:04 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

@denik denik temporarily deployed to test-trigger-is March 11, 2026 10:41 — with GitHub Actions Inactive
@denik denik force-pushed the denik/permissions-reference branch from 66d736a to cf0178e Compare March 11, 2026 16:07
@denik denik temporarily deployed to test-trigger-is March 11, 2026 16:08 — with GitHub Actions Inactive
@denik denik temporarily deployed to test-trigger-is March 11, 2026 16:46 — with GitHub Actions Inactive
@denik denik temporarily deployed to test-trigger-is March 11, 2026 16:49 — with GitHub Actions Inactive
@denik denik force-pushed the denik/permissions-reference branch from 849df2a to 7824eb6 Compare March 11, 2026 22:31
@denik denik temporarily deployed to test-trigger-is March 11, 2026 22:31 — with GitHub Actions Inactive
Copy link
Member

@simonfaltum simonfaltum left a comment

Choose a reason for hiding this comment

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

[Agent Swarm Review] Verdict: Not ready yet

  • 1 Critical
  • 2 Major
  • 2 Gap
  • 2 Nit
  • 2 Suggestion

The core idea (EmbeddedSlice convention for struct walkers) is sound and well-implemented across libs/structs/. However, there is a critical backward-compatibility issue: the JSON tag json:"_,omitempty" on PermissionsState.EmbeddedSlice means old state files using the "permissions" key will silently load as empty, erasing all permission state. The fix is straightforward: change to json:"permissions,omitempty" since the EmbeddedSlice convention is driven by Go field name, not JSON tag.

See inline comments for details.

return err
}
*p = PermissionsState(raw)
migratePermissionLevel(p.EmbeddedSlice)
Copy link
Member

Choose a reason for hiding this comment

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

[Agent Swarm Review] [Critical]

State backward compatibility: JSON key change from "permissions" to "_" breaks old state files.

EmbeddedSlice uses json:"_,omitempty", so old state files containing "permissions": [...] will silently load as empty. The custom UnmarshalJSON only migrates permission_level to level within elements, but does NOT handle the outer key name change. This erases all permission state on the first plan/deploy after upgrading.

Both reviewers independently found this issue and confirmed it in cross-review.

Suggestion: Change to json:"permissions,omitempty". The EmbeddedSlice convention is driven entirely by the Go field name, not the JSON tag. This preserves backward compat, produces readable JSON output, and eliminates the need for key-name migration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I do want to rename permissions to _. The breakage will indeed cause a drift for all permissions resources. The drift will be removed on next "bundle deploy". However, given that direct engine is well adopted, I think it makes sense to migrate the state properly.

I bumped the state version to 2 and added migration function that takes old struct and produces a new one.

// findEmbedField returns the value of the EmbeddedSlice field in struct v, if any.
// Returns an invalid reflect.Value if no EmbeddedSlice field exists.
func findEmbedField(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Struct {
Copy link
Member

Choose a reason for hiding this comment

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

[Agent Swarm Review] [Gap (Nit)]

findEmbedField does not validate the field is actually a slice. If someone names a non-slice field EmbeddedSlice, struct walkers would produce confusing errors.

Suggestion: Add a type check: if sf.Type.Kind() != reflect.Slice { panic(...) }

@denik denik force-pushed the denik/permissions-reference branch from 7824eb6 to 2b2b440 Compare March 13, 2026 12:39
@denik denik temporarily deployed to test-trigger-is March 13, 2026 12:40 — with GitHub Actions Inactive
@denik denik temporarily deployed to test-trigger-is March 13, 2026 12:53 — with GitHub Actions Inactive
denik and others added 11 commits March 13, 2026 15:20
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces the __EMBED__ JSON tag convention. When a struct field
has json:"__EMBED__", struct walkers treat it as transparent and
don't add its name to the path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fields with json:"__EMBED__" are walked at the parent path level,
making their contents appear directly without the field name in paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fields with json:"__EMBED__" are walked at the parent path level
in type traversal, consistent with Walk behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When navigating a struct and the path expects an index or key-value
selector, transparently navigate through the __EMBED__ field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When setting a value at an index node and the parent is a struct,
transparently navigate through the __EMBED__ field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When validating paths with index, bracket-star, or key-value nodes
on a struct type, transparently navigate through __EMBED__ fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fields with json:"__EMBED__" are diffed at the parent path level,
consistent with how anonymous embedding works.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This makes the permissions slice appear at the root path of
PermissionsState, matching the input config schema where permissions
are accessed as resources.jobs.foo.permissions[0].

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The __EMBED__ convention on PermissionsState fixes the structaccess.Set()
bug that prevented the direct engine from planning permissions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@denik denik force-pushed the denik/permissions-reference branch from 29d6d69 to d0a4d17 Compare March 13, 2026 14:21
@denik denik temporarily deployed to test-trigger-is March 13, 2026 14:21 — with GitHub Actions Inactive
@denik denik requested a review from simonfaltum March 13, 2026 14:27
@denik denik temporarily deployed to test-trigger-is March 13, 2026 14:29 — with GitHub Actions Inactive
@denik denik temporarily deployed to test-trigger-is March 13, 2026 14:35 — with GitHub Actions Inactive
…l duplicates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 14:37 — with GitHub Actions Inactive
Adds a new invariant config testing cross-resource permission references
(both level and group_name). Also fixes no_drift and migrate scripts to
redirect stderr separately so warnings don't corrupt the JSON plan file
passed to verify_no_drift.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 14:54 — with GitHub Actions Inactive
denik and others added 2 commits March 13, 2026 15:57
Cross-resource permission references don't work in terraform mode:
databricks_job doesn't expose permissions as output attributes, so
${databricks_job.job_b.permissions[0].level} can't be resolved by Terraform.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 15:06 — with GitHub Actions Inactive
…comment

Tests both directions of cross-resource references:
- permission fields referencing job tag values (job tag -> permission group_name)
- permission fields referencing another job's permissions (permission -> permission)

Also expands the comment explaining why permission references don't work in
terraform mode (interpolator converts paths but databricks_job has no
permissions output attributes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 15:19 — with GitHub Actions Inactive
@denik denik temporarily deployed to test-trigger-is March 13, 2026 15:30 — with GitHub Actions Inactive
Adds infrastructure to verify the current CLI can deploy on top of state
produced by an older version:

- continuity/prepare.py: run with a version (e.g. v0.293.0) to deploy each
  invariant config using that CLI version and save the resulting state files
  to continuity/<version>/<config>.state.json. Uses -forcerun to bypass the
  Local=false guard so it only runs when explicitly invoked.

- continuity/prepare/: acceptance test that does the deploy+save; excluded
  from normal CI via Local=false/Cloud=false.

- continuity/v0.293.0/: continuity test that loads the committed state file,
  deploys with the current CLI, then verifies no drift.

- 24 state files generated from v0.293.0 (released 2026-03-11). Resources
  with name-based IDs unrecognised by the mock server are excluded from the
  test (dashboard, database_catalog, secret_scope, synced_database_table).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 15:47 — with GitHub Actions Inactive
…CLI at test time

Instead of pre-generating state files from v0.293.0, the test runner now always
downloads and caches v0.293.0 and exposes it as $CLI_293. The continue_293 test
deploys using $CLI_293 against the mock server, then deploys with the current CLI,
verifying no drift. This ensures the state is always consistent with the mock server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 15:57 — with GitHub Actions Inactive
Group names (viewers, admins) in job_permission_ref and job_cross_resource_ref
configs don't exist in real workspaces, causing 404 errors on cloud.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 16:26 — with GitHub Actions Inactive
Replace 'viewers' (non-existent in real workspaces) with 'users' (built-in
Databricks group). This allows the job_permission_ref and job_cross_resource_ref
configs to run on cloud without requiring custom group setup.

Remove cloud exclusion from no_drift test that was added to work around the
non-existent group names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@denik denik temporarily deployed to test-trigger-is March 13, 2026 16:38 — with GitHub Actions Inactive
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