275 lines
6.0 KiB
Go
275 lines
6.0 KiB
Go
package tfexec
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-exec/internal/version"
|
|
)
|
|
|
|
const (
|
|
checkpointDisableEnvVar = "CHECKPOINT_DISABLE"
|
|
cliArgsEnvVar = "TF_CLI_ARGS"
|
|
inputEnvVar = "TF_INPUT"
|
|
automationEnvVar = "TF_IN_AUTOMATION"
|
|
logEnvVar = "TF_LOG"
|
|
logCoreEnvVar = "TF_LOG_CORE"
|
|
logPathEnvVar = "TF_LOG_PATH"
|
|
logProviderEnvVar = "TF_LOG_PROVIDER"
|
|
reattachEnvVar = "TF_REATTACH_PROVIDERS"
|
|
appendUserAgentEnvVar = "TF_APPEND_USER_AGENT"
|
|
workspaceEnvVar = "TF_WORKSPACE"
|
|
disablePluginTLSEnvVar = "TF_DISABLE_PLUGIN_TLS"
|
|
skipProviderVerifyEnvVar = "TF_SKIP_PROVIDER_VERIFY"
|
|
|
|
varEnvVarPrefix = "TF_VAR_"
|
|
cliArgEnvVarPrefix = "TF_CLI_ARGS_"
|
|
)
|
|
|
|
var prohibitedEnvVars = []string{
|
|
cliArgsEnvVar,
|
|
inputEnvVar,
|
|
automationEnvVar,
|
|
logEnvVar,
|
|
logCoreEnvVar,
|
|
logPathEnvVar,
|
|
logProviderEnvVar,
|
|
reattachEnvVar,
|
|
appendUserAgentEnvVar,
|
|
workspaceEnvVar,
|
|
disablePluginTLSEnvVar,
|
|
skipProviderVerifyEnvVar,
|
|
}
|
|
|
|
var prohibitedEnvVarPrefixes = []string{
|
|
varEnvVarPrefix,
|
|
cliArgEnvVarPrefix,
|
|
}
|
|
|
|
func manualEnvVars(env map[string]string, cb func(k string)) {
|
|
for k := range env {
|
|
for _, p := range prohibitedEnvVars {
|
|
if p == k {
|
|
cb(k)
|
|
goto NextEnvVar
|
|
}
|
|
}
|
|
for _, prefix := range prohibitedEnvVarPrefixes {
|
|
if strings.HasPrefix(k, prefix) {
|
|
cb(k)
|
|
goto NextEnvVar
|
|
}
|
|
}
|
|
NextEnvVar:
|
|
}
|
|
}
|
|
|
|
// ProhibitedEnv returns a slice of environment variable keys that are not allowed
|
|
// to be set manually from the passed environment.
|
|
func ProhibitedEnv(env map[string]string) []string {
|
|
var p []string
|
|
manualEnvVars(env, func(k string) {
|
|
p = append(p, k)
|
|
})
|
|
return p
|
|
}
|
|
|
|
// CleanEnv removes any prohibited environment variables from an environment map.
|
|
func CleanEnv(dirty map[string]string) map[string]string {
|
|
clean := dirty
|
|
manualEnvVars(clean, func(k string) {
|
|
delete(clean, k)
|
|
})
|
|
return clean
|
|
}
|
|
|
|
func envMap(environ []string) map[string]string {
|
|
env := map[string]string{}
|
|
for _, ev := range environ {
|
|
parts := strings.SplitN(ev, "=", 2)
|
|
if len(parts) == 0 {
|
|
continue
|
|
}
|
|
k := parts[0]
|
|
v := ""
|
|
if len(parts) == 2 {
|
|
v = parts[1]
|
|
}
|
|
env[k] = v
|
|
}
|
|
return env
|
|
}
|
|
|
|
func envSlice(environ map[string]string) []string {
|
|
env := []string{}
|
|
for k, v := range environ {
|
|
env = append(env, k+"="+v)
|
|
}
|
|
return env
|
|
}
|
|
|
|
func (tf *Terraform) buildEnv(mergeEnv map[string]string) []string {
|
|
// set Terraform level env, if env is nil, fall back to os.Environ
|
|
var env map[string]string
|
|
if tf.env == nil {
|
|
env = envMap(os.Environ())
|
|
} else {
|
|
env = make(map[string]string, len(tf.env))
|
|
for k, v := range tf.env {
|
|
env[k] = v
|
|
}
|
|
}
|
|
|
|
// override env with any command specific environment
|
|
for k, v := range mergeEnv {
|
|
env[k] = v
|
|
}
|
|
|
|
// always propagate CHECKPOINT_DISABLE env var unless it is
|
|
// explicitly overridden with tf.SetEnv or command env
|
|
if _, ok := env[checkpointDisableEnvVar]; !ok {
|
|
env[checkpointDisableEnvVar] = os.Getenv(checkpointDisableEnvVar)
|
|
}
|
|
|
|
// always override user agent
|
|
ua := mergeUserAgent(
|
|
os.Getenv(appendUserAgentEnvVar),
|
|
tf.appendUserAgent,
|
|
fmt.Sprintf("HashiCorp-terraform-exec/%s", version.ModuleVersion()),
|
|
)
|
|
env[appendUserAgentEnvVar] = ua
|
|
|
|
// always override logging
|
|
if tf.logPath == "" {
|
|
// so logging can't pollute our stderr output
|
|
env[logEnvVar] = ""
|
|
env[logCoreEnvVar] = ""
|
|
env[logPathEnvVar] = ""
|
|
env[logProviderEnvVar] = ""
|
|
} else {
|
|
env[logEnvVar] = tf.log
|
|
env[logCoreEnvVar] = tf.logCore
|
|
env[logPathEnvVar] = tf.logPath
|
|
env[logProviderEnvVar] = tf.logProvider
|
|
}
|
|
|
|
// constant automation override env vars
|
|
env[automationEnvVar] = "1"
|
|
|
|
// force usage of workspace methods for switching
|
|
env[workspaceEnvVar] = ""
|
|
|
|
if tf.disablePluginTLS {
|
|
env[disablePluginTLSEnvVar] = "1"
|
|
}
|
|
|
|
if tf.skipProviderVerify {
|
|
env[skipProviderVerifyEnvVar] = "1"
|
|
}
|
|
|
|
return envSlice(env)
|
|
}
|
|
|
|
func (tf *Terraform) buildTerraformCmd(ctx context.Context, mergeEnv map[string]string, args ...string) *exec.Cmd {
|
|
cmd := exec.CommandContext(ctx, tf.execPath, args...)
|
|
|
|
cmd.Env = tf.buildEnv(mergeEnv)
|
|
cmd.Dir = tf.workingDir
|
|
|
|
tf.logger.Printf("[INFO] running Terraform command: %s", cmd.String())
|
|
|
|
return cmd
|
|
}
|
|
|
|
func (tf *Terraform) runTerraformCmdJSON(ctx context.Context, cmd *exec.Cmd, v interface{}) error {
|
|
var outbuf = bytes.Buffer{}
|
|
cmd.Stdout = mergeWriters(cmd.Stdout, &outbuf)
|
|
|
|
err := tf.runTerraformCmd(ctx, cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dec := json.NewDecoder(&outbuf)
|
|
dec.UseNumber()
|
|
return dec.Decode(v)
|
|
}
|
|
|
|
// mergeUserAgent does some minor deduplication to ensure we aren't
|
|
// just using the same append string over and over.
|
|
func mergeUserAgent(uas ...string) string {
|
|
included := map[string]bool{}
|
|
merged := []string{}
|
|
for _, ua := range uas {
|
|
ua = strings.TrimSpace(ua)
|
|
|
|
if ua == "" {
|
|
continue
|
|
}
|
|
if included[ua] {
|
|
continue
|
|
}
|
|
included[ua] = true
|
|
merged = append(merged, ua)
|
|
}
|
|
return strings.Join(merged, " ")
|
|
}
|
|
|
|
func mergeWriters(writers ...io.Writer) io.Writer {
|
|
compact := []io.Writer{}
|
|
for _, w := range writers {
|
|
if w != nil {
|
|
compact = append(compact, w)
|
|
}
|
|
}
|
|
if len(compact) == 0 {
|
|
return ioutil.Discard
|
|
}
|
|
if len(compact) == 1 {
|
|
return compact[0]
|
|
}
|
|
return io.MultiWriter(compact...)
|
|
}
|
|
|
|
func writeOutput(ctx context.Context, r io.ReadCloser, w io.Writer) error {
|
|
// ReadBytes will block until bytes are read, which can cause a delay in
|
|
// returning even if the command's context has been canceled. Use a separate
|
|
// goroutine to prompt ReadBytes to return on cancel
|
|
closeCtx, closeCancel := context.WithCancel(ctx)
|
|
defer closeCancel()
|
|
go func() {
|
|
select {
|
|
case <-ctx.Done():
|
|
r.Close()
|
|
case <-closeCtx.Done():
|
|
return
|
|
}
|
|
}()
|
|
|
|
buf := bufio.NewReader(r)
|
|
for {
|
|
line, err := buf.ReadBytes('\n')
|
|
if len(line) > 0 {
|
|
if _, err := w.Write(line); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
}
|
|
}
|