diff --git a/go.mod b/go.mod index 462f9dc7..12845c25 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/davecgh/go-spew v1.1.1 github.com/hashicorp/go-checkpoint v0.5.0 + github.com/hashicorp/go-cleanhttp v0.5.0 github.com/hashicorp/go-getter v1.4.0 github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/terraform-json v0.5.0 diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 00000000..4e0f0d4d --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,9 @@ +package version + +const version = "0.7.0" + +// ModuleVersion returns the current version of the github.com/hashicorp/terraform-exec Go module. +// This is a function to allow for future possible enhancement using debug.BuildInfo. +func ModuleVersion() string { + return version +} diff --git a/scripts/release/release.sh b/scripts/release/release.sh index f87e0057..2e4f8c98 100755 --- a/scripts/release/release.sh +++ b/scripts/release/release.sh @@ -6,7 +6,7 @@ set -x # release.sh will: # 1. Modify changelog # 2. Run changelog links script -# 3. Modify version in tfinstall/version.go +# 3. Modify version in internal/version/version.go # 4. Commit and push changes # 5. Create a Git tag @@ -59,13 +59,13 @@ function changelogMain { } function modifyVersionFiles { - sed -i "s/const Version =.*/const Version = \"${TARGET_VERSION}\"/" tfinstall/version.go + sed -i "s/const version =.*/const version = \"${TARGET_VERSION}\"/" internal/version/version.go } function commitChanges { git add CHANGELOG.md modifyVersionFiles - git add tfinstall/version.go + git add internal/version/version.go if [ "$CI" = true ] ; then git commit --gpg-sign="${GPG_KEY_ID}" -m "v${TARGET_VERSION} [skip ci]" diff --git a/tfexec/cmd.go b/tfexec/cmd.go index f5457769..a496590a 100644 --- a/tfexec/cmd.go +++ b/tfexec/cmd.go @@ -2,10 +2,13 @@ package tfexec import ( "context" + "fmt" "io" "os" "os/exec" "strings" + + "github.com/hashicorp/terraform-exec/internal/version" ) const ( @@ -15,6 +18,7 @@ const ( automationEnvVar = "TF_IN_AUTOMATION" logPathEnvVar = "TF_LOG_PATH" reattachEnvVar = "TF_REATTACH_PROVIDERS" + appendUserAgentEnvVar = "TF_APPEND_USER_AGENT" varEnvVarPrefix = "TF_VAR_" ) @@ -25,6 +29,7 @@ var prohibitedEnvVars = []string{ logPathEnvVar, logEnvVar, reattachEnvVar, + appendUserAgentEnvVar, } func envMap(environ []string) map[string]string { @@ -75,6 +80,14 @@ func (tf *Terraform) buildEnv(mergeEnv map[string]string) []string { 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 @@ -123,3 +136,23 @@ func (tf *Terraform) runTerraformCmd(cmd *exec.Cmd) error { } return nil } + +// 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, " ") +} diff --git a/tfexec/cmd_test.go b/tfexec/cmd_test.go index 67dd3f68..ebc0bf4b 100644 --- a/tfexec/cmd_test.go +++ b/tfexec/cmd_test.go @@ -1,16 +1,47 @@ package tfexec import ( + "fmt" "os/exec" "strings" "testing" + + "github.com/hashicorp/terraform-exec/internal/version" ) -var defaultEnv = []string{ - "TF_LOG=", - "TF_LOG_PATH=", - "TF_IN_AUTOMATION=1", - "CHECKPOINT_DISABLE=", +func TestMergeUserAgent(t *testing.T) { + for i, c := range []struct { + expected string + uas []string + }{ + {"foo/1 bar/2", []string{"foo/1", "bar/2"}}, + {"foo/1 bar/2", []string{"foo/1 bar/2"}}, + {"foo/1 bar/2", []string{"", "foo/1", "bar/2"}}, + {"foo/1 bar/2", []string{"", "foo/1 bar/2"}}, + {"foo/1 bar/2", []string{" ", "foo/1 bar/2"}}, + {"foo/1 bar/2", []string{"foo/1", "", "bar/2"}}, + {"foo/1 bar/2", []string{"foo/1", " ", "bar/2"}}, + + // comments + {"foo/1 (bar/1 bar/2 bar/3) bar/2", []string{"foo/1 (bar/1 bar/2 bar/3)", "bar/2"}}, + } { + t.Run(fmt.Sprintf("%d %s", i, c.expected), func(t *testing.T) { + actual := mergeUserAgent(c.uas...) + if c.expected != actual { + t.Fatalf("expected %q, got %q", c.expected, actual) + } + }) + } +} + +func defaultEnv() []string { + return []string{ + "TF_APPEND_USER_AGENT=HashiCorp-terraform-exec/" + version.ModuleVersion(), + "TF_LOG=", + "TF_LOG_PATH=", + "TF_IN_AUTOMATION=1", + "CHECKPOINT_DISABLE=", + } } func assertCmd(t *testing.T, expectedArgs []string, expectedEnv map[string]string, actual *exec.Cmd) { @@ -29,7 +60,7 @@ func assertCmd(t *testing.T, expectedArgs []string, expectedEnv map[string]strin } // check environment - expectedEnv = envMap(append(defaultEnv, envSlice(expectedEnv)...)) + expectedEnv = envMap(append(defaultEnv(), envSlice(expectedEnv)...)) // compare against raw slice len incase of duplication or something if len(expectedEnv) != len(actual.Env) { diff --git a/tfexec/errors.go b/tfexec/errors.go index 933d5ba2..5df5709c 100644 --- a/tfexec/errors.go +++ b/tfexec/errors.go @@ -36,7 +36,7 @@ func parseError(err error, stderr string) error { break } } - + return &ErrMissingVar{name} case usageRegexp.MatchString(stderr): return &ErrCLIUsage{stderr: stderr} diff --git a/tfexec/internal/e2etest/errors_test.go b/tfexec/internal/e2etest/errors_test.go index f71262c1..765037ca 100644 --- a/tfexec/internal/e2etest/errors_test.go +++ b/tfexec/internal/e2etest/errors_test.go @@ -41,7 +41,7 @@ func TestMissingVar(t *testing.T) { err := tf.Init(context.Background()) if err != nil { t.Fatalf("err during init: %s", err) - } + } err = tf.Plan(context.Background()) if err == nil { diff --git a/tfexec/terraform.go b/tfexec/terraform.go index 3c67677b..b68b5226 100644 --- a/tfexec/terraform.go +++ b/tfexec/terraform.go @@ -17,9 +17,10 @@ type printfer interface { } type Terraform struct { - execPath string - workingDir string - env map[string]string + execPath string + workingDir string + appendUserAgent string + env map[string]string stdout io.Writer stderr io.Writer @@ -108,6 +109,12 @@ func (tf *Terraform) SetLogPath(path string) error { return nil } +// SetAppendUserAgent sets the TF_APPEND_USER_AGENT environment variable for +// Terraform CLI execution. +func (tf *Terraform) SetAppendUserAgent(ua string) { + tf.appendUserAgent = ua +} + // WorkingDir returns the working directory for Terraform. func (tf *Terraform) WorkingDir() string { return tf.workingDir diff --git a/tfinstall/http.go b/tfinstall/http.go new file mode 100644 index 00000000..a38e8d10 --- /dev/null +++ b/tfinstall/http.go @@ -0,0 +1,37 @@ +package tfinstall + +import ( + "fmt" + "net/http" + "os" + "strings" + + cleanhttp "github.com/hashicorp/go-cleanhttp" + + intversion "github.com/hashicorp/terraform-exec/internal/version" +) + +type userAgentRoundTripper struct { + inner http.RoundTripper + userAgent string +} + +func (rt *userAgentRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if _, ok := req.Header["User-Agent"]; !ok { + req.Header.Set("User-Agent", rt.userAgent) + } + return rt.inner.RoundTrip(req) +} + +func newHTTPClient() *http.Client { + appendUA := os.Getenv("TF_APPEND_USER_AGENT") + userAgent := strings.TrimSpace(fmt.Sprintf("HashiCorp-tfinstall/%s %s", intversion.ModuleVersion(), appendUA)) + + cli := cleanhttp.DefaultPooledClient() + cli.Transport = &userAgentRoundTripper{ + userAgent: userAgent, + inner: cli.Transport, + } + + return cli +} diff --git a/tfinstall/tfinstall.go b/tfinstall/tfinstall.go index c343abe6..dc0863bc 100644 --- a/tfinstall/tfinstall.go +++ b/tfinstall/tfinstall.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" "log" - "net/http" "os" "os/exec" "path/filepath" @@ -162,11 +161,9 @@ func downloadWithVerification(tfVersion string, installDir string) (string, erro } - // setup: getter client - httpHeader := make(http.Header) - httpHeader.Set("User-Agent", "HashiCorp-tfinstall/"+Version) httpGetter := &getter.HttpGetter{ - Netrc: true, + Netrc: true, + Client: newHTTPClient(), } client := getter.Client{ Getters: map[string]getter.Getter{ diff --git a/tfinstall/version.go b/tfinstall/version.go deleted file mode 100644 index 24649f02..00000000 --- a/tfinstall/version.go +++ /dev/null @@ -1,4 +0,0 @@ -package tfinstall - -// Version is the tfinstall package version, used in user agent headers -const Version = "0.7.0"