Skip to content

Commit b88e1c7

Browse files
committed
WIP: destroy mutator + test (pipelines only); Graph is now generic
1 parent 04482cc commit b88e1c7

File tree

16 files changed

+352
-120
lines changed

16 files changed

+352
-120
lines changed

acceptance/bin/read_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def print_resource_terraform(section, name, *attrs):
3030
print(section, name, " ".join(values))
3131
found += 1
3232
if not found:
33-
print(f"Resource {section=} {name=} {resource_type=} not found. Available: {raw}")
33+
print(f"State not found for {section}.{name}")
3434

3535

3636
def print_resource_terranova(section, name, *attrs):
@@ -40,7 +40,7 @@ def print_resource_terranova(section, name, *attrs):
4040
resources = data["resources"].get(section, {})
4141
result = resources.get(name)
4242
if result is None:
43-
print(f"Resource {section=} {name=} not found. Available: {raw}")
43+
print(f"State not found for {section}.{name}")
4444
return
4545
state = result["state"]
4646
state.setdefault("id", result.get("__id__"))

acceptance/bundle/resources/pipelines/output.txt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Deploying resources...
3636
Updating deployment state...
3737
Deployment complete!
3838

39+
=== Fetch pipeline ID and verify remote state
3940
>>> [CLI] pipelines get [UUID]
4041
{
4142
"creator_user_name":"[USERNAME]",
@@ -63,3 +64,25 @@ Deployment complete!
6364
},
6465
"state":"IDLE"
6566
}
67+
68+
=== Destroy the pipeline and verify that it's removed from the state and from remote
69+
>>> [CLI] bundle destroy --auto-approve
70+
The following resources will be deleted:
71+
delete pipeline my
72+
73+
All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/acc-[UNIQUE_NAME]/default
74+
75+
Deleting files...
76+
Destroy complete!
77+
78+
>>> print_requests
79+
{
80+
"method": "DELETE",
81+
"path": "/api/2.0/pipelines/[UUID]"
82+
}
83+
State not found for pipelines.my
84+
85+
>>> musterr [CLI] pipelines get [UUID]
86+
Error: Not Found
87+
88+
Exit code (musterr): 1

acceptance/bundle/resources/pipelines/script

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ trace $CLI bundle deploy
55

66
print_requests() {
77
jq --sort-keys 'select(.method != "GET" and (.path | contains("/pipelines")))' < out.requests.txt
8-
rm out.requests.txt
9-
read_state.py pipelines my id name
8+
errcode rm out.requests.txt
9+
errcode read_state.py pipelines my id name
1010
}
1111

1212
trace print_requests
@@ -17,6 +17,18 @@ trace $CLI bundle deploy
1717
# fields that it previously recevied from the backend.
1818
# The get call below verifies that it does not matter -- the pipeline is the same in the end.
1919
#trace print_requests
20+
rm out.requests.txt
21+
22+
title "Fetch pipeline ID and verify remote state"
23+
24+
ppid=`read_id.py pipelines my`
25+
26+
trace $CLI pipelines get $ppid
27+
rm out.requests.txt
28+
29+
title "Destroy the pipeline and verify that it's removed from the state and from remote"
30+
trace $CLI bundle destroy --auto-approve
31+
trace print_requests
2032

21-
trace $CLI pipelines get $(read_id.py pipelines my)
33+
trace musterr $CLI pipelines get $ppid
2234
rm out.requests.txt

acceptance/script.prepare

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,21 @@ errcode() {
1111
fi
1212
}
1313

14+
musterr() {
15+
# Temporarily disable 'set -e' to prevent the script from exiting on error
16+
set +e
17+
# Execute the provided command with all arguments
18+
"$@"
19+
local exit_code=$?
20+
# Re-enable 'set -e' if it was previously set
21+
set -e
22+
if [ $exit_code -eq 0 ]; then
23+
>&2 printf "\nUnexpected success\n"
24+
exit 1
25+
fi
26+
>&2 printf "\nExit code (musterr): $exit_code\n"
27+
}
28+
1429
trace() {
1530
>&2 printf "\n>>> %s\n" "$*"
1631

bundle/phases/destroy.go

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"errors"
66
"net/http"
7+
"strings"
78

89
"github.com/databricks/cli/bundle"
910
"github.com/databricks/cli/bundle/deploy/files"
1011
"github.com/databricks/cli/bundle/deploy/lock"
1112
"github.com/databricks/cli/bundle/deploy/terraform"
13+
"github.com/databricks/cli/bundle/terranova"
1214

1315
"github.com/databricks/cli/libs/cmdio"
1416
"github.com/databricks/cli/libs/diag"
@@ -31,16 +33,31 @@ func assertRootPathExists(ctx context.Context, b *bundle.Bundle) (bool, error) {
3133
return true, err
3234
}
3335

34-
func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
36+
func getDeleteActions(ctx context.Context, b *bundle.Bundle) ([]terraformlib.Action, error) {
37+
if b.DirectDeployment {
38+
allResources := b.ResourceDatabase.GetAllResources()
39+
var deleteActions []terraformlib.Action
40+
for _, node := range allResources {
41+
rType, _ := strings.CutSuffix(node.Section, "s")
42+
deleteActions = append(deleteActions, terraformlib.Action{
43+
Action: terraformlib.ActionTypeDelete,
44+
ResourceType: rType,
45+
ResourceName: node.Name,
46+
})
47+
}
48+
return deleteActions, nil
49+
}
50+
3551
tf := b.Terraform
52+
3653
if tf == nil {
37-
return false, errors.New("terraform not initialized")
54+
return nil, errors.New("terraform not initialized")
3855
}
3956

4057
// read plan file
4158
plan, err := tf.ShowPlanFile(ctx, b.Plan.Path)
4259
if err != nil {
43-
return false, err
60+
return nil, err
4461
}
4562

4663
var deleteActions []terraformlib.Action
@@ -54,6 +71,12 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
5471
}
5572
}
5673

74+
return deleteActions, nil
75+
}
76+
77+
func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
78+
deleteActions, err := getDeleteActions(ctx, b)
79+
5780
if len(deleteActions) > 0 {
5881
cmdio.LogString(ctx, "The following resources will be deleted:")
5982
for _, a := range deleteActions {
@@ -79,11 +102,20 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
79102
}
80103

81104
func destroyCore(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
82-
// Core destructive mutators for destroy. These require informed user consent.
83-
diags := bundle.ApplySeq(ctx, b,
84-
terraform.Apply(),
85-
files.Delete(),
86-
)
105+
var diags diag.Diagnostics
106+
107+
if b.DirectDeployment {
108+
diags = bundle.Apply(ctx, b, terranova.TerranovaDestroy())
109+
} else {
110+
// Core destructive mutators for destroy. These require informed user consent.
111+
diags = bundle.Apply(ctx, b, terraform.Apply())
112+
}
113+
114+
if diags.HasError() {
115+
return diags
116+
}
117+
118+
diags = diags.Extend(bundle.Apply(ctx, b, files.Delete()))
87119

88120
if !diags.HasError() {
89121
cmdio.LogString(ctx, "Destroy complete!")
@@ -115,12 +147,14 @@ func Destroy(ctx context.Context, b *bundle.Bundle) (diags diag.Diagnostics) {
115147
diags = diags.Extend(bundle.Apply(ctx, b, lock.Release(lock.GoalDestroy)))
116148
}()
117149

118-
diags = diags.Extend(bundle.ApplySeq(ctx, b,
119-
terraform.StatePull(),
120-
terraform.Interpolate(),
121-
terraform.Write(),
122-
terraform.Plan(terraform.PlanGoal("destroy")),
123-
))
150+
if !b.DirectDeployment {
151+
diags = diags.Extend(bundle.ApplySeq(ctx, b,
152+
terraform.StatePull(),
153+
terraform.Interpolate(),
154+
terraform.Write(),
155+
terraform.Plan(terraform.PlanGoal("destroy")),
156+
))
157+
}
124158

125159
if diags.HasError() {
126160
return diags

bundle/phases/initialize.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"path/filepath"
78

89
"github.com/databricks/cli/bundle/config/mutator/resourcemutator"
910

@@ -206,6 +207,17 @@ func Initialize(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
206207
b.Config.Bundle.Terraform = &config.Terraform{ExecPath: " "}
207208
return nil
208209
})
210+
211+
cacheDir, err := b.CacheDir(ctx)
212+
if err != nil {
213+
diags = diags.Extend(diag.FromErr(err))
214+
}
215+
216+
databasePath := filepath.Join(cacheDir, "resources.json")
217+
err = b.ResourceDatabase.Open(databasePath)
218+
if err != nil {
219+
diags = diags.Extend(diag.FromErr(err))
220+
}
209221
} else {
210222
// Reads (typed): b.Config.Bundle.Terraform (checks terraform configuration)
211223
// Updates (typed): b.Config.Bundle.Terraform (sets default values if not already set)

bundle/terranova/deploy_mutator.go

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package terranova
33
import (
44
"context"
55
"fmt"
6-
"path/filepath"
7-
"strings"
86

97
"github.com/databricks/cli/bundle"
108
"github.com/databricks/cli/bundle/terranova/terranova_resources"
@@ -35,28 +33,19 @@ func (m *terranovaDeployMutator) Apply(ctx context.Context, b *bundle.Bundle) di
3533

3634
client := b.WorkspaceClient()
3735

38-
cacheDir, err := b.CacheDir(ctx)
39-
if err != nil {
40-
return diag.FromErr(err)
41-
}
42-
43-
databasePath := filepath.Join(cacheDir, "resources.json")
44-
err = b.ResourceDatabase.Open(databasePath)
45-
if err != nil {
46-
return diag.FromErr(err)
47-
}
36+
g := dag.NewGraph[terranova_state.ResourceNode]()
4837

49-
g := dag.NewGraph()
50-
51-
_, err = dyn.MapByPattern(
38+
_, err := dyn.MapByPattern(
5239
b.Config.Value(),
5340
dyn.NewPattern(dyn.Key("resources"), dyn.AnyKey(), dyn.AnyKey()),
5441
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
5542
section := p[1].Key()
56-
resourceName := p[2].Key()
57-
node := section + "." + resourceName
43+
name := p[2].Key()
5844
// log.Warnf(ctx, "Adding node=%s", node)
59-
g.AddNode(node)
45+
g.AddNode(terranova_state.ResourceNode{
46+
Section: section,
47+
Name: name,
48+
})
6049

6150
// TODO: Scan v for references and use g.AddDirectedEdge to add dependency
6251
return v, nil
@@ -65,25 +54,16 @@ func (m *terranovaDeployMutator) Apply(ctx context.Context, b *bundle.Bundle) di
6554

6655
countDeployed := 0
6756

68-
err = g.Run(maxPoolSize, func(node string) {
57+
err = g.Run(maxPoolSize, func(node terranova_state.ResourceNode) {
6958
// TODO func(node string) bool
7059
// If function returns false, downstream callers are not called
7160
// g.Run() should return list of not executed nodes
7261
// log.Warnf(ctx, "Processing node=%s", node)
7362

74-
items := strings.SplitN(node, ".", 2)
75-
if len(items) != 2 {
76-
diags.AppendErrorf("internal error: unexpected DAG node %#v", node)
77-
return
78-
}
79-
80-
section := items[0]
81-
name := items[1]
82-
8363
// TODO: resolve all resource references inside this resource. It should be possible, if graph was constructed correctly.
8464
// If it is not possible, return error (and fail this and dependent resources)
8565

86-
config, ok := b.GetResourceConfig(section, name)
66+
config, ok := b.GetResourceConfig(node.Section, node.Name)
8767
if !ok {
8868
diags.AppendErrorf("internal error: cannot get config for %s", node)
8969
return
@@ -92,8 +72,8 @@ func (m *terranovaDeployMutator) Apply(ctx context.Context, b *bundle.Bundle) di
9272
d := Deployer{
9373
client: client,
9474
db: &b.ResourceDatabase,
95-
section: section,
96-
resourceName: name,
75+
section: node.Section,
76+
resourceName: node.Name,
9777
}
9878

9979
err = d.Deploy(ctx, config)

bundle/terranova/destroy_mutator.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package terranova
2+
3+
import (
4+
"context"
5+
6+
"github.com/databricks/cli/bundle"
7+
"github.com/databricks/cli/bundle/terranova/terranova_resources"
8+
"github.com/databricks/cli/bundle/terranova/terranova_state"
9+
"github.com/databricks/cli/libs/dag"
10+
"github.com/databricks/cli/libs/diag"
11+
)
12+
13+
type terranovaDestroyMutator struct{}
14+
15+
func TerranovaDestroy() bundle.Mutator {
16+
return &terranovaDestroyMutator{}
17+
}
18+
19+
func (m *terranovaDestroyMutator) Name() string {
20+
return "TerranovaDestroy"
21+
}
22+
23+
func (m *terranovaDestroyMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
24+
var diags diag.SafeDiagnostics
25+
client := b.WorkspaceClient()
26+
27+
allResources := b.ResourceDatabase.GetAllResources()
28+
g := dag.NewGraph[terranova_state.ResourceNode]()
29+
30+
for _, node := range allResources {
31+
g.AddNode(node)
32+
}
33+
34+
// TODO: respect dependencies; dependencies need to be part of state, not config.
35+
36+
err := g.Run(maxPoolSize, func(node terranova_state.ResourceNode) {
37+
err := terranova_resources.DestroyResource(ctx, client, node.Section, node.ID)
38+
if err != nil {
39+
diags.AppendErrorf("destroying %s: %s", node, err)
40+
return
41+
}
42+
// TODO: did DestroyResource fail because it did not exist? we can clean it up from the state as well
43+
44+
err = b.ResourceDatabase.DeleteState(node.Section, node.Name)
45+
if err != nil {
46+
diags.AppendErrorf("deleting from the state %s: %s", node, err)
47+
return
48+
}
49+
})
50+
if err != nil {
51+
diags.AppendError(err)
52+
}
53+
54+
_ = b.ResourceDatabase.Finalize()
55+
56+
return diags.Diags
57+
}

bundle/terranova/terranova_resources/app.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ type ResourceApp struct {
1515
config apps.App
1616
}
1717

18-
func NewResourceApp(client *databricks.WorkspaceClient, config resources.App) (ResourceApp, error) {
19-
return ResourceApp{
18+
func NewResourceApp(client *databricks.WorkspaceClient, config resources.App) (*ResourceApp, error) {
19+
return &ResourceApp{
2020
client: client,
2121
config: config.App,
2222
}, nil

0 commit comments

Comments
 (0)