From 3b386ff8911fb97cfd2dff57e0bd57d4620a8150 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 11:08:43 +0000 Subject: [PATCH 1/6] Skip permissions/grants sub-resources in config-remote-sync --- NEXT_CHANGELOG.md | 1 + .../skip_permissions/databricks.yml.tmpl | 26 +++++++ .../skip_permissions/out.test.toml | 4 + .../skip_permissions/output.txt | 74 +++++++++++++++++++ .../skip_permissions/script | 34 +++++++++ .../skip_permissions/test.toml | 10 +++ bundle/configsync/diff.go | 20 +++++ bundle/configsync/diff_test.go | 44 +++++++++++ 8 files changed, 213 insertions(+) create mode 100644 acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl create mode 100644 acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml create mode 100644 acceptance/bundle/config-remote-sync/skip_permissions/output.txt create mode 100644 acceptance/bundle/config-remote-sync/skip_permissions/script create mode 100644 acceptance/bundle/config-remote-sync/skip_permissions/test.toml diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index da7072e3864..ef892daf818 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -10,6 +10,7 @@ ### Bundles +* Fix `bundle config-remote-sync` failing with "failed to resolve field changes" on resources that have permissions or grants; these sub-resources are not synced back to configuration and are now skipped instead of erroring the whole sync ([#XXXX](https://github.com/databricks/cli/pull/XXXX)). * direct: add basic version of job_runs resource (experimental) ([#5603](https://github.com/databricks/cli/pull/5603)). * Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)). * Remove duplicate enum values for jsonschema.json ([#5839](https://github.com/databricks/cli/pull/5839)). diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl b/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl new file mode 100644 index 00000000000..ab1f0f1725f --- /dev/null +++ b/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl @@ -0,0 +1,26 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +# Bundle-level permissions are applied to every resource on deploy but have no +# per-resource YAML node. config-remote-sync must skip the resulting +# permissions sub-resource instead of failing to resolve it. +permissions: + - level: CAN_MANAGE + user_name: alice@example.com + +resources: + jobs: + my_job: + max_concurrent_runs: 1 + tasks: + - task_key: main + notebook_task: + notebook_path: /Users/{{workspace_user_name}}/main + new_cluster: + spark_version: $DEFAULT_SPARK_VERSION + node_type_id: $NODE_TYPE_ID + num_workers: 1 + +targets: + default: + mode: development diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml new file mode 100644 index 00000000000..579b1e4a3c9 --- /dev/null +++ b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml @@ -0,0 +1,4 @@ +Local = true +Cloud = true +GOOS.windows = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct", "terraform"] diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/output.txt b/acceptance/bundle/config-remote-sync/skip_permissions/output.txt new file mode 100644 index 00000000000..ece04bbc902 --- /dev/null +++ b/acceptance/bundle/config-remote-sync/skip_permissions/output.txt @@ -0,0 +1,74 @@ +Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups +If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. + +Consider using a adding a top-level permissions section such as the following: + + permissions: + - user_name: [USERNAME] + level: CAN_MANAGE + +See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + in databricks.yml:8:3 + +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +=== Add a permission remotely (a change on the permissions sub-resource) + +=== Modify a regular job field remotely + +=== Sync: permissions are skipped, the regular field change is written +Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups +If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. + +Consider using a adding a top-level permissions section such as the following: + + permissions: + - user_name: [USERNAME] + level: CAN_MANAGE + +See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + in databricks.yml:8:3 + +Detected changes in 1 resource(s): + +Resource: resources.jobs.my_job + max_concurrent_runs: replace + + + +=== Configuration changes + +>>> diff.py databricks.yml.backup databricks.yml +--- databricks.yml.backup ++++ databricks.yml +@@ -12,5 +12,5 @@ + jobs: + my_job: +- max_concurrent_runs: 1 ++ max_concurrent_runs: 5 + tasks: + - task_key: main + +>>> [CLI] bundle destroy --auto-approve +Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups +If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. + +Consider using a adding a top-level permissions section such as the following: + + permissions: + - user_name: [USERNAME] + level: CAN_MANAGE + +See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. + in databricks.yml:8:3 + +The following resources will be deleted: + delete resources.jobs.my_job + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/script b/acceptance/bundle/config-remote-sync/skip_permissions/script new file mode 100644 index 00000000000..b1adfcdd645 --- /dev/null +++ b/acceptance/bundle/config-remote-sync/skip_permissions/script @@ -0,0 +1,34 @@ +#!/bin/bash + +envsubst < databricks.yml.tmpl > databricks.yml + +cleanup() { + trace $CLI bundle destroy --auto-approve +} +trap cleanup EXIT + +$CLI bundle deploy +job_id="$(read_id.py my_job)" + +title "Add a permission remotely (a change on the permissions sub-resource)" +echo +# permissions set replaces the whole ACL, so keep the deploy identity as owner +# and add a new grantee: this makes the permissions sub-resource show a change +# on the next sync. +$CLI permissions set jobs $job_id --json '{"access_control_list":[{"permission_level":"IS_OWNER","user_name":"'"$CURRENT_USER_NAME"'"},{"permission_level":"CAN_MANAGE","user_name":"alice@example.com"},{"permission_level":"CAN_VIEW","user_name":"viewer@example.com"}]}' > /dev/null + +title "Modify a regular job field remotely" +echo +edit_resource.py jobs $job_id <..permissions" / ".grants". config-remote-sync + // cannot write them back to YAML: their fields (e.g. object_id) are + // server-populated and have no source location, and permissions injected + // at the bundle level have no per-resource YAML node at all, so resolving + // them fails the whole sync. Syncing permissions back is unsupported, so + // skip these sub-resources instead of hard-failing. + if isUnsupportedSubResource(resourceKey) { + continue + } + resourceChanges := make(ResourceChanges) if entry.Changes != nil { diff --git a/bundle/configsync/diff_test.go b/bundle/configsync/diff_test.go index 317ac1f32ba..8305e173d8e 100644 --- a/bundle/configsync/diff_test.go +++ b/bundle/configsync/diff_test.go @@ -3,6 +3,8 @@ package configsync import ( "testing" + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/deployplan" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -247,3 +249,45 @@ func TestMatchPattern(t *testing.T) { }) } } + +// ExtractChanges must skip permissions/grants sub-resources: they are emitted as +// their own plan keys but cannot be written back to YAML, and resolving them +// (e.g. the server-populated object_id field, or a bundle-level permissions entry +// with no per-resource YAML node) otherwise fails the whole sync. +func TestExtractChangesSkipsPermissionsAndGrants(t *testing.T) { + ctx := t.Context() + + plan := &deployplan.Plan{Plan: map[string]*deployplan.PlanEntry{ + "resources.jobs.my_job": { + Changes: deployplan.Changes{ + "description": {Action: deployplan.Update, New: nil, Remote: "remote-desc"}, + }, + }, + "resources.jobs.my_job.permissions": { + Changes: deployplan.Changes{ + "object_id": {Action: deployplan.Update, New: nil, Remote: "/jobs/123"}, + }, + }, + "resources.schemas.my_schema.grants": { + Changes: deployplan.Changes{ + "[0].privileges": {Action: deployplan.Update, New: nil, Remote: []any{"SELECT"}}, + }, + }, + }} + + for _, eng := range []engine.EngineType{engine.EngineDirect, engine.EngineTerraform} { + t.Run(string(eng), func(t *testing.T) { + changes, err := ExtractChanges(ctx, &bundle.Bundle{}, plan, eng) + require.NoError(t, err) + + _, hasPerms := changes["resources.jobs.my_job.permissions"] + assert.False(t, hasPerms, "permissions sub-resource must be skipped") + _, hasGrants := changes["resources.schemas.my_schema.grants"] + assert.False(t, hasGrants, "grants sub-resource must be skipped") + + job, hasJob := changes["resources.jobs.my_job"] + require.True(t, hasJob, "regular resource changes must be kept") + assert.Contains(t, job, "description") + }) + } +} From ef6b4fb1d9f8813463c03b56b5fd07ea9175ddfd Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 11:43:38 +0000 Subject: [PATCH 2/6] Inline permissions/grants skip check in config-remote-sync --- bundle/configsync/diff.go | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/bundle/configsync/diff.go b/bundle/configsync/diff.go index 4e34d63b11d..5cc96279aa3 100644 --- a/bundle/configsync/diff.go +++ b/bundle/configsync/diff.go @@ -154,28 +154,19 @@ func OpenDeploymentState(ctx context.Context, b *bundle.Bundle, engine engine.En return deployBundle, nil } -// isUnsupportedSubResource reports whether a plan resource key refers to a -// permissions or grants sub-resource (e.g. "resources.jobs.foo.permissions"). -// The direct and terraform engines both emit these as their own plan keys; see -// splitResourcePath in bundle/direct/bundle_plan.go. -func isUnsupportedSubResource(resourceKey string) bool { - return strings.HasSuffix(resourceKey, ".permissions") || strings.HasSuffix(resourceKey, ".grants") -} - // ExtractChanges extracts the map of remote-vs-config changes from a deploy // plan. engine selects the LocalEdit comparison below. func ExtractChanges(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, engine engine.EngineType) (Changes, error) { changes := make(Changes) for resourceKey, entry := range plan.Plan { - // permissions and grants are separate plan sub-resources keyed - // "resources...permissions" / ".grants". config-remote-sync - // cannot write them back to YAML: their fields (e.g. object_id) are - // server-populated and have no source location, and permissions injected - // at the bundle level have no per-resource YAML node at all, so resolving - // them fails the whole sync. Syncing permissions back is unsupported, so - // skip these sub-resources instead of hard-failing. - if isUnsupportedSubResource(resourceKey) { + // permissions and grants are emitted as their own plan keys + // ("resources...permissions" / ".grants"; see splitResourcePath + // in bundle/direct/bundle_plan.go). config-remote-sync cannot write them back + // to YAML: their fields (e.g. object_id) are server-populated with no source + // location, and bundle-level permissions have no per-resource YAML node at all, + // so resolving them would fail the whole sync. Skip them instead. + if strings.HasSuffix(resourceKey, ".permissions") || strings.HasSuffix(resourceKey, ".grants") { continue } From 2a2d5c07aebda77e41739152eb66d00ddec4c979 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 12:13:21 +0000 Subject: [PATCH 3/6] Make skip_permissions acceptance test local-only (synthetic principals) --- .../bundle/config-remote-sync/skip_permissions/out.test.toml | 2 +- .../bundle/config-remote-sync/skip_permissions/test.toml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml index 579b1e4a3c9..4b5914daa2c 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml +++ b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml @@ -1,4 +1,4 @@ Local = true -Cloud = true +Cloud = false GOOS.windows = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct", "terraform"] diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml index 1067069b9fa..f80321966b4 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml +++ b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml @@ -1,5 +1,6 @@ -Cloud = true - +# Local-only (no Cloud): the scenario uses synthetic grantees (alice@, viewer@) +# that need not be valid workspace principals, and the testserver reproduces the +# permissions sub-resource + object_id faithfully. RecordRequests = false Ignore = [".databricks", "databricks.yml", "databricks.yml.backup"] From 468cdfad40ff0bb6ce91e7b9dc856b39d7d9d260 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 12:47:53 +0000 Subject: [PATCH 4/6] Drop changelog entry (config-remote-sync is a hidden command) --- NEXT_CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index ef892daf818..da7072e3864 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -10,7 +10,6 @@ ### Bundles -* Fix `bundle config-remote-sync` failing with "failed to resolve field changes" on resources that have permissions or grants; these sub-resources are not synced back to configuration and are now skipped instead of erroring the whole sync ([#XXXX](https://github.com/databricks/cli/pull/XXXX)). * direct: add basic version of job_runs resource (experimental) ([#5603](https://github.com/databricks/cli/pull/5603)). * Fix permissions added to a job or pipeline by a Python (PyDABs) mutator failing to deploy with "must have exactly one owner"; the deploying identity is now set as owner, matching resources whose permissions are declared in YAML ([#5821](https://github.com/databricks/cli/pull/5821)). * Remove duplicate enum values for jsonschema.json ([#5839](https://github.com/databricks/cli/pull/5839)). From 742d04e65f32f5f3e2bc04f218c8bbfde70b919e Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 20 Jul 2026 14:55:36 +0000 Subject: [PATCH 5/6] Run skip_permissions on cloud with valid principals (current user + users group) --- .../skip_permissions/databricks.yml.tmpl | 2 +- .../skip_permissions/out.test.toml | 2 +- .../skip_permissions/output.txt | 36 ------------------- .../skip_permissions/script | 6 ++-- .../skip_permissions/test.toml | 5 ++- 5 files changed, 7 insertions(+), 44 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl b/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl index ab1f0f1725f..74055593627 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl +++ b/acceptance/bundle/config-remote-sync/skip_permissions/databricks.yml.tmpl @@ -6,7 +6,7 @@ bundle: # permissions sub-resource instead of failing to resolve it. permissions: - level: CAN_MANAGE - user_name: alice@example.com + user_name: ${workspace.current_user.userName} resources: jobs: diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml index 4b5914daa2c..579b1e4a3c9 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml +++ b/acceptance/bundle/config-remote-sync/skip_permissions/out.test.toml @@ -1,4 +1,4 @@ Local = true -Cloud = false +Cloud = true GOOS.windows = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct", "terraform"] diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/output.txt b/acceptance/bundle/config-remote-sync/skip_permissions/output.txt index ece04bbc902..e69b512fdae 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/output.txt +++ b/acceptance/bundle/config-remote-sync/skip_permissions/output.txt @@ -1,15 +1,3 @@ -Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups -If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. - -Consider using a adding a top-level permissions section such as the following: - - permissions: - - user_name: [USERNAME] - level: CAN_MANAGE - -See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. - in databricks.yml:8:3 - Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... @@ -20,18 +8,6 @@ Deployment complete! === Modify a regular job field remotely === Sync: permissions are skipped, the regular field change is written -Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups -If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. - -Consider using a adding a top-level permissions section such as the following: - - permissions: - - user_name: [USERNAME] - level: CAN_MANAGE - -See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. - in databricks.yml:8:3 - Detected changes in 1 resource(s): Resource: resources.jobs.my_job @@ -53,18 +29,6 @@ Resource: resources.jobs.my_job - task_key: main >>> [CLI] bundle destroy --auto-approve -Recommendation: permissions section should explicitly include the current deployment identity '[USERNAME]' or one of its groups -If it is not included, CAN_MANAGE permissions are only applied if the present identity is used to deploy. - -Consider using a adding a top-level permissions section such as the following: - - permissions: - - user_name: [USERNAME] - level: CAN_MANAGE - -See https://docs.databricks.com/dev-tools/bundles/permissions.html to learn more about permission configuration. - in databricks.yml:8:3 - The following resources will be deleted: delete resources.jobs.my_job diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/script b/acceptance/bundle/config-remote-sync/skip_permissions/script index b1adfcdd645..585db4a5535 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/script +++ b/acceptance/bundle/config-remote-sync/skip_permissions/script @@ -13,9 +13,9 @@ job_id="$(read_id.py my_job)" title "Add a permission remotely (a change on the permissions sub-resource)" echo # permissions set replaces the whole ACL, so keep the deploy identity as owner -# and add a new grantee: this makes the permissions sub-resource show a change -# on the next sync. -$CLI permissions set jobs $job_id --json '{"access_control_list":[{"permission_level":"IS_OWNER","user_name":"'"$CURRENT_USER_NAME"'"},{"permission_level":"CAN_MANAGE","user_name":"alice@example.com"},{"permission_level":"CAN_VIEW","user_name":"viewer@example.com"}]}' > /dev/null +# and add a grantee (the built-in "users" group) that is not in config: this +# makes the permissions sub-resource show a change on the next sync. +$CLI permissions set jobs $job_id --json '{"access_control_list":[{"permission_level":"IS_OWNER","user_name":"'"$CURRENT_USER_NAME"'"},{"permission_level":"CAN_VIEW","group_name":"users"}]}' > /dev/null title "Modify a regular job field remotely" echo diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml index f80321966b4..1067069b9fa 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml +++ b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml @@ -1,6 +1,5 @@ -# Local-only (no Cloud): the scenario uses synthetic grantees (alice@, viewer@) -# that need not be valid workspace principals, and the testserver reproduces the -# permissions sub-resource + object_id faithfully. +Cloud = true + RecordRequests = false Ignore = [".databricks", "databricks.yml", "databricks.yml.backup"] From 496b773f5dd51856118e632f88fb04d861523492 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Wed, 22 Jul 2026 08:54:51 +0000 Subject: [PATCH 6/6] Address review: use config.GetNodeAndType for perms/grants classification; inline Env in test.toml --- .../skip_permissions/test.toml | 3 +-- bundle/configsync/diff.go | 17 ++++++++++++++++- bundle/configsync/diff_test.go | 12 ++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml index 1067069b9fa..41057263c22 100644 --- a/acceptance/bundle/config-remote-sync/skip_permissions/test.toml +++ b/acceptance/bundle/config-remote-sync/skip_permissions/test.toml @@ -3,8 +3,7 @@ Cloud = true RecordRequests = false Ignore = [".databricks", "databricks.yml", "databricks.yml.backup"] -[Env] -DATABRICKS_BUNDLE_ENABLE_EXPERIMENTAL_YAML_SYNC = "true" +Env.DATABRICKS_BUNDLE_ENABLE_EXPERIMENTAL_YAML_SYNC = "true" [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["direct", "terraform"] diff --git a/bundle/configsync/diff.go b/bundle/configsync/diff.go index 5cc96279aa3..ea45903508b 100644 --- a/bundle/configsync/diff.go +++ b/bundle/configsync/diff.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/deploy" "github.com/databricks/cli/bundle/deployplan" @@ -154,6 +155,20 @@ func OpenDeploymentState(ctx context.Context, b *bundle.Bundle, engine engine.En return deployBundle, nil } +// isPermissionsOrGrantsSubResource reports whether a plan resource key is a +// permissions or grants sub-resource ("resources...permissions" / +// ".grants"). It classifies the key structurally via config.GetNodeAndType, +// which keys on the path component (index 3), so a resource literally named +// "permissions" ("resources.jobs.permissions") is not misclassified. +func isPermissionsOrGrantsSubResource(resourceKey string) bool { + path, err := dyn.NewPathFromString(resourceKey) + if err != nil { + return false + } + _, nodeType := config.GetNodeAndType(path) + return strings.HasSuffix(nodeType, ".permissions") || strings.HasSuffix(nodeType, ".grants") +} + // ExtractChanges extracts the map of remote-vs-config changes from a deploy // plan. engine selects the LocalEdit comparison below. func ExtractChanges(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan, engine engine.EngineType) (Changes, error) { @@ -166,7 +181,7 @@ func ExtractChanges(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan // to YAML: their fields (e.g. object_id) are server-populated with no source // location, and bundle-level permissions have no per-resource YAML node at all, // so resolving them would fail the whole sync. Skip them instead. - if strings.HasSuffix(resourceKey, ".permissions") || strings.HasSuffix(resourceKey, ".grants") { + if isPermissionsOrGrantsSubResource(resourceKey) { continue } diff --git a/bundle/configsync/diff_test.go b/bundle/configsync/diff_test.go index 8305e173d8e..bb1acc682ba 100644 --- a/bundle/configsync/diff_test.go +++ b/bundle/configsync/diff_test.go @@ -273,6 +273,14 @@ func TestExtractChangesSkipsPermissionsAndGrants(t *testing.T) { "[0].privileges": {Action: deployplan.Update, New: nil, Remote: []any{"SELECT"}}, }, }, + // A resource literally named "permissions" is a regular resource, not a + // permissions sub-resource, so its changes must be kept (structural + // classification via config.GetNodeAndType, not a suffix match). + "resources.jobs.permissions": { + Changes: deployplan.Changes{ + "description": {Action: deployplan.Update, New: nil, Remote: "kept"}, + }, + }, }} for _, eng := range []engine.EngineType{engine.EngineDirect, engine.EngineTerraform} { @@ -288,6 +296,10 @@ func TestExtractChangesSkipsPermissionsAndGrants(t *testing.T) { job, hasJob := changes["resources.jobs.my_job"] require.True(t, hasJob, "regular resource changes must be kept") assert.Contains(t, job, "description") + + namedPerms, hasNamedPerms := changes["resources.jobs.permissions"] + require.True(t, hasNamedPerms, "a resource named permissions must not be skipped") + assert.Contains(t, namedPerms, "description") }) } }