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
69 changes: 0 additions & 69 deletions bundle/deploy/lock/acquire.go

This file was deleted.

41 changes: 41 additions & 0 deletions bundle/deploy/lock/lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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. This factory exists so a future change can swap in alternative
// lock implementations without touching callers.
func NewDeploymentLock(b *bundle.Bundle, goal Goal) DeploymentLock {
return newWorkspaceFilesystemLock(b, 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"
)

// workspaceFilesystemLock implements DeploymentLock using a lock file in the
// bundle's workspace state path. This preserves the historical behavior of
// the previous lock.Acquire / lock.Release mutators.
type workspaceFilesystemLock struct {
b *bundle.Bundle
goal Goal
}

func newWorkspaceFilesystemLock(b *bundle.Bundle, goal Goal) *workspaceFilesystemLock {
return &workspaceFilesystemLock{b: b, goal: goal}
}

func (l *workspaceFilesystemLock) Acquire(ctx context.Context) error {
b := l.b

// Return early if locking is disabled.
if !b.Config.Bundle.Deployment.Lock.IsEnabled() {
log.Infof(ctx, "Skipping; locking is disabled")
return nil
}

user := b.Config.Workspace.CurrentUser.UserName
dir := b.Config.Workspace.StatePath
lk, err := locker.CreateLocker(user, dir, b.WorkspaceClient(ctx))
if err != nil {
return err
}

b.Locker = lk

force := b.Config.Bundle.Deployment.Lock.Force
log.Infof(ctx, "Acquiring deployment lock (force: %v)", force)
err = lk.Lock(ctx, 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, b, b.Config.Workspace.StatePath)
return diags.Error()
}

return err
}

return nil
}

func (l *workspaceFilesystemLock) Release(ctx context.Context, _ DeploymentStatus) error {
b := l.b

// Return early if locking is disabled.
if !b.Config.Bundle.Deployment.Lock.IsEnabled() {
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 b.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 b.Locker.Unlock(ctx, locker.AllowLockFileNotExist)
}
return b.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(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(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(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(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
Loading