Skip to content
Open
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
1 change: 1 addition & 0 deletions .nextchanges/bundles/bind-unreadable-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix `bundle deployment bind` silently binding over a deployment state it could not read, which could take over a resource that was already managed by the bundle.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle:
name: test-bundle

resources:
jobs:
job_1:
name: Job 1

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

21 changes: 21 additions & 0 deletions acceptance/bundle/deployment/bind/job/unreadable-state/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

=== Deploy job_1
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files...
Created jobs.job_1
Files: 5 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged

=== Leave behind a WAL from a different lineage, as a crashed deploy would
=== Bind must refuse: the state cannot be read, so we cannot tell if job_1 is already managed

>>> errcode [CLI] bundle deployment bind job_1 [EXTERNAL_JOB_ID] --auto-approve
Error: cannot check whether resources.jobs.job_1 is already bound: reading state from [TEST_TMP_DIR]/.databricks/bundle/default/resources.json: WAL recovery failed: WAL lineage ("wal-lineage-bbb") does not match state lineage ("[UUID]")


Exit code: 1

=== Nothing was bound: the WAL is still there and no bind state was written
>>> assert_exists.py .databricks/bundle/default/resources.json.wal

>>> assert_not_exists.py .databricks/bundle/default/resources.json.temp-bind
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{"lineage":"wal-lineage-bbb","serial":2}
{"k":"resources.jobs.job_1","v":{"__id__":"1001","state":{"name":"Job 1"}}}
14 changes: 14 additions & 0 deletions acceptance/bundle/deployment/bind/job/unreadable-state/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
title "Deploy job_1"
trace $CLI bundle deploy

title "Leave behind a WAL from a different lineage, as a crashed deploy would"
cp resources.json.wal .databricks/bundle/default/

title "Bind must refuse: the state cannot be read, so we cannot tell if job_1 is already managed\n"
job_id=$($CLI jobs create --json '{"name": "External Job"}' | jq -r '.job_id')
add_repl "$job_id" EXTERNAL_JOB_ID
trace errcode $CLI bundle deployment bind job_1 "$job_id" --auto-approve

title "Nothing was bound: the WAL is still there and no bind state was written"
trace assert_exists.py .databricks/bundle/default/resources.json.wal
trace assert_not_exists.py .databricks/bundle/default/resources.json.temp-bind
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Cloud = false

Ignore = ["resources.json.wal"]

EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
27 changes: 16 additions & 11 deletions bundle/direct/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,22 @@ type BindResult struct {
func (b *DeploymentBundle) Bind(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root, statePath, resourceKey, resourceID string) (*BindResult, error) {
// Check if the resource is already managed (bound to a different ID)
var checkStateDB dstate.DeploymentState
if err := checkStateDB.Open(ctx, statePath, dstate.WithRecovery(true), dstate.WithWrite(false)); err == nil {
existingID := checkStateDB.GetResourceID(resourceKey)
if _, err := checkStateDB.Finalize(ctx); err != nil {
log.Warnf(ctx, "failed to finalize state: %v", err)
}
if existingID != "" {
return nil, ErrResourceAlreadyBound{
ResourceKey: resourceKey,
ExistingID: existingID,
NewID: resourceID,
}
if err := checkStateDB.Open(ctx, statePath, dstate.WithRecovery(true), dstate.WithWrite(false)); err != nil {
// State that cannot be read is not the same as state without a binding:
// the resource may well be managed already, and binding on top of it
// would take over a resource whose ownership was never checked. A state
// file that does not exist yet opens successfully as an empty one.
return nil, fmt.Errorf("cannot check whether %s is already bound: %w", resourceKey, err)
}
existingID := checkStateDB.GetResourceID(resourceKey)
if _, err := checkStateDB.Finalize(ctx); err != nil {
log.Warnf(ctx, "failed to finalize state: %v", err)
}
if existingID != "" {
return nil, ErrResourceAlreadyBound{
ResourceKey: resourceKey,
ExistingID: existingID,
NewID: resourceID,
}
}

Expand Down
Loading