mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-04 18:38:34 +00:00
Add new --tfe-token flag for TFE backend.
The new flag takes in a Terraform Enterprise API token and attempts to write a config file for that token to ~/.terraformrc on startup. If the file already exists and its contents would change as a result of us writing it, we error out. This flag is useful if you're using the TFE backend for any of your projects as that requires a .terraformrc file with a token for authentication.
This commit is contained in:
@@ -61,6 +61,7 @@ const (
|
||||
SilenceWhitelistErrorsFlag = "silence-whitelist-errors"
|
||||
SSLCertFileFlag = "ssl-cert-file"
|
||||
SSLKeyFileFlag = "ssl-key-file"
|
||||
TFETokenFlag = "tfe-token"
|
||||
|
||||
// Flag defaults.
|
||||
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
|
||||
@@ -167,6 +168,12 @@ var stringFlags = []stringFlag{
|
||||
name: SSLKeyFileFlag,
|
||||
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
|
||||
},
|
||||
{
|
||||
name: TFETokenFlag,
|
||||
description: "API token for Terraform Enterprise. This will be used to generate a ~/.terraformrc file." +
|
||||
" Only set if using TFE as a backend." +
|
||||
" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.",
|
||||
},
|
||||
}
|
||||
var boolFlags = []boolFlag{
|
||||
{
|
||||
|
||||
@@ -349,6 +349,7 @@ func TestExecute_Defaults(t *testing.T) {
|
||||
Equals(t, false, passedConfig.RequireMergeable)
|
||||
Equals(t, "", passedConfig.SSLCertFile)
|
||||
Equals(t, "", passedConfig.SSLKeyFile)
|
||||
Equals(t, "", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
func TestExecute_ExpandHomeInDataDir(t *testing.T) {
|
||||
@@ -447,6 +448,7 @@ func TestExecute_Flags(t *testing.T) {
|
||||
cmd.RequireMergeableFlag: true,
|
||||
cmd.SSLCertFileFlag: "cert-file",
|
||||
cmd.SSLKeyFileFlag: "key-file",
|
||||
cmd.TFETokenFlag: "my-token",
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -474,6 +476,7 @@ func TestExecute_Flags(t *testing.T) {
|
||||
Equals(t, true, passedConfig.RequireMergeable)
|
||||
Equals(t, "cert-file", passedConfig.SSLCertFile)
|
||||
Equals(t, "key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "my-token", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
func TestExecute_ConfigFile(t *testing.T) {
|
||||
@@ -502,6 +505,7 @@ require-approval: true
|
||||
require-mergeable: true
|
||||
ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
tfe-token: my-token
|
||||
`)
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
c := setup(map[string]interface{}{
|
||||
@@ -533,6 +537,7 @@ ssl-key-file: key-file
|
||||
Equals(t, true, passedConfig.RequireMergeable)
|
||||
Equals(t, "cert-file", passedConfig.SSLCertFile)
|
||||
Equals(t, "key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "my-token", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
func TestExecute_EnvironmentOverride(t *testing.T) {
|
||||
@@ -560,6 +565,7 @@ repo-whitelist: "github.com/runatlantis/atlantis"
|
||||
require-approval: true
|
||||
ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
ssl-key-file: my-token
|
||||
`)
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
|
||||
@@ -588,6 +594,7 @@ ssl-key-file: key-file
|
||||
"REQUIRE_MERGEABLE": "false",
|
||||
"SSL_CERT_FILE": "override-cert-file",
|
||||
"SSL_KEY_FILE": "override-key-file",
|
||||
"TFE_TOKEN": "override-my-token",
|
||||
} {
|
||||
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
|
||||
}
|
||||
@@ -619,6 +626,7 @@ ssl-key-file: key-file
|
||||
Equals(t, false, passedConfig.RequireMergeable)
|
||||
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
func TestExecute_FlagConfigOverride(t *testing.T) {
|
||||
@@ -647,6 +655,7 @@ require-approval: true
|
||||
require-mergeable: true
|
||||
ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
tfe-token: my-token
|
||||
`)
|
||||
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
@@ -674,6 +683,7 @@ ssl-key-file: key-file
|
||||
cmd.RequireMergeableFlag: false,
|
||||
cmd.SSLCertFileFlag: "override-cert-file",
|
||||
cmd.SSLKeyFileFlag: "override-key-file",
|
||||
cmd.TFETokenFlag: "override-my-token",
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -699,6 +709,7 @@ ssl-key-file: key-file
|
||||
Equals(t, false, passedConfig.RequireMergeable)
|
||||
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
@@ -728,6 +739,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
"REQUIRE_MERGEABLE": "true",
|
||||
"SSL_CERT_FILE": "cert-file",
|
||||
"SSL_KEY_FILE": "key-file",
|
||||
"TFE_TOKEN": "my-token",
|
||||
}
|
||||
for name, value := range envVars {
|
||||
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
|
||||
@@ -763,6 +775,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
cmd.RequireMergeableFlag: false,
|
||||
cmd.SSLCertFileFlag: "override-cert-file",
|
||||
cmd.SSLKeyFileFlag: "override-key-file",
|
||||
cmd.TFETokenFlag: "override-my-token",
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -790,6 +803,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
Equals(t, false, passedConfig.RequireMergeable)
|
||||
Equals(t, "override-cert-file", passedConfig.SSLCertFile)
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
}
|
||||
|
||||
// If using bitbucket cloud, webhook secrets are not supported.
|
||||
|
||||
@@ -16,6 +16,8 @@ package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -49,7 +51,7 @@ const terraformPluginCacheDirName = "plugin-cache"
|
||||
// => 0.11.10
|
||||
var versionRegex = regexp.MustCompile("Terraform v(.*?)(\\s.*)?\n")
|
||||
|
||||
func NewClient(dataDir string) (*DefaultClient, error) {
|
||||
func NewClient(dataDir string, tfeToken string) (*DefaultClient, error) {
|
||||
_, err := exec.LookPath("terraform")
|
||||
if err != nil {
|
||||
return nil, errors.New("terraform not found in $PATH. \n\nDownload terraform from https://www.terraform.io/downloads.html")
|
||||
@@ -69,6 +71,17 @@ func NewClient(dataDir string) (*DefaultClient, error) {
|
||||
return nil, errors.Wrap(err, "parsing terraform version")
|
||||
}
|
||||
|
||||
// If tfeToken is set, we try to create a ~/.terraformrc file.
|
||||
if tfeToken != "" {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting home dir to write ~/.terraformrc file")
|
||||
}
|
||||
if err := generateRCFile(tfeToken, home); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// We will run terraform with the TF_PLUGIN_CACHE_DIR env var set to this
|
||||
// directory inside our data dir.
|
||||
cacheDir := filepath.Join(dataDir, terraformPluginCacheDirName)
|
||||
@@ -82,6 +95,32 @@ func NewClient(dataDir string) (*DefaultClient, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateRCFile generates a .terraformrc file containing config for tfeToken.
|
||||
// It will create the file in home/.terraformrc.
|
||||
func generateRCFile(tfeToken string, home string) error {
|
||||
const rcFilename = ".terraformrc"
|
||||
rcFile := filepath.Join(home, rcFilename)
|
||||
|
||||
// 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
|
||||
// want to overwrite anything.
|
||||
newContents := fmt.Sprintf(rcFileContents, tfeToken)
|
||||
if _, err := os.Stat(rcFile); err == nil {
|
||||
currContents, err := ioutil.ReadFile(rcFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "trying to read %s to ensure we're not overwriting it", rcFile)
|
||||
}
|
||||
if newContents != string(currContents) {
|
||||
return fmt.Errorf("can't write TFE token to %s because that file has contents that would be overwritten", rcFile)
|
||||
}
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(rcFile, []byte(newContents), 0600); err != nil {
|
||||
return errors.Wrapf(err, "writing generated %s file with TFE token to %s", rcFilename, rcFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Version returns the version of the terraform executable in our $PATH.
|
||||
func (c *DefaultClient) Version() *version.Version {
|
||||
return c.defaultVersion
|
||||
@@ -145,3 +184,10 @@ func MustConstraint(v string) version.Constraints {
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// 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" {
|
||||
token = %q
|
||||
}`
|
||||
|
||||
80
server/events/terraform/terraform_client_internal_test.go
Normal file
80
server/events/terraform/terraform_client_internal_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test that we write the file as expected
|
||||
func TestGenerateRCFile_WritesFile(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
err := generateRCFile("token", tmp)
|
||||
Ok(t, err)
|
||||
|
||||
expContents := `credentials "app.terraform.io" {
|
||||
token = "token"
|
||||
}`
|
||||
actContents, err := ioutil.ReadFile(filepath.Join(tmp, ".terraformrc"))
|
||||
Ok(t, err)
|
||||
Equals(t, expContents, string(actContents))
|
||||
}
|
||||
|
||||
// Test that if the file already exists and its contents will be modified if
|
||||
// we write our config that we error out.
|
||||
func TestGenerateRCFile_WillNotOverwrite(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
rcFile := filepath.Join(tmp, ".terraformrc")
|
||||
err := ioutil.WriteFile(rcFile, []byte("contents"), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
actErr := generateRCFile("token", 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)
|
||||
}
|
||||
|
||||
// Test that if the file already exists and its contents will NOT be modified if
|
||||
// we write our config that we don't error.
|
||||
func TestGenerateRCFile_NoErrIfContentsSame(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
rcFile := filepath.Join(tmp, ".terraformrc")
|
||||
contents := `credentials "app.terraform.io" {
|
||||
token = "token"
|
||||
}`
|
||||
err := ioutil.WriteFile(rcFile, []byte(contents), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = generateRCFile("token", tmp)
|
||||
Ok(t, err)
|
||||
}
|
||||
|
||||
// Test that if we can't read the existing file to see if the contents will be
|
||||
// the same that we just error out.
|
||||
func TestGenerateRCFile_ErrIfCannotRead(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
rcFile := filepath.Join(tmp, ".terraformrc")
|
||||
err := ioutil.WriteFile(rcFile, []byte("can't see me!"), 0000)
|
||||
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)
|
||||
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")
|
||||
ErrEquals(t, expErr, actErr)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
vcsClient := vcs.NewDefaultClientProxy(githubClient, gitlabClient, bitbucketCloudClient, bitbucketServerClient)
|
||||
commitStatusUpdater := &events.DefaultCommitStatusUpdater{Client: vcsClient}
|
||||
terraformClient, err := terraform.NewClient(userConfig.DataDir)
|
||||
terraformClient, err := terraform.NewClient(userConfig.DataDir, userConfig.TFEToken)
|
||||
// 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.
|
||||
|
||||
@@ -35,6 +35,7 @@ type UserConfig struct {
|
||||
SlackToken string `mapstructure:"slack-token"`
|
||||
SSLCertFile string `mapstructure:"ssl-cert-file"`
|
||||
SSLKeyFile string `mapstructure:"ssl-key-file"`
|
||||
TFEToken string `mapstructure:"tfe-token"`
|
||||
Webhooks []WebhookConfig `mapstructure:"webhooks"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user