databricks-cli/libs/plan/terraform.go

149 lines
3.8 KiB
Go

package plan
import (
"context"
"fmt"
"path/filepath"
"slices"
"sort"
"strings"
"github.com/databricks/cli/libs/log"
"github.com/hashicorp/terraform-exec/tfexec"
tfjson "github.com/hashicorp/terraform-json"
)
const planDirName = "plan"
// TODO: start using this. Also clean up the fields in the struct below.
type TerraformPlanOpts struct {
// If true, the plan will be a IsDestroy plan.
IsDestroy bool
// Path where the terraform project we'll compute the plan for is rooted at.
Root string
// Executable executable to compute the plan with.
Executable *tfexec.Terraform
}
// TODO: Remove any unnecessary fields in here.
type terraformPlan struct {
ctx context.Context
opts TerraformPlanOpts
// Path to the computed plan.
planPath string
// In memory representation of the computed plan, obtained by running terraform show.
tfPlan *tfjson.Plan
// TODO: Can we invert this?
notEmpty bool
}
// NewTerraformPlan creates a new Terraform plan for a terraform project rooted
// at the given path. If destroy is true, the plan will be a destroy plan.
func NewTerraformPlan(ctx context.Context, opts TerraformPlanOpts) (Plan, error) {
plan := &terraformPlan{
ctx: ctx,
opts: opts,
}
if plan.opts.Executable == nil {
return nil, fmt.Errorf("terraform executable not provided")
}
if plan.opts.Root == "" {
return nil, fmt.Errorf("root path to compute the terraform plan not provided")
}
// Initialize the terraform project
err := opts.Executable.Init(ctx, tfexec.Upgrade(true))
if err != nil {
return nil, fmt.Errorf("terraform init: %w", err)
}
// Path where the computed plan will be persisted.
planPath := filepath.Join(opts.Root, planDirName)
log.Debugf(ctx, "Computing terraform plan")
plan.notEmpty, err = opts.Executable.Plan(ctx, tfexec.Destroy(opts.IsDestroy), tfexec.Out(planPath))
if err != nil {
return nil, fmt.Errorf("terraform plan: %w", err)
}
log.Debugf(ctx, "Loading terraform plan by running terraform show")
plan.tfPlan, err = opts.Executable.ShowPlanFile(ctx, planPath)
if err != nil {
return nil, fmt.Errorf("terraform show: %w", err)
}
// Persist the plan path
plan.planPath = planPath
return plan, nil
}
func (p *terraformPlan) Path() string {
return p.planPath
}
// TODO: Add tests for this functionality.
func (p *terraformPlan) ActionsByTypes(query ...ActionType) []Action {
actions := make([]Action, 0)
for _, rc := range p.tfPlan.ResourceChanges {
if rc.Type == "databricks_permissions" {
// We don't need to print permissions changes.
continue
}
if rc.Type == "databricks_grants" {
// We don't need to print grants changes.
continue
}
tfActions := rc.Change.Actions
switch {
case slices.Contains(query, ActionTypeDelete) && tfActions.Delete():
actions = append(actions, Action{
rtype: strings.TrimPrefix(rc.Type, "databricks_"),
rname: rc.Name,
atype: ActionTypeDelete,
})
case slices.Contains(query, ActionTypeRecreate) && tfActions.Replace():
actions = append(actions, Action{
rtype: strings.TrimPrefix(rc.Type, "databricks_"),
rname: rc.Name,
atype: ActionTypeRecreate,
})
default:
// We don't need to track other action types yet.
}
}
// Sort by action type first, then by resource type, and finally by resource name
sort.Slice(actions, func(i, j int) bool {
if actions[i].atype != actions[j].atype {
return actions[i].atype < actions[j].atype
}
if actions[i].rtype != actions[j].rtype {
return actions[i].rtype < actions[j].rtype
}
return actions[i].rname < actions[j].rname
})
return actions
}
func (p *terraformPlan) IsEmpty() bool {
return !p.notEmpty
}
func (p *terraformPlan) Apply() error {
err := p.opts.Executable.Apply(p.ctx, tfexec.DirOrPlan(p.planPath))
if err != nil {
return fmt.Errorf("terraform apply: %w", err)
}
return nil
}