mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 22:28:25 +00:00
Add flag for configuring git-credentials-store
--write-git-creds will create a .git-credentials file and configure git to use it. To allow authentication with your git remotes over https. To access private repos.
This commit is contained in:
@@ -70,6 +70,7 @@ const (
|
||||
SSLKeyFileFlag = "ssl-key-file"
|
||||
TFEHostnameFlag = "tfe-hostname"
|
||||
TFETokenFlag = "tfe-token"
|
||||
WriteGitCredsFlag = "write-git-creds"
|
||||
|
||||
// Flag defaults.
|
||||
// NOTE: Must manually set these as defaults in the setDefaults function.
|
||||
@@ -227,6 +228,11 @@ var boolFlags = map[string]boolFlag{
|
||||
description: "Silences the posting of whitelist error comments.",
|
||||
defaultValue: false,
|
||||
},
|
||||
WriteGitCredsFlag: {
|
||||
description: "Write out a .git-credentials file with the provider user and token to allow authentication with git over HTTPS." +
|
||||
" This does write secrets to disk and should only be enabled in a secure environment.",
|
||||
defaultValue: false,
|
||||
},
|
||||
}
|
||||
var intFlags = map[string]intFlag{
|
||||
PortFlag: {
|
||||
|
||||
@@ -364,6 +364,7 @@ func TestExecute_Defaults(t *testing.T) {
|
||||
Equals(t, "", passedConfig.SSLKeyFile)
|
||||
Equals(t, "app.terraform.io", passedConfig.TFEHostname)
|
||||
Equals(t, "", passedConfig.TFEToken)
|
||||
Equals(t, false, passedConfig.WriteGitCreds)
|
||||
}
|
||||
|
||||
func TestExecute_ExpandHomeInDataDir(t *testing.T) {
|
||||
@@ -469,6 +470,7 @@ func TestExecute_Flags(t *testing.T) {
|
||||
cmd.SSLKeyFileFlag: "key-file",
|
||||
cmd.TFEHostnameFlag: "my-hostname",
|
||||
cmd.TFETokenFlag: "my-token",
|
||||
cmd.WriteGitCredsFlag: true,
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -503,6 +505,7 @@ func TestExecute_Flags(t *testing.T) {
|
||||
Equals(t, "key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "my-hostname", passedConfig.TFEHostname)
|
||||
Equals(t, "my-token", passedConfig.TFEToken)
|
||||
Equals(t, true, passedConfig.WriteGitCreds)
|
||||
}
|
||||
|
||||
func TestExecute_ConfigFile(t *testing.T) {
|
||||
@@ -538,6 +541,7 @@ ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
tfe-hostname: my-hostname
|
||||
tfe-token: my-token
|
||||
write-git-creds: true
|
||||
`)
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
c := setup(map[string]interface{}{
|
||||
@@ -576,6 +580,7 @@ tfe-token: my-token
|
||||
Equals(t, "key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "my-hostname", passedConfig.TFEHostname)
|
||||
Equals(t, "my-token", passedConfig.TFEToken)
|
||||
Equals(t, true, passedConfig.WriteGitCreds)
|
||||
}
|
||||
|
||||
func TestExecute_EnvironmentOverride(t *testing.T) {
|
||||
@@ -610,6 +615,7 @@ ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
tfe-hostname: my-hostname
|
||||
tfe-token: my-token
|
||||
write-git-creds: true
|
||||
`)
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
|
||||
@@ -645,6 +651,7 @@ tfe-token: my-token
|
||||
"SSL_KEY_FILE": "override-key-file",
|
||||
"TFE_HOSTNAME": "override-my-hostname",
|
||||
"TFE_TOKEN": "override-my-token",
|
||||
"WRITE_GIT_CREDS": "false",
|
||||
} {
|
||||
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
|
||||
}
|
||||
@@ -683,6 +690,7 @@ tfe-token: my-token
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
Equals(t, false, passedConfig.WriteGitCreds)
|
||||
}
|
||||
|
||||
func TestExecute_FlagConfigOverride(t *testing.T) {
|
||||
@@ -718,6 +726,7 @@ ssl-cert-file: cert-file
|
||||
ssl-key-file: key-file
|
||||
tfe-hostname: my-hostname
|
||||
tfe-token: my-token
|
||||
write-git-creds: true
|
||||
`)
|
||||
|
||||
defer os.Remove(tmpFile) // nolint: errcheck
|
||||
@@ -752,6 +761,7 @@ tfe-token: my-token
|
||||
cmd.SSLKeyFileFlag: "override-key-file",
|
||||
cmd.TFEHostnameFlag: "override-my-hostname",
|
||||
cmd.TFETokenFlag: "override-my-token",
|
||||
cmd.WriteGitCredsFlag: false,
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -784,6 +794,7 @@ tfe-token: my-token
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
Equals(t, false, passedConfig.WriteGitCreds)
|
||||
|
||||
}
|
||||
|
||||
@@ -821,6 +832,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
"SSL_KEY_FILE": "key-file",
|
||||
"TFE_HOSTNAME": "my-hostname",
|
||||
"TFE_TOKEN": "my-token",
|
||||
"WRITE_GIT_CREDS": "true",
|
||||
}
|
||||
for name, value := range envVars {
|
||||
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
|
||||
@@ -863,6 +875,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
cmd.SSLKeyFileFlag: "override-key-file",
|
||||
cmd.TFEHostnameFlag: "override-my-hostname",
|
||||
cmd.TFETokenFlag: "override-my-token",
|
||||
cmd.WriteGitCredsFlag: false,
|
||||
})
|
||||
err := c.Execute()
|
||||
Ok(t, err)
|
||||
@@ -897,6 +910,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
|
||||
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
|
||||
Equals(t, "override-my-hostname", passedConfig.TFEHostname)
|
||||
Equals(t, "override-my-token", passedConfig.TFEToken)
|
||||
Equals(t, false, passedConfig.WriteGitCreds)
|
||||
}
|
||||
|
||||
// If using bitbucket cloud, webhook secrets are not supported.
|
||||
|
||||
@@ -402,3 +402,13 @@ Values are chosen in this order:
|
||||
ATLANTIS_TFE_TOKEN='xxx.atlasv1.yyy' atlantis server
|
||||
```
|
||||
A token for Terraform Cloud/Terraform Enteprise integration. See [Terraform Cloud](terraform-cloud.html) for more details.
|
||||
|
||||
* ### `--write-git-creds`
|
||||
```bash
|
||||
atlantis server --write-git-creds
|
||||
```
|
||||
Write out a .git-credentials file and configure git-credentials-store. To allow authentication with your git remotes over https. See [here](https://git-scm.com/docs/git-credential-store) for more information.
|
||||
|
||||
::: warning SECURITY WARNING
|
||||
Potentially dangerous to enable
|
||||
This writes your credentials to disk and a malicious pull request could then access these details
|
||||
|
||||
51
server/events/git_cred_writer.go
Normal file
51
server/events/git_cred_writer.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WriteGitCreds generates a .git-credentials file containing the username and token
|
||||
// used for authenticating with git over HTTPS
|
||||
// It will create the file in home/.git-credentials
|
||||
func WriteGitCreds(gitUser string, gitToken string, gitHostname string, home string, logger *logging.SimpleLogger) error {
|
||||
const credsFilename = ".git-credentials"
|
||||
credsFile := filepath.Join(home, credsFilename)
|
||||
credsFileContents := `https://%s:%s@%s`
|
||||
config := fmt.Sprintf(credsFileContents, gitUser, gitToken, gitHostname)
|
||||
|
||||
// If there is already a .git-credentials 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
|
||||
if _, err := os.Stat(credsFile); err == nil {
|
||||
currContents, err := ioutil.ReadFile(credsFile) // nolint: gosec
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "trying to read %s to ensure we're not overwriting it", credsFile)
|
||||
}
|
||||
if config != string(currContents) {
|
||||
return fmt.Errorf("can't write git-credentials to %s because that file has contents that would be overwritten", credsFile)
|
||||
}
|
||||
// Otherwise we don't need to write the file because it already has
|
||||
// what we need.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(credsFile, []byte(config), 0600); err != nil {
|
||||
return errors.Wrapf(err, "writing generated %s file with user, token and hostname to %s", credsFilename, credsFile)
|
||||
}
|
||||
|
||||
logger.Info("wrote git credentials to %s", credsFile)
|
||||
|
||||
cmd := exec.Command("git", "config", "--global", "credential.helper", "store")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return errors.Wrapf(err, "There was an error running %s: %s", strings.Join(cmd.Args, " "), string(out))
|
||||
}
|
||||
logger.Info("successfully ran %s", strings.Join(cmd.Args, " "))
|
||||
return nil
|
||||
}
|
||||
98
server/events/git_cred_writer_test.go
Normal file
98
server/events/git_cred_writer_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package events_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
var logger *logging.SimpleLogger
|
||||
|
||||
// Test that we write the file as expected
|
||||
func TestWriteGitCreds_WriteFile(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
err := events.WriteGitCreds("user", "token", "hostname", tmp, logger)
|
||||
Ok(t, err)
|
||||
|
||||
expContents := `https://user:token@hostname`
|
||||
|
||||
actContents, err := ioutil.ReadFile(filepath.Join(tmp, ".git-credentials"))
|
||||
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 TestWriteGitCreds_WillNotOverwrite(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
err := ioutil.WriteFile(credsFile, []byte("contents"), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
actErr := events.WriteGitCreds("user", "token", "hostname", tmp, logger)
|
||||
expErr := fmt.Sprintf("can't write git-credentials to %s because that file has contents that would be overwritten", tmp+"/.git-credentials")
|
||||
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 TestWriteGitCreds_NoErrIfContentsSame(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
contents := `https://user:token@hostname`
|
||||
|
||||
err := ioutil.WriteFile(credsFile, []byte(contents), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = events.WriteGitCreds("user", "token", "hostname", tmp, logger)
|
||||
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 TestWriteGitCreds_ErrIfCannotRead(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
err := ioutil.WriteFile(credsFile, []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", credsFile, credsFile)
|
||||
actErr := events.WriteGitCreds("user", "token", "hostname", tmp, logger)
|
||||
ErrEquals(t, expErr, actErr)
|
||||
}
|
||||
|
||||
// Test that if we can't write, we error out.
|
||||
func TestWriteGitCreds_ErrIfCannotWrite(t *testing.T) {
|
||||
credsFile := "/this/dir/does/not/exist/.git-credentials"
|
||||
expErr := fmt.Sprintf("writing generated .git-credentials file with user, token and hostname to %s: open %s: no such file or directory", credsFile, credsFile)
|
||||
actErr := events.WriteGitCreds("user", "token", "hostname", "/this/dir/does/not/exist", logger)
|
||||
ErrEquals(t, expErr, actErr)
|
||||
}
|
||||
|
||||
// Test that git is actually configured to use the credentials
|
||||
func TestWriteGitCreds_ConfigureGit(t *testing.T) {
|
||||
tmp, cleanup := TempDir(t)
|
||||
defer cleanup()
|
||||
|
||||
err := events.WriteGitCreds("user", "token", "hostname", tmp, logger)
|
||||
Ok(t, err)
|
||||
|
||||
expOutput := `store`
|
||||
actOutput, err := exec.Command("git", "config", "--global", "credential.helper").Output()
|
||||
Ok(t, err)
|
||||
Equals(t, expOutput+"\n", string(actOutput))
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/runatlantis/atlantis/server/events/db"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
|
||||
@@ -152,6 +153,28 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if userConfig.WriteGitCreds {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file")
|
||||
}
|
||||
if userConfig.GithubUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userConfig.GitlabUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userConfig.BitbucketUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, userConfig.BitbucketBaseURL, home, logger); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var webhooksConfig []webhooks.Config
|
||||
for _, c := range userConfig.Webhooks {
|
||||
config := webhooks.Config{
|
||||
|
||||
@@ -44,6 +44,7 @@ type UserConfig struct {
|
||||
TFEToken string `mapstructure:"tfe-token"`
|
||||
DefaultTFVersion string `mapstructure:"default-tf-version"`
|
||||
Webhooks []WebhookConfig `mapstructure:"webhooks"`
|
||||
WriteGitCreds bool `mapstructure:"write-git-creds"`
|
||||
}
|
||||
|
||||
// ToLogLevel returns the LogLevel object corresponding to the user-passed
|
||||
|
||||
Reference in New Issue
Block a user