Skip to content

Commit 1a0c53b

Browse files
committed
Add flag for setting Terraform Enterprise hostname
--tfe-hostname will allow for creating a .terraformrc file with a different hostname than app.terraform.io
1 parent b7d4e99 commit 1a0c53b

10 files changed

+116
-40
lines changed

cmd/server.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,19 @@ const (
6868
SlackTokenFlag = "slack-token"
6969
SSLCertFileFlag = "ssl-cert-file"
7070
SSLKeyFileFlag = "ssl-key-file"
71+
TFEHostnameFlag = "tfe-hostname"
7172
TFETokenFlag = "tfe-token"
7273

7374
// Flag defaults.
75+
// NOTE: Must manually set these as defaults in the setDefaults function.
7476
DefaultCheckoutStrategy = "branch"
7577
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
7678
DefaultDataDir = "~/.atlantis"
7779
DefaultGHHostname = "github.com"
7880
DefaultGitlabHostname = "gitlab.com"
7981
DefaultLogLevel = "info"
8082
DefaultPort = 4141
83+
DefaultTFEHostname = "app.terraform.io"
8184
)
8285

8386
var stringFlags = map[string]stringFlag{
@@ -175,9 +178,13 @@ var stringFlags = map[string]stringFlag{
175178
SSLKeyFileFlag: {
176179
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
177180
},
181+
TFEHostnameFlag: {
182+
description: "Hostname of your Terraform Enterprise installation. If using Terraform Cloud no need to set.",
183+
defaultValue: DefaultTFEHostname,
184+
},
178185
TFETokenFlag: {
179-
description: "API token for Terraform Enterprise. This will be used to generate a ~/.terraformrc file." +
180-
" Only set if using TFE as a backend." +
186+
description: "API token for Terraform Cloud/Enterprise. This will be used to generate a ~/.terraformrc file." +
187+
" Only set if using TFC/E as a remote backend." +
181188
" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.",
182189
},
183190
DefaultTFVersionFlag: {
@@ -418,6 +425,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
418425
if c.Port == 0 {
419426
c.Port = DefaultPort
420427
}
428+
if c.TFEHostname == "" {
429+
c.TFEHostname = DefaultTFEHostname
430+
}
421431
}
422432

423433
func (s *ServerCmd) validate(userConfig server.UserConfig) error {
@@ -486,6 +496,10 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
486496
}
487497
}
488498

499+
if userConfig.TFEHostname != DefaultTFEHostname && userConfig.TFEToken == "" {
500+
return fmt.Errorf("if setting --%s, must set --%s", TFEHostnameFlag, TFETokenFlag)
501+
}
502+
489503
return nil
490504
}
491505

cmd/server_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ func TestExecute_Defaults(t *testing.T) {
362362
Equals(t, "", passedConfig.SlackToken)
363363
Equals(t, "", passedConfig.SSLCertFile)
364364
Equals(t, "", passedConfig.SSLKeyFile)
365+
Equals(t, "app.terraform.io", passedConfig.TFEHostname)
365366
Equals(t, "", passedConfig.TFEToken)
366367
}
367368

@@ -466,6 +467,7 @@ func TestExecute_Flags(t *testing.T) {
466467
cmd.SlackTokenFlag: "slack-token",
467468
cmd.SSLCertFileFlag: "cert-file",
468469
cmd.SSLKeyFileFlag: "key-file",
470+
cmd.TFEHostnameFlag: "my-hostname",
469471
cmd.TFETokenFlag: "my-token",
470472
})
471473
err := c.Execute()
@@ -499,6 +501,7 @@ func TestExecute_Flags(t *testing.T) {
499501
Equals(t, "slack-token", passedConfig.SlackToken)
500502
Equals(t, "cert-file", passedConfig.SSLCertFile)
501503
Equals(t, "key-file", passedConfig.SSLKeyFile)
504+
Equals(t, "my-hostname", passedConfig.TFEHostname)
502505
Equals(t, "my-token", passedConfig.TFEToken)
503506
}
504507

@@ -533,6 +536,7 @@ require-mergeable: true
533536
slack-token: slack-token
534537
ssl-cert-file: cert-file
535538
ssl-key-file: key-file
539+
tfe-hostname: my-hostname
536540
tfe-token: my-token
537541
`)
538542
defer os.Remove(tmpFile) // nolint: errcheck
@@ -570,6 +574,7 @@ tfe-token: my-token
570574
Equals(t, "slack-token", passedConfig.SlackToken)
571575
Equals(t, "cert-file", passedConfig.SSLCertFile)
572576
Equals(t, "key-file", passedConfig.SSLKeyFile)
577+
Equals(t, "my-hostname", passedConfig.TFEHostname)
573578
Equals(t, "my-token", passedConfig.TFEToken)
574579
}
575580

@@ -603,6 +608,7 @@ require-approval: true
603608
slack-token: slack-token
604609
ssl-cert-file: cert-file
605610
ssl-key-file: key-file
611+
tfe-hostname: my-hostname
606612
tfe-token: my-token
607613
`)
608614
defer os.Remove(tmpFile) // nolint: errcheck
@@ -637,6 +643,7 @@ tfe-token: my-token
637643
"SLACK_TOKEN": "override-slack-token",
638644
"SSL_CERT_FILE": "override-cert-file",
639645
"SSL_KEY_FILE": "override-key-file",
646+
"TFE_HOSTNAME": "override-my-hostname",
640647
"TFE_TOKEN": "override-my-token",
641648
} {
642649
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
@@ -674,6 +681,7 @@ tfe-token: my-token
674681
Equals(t, "override-slack-token", passedConfig.SlackToken)
675682
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
676683
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
684+
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
677685
Equals(t, "override-my-token", passedConfig.TFEToken)
678686
}
679687

@@ -708,6 +716,7 @@ require-mergeable: true
708716
slack-token: slack-token
709717
ssl-cert-file: cert-file
710718
ssl-key-file: key-file
719+
tfe-hostname: my-hostname
711720
tfe-token: my-token
712721
`)
713722

@@ -741,6 +750,7 @@ tfe-token: my-token
741750
cmd.SlackTokenFlag: "override-slack-token",
742751
cmd.SSLCertFileFlag: "override-cert-file",
743752
cmd.SSLKeyFileFlag: "override-key-file",
753+
cmd.TFEHostnameFlag: "override-my-hostname",
744754
cmd.TFETokenFlag: "override-my-token",
745755
})
746756
err := c.Execute()
@@ -772,6 +782,7 @@ tfe-token: my-token
772782
Equals(t, "override-slack-token", passedConfig.SlackToken)
773783
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
774784
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
785+
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
775786
Equals(t, "override-my-token", passedConfig.TFEToken)
776787

777788
}
@@ -808,6 +819,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
808819
"SLACK_TOKEN": "slack-token",
809820
"SSL_CERT_FILE": "cert-file",
810821
"SSL_KEY_FILE": "key-file",
822+
"TFE_HOSTNAME": "my-hostname",
811823
"TFE_TOKEN": "my-token",
812824
}
813825
for name, value := range envVars {
@@ -849,6 +861,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
849861
cmd.SlackTokenFlag: "override-slack-token",
850862
cmd.SSLCertFileFlag: "override-cert-file",
851863
cmd.SSLKeyFileFlag: "override-key-file",
864+
cmd.TFEHostnameFlag: "override-my-hostname",
852865
cmd.TFETokenFlag: "override-my-token",
853866
})
854867
err := c.Execute()
@@ -882,6 +895,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
882895
Equals(t, "override-slack-token", passedConfig.SlackToken)
883896
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
884897
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
898+
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
885899
Equals(t, "override-my-token", passedConfig.TFEToken)
886900
}
887901

@@ -941,6 +955,18 @@ func TestExecute_RepoCfgFlags(t *testing.T) {
941955
ErrEquals(t, "cannot use --repo-config and --repo-config-json at the same time", err)
942956
}
943957

958+
// Can't use both --tfe-hostname flag without --tfe-token.
959+
func TestExecute_TFEHostnameOnly(t *testing.T) {
960+
c := setup(map[string]interface{}{
961+
cmd.GHUserFlag: "user",
962+
cmd.GHTokenFlag: "token",
963+
cmd.RepoWhitelistFlag: "github.com",
964+
cmd.TFEHostnameFlag: "not-app.terraform.io",
965+
})
966+
err := c.Execute()
967+
ErrEquals(t, "if setting --tfe-hostname, must set --tfe-token", err)
968+
}
969+
944970
func setup(flags map[string]interface{}) *cobra.Command {
945971
vipr := viper.New()
946972
for k, v := range flags {

runatlantis.io/docs/server-configuration.md

+9
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,15 @@ Values are chosen in this order:
385385
atlantis server --ssl-cert-file="/etc/ssl/private/my-cert.key"
386386
```
387387
File containing x509 private key matching `--ssl-cert-file`.
388+
389+
* ### `--tfe-hostname`
390+
```bash
391+
atlantis server --tfe-hostname="my-terraform-enterprise.company.com"
392+
```
393+
Hostname of your Terraform Enterprise installation to be used in conjunction with
394+
`--tfe-token`. See [Terraform Cloud](terraform-cloud.html) for more details.
395+
If using Terraform Cloud (i.e. you don't have your own Terraform Enterprise installation)
396+
no need to set since it defaults to `app.terraform.io`.
388397

389398
* ### `--tfe-token`
390399
```bash

runatlantis.io/docs/terraform-cloud.md

+27-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
# Terraform Cloud
1+
# Terraform Cloud/Enterprise
22

3-
::: tip
4-
Terraform Enterprise was [recently renamed](https://www.hashicorp.com/blog/introducing-terraform-cloud-remote-state-management) Terraform Cloud.
3+
::: tip NOTE
4+
Terraform Enterprise was [recently renamed](https://www.hashicorp.com/blog/introducing-terraform-cloud-remote-state-management) Terraform Cloud
5+
and Private Terraform Enteprise was renamed Terraform Enterprise.
56
:::
67

7-
Atlantis integrates seamlessly with Terraform Cloud, whether you're using:
8+
Atlantis integrates seamlessly with Terraform Cloud and Terraform Enterprise, whether you're using:
89
* [Free Remote State Management](https://app.terraform.io/signup)
910
* Terraform Cloud Paid Tiers
10-
* Private Terraform Enterprise
11+
* A Private Installation of Terraform Enterprise
1112

1213
Read the docs below :point_down: depending on your use-case.
1314
[[toc]]
@@ -16,20 +17,21 @@ Read the docs below :point_down: depending on your use-case.
1617
To use Atlantis with Free Remote State Storage, you need to:
1718
1. Migrate your state to Terraform Cloud. See [Getting Started with the Terraform Cloud Free Tier](https://www.terraform.io/docs/enterprise/free/index.html#enable-remote-state-in-terraform-configurations)
1819
1. Update any projects that are referencing the state you migrated to use the new location
19-
1. [Generate a Terraform Cloud Token](#generating-a-terraform-cloud-token)
20+
1. [Generate a Terraform Cloud/Enterprise Token](#generating-a-terraform-cloud-enterprise-token)
2021
1. [Pass the token to Atlantis](#passing-the-token-to-atlantis)
2122

2223
That's it! Atlantis will run as normal and your state will be stored in Terraform
2324
Cloud.
2425

25-
## Using Atlantis With Terraform Cloud Paid Tiers
26-
Atlantis integrates with the full version of Terraform Cloud via its [remote backend](https://www.terraform.io/docs/backends/types/remote.html).
26+
## Using Atlantis With Terraform Cloud Remote Operations or Terraform Enterprise
27+
Atlantis integrates with the full version of Terraform Cloud and Terraform Enterprise
28+
via the [remote backend](https://www.terraform.io/docs/backends/types/remote.html).
2729

2830
Atlantis will run `terraform` commands as usual, however those commands will
29-
actually be executed *remotely* in Terraform Cloud.
31+
actually be executed *remotely* in Terraform Cloud or Terraform Enterprise.
3032

3133
### Why?
32-
Using Atlantis with Terraform Cloud gives you access to features like:
34+
Using Atlantis with Terraform Cloud or Terraform Enterprise gives you access to features like:
3335
* Real-time streaming output
3436
* Ability to cancel in-progress commands
3537
* Secret variables
@@ -38,14 +40,14 @@ Using Atlantis with Terraform Cloud gives you access to features like:
3840
**Without** having to change your pull request workflow.
3941

4042
### Getting Started
41-
To use Atlantis with Terraform Cloud Paid Tiers, you need to:
42-
1. Migrate your state to Terraform Cloud. See [Migrating State from Terraform Open Source](https://www.terraform.io/docs/enterprise/migrate/index.html)
43+
To use Atlantis with Terraform Cloud Remote Operations or Terraform Enterprise, you need to:
44+
1. Migrate your state to Terraform Cloud/Enterprise. See [Migrating State from Terraform Open Source](https://www.terraform.io/docs/enterprise/migrate/index.html)
4345
1. Update any projects that are referencing the state you migrated to use the new location
44-
1. [Generate a Terraform Cloud Token](#generating-a-terraform-cloud-token)
46+
1. [Generate a Terraform Cloud/Enterprise Token](#generating-a-terraform-cloud-enterprise-token)
4547
1. [Pass the token to Atlantis](#passing-the-token-to-atlantis)
4648

47-
## Generating a Terraform Cloud Token
48-
Atlantis needs a Terraform Cloud Token that it will use to access the API.
49+
## Generating a Terraform Cloud/Enterprise Token
50+
Atlantis needs a Terraform Cloud/Enterprise Token that it will use to access the API.
4951
Using a **Team Token is recommended**, however you can also use a User Token.
5052

5153
### Team Token
@@ -62,9 +64,18 @@ The token can be passed to Atlantis via the `ATLANTIS_TFE_TOKEN` environment var
6264
You can also use the `--tfe-token` flag, however your token would then be easily
6365
viewable in the process list.
6466

65-
That's it! Atlantis should be able to perform Terraform operations using Terraform Cloud's
67+
If you're hosting your own Terraform Enterprise installation, set the `--tfe-hostname`
68+
flag to its hostname.
69+
70+
That's it! Atlantis should be able to perform Terraform operations using Terraform Cloud/Enterprise's
6671
remote state backend now.
6772

73+
:::warning
74+
The Terraform Cloud/Enterprise integration only works with the built-in
75+
`plan` and `apply` steps. It does not work with custom `run` steps that replace
76+
plan or apply.
77+
:::
78+
6879
:::tip NOTE
6980
Under the hood, Atlantis is generating a `~/.terraformrc` file.
7081
If you already had a `~/.terraformrc` file where Atlantis is running,

server/events/terraform/terraform_client.go

+14-6
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ var versionRegex = regexp.MustCompile("Terraform v(.*?)(\\s.*)?\n")
9797
// version.
9898
// tfDownloader is used to download terraform versions.
9999
// Will asynchronously download the required version if it doesn't exist already.
100-
func NewClient(log *logging.SimpleLogger, dataDir string, tfeToken string, defaultVersionStr string, defaultVersionFlagName string, tfDownloader Downloader) (*DefaultClient, error) {
100+
func NewClient(
101+
log *logging.SimpleLogger,
102+
dataDir string,
103+
tfeToken string,
104+
tfeHostname string,
105+
defaultVersionStr string,
106+
defaultVersionFlagName string,
107+
tfDownloader Downloader) (*DefaultClient, error) {
101108
var finalDefaultVersion *version.Version
102109
var localVersion *version.Version
103110
versions := make(map[string]string)
@@ -149,7 +156,7 @@ func NewClient(log *logging.SimpleLogger, dataDir string, tfeToken string, defau
149156
if err != nil {
150157
return nil, errors.Wrap(err, "getting home dir to write ~/.terraformrc file")
151158
}
152-
if err := generateRCFile(tfeToken, home); err != nil {
159+
if err := generateRCFile(tfeToken, tfeHostname, home); err != nil {
153160
return nil, err
154161
}
155162
}
@@ -383,12 +390,13 @@ func ensureVersion(log *logging.SimpleLogger, dl Downloader, versions map[string
383390
return dest, nil
384391
}
385392

386-
// generateRCFile generates a .terraformrc file containing config for tfeToken.
393+
// generateRCFile generates a .terraformrc file containing config for tfeToken
394+
// and hostname tfeHostname.
387395
// It will create the file in home/.terraformrc.
388-
func generateRCFile(tfeToken string, home string) error {
396+
func generateRCFile(tfeToken string, tfeHostname string, home string) error {
389397
const rcFilename = ".terraformrc"
390398
rcFile := filepath.Join(home, rcFilename)
391-
config := fmt.Sprintf(rcFileContents, tfeToken)
399+
config := fmt.Sprintf(rcFileContents, tfeHostname, tfeToken)
392400

393401
// If there is already a .terraformrc file and its contents aren't exactly
394402
// what we would have written to it, then we error out because we don't
@@ -428,7 +436,7 @@ func getVersion(tfBinary string) (*version.Version, error) {
428436
// rcFileContents is a format string to be used with Sprintf that can be used
429437
// to generate the contents of a ~/.terraformrc file for authenticating with
430438
// Terraform Enterprise.
431-
var rcFileContents = `credentials "app.terraform.io" {
439+
var rcFileContents = `credentials "%s" {
432440
token = %q
433441
}`
434442

server/events/terraform/terraform_client_internal_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ func TestGenerateRCFile_WritesFile(t *testing.T) {
1818
tmp, cleanup := TempDir(t)
1919
defer cleanup()
2020

21-
err := generateRCFile("token", tmp)
21+
err := generateRCFile("token", "hostname", tmp)
2222
Ok(t, err)
2323

24-
expContents := `credentials "app.terraform.io" {
24+
expContents := `credentials "hostname" {
2525
token = "token"
2626
}`
2727
actContents, err := ioutil.ReadFile(filepath.Join(tmp, ".terraformrc"))
@@ -39,7 +39,7 @@ func TestGenerateRCFile_WillNotOverwrite(t *testing.T) {
3939
err := ioutil.WriteFile(rcFile, []byte("contents"), 0600)
4040
Ok(t, err)
4141

42-
actErr := generateRCFile("token", tmp)
42+
actErr := generateRCFile("token", "hostname", tmp)
4343
expErr := fmt.Sprintf("can't write TFE token to %s because that file has contents that would be overwritten", tmp+"/.terraformrc")
4444
ErrEquals(t, expErr, actErr)
4545
}
@@ -57,7 +57,7 @@ func TestGenerateRCFile_NoErrIfContentsSame(t *testing.T) {
5757
err := ioutil.WriteFile(rcFile, []byte(contents), 0600)
5858
Ok(t, err)
5959

60-
err = generateRCFile("token", tmp)
60+
err = generateRCFile("token", "app.terraform.io", tmp)
6161
Ok(t, err)
6262
}
6363

@@ -72,15 +72,15 @@ func TestGenerateRCFile_ErrIfCannotRead(t *testing.T) {
7272
Ok(t, err)
7373

7474
expErr := fmt.Sprintf("trying to read %s to ensure we're not overwriting it: open %s: permission denied", rcFile, rcFile)
75-
actErr := generateRCFile("token", tmp)
75+
actErr := generateRCFile("token", "hostname", tmp)
7676
ErrEquals(t, expErr, actErr)
7777
}
7878

7979
// Test that if we can't write, we error out.
8080
func TestGenerateRCFile_ErrIfCannotWrite(t *testing.T) {
8181
rcFile := "/this/dir/does/not/exist/.terraformrc"
8282
expErr := fmt.Sprintf("writing generated .terraformrc file with TFE token to %s: open %s: no such file or directory", rcFile, rcFile)
83-
actErr := generateRCFile("token", "/this/dir/does/not/exist")
83+
actErr := generateRCFile("token", "hostname", "/this/dir/does/not/exist")
8484
ErrEquals(t, expErr, actErr)
8585
}
8686

0 commit comments

Comments
 (0)