internal/terraform/tf.go (202 lines of code) (raw):

/* Copyright © 2024 Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package terraform // TODO: figure out how to set stdout and stderr in tfexec and log lines import ( "context" "encoding/json" "errors" "fmt" "os" "os/exec" "strings" "time" "github.com/hashicorp/terraform-exec/tfexec" ) func TfInit(dir string, m bool, verbose bool) error { //var w string var tfInitOptions []tfexec.InitOption ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) // If true, we need to migrate the state to a remote location if m { tfInitOptions = append(tfInitOptions, tfexec.ForceCopy(m)) } err := tf.Init(ctx, tfInitOptions...) return err } func TfPlan( dir string, varFiles []string, vars []Vars, verbose bool, ) PlanResult { var tfPlanOptions []tfexec.PlanOption var result PlanResult // Find the binary and setup the client ctx := context.Background() p, err := findBinary() if err != nil { result.Err = err return result } tf, err := buildClient(dir, p, verbose) if err != nil { result.Err = err return result } // Create the plan file coordinates tmpDir, err := os.MkdirTemp(dir, "pastures") if err != nil { result.Err = err return result } defer os.RemoveAll(tmpDir) planPath := fmt.Sprintf( "%s/%s-%v", tmpDir, "pastureplan", time.Now().Unix(), ) // Set the plan out target tfPlanOptions = append(tfPlanOptions, tfexec.Out(planPath)) // include var files if they're provided for _, v := range varFiles { tfPlanOptions = append(tfPlanOptions, tfexec.VarFile(v)) } // load up vars if they're supplied for _, v := range vars { assignment := v.Key + "=" + v.Value tfPlanOptions = append(tfPlanOptions, tfexec.Var(assignment)) } // Run the plan _, err = tf.Plan(ctx, tfPlanOptions...) // TODO: tf validate before if err != nil { result.Err = err return result } // Return the plan, or an error plan, err := tf.ShowPlanFileRaw(ctx, planPath) if err != nil { result.Err = err return result } result.Plan = plan return result } func TfApply( dir string, varFiles []string, vars []*Vars, targets []string, verbose bool, ) error { var tfApplyOptions []tfexec.ApplyOption // find the binary and setup the client ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) // include a var files if they're provided for _, v := range varFiles { tfApplyOptions = append(tfApplyOptions, tfexec.VarFile(v)) } // load up vars if they're provided for _, v := range vars { assignment := v.Key + "=" + v.Value tfApplyOptions = append(tfApplyOptions, tfexec.Var(assignment)) } // an edge case, but account for specific targets if they're supplied for _, t := range targets { tfApplyOptions = append(tfApplyOptions, tfexec.Target(t)) } // do what we came here to do err := tf.Apply(ctx, tfApplyOptions...) // TODO: tf validate before if err != nil { return err } // TODO: return output in case the caller is interested return nil } func TfDestroy( dir string, varFiles []string, vars []*Vars, targets []string, verbose bool, ) error { var tfDestroyOptions []tfexec.DestroyOption // find the binary and setup the client ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) // include a var files if they're provided for _, v := range varFiles { tfDestroyOptions = append(tfDestroyOptions, tfexec.VarFile(v)) } // load up vars if they're provided for _, v := range vars { assignment := v.Key + "=" + v.Value tfDestroyOptions = append(tfDestroyOptions, tfexec.Var(assignment)) } // an edge case, but account for specific targets if they're supplied for _, t := range targets { tfDestroyOptions = append(tfDestroyOptions, tfexec.Target(t)) } // do what we came here to do err := tf.Destroy(ctx, tfDestroyOptions...) // TODO: tf validate before if err != nil { return err } return nil } func TfOutput(dir string, outputVar string, verbose bool) (string, error) { var output string ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) outputs, err := tf.Output(ctx) if err != nil { return "", err } if outputVar != "" { raw := string(outputs[outputVar].Value) output = strings.Trim(raw, `"`) // Asked for a value not found in outputs if output == "" { err := errors.New("output value not found") return "", err } } else { bytes, _ := json.Marshal(outputs) output = string(bytes) } return output, nil } func TfShow(dir string, verbose bool) (string, error) { ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) _, err := tf.Show(ctx) // TODO: actually catch state if no error if err != nil { return "", err } return "", err // TODO: actually export state } func TfPull(dir string, verbose bool) (string, error) { ctx := context.Background() tf, _ := initializeTerraformClient(dir, verbose) s, err := tf.StatePull(ctx) if err != nil { return "", err } return s, nil } func NewVars() *Vars { return &Vars{} } func AddVar(k string, v string) *Vars { return &Vars{ Key: k, Value: v, } } func buildClient(d string, p string, v bool) (*tfexec.Terraform, error) { tf, err := tfexec.NewTerraform(d, p) if err != nil { return nil, err // error building Terraform client } if v { tf.SetStdout(os.Stdout) // Write tf logs to stdout } return tf, nil } func findBinary() (string, error) { execPath, err := exec.LookPath("terraform") if err != nil { return "", err // unable to find the terraform binary in PATH } return execPath, nil } func initializeTerraformClient(dir string, v bool) (*tfexec.Terraform, error) { p, err := findBinary() if err != nil { return nil, err } tf, err := buildClient(dir, p, v) if err != nil { return nil, err } return tf, nil }