diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/databricks.yml b/acceptance/bundle/resources/permissions/jobs/added_remotely/databricks.yml new file mode 100644 index 0000000000..bbf57577c1 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/databricks.yml @@ -0,0 +1,15 @@ +bundle: + name: test-bundle + +resources: + jobs: + job_with_permissions: + name: job permissions added remotely + tasks: + - task_key: main + notebook_task: + notebook_path: /Workspace/Users/user@example.com/notebook + source: WORKSPACE + permissions: + - level: CAN_VIEW + user_name: viewer@example.com diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json new file mode 100644 index 0000000000..344d30b011 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.direct.json @@ -0,0 +1,102 @@ +{ + "plan": { + "resources.jobs.job_with_permissions": { + "action": "skip", + "remote_state": { + "created_time": [UNIX_TIME_MILLIS], + "creator_user_name": "[USERNAME]", + "job_id": [JOB_ID], + "run_as_user_name": "[USERNAME]", + "settings": { + "deployment": { + "kind": "BUNDLE", + "metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json" + }, + "edit_mode": "UI_LOCKED", + "email_notifications": {}, + "format": "MULTI_TASK", + "max_concurrent_runs": 1, + "name": "job permissions added remotely", + "queue": { + "enabled": true + }, + "tasks": [ + { + "notebook_task": { + "notebook_path": "/Workspace/Users/user@example.com/notebook", + "source": "WORKSPACE" + }, + "task_key": "main" + } + ], + "timeout_seconds": 0, + "webhook_notifications": {} + } + }, + "changes": { + "remote": { + "email_notifications": { + "action": "skip", + "reason": "server_side_default" + }, + "timeout_seconds": { + "action": "skip", + "reason": "server_side_default" + }, + "webhook_notifications": { + "action": "skip", + "reason": "server_side_default" + } + } + } + }, + "resources.jobs.job_with_permissions.permissions": { + "depends_on": [ + { + "node": "resources.jobs.job_with_permissions", + "label": "${resources.jobs.job_with_permissions.id}" + } + ], + "action": "update", + "new_state": { + "value": { + "object_id": "/jobs/[JOB_ID]", + "permissions": [ + { + "permission_level": "CAN_VIEW", + "user_name": "viewer@example.com" + }, + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + } + ] + } + }, + "remote_state": { + "object_id": "/jobs/[JOB_ID]", + "permissions": [ + { + "permission_level": "IS_OWNER", + "user_name": "[USERNAME]" + }, + { + "permission_level": "CAN_VIEW", + "user_name": "viewer@example.com" + }, + { + "group_name": "admin-team", + "permission_level": "CAN_MANAGE" + } + ] + }, + "changes": { + "remote": { + "permissions[group_name='admin-team']": { + "action": "update" + } + } + } + } + } +} diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.terraform.json b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.terraform.json new file mode 100644 index 0000000000..ad5e06bc5e --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.plan.terraform.json @@ -0,0 +1,10 @@ +{ + "plan": { + "resources.jobs.job_with_permissions": { + "action": "skip" + }, + "resources.jobs.job_with_permissions.permissions": { + "action": "update" + } + } +} diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/out.test.toml b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/output.txt b/acceptance/bundle/resources/permissions/jobs/added_remotely/output.txt new file mode 100644 index 0000000000..10bf99e2da --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/output.txt @@ -0,0 +1,161 @@ + +>>> [CLI] bundle plan +create jobs.job_with_permissions +create jobs.job_with_permissions.permissions + +Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +>>> [CLI] permissions get jobs [JOB_ID] +{ + "access_control_list": [ + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"CAN_VIEW" + } + ], + "display_name":"viewer@example.com", + "user_name":"viewer@example.com" + }, + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"IS_OWNER" + } + ], + "display_name":"[USERNAME]", + "user_name":"[USERNAME]" + }, + { + "all_permissions": [ + { + "inherited":true, + "inherited_from_object": [ + "/jobs/" + ], + "permission_level":"CAN_MANAGE" + } + ], + "group_name":"admins" + } + ], + "object_id":"/jobs/[JOB_ID]", + "object_type":"job" +} + +=== Add permissions out of band +>>> [CLI] permissions set jobs [JOB_ID] --json @remote_add.json +{ + "access_control_list": [ + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"IS_OWNER" + } + ], + "display_name":"[USERNAME]", + "user_name":"[USERNAME]" + }, + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"CAN_VIEW" + } + ], + "display_name":"viewer@example.com", + "user_name":"viewer@example.com" + }, + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"CAN_MANAGE" + } + ], + "group_name":"admin-team" + }, + { + "all_permissions": [ + { + "inherited":true, + "inherited_from_object": [ + "/jobs/" + ], + "permission_level":"CAN_MANAGE" + } + ], + "group_name":"admins" + } + ], + "object_id":"/jobs/[JOB_ID]", + "object_type":"job" +} + +>>> [CLI] bundle plan +update jobs.job_with_permissions.permissions + +Plan: 0 to add, 1 to change, 0 to delete, 1 unchanged + +>>> [CLI] bundle plan -o json + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] bundle plan +Plan: 0 to add, 0 to change, 0 to delete, 2 unchanged + +>>> [CLI] permissions get jobs [JOB_ID] +{ + "access_control_list": [ + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"CAN_VIEW" + } + ], + "display_name":"viewer@example.com", + "user_name":"viewer@example.com" + }, + { + "all_permissions": [ + { + "inherited":false, + "permission_level":"IS_OWNER" + } + ], + "display_name":"[USERNAME]", + "user_name":"[USERNAME]" + }, + { + "all_permissions": [ + { + "inherited":true, + "inherited_from_object": [ + "/jobs/" + ], + "permission_level":"CAN_MANAGE" + } + ], + "group_name":"admins" + } + ], + "object_id":"/jobs/[JOB_ID]", + "object_type":"job" +} diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/remote_add.json b/acceptance/bundle/resources/permissions/jobs/added_remotely/remote_add.json new file mode 100644 index 0000000000..0687f92f21 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/remote_add.json @@ -0,0 +1,7 @@ +{ + "access_control_list": [ + {"permission_level": "IS_OWNER", "user_name": "tester@databricks.com"}, + {"permission_level": "CAN_VIEW", "user_name": "viewer@example.com"}, + {"permission_level": "CAN_MANAGE", "group_name": "admin-team"} + ] +} diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/script b/acceptance/bundle/resources/permissions/jobs/added_remotely/script new file mode 100644 index 0000000000..6b639dd0a8 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/script @@ -0,0 +1,18 @@ +trace $CLI bundle plan +trace $CLI bundle deploy +trace $CLI bundle plan + +job_id="$(read_id.py jobs job_with_permissions)" +echo "$job_id:JOB_ID" >> ACC_REPLS + +trace $CLI permissions get jobs "$job_id" + +title "Add permissions out of band" +trace $CLI permissions set jobs "$job_id" --json @remote_add.json + +trace $CLI bundle plan +trace $CLI bundle plan -o json > out.plan.$DATABRICKS_BUNDLE_ENGINE.json +trace $CLI bundle deploy +trace $CLI bundle plan + +trace $CLI permissions get jobs "$job_id" diff --git a/acceptance/bundle/resources/permissions/jobs/added_remotely/test.toml b/acceptance/bundle/resources/permissions/jobs/added_remotely/test.toml new file mode 100644 index 0000000000..a030353d57 --- /dev/null +++ b/acceptance/bundle/resources/permissions/jobs/added_remotely/test.toml @@ -0,0 +1 @@ +RecordRequests = false diff --git a/bundle/direct/bundle_plan.go b/bundle/direct/bundle_plan.go index 7d8ebf9c51..d5966bd0b5 100644 --- a/bundle/direct/bundle_plan.go +++ b/bundle/direct/bundle_plan.go @@ -260,10 +260,12 @@ func interpretOldStateVsRemoteState(ctx context.Context, adapter *dresources.Ada m := make(map[string]deployplan.Trigger) for _, ch := range diff { - if ch.Old == nil { + if ch.Old == nil && ch.Path.IsStringKey() { // The field was not set by us, but comes from the remote state. // This could either be server-side default or a policy. // In any case, this is not a change we should react to. + // Note, we only consider StringKeys here, because indexes and key-value pairs refer to slices and we want to react to new element in slices. + // Note, IsStringKey is also too broad - it currently covers struct fields and map keys, we don't want to include map keys here. m[ch.Path.String()] = deployplan.Trigger{ Action: deployplan.ActionTypeSkipString, Reason: "server_side_default", diff --git a/libs/structs/structpath/path.go b/libs/structs/structpath/path.go index 1627ae6f7d..8c20be814d 100644 --- a/libs/structs/structpath/path.go +++ b/libs/structs/structpath/path.go @@ -73,6 +73,10 @@ func (p *PathNode) KeyValue() (key, value string, ok bool) { return "", "", false } +func (p *PathNode) IsStringKey() bool { + return p != nil && p.index == tagStringKey +} + // StringKey returns either Field() or MapKey() if either is available func (p *PathNode) StringKey() (string, bool) { if p == nil {