Use precomputed terraform plan for `bundle deploy` (#1640)

# Changes
With https://github.com/databricks/cli/pull/1413 we started to compute
and partially print the plan if it contained deletion of UC schemas.
This PR uses the precomputed plan to avoid double planning when actually
doing the terraform plan.

This fixes a performance regression introduced in
https://github.com/databricks/cli/pull/1413.

# Tests

Tested manually.
1. Verified bundle deployment still works and deploys resources.
2. Verified that the precomputed plan is indeed being used by attaching
a debugger and removing the plan file right before the terraform apply
process is spawned and asserting that terraform apply fails because the
plan is not found.
This commit is contained in:
shreyas-goenka 2024-07-31 19:37:25 +05:30 committed by GitHub
parent 1fb8e324d5
commit c454c2fd10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 25 additions and 57 deletions

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"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/log"
"github.com/hashicorp/terraform-exec/tfexec" "github.com/hashicorp/terraform-exec/tfexec"
@ -17,28 +16,32 @@ func (w *apply) Name() string {
} }
func (w *apply) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { func (w *apply) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// return early if plan is empty
if b.Plan.IsEmpty {
log.Debugf(ctx, "No changes in plan. Skipping terraform apply.")
return nil
}
tf := b.Terraform tf := b.Terraform
if tf == nil { if tf == nil {
return diag.Errorf("terraform not initialized") return diag.Errorf("terraform not initialized")
} }
cmdio.LogString(ctx, "Deploying resources...") if b.Plan.Path == "" {
return diag.Errorf("no plan found")
err := tf.Init(ctx, tfexec.Upgrade(true))
if err != nil {
return diag.Errorf("terraform init: %v", err)
} }
err = tf.Apply(ctx) // Apply terraform according to the computed plan
err := tf.Apply(ctx, tfexec.DirOrPlan(b.Plan.Path))
if err != nil { if err != nil {
return diag.Errorf("terraform apply: %v", err) return diag.Errorf("terraform apply: %v", err)
} }
log.Infof(ctx, "Resource deployment completed") log.Infof(ctx, "terraform apply completed")
return nil return nil
} }
// Apply returns a [bundle.Mutator] that runs the equivalent of `terraform apply` // Apply returns a [bundle.Mutator] that runs the equivalent of `terraform apply ./plan`
// from the bundle's ephemeral working directory for Terraform. // from the bundle's ephemeral working directory for Terraform.
func Apply() bundle.Mutator { func Apply() bundle.Mutator {
return &apply{} return &apply{}

View File

@ -1,46 +0,0 @@
package terraform
import (
"context"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/log"
"github.com/hashicorp/terraform-exec/tfexec"
)
type destroy struct{}
func (w *destroy) Name() string {
return "terraform.Destroy"
}
func (w *destroy) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// return early if plan is empty
if b.Plan.IsEmpty {
log.Debugf(ctx, "No resources to destroy in plan. Skipping destroy.")
return nil
}
tf := b.Terraform
if tf == nil {
return diag.Errorf("terraform not initialized")
}
if b.Plan.Path == "" {
return diag.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 diag.Errorf("terraform destroy: %v", err)
}
return 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{}
}

View File

@ -2,6 +2,8 @@ package terraform
import ( import (
"context" "context"
"errors"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
@ -34,6 +36,12 @@ func (l *statePush) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostic
// Expect the state file to live under dir. // Expect the state file to live under dir.
local, err := os.Open(filepath.Join(dir, TerraformStateFileName)) local, err := os.Open(filepath.Join(dir, TerraformStateFileName))
if errors.Is(err, fs.ErrNotExist) {
// The state file can be absent if terraform apply is skipped because
// there are no changes to apply in the plan.
log.Debugf(ctx, "Local terraform state file does not exist.")
return nil
}
if err != nil { if err != nil {
return diag.FromErr(err) return diag.FromErr(err)
} }

View File

@ -92,7 +92,10 @@ func Deploy() bundle.Mutator {
// Core mutators that CRUD resources and modify deployment state. These // Core mutators that CRUD resources and modify deployment state. These
// mutators need informed consent if they are potentially destructive. // mutators need informed consent if they are potentially destructive.
deployCore := bundle.Defer( deployCore := bundle.Defer(
terraform.Apply(), bundle.Seq(
bundle.LogString("Deploying resources..."),
terraform.Apply(),
),
bundle.Seq( bundle.Seq(
terraform.StatePush(), terraform.StatePush(),
terraform.Load(), terraform.Load(),

View File

@ -82,7 +82,7 @@ func approvalForDestroy(ctx context.Context, b *bundle.Bundle) (bool, error) {
func Destroy() bundle.Mutator { func Destroy() bundle.Mutator {
// Core destructive mutators for destroy. These require informed user consent. // Core destructive mutators for destroy. These require informed user consent.
destroyCore := bundle.Seq( destroyCore := bundle.Seq(
terraform.Destroy(), terraform.Apply(),
terraform.StatePush(), terraform.StatePush(),
files.Delete(), files.Delete(),
bundle.LogString("Destroy complete!"), bundle.LogString("Destroy complete!"),