Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 214
Add bundle destroy command#300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
4966c14431306bbb3a42c1c111b8dd65065b370c97f75f379File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package files | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "github.com/databricks/bricks/bundle" | ||
| "github.com/databricks/bricks/libs/cmdio" | ||
| "github.com/databricks/databricks-sdk-go/service/workspace" | ||
| "github.com/fatih/color" | ||
| ) | ||
| type delete struct{} | ||
| func (m *delete) Name() string { | ||
| return "files.Delete" | ||
| } | ||
| func (m *delete) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) { | ||
| // Do not delete files if terraform destroy was not consented | ||
| if !b.Plan.IsEmpty && !b.Plan.ConfirmApply { | ||
| return nil, nil | ||
| } | ||
| // interface to io with the user | ||
| logger, ok := cmdio.FromContext(ctx) | ||
| if !ok { | ||
| return nil, fmt.Errorf("no logger found") | ||
| } | ||
| red := color.New(color.FgRed).SprintFunc() | ||
| fmt.Fprintf(os.Stderr, "\nRemote directory %s will be deleted\n", b.Config.Workspace.Root) | ||
| if !b.AutoApprove { | ||
| proceed, err := logger.Ask(fmt.Sprintf("%s and all files in it will be %s Proceed?: ", b.Config.Workspace.Root, red("deleted permanently!"))) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if !proceed { | ||
| return nil, nil | ||
| } | ||
| } | ||
| err := b.WorkspaceClient().Workspace.Delete(ctx, workspace.Delete{ | ||
| Path: b.Config.Workspace.Root, | ||
| Recursive: true, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| fmt.Println("Successfully deleted files!") | ||
| return nil, nil | ||
| } | ||
| func Delete() bundle.Mutator { | ||
| return &delete{} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| package terraform | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "strings" | ||
| "github.com/databricks/bricks/bundle" | ||
| "github.com/databricks/bricks/libs/cmdio" | ||
| "github.com/fatih/color" | ||
| "github.com/hashicorp/terraform-exec/tfexec" | ||
| tfjson "github.com/hashicorp/terraform-json" | ||
| ) | ||
| // TODO: This is temporary. Come up with a robust way to log mutator progress and | ||
| // status events | ||
| 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 logDestroyPlan(l *cmdio.Logger, changes []*tfjson.ResourceChange) error { | ||
| // TODO: remove once we have mutator logging in place | ||
| fmt.Fprintln(os.Stderr, "The following resources will be removed: ") | ||
| for _, c := range changes { | ||
| if c.Change.Actions.Delete() { | ||
| l.Log(&PlanResourceChange{ | ||
| ResourceType: c.Type, | ||
| Action: "delete", | ||
| ResourceName: c.Name, | ||
| }) | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| type destroy struct{} | ||
| func (w *destroy) Name() string { | ||
| return "terraform.Destroy" | ||
| } | ||
| func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) { | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The destroy mutator right now is a generic "plan apply" mutator. We should consolidate after this PR is merged. | ||
| // interface to io with the user | ||
| logger, ok := cmdio.FromContext(ctx) | ||
| if !ok { | ||
| return nil, fmt.Errorf("no logger found") | ||
| } | ||
| if b.Plan.IsEmpty { | ||
| fmt.Fprintln(os.Stderr, "No resources to destroy!") | ||
| return nil, nil | ||
| } | ||
| tf := b.Terraform | ||
| if tf == nil { | ||
| return nil, fmt.Errorf("terraform not initialized") | ||
| } | ||
| // read plan file | ||
| plan, err := tf.ShowPlanFile(ctx, b.Plan.Path) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // print the resources that will be destroyed | ||
| err = logDestroyPlan(logger, plan.ResourceChanges) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // Ask for confirmation, if needed | ||
| if !b.Plan.ConfirmApply { | ||
| red := color.New(color.FgRed).SprintFunc() | ||
| b.Plan.ConfirmApply, err = logger.Ask(fmt.Sprintf("\nThis will permanently %s resources! Proceed? [y/n]: ", red("destroy"))) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
| // return if confirmation was not provided | ||
| if !b.Plan.ConfirmApply { | ||
| return nil, nil | ||
| } | ||
| if b.Plan.Path == "" { | ||
| return nil, fmt.Errorf("no plan found") | ||
| } | ||
| // Apply terraform according to the computed destroy plan | ||
| err = tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("terraform destroy: %w", err) | ||
| } | ||
| fmt.Fprintln(os.Stderr, "Successfully destroyed resources!") | ||
shreyas-goenka marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return nil, nil | ||
| } | ||
| // Destroy returns a [bundle.Mutator] that runs the conceptual equivalent of | ||
| // `terraform destroy ./plan` from the bundle's ephemeral working directory for Terraform. | ||
| func Destroy() bundle.Mutator { | ||
| return &destroy{} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package terraform | ||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "path/filepath" | ||
| "github.com/databricks/bricks/bundle" | ||
| "github.com/databricks/bricks/libs/terraform" | ||
| "github.com/hashicorp/terraform-exec/tfexec" | ||
| ) | ||
| type PlanGoal string | ||
| var ( | ||
| PlanDeploy = PlanGoal("deploy") | ||
| PlanDestroy = PlanGoal("destroy") | ||
| ) | ||
| type plan struct { | ||
| goal PlanGoal | ||
| } | ||
| func (p *plan) Name() string { | ||
| return "terraform.Plan" | ||
| } | ||
| func (p *plan) Apply(ctx context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) { | ||
| tf := b.Terraform | ||
| if tf == nil { | ||
| return nil, fmt.Errorf("terraform not initialized") | ||
| } | ||
| err := tf.Init(ctx, tfexec.Upgrade(true)) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("terraform init: %w", err) | ||
| } | ||
| // Persist computed plan | ||
| tfDir, err := Dir(b) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| planPath := filepath.Join(tfDir, "plan") | ||
| destroy := p.goal == PlanDestroy | ||
| notEmpty, err := tf.Plan(ctx, tfexec.Destroy(destroy), tfexec.Out(planPath)) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| // Set plan in main bundle struct for downstream mutators | ||
| b.Plan = &terraform.Plan{ | ||
| Path: planPath, | ||
| ConfirmApply: b.AutoApprove, | ||
| IsEmpty: !notEmpty, | ||
| } | ||
| return nil, nil | ||
| } | ||
| // Plan returns a [bundle.Mutator] that runs the equivalent of `terraform plan -out ./plan` | ||
| // from the bundle's ephemeral working directory for Terraform. | ||
| func Plan(goal PlanGoal) bundle.Mutator { | ||
| return &plan{ | ||
| goal: goal, | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package phases | ||
| import ( | ||
| "github.com/databricks/bricks/bundle" | ||
| "github.com/databricks/bricks/bundle/deploy/files" | ||
| "github.com/databricks/bricks/bundle/deploy/lock" | ||
| "github.com/databricks/bricks/bundle/deploy/terraform" | ||
| ) | ||
| // The destroy phase deletes artifacts and resources. | ||
| func Destroy() bundle.Mutator { | ||
| return newPhase( | ||
| "destroy", | ||
| []bundle.Mutator{ | ||
| lock.Acquire(), | ||
| terraform.StatePull(), | ||
| terraform.Plan(terraform.PlanGoal("destroy")), | ||
| terraform.Destroy(), | ||
| terraform.StatePush(), | ||
| lock.Release(), | ||
shreyas-goenka marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| files.Delete(), | ||
| }, | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package bundle | ||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "github.com/databricks/bricks/bundle" | ||
| "github.com/databricks/bricks/bundle/phases" | ||
| "github.com/databricks/bricks/cmd/root" | ||
| "github.com/databricks/bricks/libs/cmdio" | ||
| "github.com/databricks/bricks/libs/flags" | ||
| "github.com/spf13/cobra" | ||
| "golang.org/x/term" | ||
| ) | ||
| var destroyCmd = &cobra.Command{ | ||
| Use: "destroy", | ||
| Short: "Destroy deployed bundle resources", | ||
| PreRunE: root.MustConfigureBundle, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| b := bundle.Get(cmd.Context()) | ||
| // If `--force` is specified, force acquisition of the deployment lock. | ||
| b.Config.Bundle.Lock.Force = force | ||
| // If `--auto-approve`` is specified, we skip confirmation checks | ||
| b.AutoApprove = autoApprove | ||
| // we require auto-approve for non tty terminals since interactive consent | ||
| // is not possible | ||
| if !term.IsTerminal(int(os.Stderr.Fd())) && !autoApprove { | ||
shreyas-goenka marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| return fmt.Errorf("please specify --auto-approve to skip interactive confirmation checks for non tty consoles") | ||
| } | ||
| ctx := cmdio.NewContext(cmd.Context(), cmdio.NewLogger(flags.ModeAppend)) | ||
| return bundle.Apply(ctx, b, []bundle.Mutator{ | ||
| phases.Initialize(), | ||
| phases.Build(), | ||
| phases.Destroy(), | ||
| }) | ||
| }, | ||
| } | ||
| var autoApprove bool | ||
| func init() { | ||
| AddCommand(destroyCmd) | ||
| destroyCmd.Flags().BoolVar(&autoApprove, "auto-approve", false, "Skip interactive approvals for deleting resources and files") | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.