| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| 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 |
| delete(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 |
| } |
| } |
| } |