Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
bundle:
name: test-bundle-$UNIQUE_NAME

# job_a's task list is split across both blocks and one task, "shared", is defined
# in both. job_b is an ordinary single-block job.
#
# The point of the fixture: an unrelated resource's change must still be applied
# in the same run as a structural change to a split element. The sync is
# unattended, so one harder change must never stop the rest.
resources:
jobs:
job_a:
tasks:
- task_key: shared
max_retries: 1
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/shared

job_b:
max_concurrent_runs: 1
tasks:
- task_key: simple
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/simple

targets:
dev:
mode: development
resources:
jobs:
job_a:
tasks:
- task_key: shared
timeout_seconds: 45

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions acceptance/bundle/config-remote-sync/split/isolation/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/dev/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== Rename the two-block task on job_a, and edit job_b in the same run
=== Sync
Detected changes in 2 resource(s):

Resource: resources.jobs.job_a
tasks[task_key='shared']: remove
tasks[task_key='shared_renamed']: add

Resource: resources.jobs.job_b
max_concurrent_runs: replace



=== job_b is updated, and job_a's rename is written in both blocks

>>> diff.py databricks.yml.backup databricks.yml
--- databricks.yml.backup
+++ databricks.yml
@@ -12,11 +12,11 @@
job_a:
tasks:
- - task_key: shared
- max_retries: 1
+ - max_retries: 1
notebook_task:
- notebook_path: /Users/{{workspace_user_name}}/shared
-
+ notebook_path: '/Users/{{workspace_user_name}}/shared'
+ task_key: shared_renamed
+ timeout_seconds: 45
job_b:
- max_concurrent_runs: 1
+ max_concurrent_runs: 6
tasks:
- task_key: simple
@@ -30,5 +30,3 @@
jobs:
job_a:
- tasks:
- - task_key: shared
- timeout_seconds: 45
+ tasks: []

>>> grep -c max_concurrent_runs: 6 databricks.yml
1

>>> grep -c task_key: shared_renamed databricks.yml
1

>>> [CLI] bundle destroy --auto-approve -t dev
The following resources will be deleted:
delete resources.jobs.job_a
delete resources.jobs.job_b

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/dev

Deleting files...
Destroy complete!
39 changes: 39 additions & 0 deletions acceptance/bundle/config-remote-sync/split/isolation/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/bin/bash

envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve -t dev
}
trap cleanup EXIT

$CLI bundle deploy -t dev
job_a_id="$(read_id.py job_a)"
job_b_id="$(read_id.py job_b)"


# A rename of the two-block task on job_a, and a plain scalar edit on job_b, in the
# SAME run. job_b's edit is independent of anything job_a does, so it must be
# applied whether or not job_a's rename can be placed.
title "Rename the two-block task on job_a, and edit job_b in the same run"
edit_resource.py jobs $job_a_id <<EOF
for task in r["tasks"]:
if task["task_key"] == "shared":
task["task_key"] = "shared_renamed"
EOF

edit_resource.py jobs $job_b_id <<EOF
r["max_concurrent_runs"] = 6
EOF

title "Sync"
echo
cp databricks.yml databricks.yml.backup
errcode $CLI bundle config-remote-sync -t dev --save

title "job_b is updated, and job_a's rename is written in both blocks"
echo
trace diff.py databricks.yml.backup databricks.yml
trace grep -c "max_concurrent_runs: 6" databricks.yml
trace grep -c "task_key: shared_renamed" databricks.yml
rm databricks.yml.backup
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
bundle:
name: test-bundle-$UNIQUE_NAME

# remove_job: gamma/beta live top-level, alpha lives in the target block. The
# merged order is [alpha, beta, gamma], so the merged indices collide with the
# physical ones -- removing by merged index deletes the wrong tasks.
#
# twoblock_remove_job: "both" is defined in the two blocks at once. Removing it
# cannot be expressed as "drop it from one scope but keep the merged result", so
# the sync must leave the source untouched rather than half-deleting it.
resources:
jobs:
remove_job:
tasks:
- task_key: gamma
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/gamma
- task_key: beta
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/beta

twoblock_remove_job:
tasks:
- task_key: both
max_retries: 2
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/both
- task_key: keep
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/keep

targets:
dev:
mode: development
resources:
jobs:
remove_job:
tasks:
- task_key: alpha
notebook_task:
notebook_path: /Users/{{workspace_user_name}}/alpha
twoblock_remove_job:
tasks:
- task_key: both
timeout_seconds: 30

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

87 changes: 87 additions & 0 deletions acceptance/bundle/config-remote-sync/split/keyed_remove/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/dev/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== Remove gamma from the top-level block and alpha from the target block
=== Sync
Detected changes in 1 resource(s):

Resource: resources.jobs.remove_job
tasks[task_key='alpha']: remove
tasks[task_key='gamma']: remove



=== Exactly gamma and alpha are gone; beta survives

>>> diff.py databricks.yml.backup databricks.yml
--- databricks.yml.backup
+++ databricks.yml
@@ -13,7 +13,4 @@
remove_job:
tasks:
- - task_key: gamma
- notebook_task:
- notebook_path: /Users/{{workspace_user_name}}/gamma
- task_key: beta
notebook_task:
@@ -36,8 +33,5 @@
jobs:
remove_job:
- tasks:
- - task_key: alpha
- notebook_task:
- notebook_path: /Users/{{workspace_user_name}}/alpha
+ tasks: []
twoblock_remove_job:
tasks:

=== Remove the task defined in BOTH blocks
=== Sync
Detected changes in 1 resource(s):

Resource: resources.jobs.twoblock_remove_job
tasks[task_key='both']: remove



=== 'both' is gone from both blocks; 'keep' survives

>>> diff.py databricks.yml.backup databricks.yml
--- databricks.yml.backup
+++ databricks.yml
@@ -19,8 +19,4 @@
twoblock_remove_job:
tasks:
- - task_key: both
- max_retries: 2
- notebook_task:
- notebook_path: /Users/{{workspace_user_name}}/both
- task_key: keep
notebook_task:
@@ -35,5 +31,3 @@
tasks: []
twoblock_remove_job:
- tasks:
- - task_key: both
- timeout_seconds: 30
+ tasks: []

>>> grep -c task_key: both databricks.yml
0

Exit code: 1

>>> grep -c task_key: keep databricks.yml
1

>>> [CLI] bundle destroy --auto-approve -t dev
The following resources will be deleted:
delete resources.jobs.remove_job
delete resources.jobs.twoblock_remove_job

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle-[UNIQUE_NAME]/dev

Deleting files...
Destroy complete!
54 changes: 54 additions & 0 deletions acceptance/bundle/config-remote-sync/split/keyed_remove/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash

envsubst < databricks.yml.tmpl > databricks.yml

cleanup() {
trace $CLI bundle destroy --auto-approve -t dev
}
trap cleanup EXIT

$CLI bundle deploy -t dev
remove_job_id="$(read_id.py remove_job)"
twoblock_job_id="$(read_id.py twoblock_remove_job)"


# Remove one task from each block in a single run. Exactly gamma and alpha must
# disappear; beta must survive even though the removals shift indices in both
# blocks.
title "Remove gamma from the top-level block and alpha from the target block"
edit_resource.py jobs $remove_job_id <<EOF
r["tasks"] = [t for t in r["tasks"] if t["task_key"] not in ("gamma", "alpha")]
EOF

title "Sync"
echo
cp databricks.yml databricks.yml.backup
errcode $CLI bundle config-remote-sync -t dev --select-ids "jobs:$remove_job_id" --save

title "Exactly gamma and alpha are gone; beta survives"
echo
trace diff.py databricks.yml.backup databricks.yml
rm databricks.yml.backup


# "both" is defined in two blocks, so it has a part in each. The merged element
# only disappears once both parts are gone, so the removal has to be written to
# both blocks. "keep" must survive in the top-level block.
title "Remove the task defined in BOTH blocks"
edit_resource.py jobs $twoblock_job_id <<EOF
r["tasks"] = [t for t in r["tasks"] if t["task_key"] != "both"]
EOF

title "Sync"
echo
cp databricks.yml databricks.yml.backup
errcode $CLI bundle config-remote-sync -t dev --select-ids "jobs:$twoblock_job_id" --save

title "'both' is gone from both blocks; 'keep' survives"
echo
trace diff.py databricks.yml.backup databricks.yml
# grep -c exits non-zero on a count of zero, which would abort the script under
# bash -e before the second assertion runs.
errcode trace grep -c "task_key: both" databricks.yml
errcode trace grep -c "task_key: keep" databricks.yml
rm databricks.yml.backup
42 changes: 42 additions & 0 deletions bundle/configsync/blockindex.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,48 @@ func (r *blockResolver) route(change resolvedChange) (sourceBlock, *structpath.P
return block, path, nil
}

// routeDestination is one physical place a change has to be written.
type routeDestination struct {
block sourceBlock
path *structpath.PatternNode
}

// routeElement maps a change that addresses a whole sequence element onto every
// block that defines it. An element assembled from two blocks has a part in each,
// so removing it means deleting both parts. Expressing the change per block keeps
// the split intact instead of collapsing the element into one scope.
func (r *blockResolver) routeElement(change resolvedChange) ([]routeDestination, error) {
if len(change.steps) == 0 {
return nil, fmt.Errorf("%w: change does not address a sequence element", errAmbiguousBlock)
}
last := change.steps[len(change.steps)-1]

// A change to one of the element's fields addresses a single location and
// routes like any other change.
if len(change.path.AsSlice()) > last.component+1 {
block, path, err := r.route(change)
if err != nil {
return nil, err
}
return []routeDestination{{block: block, path: path}}, nil
}

blocks := r.blocksOf(last.element)
if len(blocks) == 0 {
return nil, fmt.Errorf("%w: no source location for the addressed element", errAmbiguousBlock)
}

destinations := make([]routeDestination, 0, len(blocks))
for _, block := range blocks {
path, err := r.localize(block, change)
if err != nil {
return nil, err
}
destinations = append(destinations, routeDestination{block: block, path: path})
}
return destinations, nil
}

// blockFor picks the block a change belongs to.
func (r *blockResolver) blockFor(change resolvedChange) (sourceBlock, error) {
// A new element has no source of its own; it is placed relative to the
Expand Down
Loading
Loading