diff --git a/acceptance/bundle/deployment/bind/test.toml b/acceptance/bundle/deployment/bind/test.toml index b693e4ded26..a422bb529ea 100644 --- a/acceptance/bundle/deployment/bind/test.toml +++ b/acceptance/bundle/deployment/bind/test.toml @@ -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 = [""] diff --git a/acceptance/bundle/dms/bind-unbind-unsupported/script b/acceptance/bundle/dms/bind-unbind-unsupported/script deleted file mode 100644 index 03d9cf34a31..00000000000 --- a/acceptance/bundle/dms/bind-unbind-unsupported/script +++ /dev/null @@ -1,14 +0,0 @@ -envsubst < databricks.yml.tmpl > databricks.yml - -# The gate that refuses bind while recording deployment history is on the config, so it fires -# before any deploy. Create a real workspace job so bind gets past fetching the resource and -# reaches the gate, instead of failing because the resource does not exist. -title "bind is refused while recording deployment history" -job_id=$($CLI jobs create --json '{"name": "bind-target-'$UNIQUE_NAME'"}' | jq -r '.job_id') -add_repl "$job_id" JOB_ID -trace musterr $CLI bundle deployment bind foo "$job_id" - -# 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 too" -trace $CLI bundle deploy -trace musterr $CLI bundle deployment unbind foo diff --git a/acceptance/bundle/dms/bind/databricks.yml.tmpl b/acceptance/bundle/dms/bind/databricks.yml.tmpl new file mode 100644 index 00000000000..e5b40a1ed15 --- /dev/null +++ b/acceptance/bundle/dms/bind/databricks.yml.tmpl @@ -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 diff --git a/acceptance/bundle/dms/bind/out.test.toml b/acceptance/bundle/dms/bind/out.test.toml new file mode 100644 index 00000000000..84b22ba6735 --- /dev/null +++ b/acceptance/bundle/dms/bind/out.test.toml @@ -0,0 +1,2 @@ +Cloud = true +EnvMatrix.DMS = ["true"] diff --git a/acceptance/bundle/dms/bind/output.txt b/acceptance/bundle/dms/bind/output.txt new file mode 100644 index 00000000000..b26e9964bf6 --- /dev/null +++ b/acceptance/bundle/dms/bind/output.txt @@ -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 diff --git a/acceptance/bundle/dms/bind/script b/acceptance/bundle/dms/bind/script new file mode 100644 index 00000000000..bfec1c337c4 --- /dev/null +++ b/acceptance/bundle/dms/bind/script @@ -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}' diff --git a/acceptance/bundle/dms/record-op-fault/databricks.yml.tmpl b/acceptance/bundle/dms/record-op-fault/databricks.yml.tmpl new file mode 100644 index 00000000000..235760efa24 --- /dev/null +++ b/acceptance/bundle/dms/record-op-fault/databricks.yml.tmpl @@ -0,0 +1,9 @@ +bundle: + name: dms-record-op-fault-$UNIQUE_NAME +experimental: + deployment_history: true + +resources: + jobs: + foo: + name: foo diff --git a/acceptance/bundle/dms/bind-unbind-unsupported/out.test.toml b/acceptance/bundle/dms/record-op-fault/out.test.toml similarity index 100% rename from acceptance/bundle/dms/bind-unbind-unsupported/out.test.toml rename to acceptance/bundle/dms/record-op-fault/out.test.toml diff --git a/acceptance/bundle/dms/record-op-fault/output.txt b/acceptance/bundle/dms/record-op-fault/output.txt new file mode 100644 index 00000000000..b6cadc04a66 --- /dev/null +++ b/acceptance/bundle/dms/record-op-fault/output.txt @@ -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" + } +} diff --git a/acceptance/bundle/dms/record-op-fault/script b/acceptance/bundle/dms/record-op-fault/script new file mode 100644 index 00000000000..6bca414104c --- /dev/null +++ b/acceptance/bundle/dms/record-op-fault/script @@ -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 diff --git a/acceptance/bundle/dms/record-op-fault/test.toml b/acceptance/bundle/dms/record-op-fault/test.toml new file mode 100644 index 00000000000..24f1970a8f2 --- /dev/null +++ b/acceptance/bundle/dms/record-op-fault/test.toml @@ -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 diff --git a/acceptance/bundle/dms/bind-unbind-unsupported/databricks.yml.tmpl b/acceptance/bundle/dms/unbind-unsupported/databricks.yml.tmpl similarity index 71% rename from acceptance/bundle/dms/bind-unbind-unsupported/databricks.yml.tmpl rename to acceptance/bundle/dms/unbind-unsupported/databricks.yml.tmpl index e60f4605610..6ba28905b76 100644 --- a/acceptance/bundle/dms/bind-unbind-unsupported/databricks.yml.tmpl +++ b/acceptance/bundle/dms/unbind-unsupported/databricks.yml.tmpl @@ -1,5 +1,5 @@ bundle: - name: dms-bind-unbind-$UNIQUE_NAME + name: dms-unbind-$UNIQUE_NAME experimental: deployment_history: true diff --git a/acceptance/bundle/dms/unbind-unsupported/out.test.toml b/acceptance/bundle/dms/unbind-unsupported/out.test.toml new file mode 100644 index 00000000000..88e6fbd847c --- /dev/null +++ b/acceptance/bundle/dms/unbind-unsupported/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DMS = ["true"] diff --git a/acceptance/bundle/dms/bind-unbind-unsupported/output.txt b/acceptance/bundle/dms/unbind-unsupported/output.txt similarity index 51% rename from acceptance/bundle/dms/bind-unbind-unsupported/output.txt rename to acceptance/bundle/dms/unbind-unsupported/output.txt index d2d0a366778..a1391c4aa08 100644 --- a/acceptance/bundle/dms/bind-unbind-unsupported/output.txt +++ b/acceptance/bundle/dms/unbind-unsupported/output.txt @@ -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 diff --git a/acceptance/bundle/dms/unbind-unsupported/script b/acceptance/bundle/dms/unbind-unsupported/script new file mode 100644 index 00000000000..d72aafe94b5 --- /dev/null +++ b/acceptance/bundle/dms/unbind-unsupported/script @@ -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 diff --git a/acceptance/bundle/dms/bind-unbind-unsupported/test.toml b/acceptance/bundle/dms/unbind-unsupported/test.toml similarity index 58% rename from acceptance/bundle/dms/bind-unbind-unsupported/test.toml rename to acceptance/bundle/dms/unbind-unsupported/test.toml index 140ee938238..267d07f61de 100644 --- a/acceptance/bundle/dms/bind-unbind-unsupported/test.toml +++ b/acceptance/bundle/dms/unbind-unsupported/test.toml @@ -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 diff --git a/bundle/phases/bind.go b/bundle/phases/bind.go index d8ff6aeaf3e..03296e3c8fb 100644 --- a/bundle/phases/bind.go +++ b/bundle/phases/bind.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "maps" + "os" "slices" "github.com/databricks/cli/bundle" @@ -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) @@ -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 { diff --git a/bundle/phases/bind_dms.go b/bundle/phases/bind_dms.go new file mode 100644 index 00000000000..21a4e7e7ad7 --- /dev/null +++ b/bundle/phases/bind_dms.go @@ -0,0 +1,129 @@ +package phases + +import ( + "context" + "fmt" + "maps" + "strings" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/direct/dstate" + "github.com/databricks/cli/bundle/statemgmt" + "github.com/databricks/cli/libs/cmdctx" + "github.com/databricks/cli/libs/dms" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/databricks-sdk-go/service/bundledeployments" +) + +// recordedDeployment identifies the deployment a bind registers with, as read before the bind. +type recordedDeployment struct { + deploymentID string + lastVersion int +} + +// snapshotRecordedState writes the resources the service records to a plain local state file, so +// the bind can plan against them as it does against a state file. It closes the recorded state, +// which the caller opened for read. The deployment ID is empty when nothing is recorded yet. +func snapshotRecordedState(ctx context.Context, b *bundle.Bundle) (recordedDeployment, string, error) { + db := &b.DeploymentBundle.StateDB + recorded := recordedDeployment{deploymentID: db.DeploymentID, lastVersion: db.VersionID} + data := dstate.NewDatabase(db.Data.Lineage, db.Data.Serial) + maps.Copy(data.State, db.Data.State) + if _, err := db.Finalize(ctx); err != nil { + return recordedDeployment{}, "", err + } + + _, localPath := b.StateFilenameDirect(ctx) + snapshotPath := localPath + ".bind-snapshot" + var snapshot dstate.DeploymentState + snapshot.OpenWithData(snapshotPath, data) + if err := snapshot.Persist(); err != nil { + return recordedDeployment{}, "", err + } + _, err := snapshot.Finalize(ctx) + return recorded, snapshotPath, err +} + +// recordBind registers the bound resource with the service: in a new version it records one bind +// operation carrying the state the bind resolved, creating the deployment on a first bind. +func recordBind(ctx context.Context, b *bundle.Bundle, recorded recordedDeployment, resolvedPath, resourceKey string) { + entry, err := readResolvedEntry(ctx, resolvedPath, resourceKey) + if err != nil { + logdiag.LogError(ctx, err) + return + } + + // The operation buffer records each state write through the workspace client on the context. + w := b.WorkspaceClient(ctx) + if !cmdctx.HasWorkspaceClient(ctx) { + ctx = cmdctx.SetWorkspaceClient(ctx, w) + } + + var current *bundledeployments.Deployment + if recorded.deploymentID != "" { + current, err = w.BundleDeployments.GetDeployment(ctx, bundledeployments.GetDeploymentRequest{Name: dms.DeploymentName(recorded.deploymentID)}) + if err != nil { + logdiag.LogError(ctx, err) + return + } + } + + db := &b.DeploymentBundle.StateDB + _, localPath := b.StateFilenameDirect(ctx) + err = db.Open(ctx, localPath, dstate.WithRecovery(false), dstate.WithWrite(false), dstate.WithDeploymentHistory(true), dstate.OpenDmsArgs{DeploymentID: recorded.deploymentID, LastVersionID: recorded.lastVersion}) + if err != nil { + logdiag.LogError(ctx, err) + return + } + // Closes the state on an early return; a no-op once it is finalized below. + defer func() { + if _, err := db.Finalize(ctx); err != nil { + logdiag.LogError(ctx, err) + } + }() + + createOrUpdateDeployment(ctx, b, current) + if logdiag.HasError(ctx) { + return + } + if err := db.UpgradeToWrite(); err != nil { + logdiag.LogError(ctx, err) + return + } + staged := []bundledeployments.StagedOperation{{ + ResourceKey: strings.TrimPrefix(resourceKey, dms.StatePrefix), + ActionType: bundledeployments.OperationActionTypeOperationActionTypeBind, + }} + if err := startVersion(ctx, b, dms.VersionTypeDeploy, staged); err != nil { + logdiag.LogError(ctx, err) + return + } + + if err := db.SaveState(ctx, resourceKey, entry.ID, entry.State, entry.DependsOn); err != nil { + logdiag.LogError(ctx, err) + } + if _, err := db.Finalize(ctx); err != nil { + logdiag.LogError(ctx, err) + } + // As in deploy, push the state even after a failure, then close the version out. + statemgmt.PushResourcesState(ctx, b) + if _, err := db.CompleteVersion(ctx, !logdiag.HasError(ctx)); err != nil { + logdiag.LogError(ctx, err) + } +} + +// readResolvedEntry reads the state the bind resolved for resourceKey from its temporary state file. +func readResolvedEntry(ctx context.Context, path, resourceKey string) (dstate.ResourceEntry, error) { + var resolved dstate.DeploymentState + if err := resolved.Open(ctx, path, dstate.WithRecovery(true), dstate.WithWrite(false), dstate.WithDeploymentHistory(false), dstate.OpenDmsArgs{}); err != nil { + return dstate.ResourceEntry{}, err + } + entry, ok := resolved.GetResourceEntry(resourceKey) + if _, err := resolved.Finalize(ctx); err != nil { + return dstate.ResourceEntry{}, err + } + if !ok { + return dstate.ResourceEntry{}, fmt.Errorf("internal error: bind resolved no state for %s", resourceKey) + } + return entry, nil +} diff --git a/cmd/apps/import.go b/cmd/apps/import.go index 355c51c690e..d862d3b3733 100644 --- a/cmd/apps/import.go +++ b/cmd/apps/import.go @@ -330,6 +330,13 @@ func runImport(ctx context.Context, w *databricks.WorkspaceClient, appName, outp return fmt.Errorf("app '%s' no longer exists in workspace", app.Name) } + // A recorded deployment's resources are read from the metadata service. + if stateDesc.IsDMS() { + if err := bundleutils.OpenDirectStateForRead(ctx, b, stateDesc); err != nil { + return err + } + } + // Bind the resource phases.Bind(ctx, b, &phases.BindOptions{ AutoApprove: true, diff --git a/cmd/bundle/deployment/bind_resource.go b/cmd/bundle/deployment/bind_resource.go index ef38c972362..659257e968b 100644 --- a/cmd/bundle/deployment/bind_resource.go +++ b/cmd/bundle/deployment/bind_resource.go @@ -42,6 +42,13 @@ func BindResource(cmd *cobra.Command, resourceKey, resourceId string, autoApprov return fmt.Errorf("%s with an id '%s' is not found", resource.ResourceDescription().SingularName, resourceId) } + // A recorded deployment's resources are read from the metadata service. + if stateDesc.IsDMS() { + if err := utils.OpenDirectStateForRead(ctx, b, stateDesc); err != nil { + return err + } + } + phases.Bind(ctx, b, &phases.BindOptions{ AutoApprove: autoApprove, Group: resource.ResourceDescription().PluralName,