From c8f68e22975bb614650b5ef403e9ce286533601b Mon Sep 17 00:00:00 2001 From: Luke Kysow Date: Tue, 20 Mar 2018 12:13:45 -0700 Subject: [PATCH] Refactor env var work and add tests. - added tests - moved AutomaticEnv() call into the init() function of the server flag so it's set when we run the tests. Before it was in main(). - Set SetTypeByDefaultValue to true so boolean flags can be specified as env vars. --- README.md | 8 +- cmd/server.go | 27 +++-- cmd/server_test.go | 262 +++++++++++++++++++++++++++++++++++---------- main.go | 6 -- 4 files changed, 226 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 7a6298330..bacce1079 100644 --- a/README.md +++ b/README.md @@ -442,10 +442,10 @@ If you'd like to test out Atlantis before running it on your own repositories yo - `atlantis apply` will run `terraform apply`. Since our pull request creates a `null_resource` (which does nothing) this is safe to do. ## Server Configuration -Atlantis configuration can be specified via command line flags or a YAML config file. -The `gh-token` and `gitlab-token` flags can also be specified via the `ATLANTIS_GH_TOKEN` and `ATLANTIS_GITLAB_TOKEN` environment variables respectively. +Configuration for `atlantis server` can be specified via command line flags, environment variables or a YAML config file. Config file values are overridden by environment variables which in turn are overridden by flags. +### YAML To use a yaml config file, run atlantis with `--config /path/to/config.yaml`. The keys of your config file should be the same as the flag, ex. ```yaml @@ -454,6 +454,10 @@ gh-token: ... log-level: ... ``` +### Environment Variables +All flags can be specified as environment variables. You need to convert the flag's `-`'s to `_`'s, uppercase all the letters and prefix with `ATLANTIS_`. +For example, `--gh-user` can be set via the environment variable `ATLANTIS_GH_USER`. + To see a list of all flags and their descriptions run `atlantis server --help` ## AWS Credentials diff --git a/cmd/server.go b/cmd/server.go index 95b0dae54..8504b129f 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -61,7 +61,7 @@ var stringFlags = []stringFlag{ }, { name: ConfigFlag, - description: "Path to config file.", + description: "Path to config file. All flags can be set in a YAML config file instead.", }, { name: DataDirFlag, @@ -80,7 +80,6 @@ var stringFlags = []stringFlag{ { name: GHTokenFlag, description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.", - env: "ATLANTIS_GH_TOKEN", }, { name: GHWebHookSecret, @@ -88,7 +87,6 @@ var stringFlags = []stringFlag{ " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_GH_WEBHOOK_SECRET environment variable.", - env: "ATLANTIS_GH_WEBHOOK_SECRET", }, { name: GitlabHostnameFlag, @@ -102,7 +100,6 @@ var stringFlags = []stringFlag{ { name: GitlabTokenFlag, description: "GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.", - env: "ATLANTIS_GITLAB_TOKEN", }, { name: GitlabWebHookSecret, @@ -110,7 +107,6 @@ var stringFlags = []stringFlag{ " SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. " + "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_GITLAB_WEBHOOK_SECRET environment variable.", - env: "ATLANTIS_GITLAB_WEBHOOK_SECRET", }, { name: LogLevelFlag, @@ -156,7 +152,6 @@ type stringFlag struct { name string description string value string - env string } type intFlag struct { name string @@ -203,12 +198,9 @@ func (d *DefaultServerCreator) NewServer(userConfig server.UserConfig, config se // Init returns the runnable cobra command. func (s *ServerCmd) Init() *cobra.Command { c := &cobra.Command{ - Use: "server", - Short: "Start the atlantis server", - Long: `Start the atlantis server - -Flags can also be set in a yaml config file (see --` + ConfigFlag + `). -Config file values are overridden by environment variables which in turn are overridden by flags.`, + Use: "server", + Short: "Start the atlantis server", + Long: `Start the atlantis server and listen for webhook calls.`, SilenceErrors: true, SilenceUsage: s.SilenceOutput, PreRunE: s.withErrPrint(func(cmd *cobra.Command, args []string) error { @@ -218,6 +210,14 @@ Config file values are overridden by environment variables which in turn are ove return s.run() }), } + + // Configure viper to accept env vars prefixed with ATLANTIS_ that can be + // used instead of flags. + s.Viper.SetEnvPrefix("ATLANTIS") + s.Viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + s.Viper.AutomaticEnv() + s.Viper.SetTypeByDefaultValue(true) + // Replace the call in their template to use the usage function that wraps // columns to make for a nicer output. usageWithWrappedCols := strings.Replace(c.UsageTemplate(), ".FlagUsages", ".FlagUsagesWrapped 120", -1) @@ -232,9 +232,6 @@ Config file values are overridden by environment variables which in turn are ove // Set string flags. for _, f := range stringFlags { c.Flags().String(f.name, f.value, "> "+f.description) - if f.env != "" { - s.Viper.BindEnv(f.name, f.env) // nolint: errcheck - } s.Viper.BindPFlag(f.name, c.Flags().Lookup(f.name)) // nolint: errcheck } diff --git a/cmd/server_test.go b/cmd/server_test.go index e976471af..4a41f7206 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -273,26 +273,30 @@ func TestExecute_Defaults(t *testing.T) { err := c.Execute() Ok(t, err) - Equals(t, "user", passedConfig.GithubUser) - Equals(t, "token", passedConfig.GithubToken) - Equals(t, "", passedConfig.GithubWebHookSecret) - Equals(t, "gitlab-user", passedConfig.GitlabUser) - Equals(t, "gitlab-token", passedConfig.GitlabToken) - Equals(t, "", passedConfig.GitlabWebHookSecret) // Get our hostname since that's what gets defaulted to hostname, err := os.Hostname() Ok(t, err) Equals(t, "http://"+hostname+":4141", passedConfig.AtlantisURL) + Equals(t, false, passedConfig.AllowForkPRs) // Get our home dir since that's what gets defaulted to dataDir, err := homedir.Expand("~/.atlantis") Ok(t, err) Equals(t, dataDir, passedConfig.DataDir) + Equals(t, "github.com", passedConfig.GithubHostname) + Equals(t, "token", passedConfig.GithubToken) + Equals(t, "user", passedConfig.GithubUser) + Equals(t, "", passedConfig.GithubWebHookSecret) Equals(t, "gitlab.com", passedConfig.GitlabHostname) + Equals(t, "gitlab-token", passedConfig.GitlabToken) + Equals(t, "gitlab-user", passedConfig.GitlabUser) + Equals(t, "", passedConfig.GitlabWebHookSecret) Equals(t, "info", passedConfig.LogLevel) - Equals(t, false, passedConfig.RequireApproval) Equals(t, 4141, passedConfig.Port) + Equals(t, false, passedConfig.RequireApproval) + Equals(t, "", passedConfig.SSLCertFile) + Equals(t, "", passedConfig.SSLKeyFile) } func TestExecute_ExpandHomeInDataDir(t *testing.T) { @@ -356,56 +360,66 @@ func TestExecute_Flags(t *testing.T) { t.Log("Should use all flags that are set.") c := setup(map[string]interface{}{ cmd.AtlantisURLFlag: "url", + cmd.AllowForkPRsFlag: true, cmd.DataDirFlag: "/path", cmd.GHHostnameFlag: "ghhostname", - cmd.GHUserFlag: "user", cmd.GHTokenFlag: "token", + cmd.GHUserFlag: "user", cmd.GHWebHookSecret: "secret", cmd.GitlabHostnameFlag: "gitlab-hostname", - cmd.GitlabUserFlag: "gitlab-user", cmd.GitlabTokenFlag: "gitlab-token", + cmd.GitlabUserFlag: "gitlab-user", cmd.GitlabWebHookSecret: "gitlab-secret", cmd.LogLevelFlag: "debug", cmd.PortFlag: 8181, - cmd.RequireApprovalFlag: true, cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis", + cmd.RequireApprovalFlag: true, + cmd.SSLCertFileFlag: "cert-file", + cmd.SSLKeyFileFlag: "key-file", }) err := c.Execute() Ok(t, err) Equals(t, "url", passedConfig.AtlantisURL) + Equals(t, true, passedConfig.AllowForkPRs) Equals(t, "/path", passedConfig.DataDir) Equals(t, "ghhostname", passedConfig.GithubHostname) - Equals(t, "user", passedConfig.GithubUser) Equals(t, "token", passedConfig.GithubToken) + Equals(t, "user", passedConfig.GithubUser) Equals(t, "secret", passedConfig.GithubWebHookSecret) Equals(t, "gitlab-hostname", passedConfig.GitlabHostname) - Equals(t, "gitlab-user", passedConfig.GitlabUser) Equals(t, "gitlab-token", passedConfig.GitlabToken) + Equals(t, "gitlab-user", passedConfig.GitlabUser) Equals(t, "gitlab-secret", passedConfig.GitlabWebHookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) - Equals(t, true, passedConfig.RequireApproval) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) + Equals(t, true, passedConfig.RequireApproval) + Equals(t, "cert-file", passedConfig.SSLCertFile) + Equals(t, "key-file", passedConfig.SSLKeyFile) } func TestExecute_ConfigFile(t *testing.T) { t.Log("Should use all the values from the config file.") tmpFile := tempFile(t, `--- atlantis-url: "url" +allow-fork-prs: true data-dir: "/path" gh-hostname: "ghhostname" -gh-user: "user" gh-token: "token" +gh-user: "user" gh-webhook-secret: "secret" gitlab-hostname: "gitlab-hostname" -gitlab-user: "gitlab-user" gitlab-token: "gitlab-token" +gitlab-user: "gitlab-user" gitlab-webhook-secret: "gitlab-secret" log-level: "debug" port: 8181 +repo-whitelist: "github.com/runatlantis/atlantis" require-approval: true -repo-whitelist: "github.com/runatlantis/atlantis"`) +ssl-cert-file: cert-file +ssl-key-file: key-file +`) defer os.Remove(tmpFile) // nolint: errcheck c := setup(map[string]interface{}{ cmd.ConfigFlag: tmpFile, @@ -414,80 +428,220 @@ repo-whitelist: "github.com/runatlantis/atlantis"`) err := c.Execute() Ok(t, err) Equals(t, "url", passedConfig.AtlantisURL) + Equals(t, true, passedConfig.AllowForkPRs) Equals(t, "/path", passedConfig.DataDir) Equals(t, "ghhostname", passedConfig.GithubHostname) - Equals(t, "user", passedConfig.GithubUser) Equals(t, "token", passedConfig.GithubToken) + Equals(t, "user", passedConfig.GithubUser) Equals(t, "secret", passedConfig.GithubWebHookSecret) Equals(t, "gitlab-hostname", passedConfig.GitlabHostname) - Equals(t, "gitlab-user", passedConfig.GitlabUser) Equals(t, "gitlab-token", passedConfig.GitlabToken) + Equals(t, "gitlab-user", passedConfig.GitlabUser) Equals(t, "gitlab-secret", passedConfig.GitlabWebHookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) - Equals(t, true, passedConfig.RequireApproval) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) + Equals(t, true, passedConfig.RequireApproval) + Equals(t, "cert-file", passedConfig.SSLCertFile) + Equals(t, "key-file", passedConfig.SSLKeyFile) } func TestExecute_EnvironmentOverride(t *testing.T) { t.Log("Environment variables should override config file flags.") - tmpFile := tempFile(t, "gh-user: config\ngh-token: config2") - defer os.Remove(tmpFile) // nolint: errcheck - os.Setenv("ATLANTIS_GH_TOKEN", "override") // nolint: errcheck + tmpFile := tempFile(t, `--- +atlantis-url: "url" +allow-fork-prs: true +data-dir: "/path" +gh-hostname: "ghhostname" +gh-token: "token" +gh-user: "user" +gh-webhook-secret: "secret" +gitlab-hostname: "gitlab-hostname" +gitlab-token: "gitlab-token" +gitlab-user: "gitlab-user" +gitlab-webhook-secret: "gitlab-secret" +log-level: "debug" +port: 8181 +repo-whitelist: "github.com/runatlantis/atlantis" +require-approval: true +ssl-cert-file: cert-file +ssl-key-file: key-file +`) + defer os.Remove(tmpFile) // nolint: errcheck + + // NOTE: We add the ATLANTIS_ prefix below. + for name, value := range map[string]string{ + "ATLANTIS_URL": "override-url", + "ALLOW_FORK_PRS": "false", + "DATA_DIR": "/override-path", + "GH_HOSTNAME": "override-gh-hostname", + "GH_TOKEN": "override-gh-token", + "GH_USER": "override-gh-user", + "GH_WEBHOOK_SECRET": "override-gh-webhook-secret", + "GITLAB_HOSTNAME": "override-gitlab-hostname", + "GITLAB_TOKEN": "override-gitlab-token", + "GITLAB_USER": "override-gitlab-user", + "GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret", + "LOG_LEVEL": "info", + "PORT": "8282", + "REPO_WHITELIST": "override,override", + "REQUIRE_APPROVAL": "false", + "SSL_CERT_FILE": "override-cert-file", + "SSL_KEY_FILE": "override-key-file", + } { + os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck + } c := setup(map[string]interface{}{ - cmd.ConfigFlag: tmpFile, - cmd.RepoWhitelistFlag: "*", + cmd.ConfigFlag: tmpFile, }) err := c.Execute() Ok(t, err) - Equals(t, "override", passedConfig.GithubToken) + Equals(t, "override-url", passedConfig.AtlantisURL) + Equals(t, false, passedConfig.AllowForkPRs) + Equals(t, "/override-path", passedConfig.DataDir) + Equals(t, "override-gh-hostname", passedConfig.GithubHostname) + Equals(t, "override-gh-token", passedConfig.GithubToken) + Equals(t, "override-gh-user", passedConfig.GithubUser) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) + Equals(t, "override-gitlab-token", passedConfig.GitlabToken) + Equals(t, "override-gitlab-user", passedConfig.GitlabUser) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "info", passedConfig.LogLevel) + Equals(t, 8282, passedConfig.Port) + Equals(t, "override,override", passedConfig.RepoWhitelist) + Equals(t, false, passedConfig.RequireApproval) + Equals(t, "override-cert-file", passedConfig.SSLCertFile) + Equals(t, "override-key-file", passedConfig.SSLKeyFile) } func TestExecute_FlagConfigOverride(t *testing.T) { t.Log("Flags should override config file flags.") - os.Setenv("ATLANTIS_GH_TOKEN", "env-var") // nolint: errcheck + tmpFile := tempFile(t, `--- +atlantis-url: "url" +allow-fork-prs: true +data-dir: "/path" +gh-hostname: "ghhostname" +gh-token: "token" +gh-user: "user" +gh-webhook-secret: "secret" +gitlab-hostname: "gitlab-hostname" +gitlab-token: "gitlab-token" +gitlab-user: "gitlab-user" +gitlab-webhook-secret: "gitlab-secret" +log-level: "debug" +port: 8181 +repo-whitelist: "github.com/runatlantis/atlantis" +require-approval: true +ssl-cert-file: cert-file +ssl-key-file: key-file +`) + + defer os.Remove(tmpFile) // nolint: errcheck c := setup(map[string]interface{}{ - cmd.GHUserFlag: "user", - cmd.GHTokenFlag: "override", - cmd.RepoWhitelistFlag: "*", + cmd.AtlantisURLFlag: "override-url", + cmd.AllowForkPRsFlag: false, + cmd.DataDirFlag: "/override-path", + cmd.GHHostnameFlag: "override-gh-hostname", + cmd.GHTokenFlag: "override-gh-token", + cmd.GHUserFlag: "override-gh-user", + cmd.GHWebHookSecret: "override-gh-webhook-secret", + cmd.GitlabHostnameFlag: "override-gitlab-hostname", + cmd.GitlabTokenFlag: "override-gitlab-token", + cmd.GitlabUserFlag: "override-gitlab-user", + cmd.GitlabWebHookSecret: "override-gitlab-webhook-secret", + cmd.LogLevelFlag: "info", + cmd.PortFlag: 8282, + cmd.RepoWhitelistFlag: "override,override", + cmd.RequireApprovalFlag: false, + cmd.SSLCertFileFlag: "override-cert-file", + cmd.SSLKeyFileFlag: "override-key-file", }) err := c.Execute() Ok(t, err) - Equals(t, "override", passedConfig.GithubToken) + Equals(t, "override-url", passedConfig.AtlantisURL) + Equals(t, false, passedConfig.AllowForkPRs) + Equals(t, "/override-path", passedConfig.DataDir) + Equals(t, "override-gh-hostname", passedConfig.GithubHostname) + Equals(t, "override-gh-token", passedConfig.GithubToken) + Equals(t, "override-gh-user", passedConfig.GithubUser) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) + Equals(t, "override-gitlab-token", passedConfig.GitlabToken) + Equals(t, "override-gitlab-user", passedConfig.GitlabUser) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "info", passedConfig.LogLevel) + Equals(t, 8282, passedConfig.Port) + Equals(t, "override,override", passedConfig.RepoWhitelist) + Equals(t, false, passedConfig.RequireApproval) + Equals(t, "override-cert-file", passedConfig.SSLCertFile) + Equals(t, "override-key-file", passedConfig.SSLKeyFile) } func TestExecute_FlagEnvVarOverride(t *testing.T) { t.Log("Flags should override environment variables.") - tmpFile := tempFile(t, "gh-user: config\ngh-token: config2") - defer os.Remove(tmpFile) // nolint: errcheck - c := setup(map[string]interface{}{ - cmd.ConfigFlag: tmpFile, - cmd.GHTokenFlag: "override", - cmd.RepoWhitelistFlag: "*", - }) - err := c.Execute() - Ok(t, err) - Equals(t, "override", passedConfig.GithubToken) -} + for name, value := range map[string]string{ + "ATLANTIS_URL": "url", + "ALLOW_FORK_PRS": "true", + "DATA_DIR": "/path", + "GH_HOSTNAME": "gh-hostname", + "GH_TOKEN": "gh-token", + "GH_USER": "gh-user", + "GH_WEBHOOK_SECRET": "gh-webhook-secret", + "GITLAB_HOSTNAME": "gitlab-hostname", + "GITLAB_TOKEN": "gitlab-token", + "GITLAB_USER": "gitlab-user", + "GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret", + "LOG_LEVEL": "debug", + "PORT": "8181", + "REPO_WHITELIST": "*", + "REQUIRE_APPROVAL": "true", + "SSL_CERT_FILE": "cert-file", + "SSL_KEY_FILE": "key-file", + } { + os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck + } -func TestExecute_EnvVars(t *testing.T) { - t.Log("Setting flags by env var should work.") - os.Setenv("ATLANTIS_GH_TOKEN", "gh-token") // nolint: errcheck - os.Setenv("ATLANTIS_GH_WEBHOOK_SECRET", "gh-webhook") // nolint: errcheck - os.Setenv("ATLANTIS_GITLAB_TOKEN", "gitlab-token") // nolint: errcheck - os.Setenv("ATLANTIS_GITLAB_WEBHOOK_SECRET", "gitlab-webhook") // nolint: errcheck c := setup(map[string]interface{}{ - cmd.GHUserFlag: "user", - cmd.GitlabUserFlag: "user", - cmd.RepoWhitelistFlag: "*", + cmd.AtlantisURLFlag: "override-url", + cmd.AllowForkPRsFlag: false, + cmd.DataDirFlag: "/override-path", + cmd.GHHostnameFlag: "override-gh-hostname", + cmd.GHTokenFlag: "override-gh-token", + cmd.GHUserFlag: "override-gh-user", + cmd.GHWebHookSecret: "override-gh-webhook-secret", + cmd.GitlabHostnameFlag: "override-gitlab-hostname", + cmd.GitlabTokenFlag: "override-gitlab-token", + cmd.GitlabUserFlag: "override-gitlab-user", + cmd.GitlabWebHookSecret: "override-gitlab-webhook-secret", + cmd.LogLevelFlag: "info", + cmd.PortFlag: 8282, + cmd.RepoWhitelistFlag: "override,override", + cmd.RequireApprovalFlag: false, + cmd.SSLCertFileFlag: "override-cert-file", + cmd.SSLKeyFileFlag: "override-key-file", }) err := c.Execute() Ok(t, err) - Equals(t, "gh-token", passedConfig.GithubToken) - Equals(t, "gh-webhook", passedConfig.GithubWebHookSecret) - Equals(t, "gitlab-token", passedConfig.GitlabToken) - Equals(t, "gitlab-webhook", passedConfig.GitlabWebHookSecret) + + Equals(t, "override-url", passedConfig.AtlantisURL) + Equals(t, false, passedConfig.AllowForkPRs) + Equals(t, "/override-path", passedConfig.DataDir) + Equals(t, "override-gh-hostname", passedConfig.GithubHostname) + Equals(t, "override-gh-token", passedConfig.GithubToken) + Equals(t, "override-gh-user", passedConfig.GithubUser) + Equals(t, "override-gh-webhook-secret", passedConfig.GithubWebHookSecret) + Equals(t, "override-gitlab-hostname", passedConfig.GitlabHostname) + Equals(t, "override-gitlab-token", passedConfig.GitlabToken) + Equals(t, "override-gitlab-user", passedConfig.GitlabUser) + Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebHookSecret) + Equals(t, "info", passedConfig.LogLevel) + Equals(t, 8282, passedConfig.Port) + Equals(t, "override,override", passedConfig.RepoWhitelist) + Equals(t, false, passedConfig.RequireApproval) + Equals(t, "override-cert-file", passedConfig.SSLCertFile) + Equals(t, "override-key-file", passedConfig.SSLKeyFile) } func setup(flags map[string]interface{}) *cobra.Command { diff --git a/main.go b/main.go index 4f0be780f..0063e5664 100644 --- a/main.go +++ b/main.go @@ -17,18 +17,12 @@ package main import ( "github.com/runatlantis/atlantis/cmd" "github.com/spf13/viper" - "strings" ) const atlantisVersion = "0.3.3" func main() { v := viper.New() - // Get environment variables with ATLANTIS prefix - v.SetEnvPrefix("ATLANTIS") - replacer := strings.NewReplacer("-","_") - v.SetEnvKeyReplacer(replacer) - v.AutomaticEnv() // We're creating commands manually here rather than using init() functions // (as recommended by cobra) because it makes testing easier.