mirror of https://github.com/databricks/cli.git
Move to a single prompt during bundle destroy (#1583)
## Changes Right now we ask users for two confirmations when destroying a bundle. One to destroy the resources and one to delete the files. This PR consolidates the two prompts into one. ## Tests Manually Destroying a bundle with no resources: ``` ➜ bundle-playground git:(master) ✗ cli bundle destroy All files and directories at the following location will be deleted: /Users/shreyas.goenka@databricks.com/.bundle/bundle-playground/default Would you like to proceed? [y/n]: y No resources to destroy Updating deployment state... Deleting files... Destroy complete! ``` Destroying a bundle with no remote state: ``` ➜ bundle-playground git:(master) ✗ cli bundle destroy No active deployment found to destroy! ``` When a user cancells a deployment: ``` ➜ bundle-playground git:(master) ✗ cli bundle destroy The following resources will be deleted: delete job job_1 delete job job_2 delete pipeline foo All files and directories at the following location will be deleted: /Users/shreyas.goenka@databricks.com/.bundle/bundle-playground/default Would you like to proceed? [y/n]: n Destroy cancelled! ``` When a user destroys resources: ``` ➜ bundle-playground git:(master) ✗ cli bundle destroy The following resources will be deleted: delete job job_1 delete job job_2 delete pipeline foo All files and directories at the following location will be deleted: /Users/shreyas.goenka@databricks.com/.bundle/bundle-playground/default Would you like to proceed? [y/n]: y Updating deployment state... Deleting files... Destroy complete! ```
This commit is contained in:
parent
39fc86e83b
commit
e6241e196f
|
@ -12,7 +12,6 @@ import (
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/sync"
|
"github.com/databricks/cli/libs/sync"
|
||||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
"github.com/fatih/color"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type delete struct{}
|
type delete struct{}
|
||||||
|
@ -22,24 +21,7 @@ func (m *delete) Name() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
// Do not delete files if terraform destroy was not consented
|
cmdio.LogString(ctx, "Deleting files...")
|
||||||
if !b.Plan.IsEmpty && !b.Plan.ConfirmApply {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cmdio.LogString(ctx, "Starting deletion of remote bundle files")
|
|
||||||
cmdio.LogString(ctx, fmt.Sprintf("Bundle remote directory is %s", b.Config.Workspace.RootPath))
|
|
||||||
|
|
||||||
red := color.New(color.FgRed).SprintFunc()
|
|
||||||
if !b.AutoApprove {
|
|
||||||
proceed, err := cmdio.AskYesOrNo(ctx, fmt.Sprintf("\n%s and all files in it will be %s Proceed?", b.Config.Workspace.RootPath, red("deleted permanently!")))
|
|
||||||
if err != nil {
|
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
if !proceed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err := b.WorkspaceClient().Workspace.Delete(ctx, workspace.Delete{
|
err := b.WorkspaceClient().Workspace.Delete(ctx, workspace.Delete{
|
||||||
Path: b.Config.Workspace.RootPath,
|
Path: b.Config.Workspace.RootPath,
|
||||||
|
@ -54,8 +36,6 @@ func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return diag.FromErr(err)
|
return diag.FromErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdio.LogString(ctx, "Successfully deleted files!")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,61 +2,13 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/libs/cmdio"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/fatih/color"
|
"github.com/databricks/cli/libs/log"
|
||||||
"github.com/hashicorp/terraform-exec/tfexec"
|
"github.com/hashicorp/terraform-exec/tfexec"
|
||||||
tfjson "github.com/hashicorp/terraform-json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type PlanResourceChange struct {
|
|
||||||
ResourceType string `json:"resource_type"`
|
|
||||||
Action string `json:"action"`
|
|
||||||
ResourceName string `json:"resource_name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PlanResourceChange) String() string {
|
|
||||||
result := strings.Builder{}
|
|
||||||
switch c.Action {
|
|
||||||
case "delete":
|
|
||||||
result.WriteString(" delete ")
|
|
||||||
default:
|
|
||||||
result.WriteString(c.Action + " ")
|
|
||||||
}
|
|
||||||
switch c.ResourceType {
|
|
||||||
case "databricks_job":
|
|
||||||
result.WriteString("job ")
|
|
||||||
case "databricks_pipeline":
|
|
||||||
result.WriteString("pipeline ")
|
|
||||||
default:
|
|
||||||
result.WriteString(c.ResourceType + " ")
|
|
||||||
}
|
|
||||||
result.WriteString(c.ResourceName)
|
|
||||||
return result.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PlanResourceChange) IsInplaceSupported() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func logDestroyPlan(ctx context.Context, changes []*tfjson.ResourceChange) error {
|
|
||||||
cmdio.LogString(ctx, "The following resources will be removed:")
|
|
||||||
for _, c := range changes {
|
|
||||||
if c.Change.Actions.Delete() {
|
|
||||||
cmdio.Log(ctx, &PlanResourceChange{
|
|
||||||
ResourceType: c.Type,
|
|
||||||
Action: "delete",
|
|
||||||
ResourceName: c.Name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type destroy struct{}
|
type destroy struct{}
|
||||||
|
|
||||||
func (w *destroy) Name() string {
|
func (w *destroy) Name() string {
|
||||||
|
@ -66,7 +18,7 @@ func (w *destroy) Name() string {
|
||||||
func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
// return early if plan is empty
|
// return early if plan is empty
|
||||||
if b.Plan.IsEmpty {
|
if b.Plan.IsEmpty {
|
||||||
cmdio.LogString(ctx, "No resources to destroy in plan. Skipping destroy!")
|
log.Debugf(ctx, "No resources to destroy in plan. Skipping destroy.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,45 +27,15 @@ func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
|
||||||
return diag.Errorf("terraform not initialized")
|
return diag.Errorf("terraform not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// read plan file
|
|
||||||
plan, err := tf.ShowPlanFile(ctx, b.Plan.Path)
|
|
||||||
if err != nil {
|
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// print the resources that will be destroyed
|
|
||||||
err = logDestroyPlan(ctx, plan.ResourceChanges)
|
|
||||||
if err != nil {
|
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for confirmation, if needed
|
|
||||||
if !b.Plan.ConfirmApply {
|
|
||||||
red := color.New(color.FgRed).SprintFunc()
|
|
||||||
b.Plan.ConfirmApply, err = cmdio.AskYesOrNo(ctx, fmt.Sprintf("\nThis will permanently %s resources! Proceed?", red("destroy")))
|
|
||||||
if err != nil {
|
|
||||||
return diag.FromErr(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// return if confirmation was not provided
|
|
||||||
if !b.Plan.ConfirmApply {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if b.Plan.Path == "" {
|
if b.Plan.Path == "" {
|
||||||
return diag.Errorf("no plan found")
|
return diag.Errorf("no plan found")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdio.LogString(ctx, "Starting to destroy resources")
|
|
||||||
|
|
||||||
// Apply terraform according to the computed destroy plan
|
// Apply terraform according to the computed destroy plan
|
||||||
err = tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
|
err := tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return diag.Errorf("terraform destroy: %v", err)
|
return diag.Errorf("terraform destroy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdio.LogString(ctx, "Successfully destroyed resources!")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/libs/cmdio"
|
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/cli/libs/log"
|
||||||
"github.com/databricks/cli/libs/terraform"
|
"github.com/databricks/cli/libs/terraform"
|
||||||
"github.com/hashicorp/terraform-exec/tfexec"
|
"github.com/hashicorp/terraform-exec/tfexec"
|
||||||
)
|
)
|
||||||
|
@ -33,8 +33,6 @@ func (p *plan) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
return diag.Errorf("terraform not initialized")
|
return diag.Errorf("terraform not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdio.LogString(ctx, "Starting plan computation")
|
|
||||||
|
|
||||||
err := tf.Init(ctx, tfexec.Upgrade(true))
|
err := tf.Init(ctx, tfexec.Upgrade(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return diag.Errorf("terraform init: %v", err)
|
return diag.Errorf("terraform init: %v", err)
|
||||||
|
@ -55,12 +53,11 @@ func (p *plan) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
|
||||||
// Set plan in main bundle struct for downstream mutators
|
// Set plan in main bundle struct for downstream mutators
|
||||||
b.Plan = &terraform.Plan{
|
b.Plan = &terraform.Plan{
|
||||||
Path: planPath,
|
Path: planPath,
|
||||||
ConfirmApply: b.AutoApprove,
|
IsEmpty: !notEmpty,
|
||||||
IsEmpty: !notEmpty,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdio.LogString(ctx, fmt.Sprintf("Planning complete and persisted at %s\n", planPath))
|
log.Debugf(ctx, fmt.Sprintf("Planning complete and persisted at %s\n", planPath))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,18 @@ package phases
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/deploy/files"
|
"github.com/databricks/cli/bundle/deploy/files"
|
||||||
"github.com/databricks/cli/bundle/deploy/lock"
|
"github.com/databricks/cli/bundle/deploy/lock"
|
||||||
"github.com/databricks/cli/bundle/deploy/terraform"
|
"github.com/databricks/cli/bundle/deploy/terraform"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/libs/cmdio"
|
||||||
|
|
||||||
"github.com/databricks/cli/libs/log"
|
"github.com/databricks/cli/libs/log"
|
||||||
|
terraformlib "github.com/databricks/cli/libs/terraform"
|
||||||
"github.com/databricks/databricks-sdk-go/apierr"
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,8 +31,63 @@ func assertRootPathExists(ctx context.Context, b *bundle.Bundle) (bool, error) {
|
||||||
return true, err
|
return true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
|
||||||
|
tf := b.Terraform
|
||||||
|
if tf == nil {
|
||||||
|
return false, fmt.Errorf("terraform not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read plan file
|
||||||
|
plan, err := tf.ShowPlanFile(ctx, b.Plan.Path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteActions := make([]terraformlib.Action, 0)
|
||||||
|
for _, rc := range plan.ResourceChanges {
|
||||||
|
if rc.Change.Actions.Delete() {
|
||||||
|
deleteActions = append(deleteActions, terraformlib.Action{
|
||||||
|
Action: terraformlib.ActionTypeDelete,
|
||||||
|
ResourceType: rc.Type,
|
||||||
|
ResourceName: rc.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(deleteActions) > 0 {
|
||||||
|
cmdio.LogString(ctx, "The following resources will be deleted:")
|
||||||
|
for _, a := range deleteActions {
|
||||||
|
cmdio.Log(ctx, a)
|
||||||
|
}
|
||||||
|
cmdio.LogString(ctx, "")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdio.LogString(ctx, fmt.Sprintf("All files and directories at the following location will be deleted: %s", b.Config.Workspace.RootPath))
|
||||||
|
cmdio.LogString(ctx, "")
|
||||||
|
|
||||||
|
if b.AutoApprove {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
approved, err := cmdio.AskYesOrNo(ctx, "Would you like to proceed?")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return approved, nil
|
||||||
|
}
|
||||||
|
|
||||||
// The destroy phase deletes artifacts and resources.
|
// The destroy phase deletes artifacts and resources.
|
||||||
func Destroy() bundle.Mutator {
|
func Destroy() bundle.Mutator {
|
||||||
|
// Core destructive mutators for destroy. These require informed user consent.
|
||||||
|
destroyCore := bundle.Seq(
|
||||||
|
terraform.Destroy(),
|
||||||
|
terraform.StatePush(),
|
||||||
|
files.Delete(),
|
||||||
|
bundle.LogString("Destroy complete!"),
|
||||||
|
)
|
||||||
|
|
||||||
destroyMutator := bundle.Seq(
|
destroyMutator := bundle.Seq(
|
||||||
lock.Acquire(),
|
lock.Acquire(),
|
||||||
bundle.Defer(
|
bundle.Defer(
|
||||||
|
@ -36,13 +96,14 @@ func Destroy() bundle.Mutator {
|
||||||
terraform.Interpolate(),
|
terraform.Interpolate(),
|
||||||
terraform.Write(),
|
terraform.Write(),
|
||||||
terraform.Plan(terraform.PlanGoal("destroy")),
|
terraform.Plan(terraform.PlanGoal("destroy")),
|
||||||
terraform.Destroy(),
|
bundle.If(
|
||||||
terraform.StatePush(),
|
approvalForDestroy,
|
||||||
files.Delete(),
|
destroyCore,
|
||||||
|
bundle.LogString("Destroy cancelled!"),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
lock.Release(lock.GoalDestroy),
|
lock.Release(lock.GoalDestroy),
|
||||||
),
|
),
|
||||||
bundle.LogString("Destroy complete!"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return newPhase(
|
return newPhase(
|
||||||
|
|
|
@ -1,13 +1,44 @@
|
||||||
package terraform
|
package terraform
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
type Plan struct {
|
type Plan struct {
|
||||||
// Path to the plan
|
// Path to the plan
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
// Holds whether the user can consented to destruction. Either by interactive
|
|
||||||
// confirmation or by passing a command line flag
|
|
||||||
ConfirmApply bool
|
|
||||||
|
|
||||||
// If true, the plan is empty and applying it will not do anything
|
// If true, the plan is empty and applying it will not do anything
|
||||||
IsEmpty bool
|
IsEmpty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
// Type and name of the resource
|
||||||
|
ResourceType string `json:"resource_type"`
|
||||||
|
ResourceName string `json:"resource_name"`
|
||||||
|
|
||||||
|
Action ActionType `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Action) String() string {
|
||||||
|
// terraform resources have the databricks_ prefix, which is not needed.
|
||||||
|
rtype := strings.TrimPrefix(a.ResourceType, "databricks_")
|
||||||
|
return strings.Join([]string{" ", string(a.Action), rtype, a.ResourceName}, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Action) IsInplaceSupported() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// These enum values correspond to action types defined in the tfjson library.
|
||||||
|
// "recreate" maps to the tfjson.Actions.Replace() function.
|
||||||
|
// "update" maps to tfjson.Actions.Update() and so on. source:
|
||||||
|
// https://github.com/hashicorp/terraform-json/blob/0104004301ca8e7046d089cdc2e2db2179d225be/action.go#L14
|
||||||
|
type ActionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionTypeCreate ActionType = "create"
|
||||||
|
ActionTypeDelete ActionType = "delete"
|
||||||
|
ActionTypeUpdate ActionType = "update"
|
||||||
|
ActionTypeNoOp ActionType = "no-op"
|
||||||
|
ActionTypeRead ActionType = "read"
|
||||||
|
ActionTypeRecreate ActionType = "recreate"
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue