Skip to content
Draft
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
3 changes: 2 additions & 1 deletion acceptance/bundle/deployment/bind/test.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Bind operations are not yet supported by the Deployment Metadata Service (DMS)
# These tests check the local state file after bind; with the Deployment Metadata Service (DMS) the
# bind is registered with the service instead, which bundle/dms/bind covers.
EnvMatrix.DMS = [""]
14 changes: 0 additions & 14 deletions acceptance/bundle/dms/bind-unbind-unsupported/script

This file was deleted.

18 changes: 18 additions & 0 deletions acceptance/bundle/dms/bind/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
bundle:
name: dms-bind-$UNIQUE_NAME
experimental:
deployment_history: true

resources:
schemas:
sc:
name: dms_bind_$UNIQUE_NAME
catalog_name: main
jobs:
foo:
name: foo
permissions:
- group_name: users
level: CAN_MANAGE
bar:
name: bar
2 changes: 2 additions & 0 deletions acceptance/bundle/dms/bind/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
EnvMatrix.DMS = ["true"]
120 changes: 120 additions & 0 deletions acceptance/bundle/dms/bind/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@

>>> [CLI] schemas create dms_bind_[UNIQUE_NAME] main
{
"full_name": "main.dms_bind_[UNIQUE_NAME]"
}

=== A missing resource cannot be bound
>>> musterr [CLI] bundle deployment bind sc main.missing_[UNIQUE_NAME] --auto-approve
Error: schema with an id 'main.missing_[UNIQUE_NAME]' is not found

=== Bind sc, the first bind: it creates the deployment and registers the schema in version 1
>>> [CLI] bundle deployment bind sc main.dms_bind_[UNIQUE_NAME]
Successfully bound schema with an id 'main.dms_bind_[UNIQUE_NAME]'
Run 'bundle deploy' to deploy changes to your workspace

>>> print_requests.py --dms --method POST //versions --nostamp
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions",
"q": {
"version_id": "1"
},
"body": {
"cli_version": "[CLI_VERSION]",
"operations": [
{
"action_type": "OPERATION_ACTION_TYPE_BIND",
"resource_key": "schemas.sc"
}
],
"version_type": "VERSION_TYPE_DEPLOY"
}
}
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/complete",
"body": {
"completion_reason": "VERSION_COMPLETE_SUCCESS"
}
}

=== foo's config differs from the existing job, so binding it asks for approval
=== Bind foo: version 2 registers only foo, and the job is left unchanged
>>> [CLI] bundle deployment bind foo [FOO_ID] --auto-approve
Successfully bound job with an id '[FOO_ID]'
Run 'bundle deploy' to deploy changes to your workspace

>>> print_requests.py --dms --method POST //versions --nostamp
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions",
"q": {
"version_id": "2"
},
"body": {
"cli_version": "[CLI_VERSION]",
"operations": [
{
"action_type": "OPERATION_ACTION_TYPE_BIND",
"resource_key": "jobs.foo"
}
],
"previous_version_id": "1",
"version_type": "VERSION_TYPE_DEPLOY"
}
}
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/complete",
"body": {
"completion_reason": "VERSION_COMPLETE_SUCCESS"
}
}

>>> [CLI] jobs get [FOO_ID]
{
"name": "bind-target-[UNIQUE_NAME]"
}

=== An already-bound resource cannot be rebound
>>> musterr [CLI] bundle deployment bind foo [FOO_ID] --auto-approve
Error: Resource already managed

The bundle is already managing resource for resources.jobs.foo and it is bound to the requested ID [FOO_ID].


=== The next deploy applies foo's config and permissions, and creates bar
>>> [CLI] bundle plan
create jobs.bar
update jobs.foo
create jobs.foo.permissions

Plan: 2 to add, 1 to change, 0 to delete, 1 unchanged

>>> [CLI] bundle deploy --auto-approve
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-[UNIQUE_NAME]/default/files...
Created jobs.bar
Created jobs.foo.permissions
Updated jobs.foo
Files: 4 uploaded, 0 deleted
Resources: 2 created, 1 changed, 0 deleted, 1 unchanged

>>> [CLI] jobs get [FOO_ID]
{
"name": "foo"
}

=== Test cleanup: the bound resources are now bundle-managed, so destroy removes them
>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.jobs.bar
delete resources.jobs.foo
delete resources.schemas.sc

This action will result in the deletion of the following UC schemas. Any underlying data may be lost:
delete resources.schemas.sc

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

Destroy: 3 deleted
37 changes: 37 additions & 0 deletions acceptance/bundle/dms/bind/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
envsubst < databricks.yml.tmpl > databricks.yml

# sc's config matches this existing schema exactly.
trace $CLI schemas create "dms_bind_$UNIQUE_NAME" main | jq '{full_name}'
# foo's config ("foo") differs from this existing job.
foo_id=$($CLI jobs create --json '{"name": "bind-target-'$UNIQUE_NAME'"}' | jq -r '.job_id')
add_repl "$foo_id" FOO_ID

cleanup() {
title "Test cleanup: the bound resources are now bundle-managed, so destroy removes them"
trace $CLI bundle destroy --auto-approve
}
trap cleanup EXIT

title "A missing resource cannot be bound"
trace musterr $CLI bundle deployment bind sc "main.missing_$UNIQUE_NAME" --auto-approve

title "Bind sc, the first bind: it creates the deployment and registers the schema in version 1"
trace $CLI bundle deployment bind sc "main.dms_bind_$UNIQUE_NAME"
trace print_requests.py --dms --method POST //versions --nostamp

title "foo's config differs from the existing job, so binding it asks for approval"
trace musterr $CLI bundle deployment bind foo "$foo_id" &> LOG.confirm
contains.py 'requires user confirmation' 'Plan: update resources.jobs.foo' < LOG.confirm > LOG.confirm.checked

title "Bind foo: version 2 registers only foo, and the job is left unchanged"
trace $CLI bundle deployment bind foo "$foo_id" --auto-approve
trace print_requests.py --dms --method POST //versions --nostamp
trace $CLI jobs get "$foo_id" | jq '{name: .settings.name}'

title "An already-bound resource cannot be rebound"
trace musterr $CLI bundle deployment bind foo "$foo_id" --auto-approve

title "The next deploy applies foo's config and permissions, and creates bar"
trace $CLI bundle plan
trace $CLI bundle deploy --auto-approve
trace $CLI jobs get "$foo_id" | jq '{name: .settings.name}'
9 changes: 9 additions & 0 deletions acceptance/bundle/dms/record-op-fault/databricks.yml.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
bundle:
name: dms-record-op-fault-$UNIQUE_NAME
experimental:
deployment_history: true

resources:
jobs:
foo:
name: foo
71 changes: 71 additions & 0 deletions acceptance/bundle/dms/record-op-fault/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

=== The service rejects the operation upload, so registering the bind fails
>>> fault.py PATCH /api/2.0/bundle/* 500 0 1

>>> musterr [CLI] bundle deployment bind foo [JOB_ID] --auto-approve
Error: recording operation for resources.jobs.foo: Fault injected by test. (500 INJECTED)

Endpoint: PATCH [DATABRICKS_URL]/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/operations/jobs.foo?update_mask=state%2Cerror_message%2Cresource_id%2Cstatus
HTTP Status: 500 Internal Server Error
API error_code: INJECTED
API message: Fault injected by test.


=== The version is still completed - with failure - rather than left holding its lease
>>> print_requests.py --dms --method POST //versions
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions",
"q": {
"version_id": "1"
},
"body": {
"cli_version": "[CLI_VERSION]",
"operations": [
{
"action_type": "OPERATION_ACTION_TYPE_BIND",
"resource_key": "jobs.foo"
}
],
"version_type": "VERSION_TYPE_DEPLOY"
}
}
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/1/complete",
"body": {
"completion_reason": "VERSION_COMPLETE_FAILURE"
}
}

=== Retrying the bind succeeds
>>> [CLI] bundle deployment bind foo [JOB_ID] --auto-approve
Successfully bound job with an id '[JOB_ID]'
Run 'bundle deploy' to deploy changes to your workspace

>>> print_requests.py --dms --method POST //versions
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions",
"q": {
"version_id": "2"
},
"body": {
"cli_version": "[CLI_VERSION]",
"operations": [
{
"action_type": "OPERATION_ACTION_TYPE_BIND",
"resource_key": "jobs.foo"
}
],
"previous_version_id": "1",
"version_type": "VERSION_TYPE_DEPLOY"
}
}
{
"method": "POST",
"path": "/api/2.0/bundle/deployments/[DEPLOYMENT_ID]/versions/2/complete",
"body": {
"completion_reason": "VERSION_COMPLETE_SUCCESS"
}
}
15 changes: 15 additions & 0 deletions acceptance/bundle/dms/record-op-fault/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
envsubst < databricks.yml.tmpl > databricks.yml

job_id=$($CLI jobs create --json '{"name": "target-'$UNIQUE_NAME'"}' | jq -r '.job_id')
add_repl "$job_id" JOB_ID

title "The service rejects the operation upload, so registering the bind fails"
trace fault.py "PATCH /api/2.0/bundle/*" 500 0 1
trace musterr $CLI bundle deployment bind foo "$job_id" --auto-approve

title "The version is still completed - with failure - rather than left holding its lease"
trace print_requests.py --dms --method POST //versions

title "Retrying the bind succeeds"
trace $CLI bundle deployment bind foo "$job_id" --auto-approve
trace print_requests.py --dms --method POST //versions
3 changes: 3 additions & 0 deletions acceptance/bundle/dms/record-op-fault/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Local only: the case needs an injected failure (DMS must reject the operation upload once), and
# fault.py registers its rule on the fake, which a cloud run does not have.
Cloud = false
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
bundle:
name: dms-bind-unbind-$UNIQUE_NAME
name: dms-unbind-$UNIQUE_NAME
experimental:
deployment_history: true

Expand Down
2 changes: 2 additions & 0 deletions acceptance/bundle/dms/unbind-unsupported/out.test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = false
EnvMatrix.DMS = ["true"]
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@

=== bind is refused while recording deployment history
>>> musterr [CLI] bundle deployment bind foo [JOB_ID]
Error: bind is not supported for a bundle target that records deployment history


=== Deploy so the bundle records state, then unbind is refused too
=== Deploy so the bundle records state, then unbind is refused
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-bind-unbind-[UNIQUE_NAME]/default/files...
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/dms-unbind-[UNIQUE_NAME]/default/files...
Created jobs.foo
Files: 2 uploaded, 0 deleted
Resources: 1 created, 0 changed, 0 deleted, 0 unchanged
Expand Down
6 changes: 6 additions & 0 deletions acceptance/bundle/dms/unbind-unsupported/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
envsubst < databricks.yml.tmpl > databricks.yml

# unbind refuses when it opens an existing recording state, so it needs a deployment first.
title "Deploy so the bundle records state, then unbind is refused"
trace $CLI bundle deploy
trace musterr $CLI bundle deployment unbind foo
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
RecordRequests = false
# bind/unbind refusal is a client-side gate on the recording config; it needs no real
# The unbind refusal is a client-side gate on the recording config; it needs no real
# workspace, so this stays local-only rather than inheriting the suite's cloud run.
Cloud = false
25 changes: 20 additions & 5 deletions bundle/phases/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"maps"
"os"
"slices"

"github.com/databricks/cli/bundle"
Expand Down Expand Up @@ -47,15 +48,23 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *BindOptions, stateDesc *s
return
}

if stateDesc.IsDMS() {
logdiag.LogError(ctx, errors.New("bind is not supported for a bundle target that records deployment history"))
return
}

// Direct engine: import into temp state, run plan, check for changes.
resourceKey := fmt.Sprintf("resources.%s.%s", opts.Group, opts.ResourceKey)
_, statePath := b.StateFilenameDirect(ctx)

// A recorded deployment keeps its resources in the metadata service, so the bind plans against
// a local copy of them and registers the result with the service instead of the state file.
var recorded recordedDeployment
if stateDesc.IsDMS() {
var err error
recorded, statePath, err = snapshotRecordedState(ctx, b)
if err != nil {
logdiag.LogError(ctx, err)
return
}
defer os.Remove(statePath)
}

result, err := b.DeploymentBundle.Bind(ctx, b.WorkspaceClient(ctx), &b.Config, statePath, resourceKey, opts.ResourceId)
if err != nil {
logdiag.LogError(ctx, err)
Expand Down Expand Up @@ -100,6 +109,12 @@ func Bind(ctx context.Context, b *bundle.Bundle, opts *BindOptions, stateDesc *s
}
}

if stateDesc.IsDMS() {
recordBind(ctx, b, recorded, result.TempStatePath, resourceKey)
result.Cancel()
return
}

// Finalize: rename temp state to final location
err = result.Finalize()
if err != nil {
Expand Down
Loading
Loading