Skip to content

Commit

Permalink
Merge pull request #105 from VinozzZ/user-agent
Browse files Browse the repository at this point in the history
Set azure user agent env for azure provider
  • Loading branch information
VinozzZ committed Jan 12, 2023
2 parents 45f6c31 + 7a1c13a commit 17f2d52
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 33 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,28 @@ Then use `initFile` to specify the relative path to this file within workingDir.
This will dramatically improve Docker image layer caching and performance when building, publishing and installing the bundle.
> Note: this approach isn't suitable when using terraform modules as those need to be "initilized" as well but aren't specified in the `initFile`. You shouldn't specifiy an `initFile` in this situation.
### User Agent Opt Out

When you declare the mixin, you can disable the mixin from customizing the azure user agent string

```yaml
mixins:
- terraform:
userAgentOptOut: true
```

By default, the terraform mixin adds the porter and mixin version to the user agent string used by the azure provider.
We use this to understand which version of porter and the mixin are being used by a bundle, and assist with troubleshooting.
Below is an example of what the user agent string looks like:

```
AZURE_HTTP_USER_AGENT="getporter/porter/v1.0.0 getporter/terraform/v1.2.3"
```

You can add your own custom strings to the user agent string by editing your [template Dockerfile] and setting the AZURE_HTTP_USER_AGENT environment variable.

[template Dockerfile]: https://getporter.org/bundle/custom-dockerfile/

## Terraform state

### Let Porter do the heavy lifting
Expand Down
25 changes: 22 additions & 3 deletions pkg/terraform/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
)

const dockerfileLines = `
ENV PORTER_TERRAFORM_MIXIN_USER_AGENT_OPT_OUT="{{ .UserAgentOptOut}}"
ENV AZURE_HTTP_USER_AGENT="{{ .AzureUserAgent }}"
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y wget unzip && \
wget https://releases.hashicorp.com/terraform/{{.ClientVersion}}/terraform_{{.ClientVersion}}_linux_amd64.zip --progress=dot:giga && \
Expand All @@ -32,9 +34,21 @@ type BuildInput struct {
// - terraform:
// version: 0.12.17
type MixinConfig struct {
// ClientVersion is the version of the terraform CLI to install
ClientVersion string `yaml:"clientVersion,omitempty"`
InitFile string `yaml:"initFile,omitempty"`
WorkingDir string `yaml:"workingDir,omitempty"`

// UserAgentOptOut allows a bundle author to opt out from adding porter and the mixin's version to the terraform user agent string.
UserAgentOptOut bool `yaml:"userAgentOptOut,omitempty"`

InitFile string `yaml:"initFile,omitempty"`
WorkingDir string `yaml:"workingDir,omitempty"`
}

type buildConfig struct {
MixinConfig

// AzureUserAgent is the contents of the azure user agent environment variable
AzureUserAgent string
}

func (m *Mixin) Build(ctx context.Context) error {
Expand All @@ -54,5 +68,10 @@ func (m *Mixin) Build(ctx context.Context) error {
return errors.Wrapf(err, "error parsing terraform mixin Dockerfile template")
}

return tmpl.Execute(m.Out, input.Config)
cfg := buildConfig{MixinConfig: *input.Config}
if !input.Config.UserAgentOptOut {
cfg.AzureUserAgent = m.userAgent
}

return tmpl.Execute(m.Out, cfg)
}
61 changes: 35 additions & 26 deletions pkg/terraform/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,44 @@ import (
"io/ioutil"
"testing"

"get.porter.sh/mixin/terraform/pkg"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMixin_Build(t *testing.T) {
t.Run("build with the default Terraform version", func(t *testing.T) {
m := NewTestMixin(t)

ctx := context.Background()
err := m.Build(ctx)
require.NoError(t, err)

gotOutput := m.TestContext.GetOutput()
assert.Contains(t, gotOutput, "https://releases.hashicorp.com/terraform/1.2.9/terraform_1.2.9_linux_amd64.zip")
assert.NotContains(t, "{{.", gotOutput, "Not all of the template values were consumed")
})

t.Run("build with custom Terrafrom version", func(t *testing.T) {
b, err := ioutil.ReadFile("testdata/build-input-with-version.yaml")
require.NoError(t, err)

ctx := context.Background()
m := NewTestMixin(t)
m.In = bytes.NewReader(b)
err = m.Build(ctx)
require.NoError(t, err)

gotOutput := m.TestContext.GetOutput()
assert.Contains(t, gotOutput, "https://releases.hashicorp.com/terraform/0.13.0-rc1/terraform_0.13.0-rc1_linux_amd64.zip")
assert.NotContains(t, "{{.", gotOutput, "Not all of the template values were consumed")
})
testcases := []struct {
name string
inputFile string
expectedVersion string
expectedUserAgent string
}{
{name: "build with custom config", inputFile: "testdata/build-input-with-config.yaml", expectedVersion: "https://releases.hashicorp.com/terraform/0.13.0-rc1/terraform_0.13.0-rc1_linux_amd64.zip", expectedUserAgent: "ENV PORTER_TERRAFORM_MIXIN_USER_AGENT_OPT_OUT=\"true\"\nENV AZURE_HTTP_USER_AGENT=\"\""},
{name: "build with the default Terraform config", expectedVersion: "https://releases.hashicorp.com/terraform/1.2.9/terraform_1.2.9_linux_amd64.zip", expectedUserAgent: "ENV PORTER_TERRAFORM_MIXIN_USER_AGENT_OPT_OUT=\"false\"\nENV AZURE_HTTP_USER_AGENT=\"getporter/porter getporter/terraform/v1.2.3"},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
// Set a fake version of the mixin and porter for our user agent
pkg.Version = "v1.2.3"

var data []byte
var err error
if tc.inputFile != "" {
data, err = ioutil.ReadFile(tc.inputFile)
require.NoError(t, err)
}

m := NewTestMixin(t)
m.In = bytes.NewReader(data)

err = m.Build(context.Background())
require.NoError(t, err, "build failed")

gotOutput := m.TestContext.GetOutput()
assert.Contains(t, gotOutput, tc.expectedVersion)
assert.Contains(t, gotOutput, tc.expectedUserAgent)
assert.NotContains(t, "{{.", gotOutput, "Not all of the template values were consumed")
})
}
}
63 changes: 63 additions & 0 deletions pkg/terraform/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package terraform

import (
"strconv"
"strings"

"get.porter.sh/porter/pkg"
)

const (
// AzureUserAgentEnvVar is the environment variable used by the azure provider to set
// the user agent string sent to Azure.
AzureUserAgentEnvVar = "AZURE_HTTP_USER_AGENT"

// UserAgentOptOutEnvVar is the name of the environment variable that disables
// user agent reporting.
UserAgentOptOutEnvVar = "PORTER_TERRAFORM_MIXIN_USER_AGENT_OPT_OUT"
)

// SetUserAgent sets the AZURE_HTTP_USER_AGENT environment variable with
// the full user agent string, which includes both a portion for porter and the
// mixin.
func (m *Mixin) SetUserAgent() {
// Check if PORTER_TERRAFORM_MIXIN_USER_AGENT_OPT_OUT=true, which disables editing the user agent string
if optOut, _ := strconv.ParseBool(m.Getenv(UserAgentOptOutEnvVar)); optOut {
return
}

// Check if we have already set the user agent
if m.userAgent != "" {
return
}

porterUserAgent := pkg.UserAgent()
mixinUserAgent := m.GetMixinUserAgent()
userAgent := []string{porterUserAgent, mixinUserAgent}
// Append porter and the mixin's version to the user agent string. Some clouds and
// environments will have set the environment variable already and we don't want
// to clobber it.
value := strings.Join(userAgent, " ")
if agentStr, ok := m.LookupEnv(AzureUserAgentEnvVar); ok {

// Check if we have already set the user agent
if strings.Contains(agentStr, value) {
value = agentStr
} else {
userAgent = append(userAgent, agentStr)
value = strings.Join(userAgent, " ")
}
}

m.userAgent = value

// Set the environment variable so that when we call the
// azure provider, it's automatically passed too.
m.Setenv(AzureUserAgentEnvVar, m.userAgent)
}

// GetMixinUserAgent returns the portion of the user agent string for the mixin.
func (m *Mixin) GetMixinUserAgent() string {
v := m.Version()
return "getporter/" + v.Name + "/" + v.Version
}
88 changes: 88 additions & 0 deletions pkg/terraform/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package terraform

import (
"os"
"testing"

"get.porter.sh/mixin/terraform/pkg"
"get.porter.sh/porter/pkg/runtime"
"github.com/stretchr/testify/require"
)

func TestMixinSetsUserAgentEnvVar(t *testing.T) {
// CI sets this value and we need to clear it out to make the test reproducible
os.Unsetenv(AzureUserAgentEnvVar)

t.Run("sets env var", func(t *testing.T) {
pkg.Commit = "abc123"
pkg.Version = "v1.2.3"
m := New()
expected := "getporter/porter getporter/terraform/" + pkg.Version
require.Equal(t, expected, m.Getenv(AzureUserAgentEnvVar))
require.Equal(t, expected, m.userAgent, "validate we remember the user agent string for later")
})
t.Run("edits env var", func(t *testing.T) {
os.Unsetenv(AzureUserAgentEnvVar)
// Validate that if the user customizations of the env var are preserved
pkg.Commit = "abc123"
pkg.Version = "v1.2.3"
cfg := runtime.NewConfig()
customUserAgent := "mycustom/v1.2.3"
cfg.Setenv(AzureUserAgentEnvVar, customUserAgent)
m := NewFor(cfg)
expected := "getporter/porter getporter/terraform/v1.2.3 mycustom/v1.2.3"
require.Equal(t, expected, m.Getenv(AzureUserAgentEnvVar))
require.Equal(t, expected, m.userAgent, "validate we remember the user agent string for later")
})

t.Run("env var already set", func(t *testing.T) {
// Validate that calling multiple times doesn't make a messed up env var
os.Unsetenv(AzureUserAgentEnvVar)
pkg.Commit = "abc123"
pkg.Version = "v1.2.3"
cfg := runtime.NewConfig()
customUserAgent := "getporter/porter getporter/terraform/v1.2.3"
cfg.Setenv(AzureUserAgentEnvVar, customUserAgent)
m := New()
m.SetUserAgent()
expected := "getporter/porter getporter/terraform/v1.2.3"
require.Equal(t, expected, m.Getenv(AzureUserAgentEnvVar))
require.Equal(t, expected, m.userAgent, "validate we remember the user agent string for later")
})
t.Run("call multiple times", func(t *testing.T) {
// Validate that calling multiple times doesn't make a messed up env var
os.Unsetenv(AzureUserAgentEnvVar)
pkg.Commit = "abc123"
pkg.Version = "v1.2.3"
m := New()
m.SetUserAgent()
m.SetUserAgent()
expected := "getporter/porter getporter/terraform/v1.2.3"
require.Equal(t, expected, m.Getenv(AzureUserAgentEnvVar))
require.Equal(t, expected, m.userAgent, "validate we remember the user agent string for later")
})
}

func TestMixinSetsUserAgentEnvVar_OptOut(t *testing.T) {
// CI sets this value and we need to clear it out to make the test reproducible
os.Unsetenv(AzureUserAgentEnvVar)

t.Run("opt-out", func(t *testing.T) {
// Validate that at runtime when we are calling the az cli, that if the bundle author opted-out, we don't set the user agent string
cfg := runtime.NewConfig()
cfg.Setenv(UserAgentOptOutEnvVar, "true")
m := NewFor(cfg)
_, hasEnv := m.LookupEnv(AzureUserAgentEnvVar)
require.False(t, hasEnv, "expected the opt out to skip setting the AZURE_HTTP_USER_AGENT environment variable")
})
t.Run("opt-out preserves original value", func(t *testing.T) {
// Validate that at runtime when we are calling the az cli, that if the bundle author opted-out, we don't set the user agent string
cfg := runtime.NewConfig()
cfg.Setenv(UserAgentOptOutEnvVar, "true")
customUserAgent := "mycustom/v1.2.3"
cfg.Setenv(AzureUserAgentEnvVar, customUserAgent)
m := NewFor(cfg)
require.Equal(t, customUserAgent, m.Getenv(AzureUserAgentEnvVar), "expected opting out to not prevent the user from setting a custom user agent")
require.Empty(t, m.userAgent, "validate we remember that we opted out")
})
}
14 changes: 12 additions & 2 deletions pkg/terraform/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,28 @@ const (
type Mixin struct {
runtime.RuntimeConfig
config MixinConfig

userAgent string
}

// New terraform mixin client, initialized with useful defaults.
func New() *Mixin {
return &Mixin{
RuntimeConfig: runtime.NewConfig(),
return NewFor(runtime.NewConfig())
}

func NewFor(cfg runtime.RuntimeConfig) *Mixin {

m := &Mixin{
RuntimeConfig: cfg,
config: MixinConfig{
WorkingDir: DefaultWorkingDir,
ClientVersion: DefaultClientVersion,
InitFile: DefaultInitFile,
},
}

m.SetUserAgent()
return m
}

func (m *Mixin) getPayloadData() ([]byte, error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
config:
clientVersion: 0.13.0-rc1
userAgentOptOut: true
install:
- terraform:
description: "noop"
7 changes: 5 additions & 2 deletions pkg/terraform/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import (
)

func (m *Mixin) PrintVersion(opts version.Options) error {
metadata := mixin.Metadata{
return version.PrintVersion(m.Context, opts, m.Version())
}

func (m *Mixin) Version() mixin.Metadata {
return mixin.Metadata{
Name: "terraform",
VersionInfo: pkgmgmt.VersionInfo{
Version: pkg.Version,
Commit: pkg.Commit,
Author: "Porter Authors",
},
}
return version.PrintVersion(m.Context, opts, metadata)
}

0 comments on commit 17f2d52

Please sign in to comment.