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
4 changes: 0 additions & 4 deletions bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/cache"
"github.com/databricks/cli/libs/fileset"
"github.com/databricks/cli/libs/locker"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/logdiag"
libsync "github.com/databricks/cli/libs/sync"
Expand Down Expand Up @@ -129,9 +128,6 @@ type Bundle struct {
// Stores an initialized copy of this bundle's Terraform wrapper.
Terraform *tfexec.Terraform

// Stores the locker responsible for acquiring/releasing a deployment lock.
Locker *locker.Locker

// TerraformPlanPath is the path to the plan from the terraform CLI
TerraformPlanPath string

Expand Down
69 changes: 0 additions & 69 deletions bundle/deploy/lock/acquire.go

This file was deleted.

49 changes: 49 additions & 0 deletions bundle/deploy/lock/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package lock

import (
"context"

"github.com/databricks/cli/bundle"
)

// Goal describes the purpose of a deployment operation.
type Goal string

const (
GoalBind = Goal("bind")
GoalUnbind = Goal("unbind")
GoalDeploy = Goal("deploy")
GoalDestroy = Goal("destroy")
)

// DeploymentStatus indicates whether the deployment operation succeeded or failed.
type DeploymentStatus int

const (
DeploymentSuccess DeploymentStatus = iota
DeploymentFailure
)

// DeploymentLock manages the deployment lock lifecycle.
type DeploymentLock interface {
// Acquire acquires the deployment lock.
Acquire(ctx context.Context) error

// Release releases the deployment lock with the given deployment status.
Release(ctx context.Context, status DeploymentStatus) error
}

// NewDeploymentLock returns a DeploymentLock backed by the workspace
// filesystem. Captures everything the lock needs from the bundle at
// construction time.
func NewDeploymentLock(ctx context.Context, b *bundle.Bundle, goal Goal) DeploymentLock {
return &workspaceFilesystemLock{
client: b.WorkspaceClient(ctx),
user: b.Config.Workspace.CurrentUser.UserName,
statePath: b.Config.Workspace.StatePath,
enabled: b.Config.Bundle.Deployment.Lock.IsEnabled(),
force: b.Config.Bundle.Deployment.Lock.Force,
b: b,
goal: goal,
}
}
58 changes: 0 additions & 58 deletions bundle/deploy/lock/release.go

This file was deleted.

84 changes: 84 additions & 0 deletions bundle/deploy/lock/workspace_filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package lock

import (
"context"
"errors"
"io/fs"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/permissions"
"github.com/databricks/cli/libs/locker"
"github.com/databricks/cli/libs/log"
"github.com/databricks/databricks-sdk-go"
)

// workspaceFilesystemLock implements DeploymentLock using a lock file in the
// bundle's workspace state path. Holds only the primitives it needs from the
// bundle plus a reference for the permission-error path.
type workspaceFilesystemLock struct {
client *databricks.WorkspaceClient
user string
statePath string
enabled bool
force bool

// b is retained only for permissions.ReportPossiblePermissionDenied on
// the Acquire error path.
b *bundle.Bundle

locker *locker.Locker
goal Goal
}

func (l *workspaceFilesystemLock) Acquire(ctx context.Context) error {
// Return early if locking is disabled.
if !l.enabled {
log.Infof(ctx, "Skipping; locking is disabled")
return nil
}

lk, err := locker.CreateLocker(l.user, l.statePath, l.client)
if err != nil {
return err
}

l.locker = lk

log.Infof(ctx, "Acquiring deployment lock (force: %v)", l.force)
err = lk.Lock(ctx, l.force)
if err != nil {
log.Errorf(ctx, "Failed to acquire deployment lock: %v", err)

// If we get a permission or "doesn't exist" error from the API this
// indicates we either don't have permissions or the path is invalid.
if errors.Is(err, fs.ErrPermission) || errors.Is(err, fs.ErrNotExist) {
diags := permissions.ReportPossiblePermissionDenied(ctx, l.b, l.statePath)
return diags.Error()
}

return err
}

return nil
}

func (l *workspaceFilesystemLock) Release(ctx context.Context, _ DeploymentStatus) error {
// Return early if locking is disabled.
if !l.enabled {
log.Infof(ctx, "Skipping; locking is disabled")
return nil
}

// Return early if the locker is not set.
// It is likely an error occurred prior to initialization of the locker instance.
if l.locker == nil {
log.Warnf(ctx, "Unable to release lock if locker is not configured")
return nil
}

log.Infof(ctx, "Releasing deployment lock")
if l.goal == GoalDestroy {
return l.locker.Unlock(ctx, locker.AllowLockFileNotExist)
}
return l.locker.Unlock(ctx)
}
26 changes: 20 additions & 6 deletions bundle/phases/bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ import (
func Bind(ctx context.Context, b *bundle.Bundle, opts *terraform.BindOptions, engine engine.EngineType) {
log.Info(ctx, "Phase: bind")

bundle.ApplyContext(ctx, b, lock.Acquire())
if logdiag.HasError(ctx) {
dl := lock.NewDeploymentLock(ctx, b, lock.GoalBind)
if err := dl.Acquire(ctx); err != nil {
logdiag.LogError(ctx, err)
return
}

defer func() {
bundle.ApplyContext(ctx, b, lock.Release(lock.GoalBind))
status := lock.DeploymentSuccess
if logdiag.HasError(ctx) {
status = lock.DeploymentFailure
}
if err := dl.Release(ctx, status); err != nil {
log.Warnf(ctx, "Failed to release deployment lock: %v", err)
}
}()

if engine.IsDirect() {
Expand Down Expand Up @@ -119,13 +126,20 @@ func jsonDump(ctx context.Context, v any, field string) string {
func Unbind(ctx context.Context, b *bundle.Bundle, bundleType, tfResourceType, resourceKey string, engine engine.EngineType) {
log.Info(ctx, "Phase: unbind")

bundle.ApplyContext(ctx, b, lock.Acquire())
if logdiag.HasError(ctx) {
dl := lock.NewDeploymentLock(ctx, b, lock.GoalUnbind)
if err := dl.Acquire(ctx); err != nil {
logdiag.LogError(ctx, err)
return
}

defer func() {
bundle.ApplyContext(ctx, b, lock.Release(lock.GoalUnbind))
status := lock.DeploymentSuccess
if logdiag.HasError(ctx) {
status = lock.DeploymentFailure
}
if err := dl.Release(ctx, status); err != nil {
log.Warnf(ctx, "Failed to release deployment lock: %v", err)
}
}()

if engine.IsDirect() {
Expand Down
20 changes: 14 additions & 6 deletions bundle/phases/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,27 @@ func Deploy(ctx context.Context, b *bundle.Bundle, outputHandler sync.OutputHand

// Core mutators that CRUD resources and modify deployment state. These
// mutators need informed consent if they are potentially destructive.
bundle.ApplySeqContext(ctx, b,
scripts.Execute(config.ScriptPreDeploy),
lock.Acquire(),
)

bundle.ApplyContext(ctx, b, scripts.Execute(config.ScriptPreDeploy))
if logdiag.HasError(ctx) {
// lock is not acquired here
return
}

dl := lock.NewDeploymentLock(ctx, b, lock.GoalDeploy)
if err := dl.Acquire(ctx); err != nil {
logdiag.LogError(ctx, err)
return
}

// lock is acquired here
defer func() {
bundle.ApplyContext(ctx, b, lock.Release(lock.GoalDeploy))
status := lock.DeploymentSuccess
if logdiag.HasError(ctx) {
status = lock.DeploymentFailure
}
if err := dl.Release(ctx, status); err != nil {
log.Warnf(ctx, "Failed to release deployment lock: %v", err)
}
}()

uploadLibraries(ctx, b, libs)
Expand Down
13 changes: 10 additions & 3 deletions bundle/phases/destroy.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,20 @@ func Destroy(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) {
return
}

bundle.ApplyContext(ctx, b, lock.Acquire())
if logdiag.HasError(ctx) {
dl := lock.NewDeploymentLock(ctx, b, lock.GoalDestroy)
if err := dl.Acquire(ctx); err != nil {
logdiag.LogError(ctx, err)
return
}

defer func() {
bundle.ApplyContext(ctx, b, lock.Release(lock.GoalDestroy))
status := lock.DeploymentSuccess
if logdiag.HasError(ctx) {
status = lock.DeploymentFailure
}
if err := dl.Release(ctx, status); err != nil {
log.Warnf(ctx, "Failed to release deployment lock: %v", err)
}
}()

if !engine.IsDirect() {
Expand Down