custom-targets/terraform/terraform-deployer/terraform.go (101 lines of code) (raw):

// Copyright 2023 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 // https://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 main import ( "bytes" "encoding/json" "fmt" "io" "os" "os/exec" ) const ( terraformBin = "terraform" ) // terraformInitOptions configures the args provided to `terraform init`. type terraformInitOptions struct { disableBackendInitialization bool disableModuleDownloads bool } // terraformInit runs `terraform init` in the provided directory. func terraformInit(workingDir string, opts *terraformInitOptions) ([]byte, error) { args := []string{"init", "-no-color"} if opts.disableBackendInitialization { args = append(args, "-backend=false") } if opts.disableModuleDownloads { args = append(args, "-get=false") } fmt.Printf("Running terraform init in %s\n", workingDir) return runCmd(terraformBin, args, false, setWorkingDir(workingDir)) } // terraformValidate runs `terraform validate` in the provided directory. func terraformValidate(workingDir string) ([]byte, error) { args := []string{"validate", "-no-color"} fmt.Printf("Running terraform validate in %s\n", workingDir) return runCmd(terraformBin, args, false, setWorkingDir(workingDir)) } // terraformPlan runs `terraform plan` in the provided directory and creates the // plan in the working directory with the provided file name. func terraformPlan(workingDir, planFile string) ([]byte, error) { args := []string{"plan", "-no-color", fmt.Sprintf("-out=%s", planFile)} fmt.Printf("Running terraform plan in %s\n", workingDir) return runCmd(terraformBin, args, false, setWorkingDir(workingDir)) } // terraformShowPlan runs `terraform show` in the provided directory for a provided // plan file. The output from this command is not written to stdout. func terraformShowPlan(workingDir, planFile string) ([]byte, error) { args := []string{"show", "-no-color", planFile} fmt.Printf("Running terraform show plan in %s\n", workingDir) return runCmd(terraformBin, args, true, setWorkingDir(workingDir)) } // terraformApplyOptions configures the args provided to `terraform apply`. type terraformApplyOptions struct { applyParallelism int lockTimeout string } // terraformApply runs `terraform apply` in the provided directory. func terraformApply(workingDir string, opts *terraformApplyOptions) ([]byte, error) { args := []string{"apply", "-auto-approve", "-no-color"} if len(opts.lockTimeout) != 0 { args = append(args, fmt.Sprintf("-lock-timeout=%s", opts.lockTimeout)) } if opts.applyParallelism > 0 { args = append(args, fmt.Sprintf("-parallelism=%d", opts.applyParallelism)) } fmt.Printf("Running terraform apply in %s\n", workingDir) return runCmd(terraformBin, args, false, setWorkingDir(workingDir)) } // terraformShowState runs `terraform show` in the provided directory. The output // from this command is not written to stdout. func terraformShowState(workingDir string) ([]byte, error) { args := []string{"show", "-json"} fmt.Printf("Running terraform show in %s\n", workingDir) out, err := runCmd(terraformBin, args, true, setWorkingDir(workingDir)) if err != nil { return nil, err } return addIndentationToJSON(out) } // addIndentationToJson returns a copy of the provided JSON with indentation added. // This is used to make the data more human-readable. func addIndentationToJSON(in []byte) ([]byte, error) { var pjson bytes.Buffer if err := json.Indent(&pjson, in, "", " "); err != nil { return nil, fmt.Errorf("error adding indentation to json: %v", err) } return pjson.Bytes(), nil } // commandOption configures an exec.Cmd object with additional options. type commandOption func(ce *exec.Cmd) // setWorkingDir returns a commandOption for setting the working directory. func setWorkingDir(workingDir string) commandOption { return func(cmd *exec.Cmd) { cmd.Dir = workingDir } } // runCmd starts and waits for the provided command with args to complete. If the command // succeeds it returns the stdout of the command. func runCmd(binPath string, args []string, closeOSStdout bool, options ...commandOption) ([]byte, error) { fmt.Printf("Running the following command: %s %s\n", binPath, args) cmd := exec.Command(binPath, args...) var stderr bytes.Buffer cmd.Stderr = io.MultiWriter(&stderr, os.Stderr) var stdout bytes.Buffer if closeOSStdout { cmd.Stdout = &stdout } else { cmd.Stdout = io.MultiWriter(&stdout, os.Stdout) } for _, opt := range options { opt(cmd) } if err := cmd.Start(); err != nil { return nil, fmt.Errorf("failed to start command: %v", err) } if err := cmd.Wait(); err != nil { return nil, fmt.Errorf("error running command: %v\n%s", err, stderr.Bytes()) } return stdout.Bytes(), nil }