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 } } }