Skip to content

Commit 2bddd05

Browse files
authored
direct: handle remotely deleted resources (#3710)
## Changes - When planning resource deletion, skip resource that were already deleted. - When DoDelete returns 'resource not found', consider it a success and continue. - Fix metadata computation not to error on resources that are deleted. - Combine CalculatePlanFor{Deploy,Destroy} into one function. ## Why Better plan, matches terraform. ## Tests New acceptance test that remotely delete a job and test deploy/destroy.
1 parent c112faf commit 2bddd05

File tree

8 files changed

+91
-78
lines changed

8 files changed

+91
-78
lines changed

bundle/deploy/metadata/compute.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/databricks/cli/bundle/config"
99
"github.com/databricks/cli/bundle/metadata"
1010
"github.com/databricks/cli/libs/diag"
11+
"github.com/databricks/cli/libs/log"
1112
)
1213

1314
type compute struct{}
@@ -20,7 +21,7 @@ func (m *compute) Name() string {
2021
return "metadata.Compute"
2122
}
2223

23-
func (m *compute) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
24+
func (m *compute) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
2425
b.Metadata = metadata.Metadata{
2526
Version: metadata.Version,
2627
Config: metadata.Config{},
@@ -40,10 +41,17 @@ func (m *compute) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
4041
// Compute config file path the job is defined in, relative to the bundle
4142
// root
4243
l := b.Config.GetLocation("resources.jobs." + name)
44+
if l.File == "" {
45+
// b.Config.Resources.Jobs may include a job that only exists in state but not in config
46+
continue
47+
}
48+
4349
relativePath, err := filepath.Rel(b.BundleRootPath, l.File)
4450
if err != nil {
45-
return diag.Errorf("failed to compute relative path for job %s: %v", name, err)
51+
log.Warnf(ctx, "failed to compute relative path for job %q: %s", name, err)
52+
relativePath = ""
4653
}
54+
4755
// Metadata for the job
4856
jobsMetadata[name] = &metadata.Resource{
4957
ID: job.ID,

bundle/deployplan/plan.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,9 @@ func (p *Plan) UnlockEntry(resourceKey string) {
9797
defer p.mutex.Unlock()
9898
p.locks[resourceKey] = false
9999
}
100+
101+
func (p *Plan) RemoveEntry(resourceKey string) {
102+
p.mutex.Lock()
103+
defer p.mutex.Unlock()
104+
delete(p.Plan, resourceKey)
105+
}

bundle/direct/apply.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func (d *DeploymentUnit) Create(ctx context.Context, db *dstate.DeploymentState,
9393

9494
func (d *DeploymentUnit) Recreate(ctx context.Context, db *dstate.DeploymentState, oldID string, newState any) error {
9595
err := d.Adapter.DoDelete(ctx, oldID)
96-
if err != nil {
96+
if err != nil && !isResourceGone(err) {
9797
return fmt.Errorf("deleting old id=%s: %w", oldID, err)
9898
}
9999

@@ -172,9 +172,8 @@ func (d *DeploymentUnit) UpdateWithID(ctx context.Context, db *dstate.Deployment
172172
}
173173

174174
func (d *DeploymentUnit) Delete(ctx context.Context, db *dstate.DeploymentState, oldID string) error {
175-
// TODO: recognize 404 and 403 as "deleted" and proceed to removing state
176175
err := d.Adapter.DoDelete(ctx, oldID)
177-
if err != nil {
176+
if err != nil && !isResourceGone(err) {
178177
return fmt.Errorf("deleting id=%s: %w", oldID, err)
179178
}
180179

bundle/direct/bundle_apply.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func (b *DeploymentBundle) LookupReferenceRemote(ctx context.Context, path *stru
161161
return structaccess.Get(remoteState, fieldPath)
162162
}
163163

164-
func jsonDump(obj map[string]string) string {
164+
func jsonDump(obj any) string {
165165
bytes, err := json.MarshalIndent(obj, "", " ")
166166
if err != nil {
167167
return err.Error()

bundle/direct/bundle_plan.go

Lines changed: 59 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"maps"
78
"reflect"
89
"slices"
910
"strings"
1011

1112
"github.com/databricks/cli/bundle/config"
1213
"github.com/databricks/cli/bundle/deployplan"
1314
"github.com/databricks/cli/bundle/direct/dresources"
15+
"github.com/databricks/cli/bundle/direct/dstate"
1416
"github.com/databricks/cli/libs/dyn"
1517
"github.com/databricks/cli/libs/dyn/dynvar"
1618
"github.com/databricks/cli/libs/log"
@@ -21,7 +23,6 @@ import (
2123
"github.com/databricks/cli/libs/structs/structvar"
2224
"github.com/databricks/cli/libs/utils"
2325
"github.com/databricks/databricks-sdk-go"
24-
"github.com/databricks/databricks-sdk-go/apierr"
2526
)
2627

2728
var errDelayed = errors.New("must be resolved after apply")
@@ -43,14 +44,14 @@ func (b *DeploymentBundle) Init(client *databricks.WorkspaceClient) error {
4344
return err
4445
}
4546

46-
func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) {
47+
func (b *DeploymentBundle) CalculatePlan(ctx context.Context, client *databricks.WorkspaceClient, configRoot *config.Root) (*deployplan.Plan, error) {
4748
b.StateDB.AssertOpened()
4849
err := b.Init(client)
4950
if err != nil {
5051
return nil, err
5152
}
5253

53-
plan, err := b.makePlan(ctx, configRoot)
54+
plan, err := b.makePlan(ctx, configRoot, &b.StateDB.Data)
5455
if err != nil {
5556
return nil, fmt.Errorf("reading config: %w", err)
5657
}
@@ -67,7 +68,7 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
6768
return nil, err
6869
}
6970

70-
// We're processing resources in DAG order because we're resolving refernces (that can be resolved at plan stage).
71+
// We're processing resources in DAG order because we're resolving references (that can be resolved at plan stage).
7172
g.Run(defaultParallelism, func(resourceKey string, failedDependency *string) bool {
7273
errorPrefix := "cannot plan " + resourceKey
7374

@@ -90,6 +91,27 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
9091
return false
9192
}
9293

94+
if entry.Action == deployplan.ActionTypeDelete.String() {
95+
dbentry, hasEntry := b.StateDB.GetResourceEntry(resourceKey)
96+
if !hasEntry {
97+
logdiag.LogError(ctx, fmt.Errorf("%s: internal error, missing in state", errorPrefix))
98+
return false
99+
}
100+
101+
_, err := adapter.DoRefresh(ctx, dbentry.ID)
102+
if err != nil {
103+
if isResourceGone(err) {
104+
// no such resource
105+
plan.RemoveEntry(resourceKey)
106+
} else {
107+
log.Warnf(ctx, "cannot read %s id=%q: %s", resourceKey, dbentry.ID, err)
108+
return false
109+
}
110+
}
111+
112+
return true
113+
}
114+
93115
// Process all references in the resource using Refs map
94116
// Refs maps path inside resource to references e.g. "${resources.jobs.foo.id} ${resources.jobs.foo.name}"
95117
if !b.resolveReferences(ctx, entry, errorPrefix, true) {
@@ -139,7 +161,7 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
139161

140162
remoteState, err := adapter.DoRefresh(ctx, dbentry.ID)
141163
if err != nil {
142-
if errors.Is(err, apierr.ErrResourceDoesNotExist) || errors.Is(err, apierr.ErrNotFound) {
164+
if isResourceGone(err) {
143165
remoteState = nil
144166
} else {
145167
logdiag.LogError(ctx, fmt.Errorf("%s: failed to read id=%q: %w (localAction=%q)", errorPrefix, dbentry.ID, err, localAction.String()))
@@ -188,28 +210,6 @@ func (b *DeploymentBundle) CalculatePlanForDeploy(ctx context.Context, client *d
188210
return nil, errors.New("planning failed")
189211
}
190212

191-
// Note, we cannot simply remove 'skip' entries here as then we'd need to ensure there are no edges to them
192-
193-
state := b.StateDB.ExportState(ctx)
194-
195-
// Remained in state are resources that no longer present in the config
196-
for _, group := range utils.SortedKeys(state) {
197-
_, ok := b.Adapters[group]
198-
199-
if !ok {
200-
log.Warnf(ctx, "%s: resource type not supported on direct backend", group)
201-
continue
202-
}
203-
for _, key := range utils.SortedKeys(state[group]) {
204-
n := "resources." + group + "." + key
205-
_, exists := plan.Plan[n]
206-
if exists {
207-
continue
208-
}
209-
plan.Plan[n] = &deployplan.PlanEntry{Action: deployplan.ActionTypeDelete.String()}
210-
}
211-
}
212-
213213
for _, entry := range plan.Plan {
214214
if entry.Action == deployplan.ActionTypeSkipString {
215215
entry.NewState = nil
@@ -271,32 +271,6 @@ func interpretOldStateVsRemoteState(ctx context.Context, adapter *dresources.Ada
271271
return action, m
272272
}
273273

274-
func (b *DeploymentBundle) CalculatePlanForDestroy(ctx context.Context, client *databricks.WorkspaceClient) (*deployplan.Plan, error) {
275-
b.StateDB.AssertOpened()
276-
277-
err := b.Init(client)
278-
if err != nil {
279-
return nil, err
280-
}
281-
282-
plan := deployplan.NewPlan()
283-
284-
state := b.StateDB.ExportState(ctx)
285-
for group, groupData := range state {
286-
_, ok := b.Adapters[group]
287-
if !ok {
288-
logdiag.LogError(ctx, fmt.Errorf("cannot destroy %s: resource type not supported on direct backend", group))
289-
continue
290-
}
291-
for key := range groupData {
292-
n := "resources." + group + "." + key
293-
plan.Plan[n] = &deployplan.PlanEntry{Action: deployplan.ActionTypeDelete.String()}
294-
}
295-
}
296-
297-
return plan, nil
298-
}
299-
300274
func (b *DeploymentBundle) LookupReferenceLocal(ctx context.Context, path *structpath.PathNode) (any, error) {
301275
targetResourceKey := path.Prefix(3).String()
302276
fieldPath := path.SkipPrefix(3)
@@ -432,35 +406,41 @@ func (b *DeploymentBundle) resolveReferences(ctx context.Context, entry *deployp
432406
return true
433407
}
434408

435-
func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root) (*deployplan.Plan, error) {
409+
func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root, db *dstate.Database) (*deployplan.Plan, error) {
436410
p := deployplan.NewPlan()
437411

438412
// Collect and sort nodes first, because MapByPattern gives them in randomized order
439413
var nodes []string
440414

441-
// Walk?
442-
_, err := dyn.MapByPattern(
443-
configRoot.Value(),
444-
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
445-
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
446-
group := p[1].Key()
415+
existingKeys := maps.Clone(db.State)
447416

448-
_, ok := dresources.SupportedResources[group]
449-
if !ok {
450-
return v, fmt.Errorf("unsupported resource: %s", group)
451-
}
417+
// Walk?
418+
if configRoot != nil {
419+
_, err := dyn.MapByPattern(
420+
configRoot.Value(),
421+
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
422+
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
423+
group := p[1].Key()
424+
425+
_, ok := dresources.SupportedResources[group]
426+
if !ok {
427+
return v, fmt.Errorf("unsupported resource: %s", group)
428+
}
452429

453-
nodes = append(nodes, p.String())
454-
return dyn.InvalidValue, nil
455-
},
456-
)
457-
if err != nil {
458-
return nil, fmt.Errorf("reading config: %w", err)
430+
nodes = append(nodes, p.String())
431+
return dyn.InvalidValue, nil
432+
},
433+
)
434+
if err != nil {
435+
return nil, fmt.Errorf("reading config: %w", err)
436+
}
459437
}
460438

461439
slices.Sort(nodes)
462440

463441
for _, node := range nodes {
442+
delete(existingKeys, node)
443+
464444
prefix := "cannot plan " + node
465445
inputConfig, ok := configRoot.GetResourceConfig(node)
466446
if !ok {
@@ -541,6 +521,15 @@ func (b *DeploymentBundle) makePlan(ctx context.Context, configRoot *config.Root
541521
p.Plan[node] = &e
542522
}
543523

524+
for n := range existingKeys {
525+
if p.Plan[n] != nil {
526+
panic("unexpected node " + n)
527+
}
528+
p.Plan[n] = &deployplan.PlanEntry{
529+
Action: deployplan.ActionTypeDelete.String(),
530+
}
531+
}
532+
544533
return p, nil
545534
}
546535

bundle/direct/util.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package direct
2+
3+
import (
4+
"errors"
5+
6+
"github.com/databricks/databricks-sdk-go/apierr"
7+
)
8+
9+
func isResourceGone(err error) bool {
10+
return errors.Is(err, apierr.ErrResourceDoesNotExist) || errors.Is(err, apierr.ErrNotFound)
11+
}

bundle/phases/deploy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ func planWithoutPrepare(ctx context.Context, b *bundle.Bundle) *deployplan.Plan
209209
logdiag.LogError(ctx, err)
210210
return nil
211211
}
212-
plan, err := b.DeploymentBundle.CalculatePlanForDeploy(ctx, b.WorkspaceClient(), &b.Config)
212+
plan, err := b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(), &b.Config)
213213
if err != nil {
214214
logdiag.LogError(ctx, err)
215215
return nil

bundle/phases/destroy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ func Destroy(ctx context.Context, b *bundle.Bundle) {
156156
logdiag.LogError(ctx, err)
157157
return
158158
}
159-
plan, err = b.DeploymentBundle.CalculatePlanForDestroy(ctx, b.WorkspaceClient())
159+
plan, err = b.DeploymentBundle.CalculatePlan(ctx, b.WorkspaceClient(), nil)
160160
if err != nil {
161161
logdiag.LogError(ctx, err)
162162
return

0 commit comments

Comments
 (0)