Skip to content

Commit

Permalink
Add flag for setting Terraform Enterprise hostname
Browse files Browse the repository at this point in the history
--tfe-hostname will allow for creating a .terraformrc file with a
different hostname than app.terraform.io
  • Loading branch information
lkysow committed Jul 12, 2019
1 parent b7d4e99 commit 010c6c4
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 40 deletions.
18 changes: 16 additions & 2 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,19 @@ const (
SlackTokenFlag = "slack-token"
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
TFEHostnameFlag = "tfe-hostname"
TFETokenFlag = "tfe-token"

// Flag defaults.
// NOTE: Must manually set these as defaults in the setDefaults function.
DefaultCheckoutStrategy = "branch"
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
DefaultDataDir = "~/.atlantis"
DefaultGHHostname = "github.com"
DefaultGitlabHostname = "gitlab.com"
DefaultLogLevel = "info"
DefaultPort = 4141
DefaultTFEHostname = "app.terraform.io"
)

var stringFlags = map[string]stringFlag{
Expand Down Expand Up @@ -175,9 +178,13 @@ var stringFlags = map[string]stringFlag{
SSLKeyFileFlag: {
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
},
TFEHostnameFlag: {
description: "Hostname of your Terraform Enterprise installation. If using Terraform Cloud no need to set.",
defaultValue: DefaultTFEHostname,
},
TFETokenFlag: {
description: "API token for Terraform Enterprise. This will be used to generate a ~/.terraformrc file." +
" Only set if using TFE as a backend." +
description: "API token for Terraform Cloud/Enterprise. This will be used to generate a ~/.terraformrc file." +
" Only set if using TFC/E as a remote backend." +
" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.",
},
DefaultTFVersionFlag: {
Expand Down Expand Up @@ -418,6 +425,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.Port == 0 {
c.Port = DefaultPort
}
if c.TFEHostname == "" {
c.TFEHostname = DefaultTFEHostname
}
}

func (s *ServerCmd) validate(userConfig server.UserConfig) error {
Expand Down Expand Up @@ -486,6 +496,10 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
}
}

if userConfig.TFEHostname != DefaultTFEHostname && userConfig.TFEToken == "" {
return fmt.Errorf("if setting --%s, must set --%s", TFEHostnameFlag, TFETokenFlag)
}

return nil
}

Expand Down
12 changes: 12 additions & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,18 @@ func TestExecute_RepoCfgFlags(t *testing.T) {
ErrEquals(t, "cannot use --repo-config and --repo-config-json at the same time", err)
}

// Can't use both --tfe-hostname flag without --tfe-token.
func TestExecute_TFEHostnameOnly(t *testing.T) {
c := setup(map[string]interface{}{
cmd.GHUserFlag: "user",
cmd.GHTokenFlag: "token",
cmd.RepoWhitelistFlag: "github.com",
cmd.TFEHostnameFlag: "not-app.terraform.io",
})
err := c.Execute()
ErrEquals(t, "if setting --tfe-hostname, must set --tfe-token", err)
}

func setup(flags map[string]interface{}) *cobra.Command {
vipr := viper.New()
for k, v := range flags {
Expand Down
9 changes: 9 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ Values are chosen in this order:
atlantis server --ssl-cert-file="/etc/ssl/private/my-cert.key"
```
File containing x509 private key matching `--ssl-cert-file`.

* ### `--tfe-hostname`
```bash
atlantis server --tfe-hostname="my-terraform-enterprise.company.com"
```
Hostname of your Terraform Enterprise installation to be used in conjunction with
`--tfe-token`. See [Terraform Cloud](terraform-cloud.html) for more details.
If using Terraform Cloud (i.e. you don't have your own Terraform Enterprise installation)
no need to set since it defaults to `app.terraform.io`.

* ### `--tfe-token`
```bash
Expand Down
43 changes: 27 additions & 16 deletions runatlantis.io/docs/terraform-cloud.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Terraform Cloud
# Terraform Cloud/Enterprise

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

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

Read the docs below :point_down: depending on your use-case.
[[toc]]
Expand All @@ -16,20 +17,21 @@ Read the docs below :point_down: depending on your use-case.
To use Atlantis with Free Remote State Storage, you need to:
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)
1. Update any projects that are referencing the state you migrated to use the new location
1. [Generate a Terraform Cloud Token](#generating-a-terraform-cloud-token)
1. [Generate a Terraform Cloud/Enterprise Token](#generating-a-terraform-cloud-enterprise-token)
1. [Pass the token to Atlantis](#passing-the-token-to-atlantis)

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

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

Atlantis will run `terraform` commands as usual, however those commands will
actually be executed *remotely* in Terraform Cloud.
actually be executed *remotely* in Terraform Cloud or Terraform Enterprise.

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

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

## Generating a Terraform Cloud Token
Atlantis needs a Terraform Cloud Token that it will use to access the API.
## Generating a Terraform Cloud/Enterprise Token
Atlantis needs a Terraform Cloud/Enterprise Token that it will use to access the API.
Using a **Team Token is recommended**, however you can also use a User Token.

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

That's it! Atlantis should be able to perform Terraform operations using Terraform Cloud's
If you're hosting your own Terraform Enterprise installation, set the `--tfe-hostname`
flag to its hostname.

That's it! Atlantis should be able to perform Terraform operations using Terraform Cloud/Enterprise's
remote state backend now.

:::warning
The Terraform Cloud/Enterprise integration only works with the built-in
`plan` and `apply` steps. It does not work with custom `run` steps that replace
plan or apply.
:::

:::tip NOTE
Under the hood, Atlantis is generating a `~/.terraformrc` file.
If you already had a `~/.terraformrc` file where Atlantis is running,
Expand Down
20 changes: 14 additions & 6 deletions server/events/terraform/terraform_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@ var versionRegex = regexp.MustCompile("Terraform v(.*?)(\\s.*)?\n")
// version.
// tfDownloader is used to download terraform versions.
// Will asynchronously download the required version if it doesn't exist already.
func NewClient(log *logging.SimpleLogger, dataDir string, tfeToken string, defaultVersionStr string, defaultVersionFlagName string, tfDownloader Downloader) (*DefaultClient, error) {
func NewClient(
log *logging.SimpleLogger,
dataDir string,
tfeToken string,
tfeHostname string,
defaultVersionStr string,
defaultVersionFlagName string,
tfDownloader Downloader) (*DefaultClient, error) {
var finalDefaultVersion *version.Version
var localVersion *version.Version
versions := make(map[string]string)
Expand Down Expand Up @@ -149,7 +156,7 @@ func NewClient(log *logging.SimpleLogger, dataDir string, tfeToken string, defau
if err != nil {
return nil, errors.Wrap(err, "getting home dir to write ~/.terraformrc file")
}
if err := generateRCFile(tfeToken, home); err != nil {
if err := generateRCFile(tfeToken, tfeHostname, home); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -383,12 +390,13 @@ func ensureVersion(log *logging.SimpleLogger, dl Downloader, versions map[string
return dest, nil
}

// generateRCFile generates a .terraformrc file containing config for tfeToken.
// generateRCFile generates a .terraformrc file containing config for tfeToken
// and hostname tfeHostname.
// It will create the file in home/.terraformrc.
func generateRCFile(tfeToken string, home string) error {
func generateRCFile(tfeToken string, tfeHostname string, home string) error {
const rcFilename = ".terraformrc"
rcFile := filepath.Join(home, rcFilename)
config := fmt.Sprintf(rcFileContents, tfeToken)
config := fmt.Sprintf(rcFileContents, tfeHostname, tfeToken)

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

Expand Down
12 changes: 6 additions & 6 deletions server/events/terraform/terraform_client_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ func TestGenerateRCFile_WritesFile(t *testing.T) {
tmp, cleanup := TempDir(t)
defer cleanup()

err := generateRCFile("token", tmp)
err := generateRCFile("token", "hostname", tmp)
Ok(t, err)

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

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

err = generateRCFile("token", tmp)
err = generateRCFile("token", "app.terraform.io", tmp)
Ok(t, err)
}

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

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

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

Expand Down
16 changes: 8 additions & 8 deletions server/events/terraform/terraform_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()

c, err := terraform.NewClient(nil, tmp, "", "", cmd.DefaultTFVersionFlag, nil)
c, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, nil)
Ok(t, err)

Ok(t, err)
Expand Down Expand Up @@ -95,7 +95,7 @@ is 0.11.13. You can update by downloading from www.terraform.io/downloads.html
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()

c, err := terraform.NewClient(nil, tmp, "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
Ok(t, err)

Ok(t, err)
Expand All @@ -115,7 +115,7 @@ func TestNewClient_NoTF(t *testing.T) {
// Set PATH to only include our empty directory.
defer tempSetEnv(t, "PATH", tmp)()

_, err := terraform.NewClient(nil, tmp, "", "", cmd.DefaultTFVersionFlag, nil)
_, err := terraform.NewClient(nil, tmp, "", "", "", cmd.DefaultTFVersionFlag, nil)
ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://www.terraform.io/downloads.html", err)
}

Expand All @@ -132,7 +132,7 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()

c, err := terraform.NewClient(nil, tmp, "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
Ok(t, err)

Ok(t, err)
Expand All @@ -156,7 +156,7 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {
Ok(t, err)
defer tempSetEnv(t, "PATH", fmt.Sprintf("%s:%s", tmp, os.Getenv("PATH")))()

c, err := terraform.NewClient(nil, tmp, "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, nil)
Ok(t, err)

Ok(t, err)
Expand All @@ -182,7 +182,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
err := ioutil.WriteFile(params[0].(string), []byte("#!/bin/sh\necho '\nTerraform v0.11.10\n'"), 0755)
return []pegomock.ReturnValue{err}
})
c, err := terraform.NewClient(nil, tmp, "", "0.11.10", cmd.DefaultTFVersionFlag, mockDownloader)
c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, mockDownloader)
Ok(t, err)

Ok(t, err)
Expand All @@ -206,7 +206,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
func TestNewClient_BadVersion(t *testing.T) {
tmp, cleanup := TempDir(t)
defer cleanup()
_, err := terraform.NewClient(nil, tmp, "", "malformed", cmd.DefaultTFVersionFlag, nil)
_, err := terraform.NewClient(nil, tmp, "", "", "malformed", cmd.DefaultTFVersionFlag, nil)
ErrEquals(t, "Malformed version: malformed", err)
}

Expand All @@ -229,7 +229,7 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
return []pegomock.ReturnValue{err}
})

c, err := terraform.NewClient(nil, tmp, "", "0.11.10", cmd.DefaultTFVersionFlag, mockDownloader)
c, err := terraform.NewClient(nil, tmp, "", "", "0.11.10", cmd.DefaultTFVersionFlag, mockDownloader)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())

Expand Down
2 changes: 1 addition & 1 deletion server/events_controller_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ func setupE2E(t *testing.T, repoDir string) (server.EventsController, *vcsmocks.
GithubUser: "github-user",
GitlabUser: "gitlab-user",
}
terraformClient, err := terraform.NewClient(logger, dataDir, "", "", "default-tf-version", &NoopTFDownloader{})
terraformClient, err := terraform.NewClient(logger, dataDir, "", "", "", "default-tf-version", &NoopTFDownloader{})
Ok(t, err)
boltdb, err := db.New(dataDir)
Ok(t, err)
Expand Down
9 changes: 8 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
vcsClient := vcs.NewClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient)
commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient}
terraformClient, err := terraform.NewClient(logger, userConfig.DataDir, userConfig.TFEToken, userConfig.DefaultTFVersion, config.DefaultTFVersionFlag, &terraform.DefaultDownloader{})
terraformClient, err := terraform.NewClient(
logger,
userConfig.DataDir,
userConfig.TFEToken,
userConfig.TFEHostname,
userConfig.DefaultTFVersion,
config.DefaultTFVersionFlag,
&terraform.DefaultDownloader{})
// The flag.Lookup call is to detect if we're running in a unit test. If we
// are, then we don't error out because we don't have/want terraform
// installed on our CI system where the unit tests run.
Expand Down
1 change: 1 addition & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type UserConfig struct {
SlackToken string `mapstructure:"slack-token"`
SSLCertFile string `mapstructure:"ssl-cert-file"`
SSLKeyFile string `mapstructure:"ssl-key-file"`
TFEHostname string `mapstructure:"tfe-hostname"`
TFEToken string `mapstructure:"tfe-token"`
DefaultTFVersion string `mapstructure:"default-tf-version"`
Webhooks []WebhookConfig `mapstructure:"webhooks"`
Expand Down

0 comments on commit 010c6c4

Please sign in to comment.