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/sync"
|
||||
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
type delete struct{}
|
||||
|
@ -22,24 +21,7 @@ func (m *delete) Name() string {
|
|||
}
|
||||
|
||||
func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||
// Do not delete files if terraform destroy was not consented
|
||||
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
|
||||
}
|
||||
}
|
||||
cmdio.LogString(ctx, "Deleting files...")
|
||||
|
||||
err := b.WorkspaceClient().Workspace.Delete(ctx, workspace.Delete{
|
||||
Path: b.Config.Workspace.RootPath,
|
||||
|
@ -54,8 +36,6 @@ func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
|||
if err != nil {
|
||||
return diag.FromErr(err)
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Successfully deleted files!")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -2,61 +2,13 @@ package terraform
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/cmdio"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/fatih/color"
|
||||
"github.com/databricks/cli/libs/log"
|
||||
"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{}
|
||||
|
||||
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 {
|
||||
// return early if plan is empty
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -75,45 +27,15 @@ func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics
|
|||
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 == "" {
|
||||
return diag.Errorf("no plan found")
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Starting to destroy resources")
|
||||
|
||||
// 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 {
|
||||
return diag.Errorf("terraform destroy: %v", err)
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Successfully destroyed resources!")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/libs/cmdio"
|
||||
"github.com/databricks/cli/libs/diag"
|
||||
"github.com/databricks/cli/libs/log"
|
||||
"github.com/databricks/cli/libs/terraform"
|
||||
"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")
|
||||
}
|
||||
|
||||
cmdio.LogString(ctx, "Starting plan computation")
|
||||
|
||||
err := tf.Init(ctx, tfexec.Upgrade(true))
|
||||
if err != nil {
|
||||
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
|
||||
b.Plan = &terraform.Plan{
|
||||
Path: planPath,
|
||||
ConfirmApply: b.AutoApprove,
|
||||
IsEmpty: !notEmpty,
|
||||
Path: planPath,
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,18 @@ package phases
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/databricks/cli/bundle"
|
||||
"github.com/databricks/cli/bundle/deploy/files"
|
||||
"github.com/databricks/cli/bundle/deploy/lock"
|
||||
"github.com/databricks/cli/bundle/deploy/terraform"
|
||||
|
||||
"github.com/databricks/cli/libs/cmdio"
|
||||
|
||||
"github.com/databricks/cli/libs/log"
|
||||
terraformlib "github.com/databricks/cli/libs/terraform"
|
||||
"github.com/databricks/databricks-sdk-go/apierr"
|
||||
)
|
||||
|
||||
|
@ -26,8 +31,63 @@ func assertRootPathExists(ctx context.Context, b *bundle.Bundle) (bool, error) {
|
|||
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.
|
||||
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(
|
||||
lock.Acquire(),
|
||||
bundle.Defer(
|
||||
|
@ -36,13 +96,14 @@ func Destroy() bundle.Mutator {
|
|||
terraform.Interpolate(),
|
||||
terraform.Write(),
|
||||
terraform.Plan(terraform.PlanGoal("destroy")),
|
||||
terraform.Destroy(),
|
||||
terraform.StatePush(),
|
||||
files.Delete(),
|
||||
bundle.If(
|
||||
approvalForDestroy,
|
||||
destroyCore,
|
||||
bundle.LogString("Destroy cancelled!"),
|
||||
),
|
||||
),
|
||||
lock.Release(lock.GoalDestroy),
|
||||
),
|
||||
bundle.LogString("Destroy complete!"),
|
||||
)
|
||||
|
||||
return newPhase(
|
||||
|
|
|
@ -1,13 +1,44 @@
|
|||
package terraform
|
||||
|
||||
import "strings"
|
||||
|
||||
type Plan struct {
|
||||
// Path to the plan
|
||||
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
|
||||
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