Skip to content

Commit 307326f

Browse files
committed
allow remote service creds to be configured using env
Introduces a new method of configuring token service credentials using a host-specific environment variable. This configuration was previously possible using the [terraform-credentials-env](https://github.com/apparentlymart/terraform-credentials-env) credentials helper. This new method is now consulted first, as it is seen to be the most proximate source of credentials before CLI configuration while still falling back to the credentials helper.
1 parent 7147404 commit 307326f

4 files changed

Lines changed: 95 additions & 7 deletions

File tree

‎internal/command/cliconfig/credentials.go‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"os"
1010
"path/filepath"
11+
"strings"
1112

1213
"github.com/zclconf/go-cty/cty"
1314
ctyjson "github.com/zclconf/go-cty/cty/json"
@@ -113,6 +114,29 @@ func (c *Config) credentialsSource(helperType string, helper svcauth.Credentials
113114
}
114115
}
115116

117+
// hostCredentialsFromEnv returns a token credential by searching for a hostname-specific
118+
// environment variable. If the variable based on the hostname is not defined, nil is returned.
119+
// Variable names must have dot characters translated to underscores, which are normally not
120+
// allowed in DNS names. For example, token credentials for app.terraform.io should be set
121+
// in the variable named TF_TOKEN_app_terraform_io. Internationalized hostnames containing
122+
// non-ASCII characters expressed as variable names are expected to be in the "comparison" form,
123+
// such as "TF_TOKEN_xn--caf-dma_fr" for the hostname "café.fr"
124+
func hostCredentialsFromEnv(host svchost.Hostname) svcauth.HostCredentials {
125+
if len(host) == 0 {
126+
return nil
127+
}
128+
129+
// Convert dots to underscores when looking for environment configuration for a specific host.
130+
// DNS names do not allow underscore characters so this is unambiguous.
131+
variableName := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(host.String(), ".", "_"))
132+
133+
if token, ok := os.LookupEnv(variableName); ok {
134+
return svcauth.HostCredentialsToken(token)
135+
}
136+
137+
return nil
138+
}
139+
116140
// CredentialsSource is an implementation of svcauth.CredentialsSource
117141
// that can read and write the CLI configuration, and possibly also delegate
118142
// to a credentials helper when configured.
@@ -153,11 +177,18 @@ type CredentialsSource struct {
153177
var _ svcauth.CredentialsSource = (*CredentialsSource)(nil)
154178

155179
func (s *CredentialsSource) ForHost(host svchost.Hostname) (svcauth.HostCredentials, error) {
180+
// The first order of precedence for credentials is a host-specific environment variable
181+
if envCreds := hostCredentialsFromEnv(host); envCreds != nil {
182+
return envCreds, nil
183+
}
184+
185+
// Then, any credentials block present in the CLI config
156186
v, ok := s.configured[host]
157187
if ok {
158188
return svcauth.HostCredentialsFromObject(v), nil
159189
}
160190

191+
// And finally, the credentials helper
161192
if s.helper != nil {
162193
return s.helper.ForHost(host)
163194
}

‎internal/command/cliconfig/credentials_test.go‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/google/go-cmp/cmp"
99
"github.com/zclconf/go-cty/cty"
1010

11-
"github.com/hashicorp/terraform-svchost"
11+
svchost "github.com/hashicorp/terraform-svchost"
1212
svcauth "github.com/hashicorp/terraform-svchost/auth"
1313
)
1414

@@ -83,6 +83,53 @@ func TestCredentialsForHost(t *testing.T) {
8383
t.Errorf("wrong result\ngot: %s\nwant: %s", got, want)
8484
}
8585
})
86+
t.Run("set in environment", func(t *testing.T) {
87+
envName := "TF_TOKEN_configured_example_com"
88+
t.Cleanup(func() {
89+
os.Unsetenv(envName)
90+
})
91+
92+
expectedToken := "configured-by-env"
93+
os.Setenv(envName, expectedToken)
94+
95+
creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com"))
96+
if err != nil {
97+
t.Fatalf("unexpected error: %s", err)
98+
}
99+
100+
if creds == nil {
101+
t.Fatal("no credentials found")
102+
}
103+
104+
if got := creds.Token(); got != expectedToken {
105+
t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
106+
}
107+
})
108+
109+
t.Run("punycode name set in environment", func(t *testing.T) {
110+
envName := "TF_TOKEN_env_xn--eckwd4c7cu47r2wf_com"
111+
t.Cleanup(func() {
112+
os.Unsetenv(envName)
113+
})
114+
115+
expectedToken := "configured-by-env"
116+
os.Setenv(envName, expectedToken)
117+
118+
hostname, _ := svchost.ForComparison("env.ドメイン名例.com")
119+
creds, err := credSrc.ForHost(hostname)
120+
121+
if err != nil {
122+
t.Fatalf("unexpected error: %s", err)
123+
}
124+
125+
if creds == nil {
126+
t.Fatal("no credentials found")
127+
}
128+
129+
if got := creds.Token(); got != expectedToken {
130+
t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
131+
}
132+
})
86133
}
87134

88135
func TestCredentialsStoreForget(t *testing.T) {

‎website/docs/cli/config/config-file.mdx‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,22 @@ is available at multiple hostnames, use only one of them consistently.
115115
Terraform Cloud responds to API calls at both its current hostname
116116
`app.terraform.io`, and its historical hostname `atlas.hashicorp.com`.
117117

118-
### Credentials Helpers
118+
### Environment Variable Credentials
119119

120120
If you would prefer not to store your API tokens directly in the CLI
121-
configuration as described in the previous section, you can optionally instruct
122-
Terraform to use a different credentials storage mechanism by configuring a
123-
special kind of plugin program called a _credentials helper_.
121+
configuration, you may use a host-specific environment variable. For example,
122+
the value of a variable named `TF_TOKEN_app_terraform_io` will be used as an
123+
Authorization: Bearer token whenever making service requests to the hostname
124+
"app.terraform.io".
125+
126+
Internationalized domain names should be converted to their [punycode equivalent](https://www.charset.org/punycode)
127+
with an ACE prefix when used as a variable name. For example, token credentials
128+
for 例えば.com should be set in a variable called `TF_TOKEN_xn--r8j3dr99h_com`.
129+
130+
### Credentials Helpers
131+
132+
Finally, you may instruct Terraform to use a different credentials storage mechanism
133+
by configuring a special kind of plugin program called a _credentials helper_.
124134

125135
```hcl
126136
credentials_helper "example" {

‎website/docs/internals/remote-service-discovery.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ At present, the following service identifiers are in use:
9090
## Authentication
9191

9292
If credentials for the given hostname are available in
93-
[the CLI config](/cli/config/config-file) then they will be included
94-
in the request for the discovery document.
93+
[the CLI config, by a credential helper, or using a host-specific environment variable](/cli/config/config-file#Credentials)
94+
then they will be included in the request for the discovery document.
9595

9696
The credentials may also be provided to endpoints declared in the discovery
9797
document, depending on the requirements of the service in question.

0 commit comments

Comments
 (0)