Skip to content

Commit 423ca32

Browse files
committed
Address review feedback on cascade_on_destroy
- Add docs URL to the terraform-engine validation error so users can learn about direct deployment mode. - Omit CascadeOnDestroy from PipelineRemote: it is an input-only, delete-time setting the GET response never returns, so drift is suppressed automatically (missing_in_remote). Matches the postgres input-only field pattern. - Propagate errors from pipelineDeletionCascades instead of silently defaulting, and add unit coverage. Co-authored-by: Isaac
1 parent cb61ad7 commit 423ca32

9 files changed

Lines changed: 103 additions & 20 deletions

File tree

acceptance/pipelines/destroy/cascade-on-destroy-terraform-error/output.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@
44
Error: cascade_on_destroy is only supported in direct deployment mode
55
in databricks.yml:7:27
66

7+
cascade_on_destroy is not yet supported by the terraform provider. Set the DATABRICKS_BUNDLE_ENGINE environment variable to 'direct' or set 'bundle.engine: direct' in your databricks.yml to use it.
8+
Learn more at https://docs.databricks.com/dev-tools/bundles/direct
9+
710

811
Exit code: 1

bundle/config/mutator/validate_cascade_on_destroy.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ func (m *validateCascadeOnDestroy) Apply(_ context.Context, b *bundle.Bundle) di
3636
}
3737
path := "resources.pipelines." + key + ".cascade_on_destroy"
3838
diags = diags.Append(diag.Diagnostic{
39-
Severity: diag.Error,
40-
Summary: "cascade_on_destroy is only supported in direct deployment mode",
39+
Severity: diag.Error,
40+
Summary: "cascade_on_destroy is only supported in direct deployment mode",
41+
Detail: "cascade_on_destroy is not yet supported by the terraform provider. " +
42+
"Set the DATABRICKS_BUNDLE_ENGINE environment variable to 'direct' or set 'bundle.engine: direct' in your databricks.yml to use it.\n" +
43+
"Learn more at https://docs.databricks.com/dev-tools/bundles/direct",
4144
Locations: b.Config.GetLocations(path),
4245
})
4346
}

bundle/config/mutator/validate_cascade_on_destroy_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ func TestValidateCascadeOnDestroyTerraformEngineSetEmitsError(t *testing.T) {
6565
diags := bundle.Apply(t.Context(), b, mutator.ValidateCascadeOnDestroy(engine.EngineTerraform))
6666
assert.Len(t, diags, 1)
6767
assert.Equal(t, "cascade_on_destroy is only supported in direct deployment mode", diags[0].Summary)
68+
assert.Contains(t, diags[0].Detail, "https://docs.databricks.com/dev-tools/bundles/direct")
6869
}

bundle/direct/dresources/pipeline.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ func (s PipelineState) MarshalJSON() ([]byte, error) {
3131

3232
// PipelineRemote is the return type for DoRead. It embeds CreatePipeline so that all
3333
// paths in StateType are valid paths in RemoteType.
34+
// Note that cascade_on_destroy is intentionally absent: it is a delete-time-only setting
35+
// that the GET response never returns, so the engine suppresses its drift automatically
3436
type PipelineRemote struct {
3537
pipelines.CreatePipeline
3638

37-
CascadeOnDestroy *bool `json:"cascade_on_destroy,omitempty"`
38-
3939
// Remote-specific fields from pipelines.GetPipelineResponse
4040
Cause string `json:"cause,omitempty"`
4141
ClusterId string `json:"cluster_id,omitempty"`
@@ -79,8 +79,9 @@ func (*ResourcePipeline) PrepareState(input *resources.Pipeline) *PipelineState
7979

8080
func (*ResourcePipeline) RemapState(remote *PipelineRemote) *PipelineState {
8181
return &PipelineState{
82-
CreatePipeline: remote.CreatePipeline,
83-
CascadeOnDestroy: remote.CascadeOnDestroy,
82+
CreatePipeline: remote.CreatePipeline,
83+
// cascade_on_destroy is input-only and absent from PipelineRemote, so it stays nil here.
84+
CascadeOnDestroy: nil,
8485
}
8586
}
8687

@@ -136,9 +137,7 @@ func makePipelineRemote(p *pipelines.GetPipelineResponse) *PipelineRemote {
136137
}
137138
}
138139
return &PipelineRemote{
139-
CreatePipeline: createPipeline,
140-
// cascade_on_destroy is input-only; the GET response never carries it, so leave it nil.
141-
CascadeOnDestroy: nil,
140+
CreatePipeline: createPipeline,
142141
Cause: p.Cause,
143142
ClusterId: p.ClusterId,
144143
CreatorUserName: p.CreatorUserName,

bundle/direct/dresources/resources.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,6 @@ resources:
180180
reason: "!drop"
181181
- field: run_as
182182
reason: input_only
183-
# cascade_on_destroy is a delete-time-only setting
184-
# it is never returned by GET, so suppress remote drift for it.
185-
- field: cascade_on_destroy
186-
reason: input_only
187183

188184
ignore_local_changes:
189185
- field: deployment.version_id

bundle/direct/dresources/type_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ var knownMissingInRemoteType = map[string][]string{
2525
"model_serving_endpoints": {
2626
"rate_limits",
2727
},
28+
"pipelines": {
29+
// Note that this is a deliberate omission: cascade_on_destroy is a client-side-only field.
30+
// It has no remote counterpart, so it is never expected in RemoteType.
31+
"cascade_on_destroy",
32+
},
2833
"quality_monitors": {
2934
"skip_builtin_dashboard",
3035
"warehouse_id",

bundle/phases/destroy.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,16 @@ var destroyApprovalGroups = []approvalGroup{
4848

4949
// logPipelineDeleteApproval prints the pipeline deletions. If cascade_on_destroy is true, we will include
5050
// a note that datasets will be deleted as well.
51-
func logPipelineDeleteApproval(ctx context.Context, b *bundle.Bundle, actions []deployplan.Action) {
51+
func logPipelineDeleteApproval(ctx context.Context, b *bundle.Bundle, actions []deployplan.Action) error {
5252
pipelineDeletes := filterGroup(actions, "pipelines", deployplan.Delete)
5353

5454
var cascading, retaining []deployplan.Action
5555
for _, a := range pipelineDeletes {
56-
if pipelineDeletionCascades(b, a) {
56+
cascade, err := pipelineDeletionCascades(b, a)
57+
if err != nil {
58+
return err
59+
}
60+
if cascade {
5761
cascading = append(cascading, a)
5862
} else {
5963
retaining = append(retaining, a)
@@ -76,6 +80,7 @@ func logPipelineDeleteApproval(ctx context.Context, b *bundle.Bundle, actions []
7680
}
7781
cmdio.LogString(ctx, "")
7882
}
83+
return nil
7984
}
8085

8186
func approvalForDestroy(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan) (bool, error) {
@@ -102,7 +107,9 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle, plan *deployplan.
102107
}
103108

104109
logApprovalGroups(ctx, deleteActions, destroyApprovalGroups, true, deployplan.Delete)
105-
logPipelineDeleteApproval(ctx, b, deleteActions)
110+
if err := logPipelineDeleteApproval(ctx, b, deleteActions); err != nil {
111+
return false, err
112+
}
106113

107114
cmdio.LogString(ctx, "All files and directories at the following location will be deleted: "+b.Config.Workspace.RootPath)
108115
cmdio.LogString(ctx, "")

bundle/phases/plan.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,22 @@ func PreDeployChecks(ctx context.Context, b *bundle.Bundle, isPlan bool, engine
3636
// cascade_on_destroy is explicitly set to false. A lookup miss (unset field, or a non-pipeline
3737
// resource) means the default applies, so it returns true.
3838
// Reads from config like checkForPreventDestroy.
39-
func pipelineDeletionCascades(b *bundle.Bundle, action deployplan.Action) bool {
39+
func pipelineDeletionCascades(b *bundle.Bundle, action deployplan.Action) (bool, error) {
4040
path, err := dyn.NewPathFromString(action.ResourceKey)
4141
if err != nil {
42-
return true
42+
return false, fmt.Errorf("failed to parse %q", action.ResourceKey)
4343
}
4444
path = append(path, dyn.Key("cascade_on_destroy"))
4545

4646
v, err := dyn.GetByPath(b.Config.Value(), path)
4747
if err != nil {
48-
return true
48+
return true, nil
4949
}
5050
cascade, ok := v.AsBool()
51-
return !ok || cascade
51+
if !ok {
52+
return false, fmt.Errorf("internal error: cascade_on_destroy is not a boolean for %s", action.ResourceKey)
53+
}
54+
return cascade, nil
5255
}
5356

5457
// checkForPreventDestroy checks if the resource has lifecycle.prevent_destroy set, but the plan calls for this resource to be recreated or destroyed.

bundle/phases/plan_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,75 @@ import (
77
"github.com/databricks/cli/bundle/config"
88
"github.com/databricks/cli/bundle/deployplan"
99
"github.com/databricks/cli/libs/dyn"
10+
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112
)
1213

14+
func TestPipelineDeletionCascades(t *testing.T) {
15+
b := &bundle.Bundle{}
16+
err := b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
17+
return dyn.Set(v, "resources", dyn.NewValue(map[string]dyn.Value{
18+
"pipelines": dyn.NewValue(map[string]dyn.Value{
19+
"cascade_true": dyn.NewValue(map[string]dyn.Value{
20+
"cascade_on_destroy": dyn.NewValue(true, nil),
21+
}, nil),
22+
"cascade_false": dyn.NewValue(map[string]dyn.Value{
23+
"cascade_on_destroy": dyn.NewValue(false, nil),
24+
}, nil),
25+
"cascade_unset": dyn.NewValue(map[string]dyn.Value{}, nil),
26+
"cascade_not_bool": dyn.NewValue(map[string]dyn.Value{
27+
"cascade_on_destroy": dyn.NewValue("yes", nil),
28+
}, nil),
29+
}, nil),
30+
}, nil))
31+
})
32+
require.NoError(t, err)
33+
34+
tests := []struct {
35+
name string
36+
resourceKey string
37+
want bool
38+
wantErr string
39+
}{
40+
{
41+
name: "cascade_on_destroy explicitly true",
42+
resourceKey: "resources.pipelines.cascade_true",
43+
want: true,
44+
},
45+
{
46+
name: "cascade_on_destroy explicitly false",
47+
resourceKey: "resources.pipelines.cascade_false",
48+
want: false,
49+
},
50+
{
51+
name: "cascade_on_destroy unset defaults to cascade",
52+
resourceKey: "resources.pipelines.cascade_unset",
53+
want: true,
54+
},
55+
{
56+
name: "cascade_on_destroy not a bool errors",
57+
resourceKey: "resources.pipelines.cascade_not_bool",
58+
wantErr: "internal error: cascade_on_destroy is not a boolean for resources.pipelines.cascade_not_bool",
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
action := deployplan.Action{
65+
ResourceKey: tt.resourceKey,
66+
ActionType: deployplan.Delete,
67+
}
68+
cascade, err := pipelineDeletionCascades(b, action)
69+
if tt.wantErr != "" {
70+
require.EqualError(t, err, tt.wantErr)
71+
return
72+
}
73+
require.NoError(t, err)
74+
assert.Equal(t, tt.want, cascade)
75+
})
76+
}
77+
}
78+
1379
func TestCheckPreventDestroyForAllResources(t *testing.T) {
1480
for resourceType := range config.SupportedResources() {
1581
t.Run(resourceType, func(t *testing.T) {

0 commit comments

Comments
 (0)