From 18355c6f623b44516322b21cce3be1bc29a7716b Mon Sep 17 00:00:00 2001 From: Andriy Knysh Date: Tue, 16 Oct 2018 17:42:01 -0400 Subject: [PATCH] Add `repo-config` command line argument (#12) --- cmd/server.go | 18 +++++++- cmd/server_test.go | 39 ++++++++++++++---- helm/atlantis/values.yaml | 2 +- runatlantis.io/docs/server-configuration.md | 14 ++++++- server/events/comment_parser.go | 5 +-- server/events/comment_parser_test.go | 8 ++-- server/events/project_command_builder.go | 27 ++++++------ server/events/project_command_builder_test.go | 26 +++++++++--- server/events/yaml/parser_validator.go | 21 ++++------ server/events/yaml/parser_validator_test.go | 41 +++++++++++-------- server/events_controller_e2e_test.go | 11 ++--- server/server.go | 2 + 12 files changed, 144 insertions(+), 70 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index c58d4a2b05..bc5238c118 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -54,6 +54,7 @@ const ( GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec LogLevelFlag = "log-level" PortFlag = "port" + RepoConfigFlag = "repo-config" RepoWhitelistFlag = "repo-whitelist" RequireApprovalFlag = "require-approval" SSLCertFileFlag = "ssl-cert-file" @@ -67,6 +68,7 @@ const ( DefaultGitlabHostname = "gitlab.com" DefaultLogLevel = "info" DefaultPort = 4141 + DefaultRepoConfig = "atlantis.yaml" ) const redTermStart = "\033[31m" @@ -163,6 +165,11 @@ var stringFlags = []stringFlag{ defaultValue: DefaultLogLevel, }, { + name: RepoConfigFlag, + description: "Optional path to the Atlantis YAML config file contained in each repo that this server should use. " + + "This allows different Atlantis servers to point at different configs in the same repo.", + defaultValue: DefaultRepoConfig, + }, { name: RepoWhitelistFlag, description: "Comma separated list of repositories that Atlantis will operate on. " + "The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any characters until the next comma and can be used for example to whitelist " + @@ -186,7 +193,7 @@ var boolFlags = []boolFlag{ }, { name: AllowRepoConfigFlag, - description: "Allow repositories to use atlantis.yaml files to customize the commands Atlantis runs." + + description: "Allow repositories to use atlantis repo config YAML files to customize the commands Atlantis runs." + " Should only be enabled in a trusted environment since it enables a pull request to run arbitrary commands" + " on the Atlantis server.", defaultValue: false, @@ -379,6 +386,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.GithubTeamWhitelist == "" { c.GithubTeamWhitelist = DefaultGHTeamWhitelist } + if c.RepoConfig == "" { + c.RepoConfig = DefaultRepoConfig + } } func (s *ServerCmd) validate(userConfig server.UserConfig) error { @@ -424,6 +434,12 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error { if parsed.Scheme != "http" && parsed.Scheme != "https" { return fmt.Errorf("--%s must have http:// or https://, got %q", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL) } + + // Cannot accept custom repo config if we know repo configs are disabled + if (userConfig.RepoConfig != DefaultRepoConfig) && (!userConfig.AllowRepoConfig) { + return fmt.Errorf("custom --%s cannot be specified if --%s is false", RepoConfigFlag, AllowRepoConfigFlag) + } + return nil } diff --git a/cmd/server_test.go b/cmd/server_test.go index 8fc146f92e..45f98c9c2a 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -314,6 +314,7 @@ func TestExecute_Defaults(t *testing.T) { cmd.GitlabTokenFlag: "gitlab-token", cmd.BitbucketUserFlag: "bitbucket-user", cmd.BitbucketTokenFlag: "bitbucket-token", + cmd.RepoConfigFlag: "atlantis.yaml", cmd.RepoWhitelistFlag: "*", }) err := c.Execute() @@ -441,6 +442,7 @@ func TestExecute_Flags(t *testing.T) { cmd.GitlabWebhookSecretFlag: "gitlab-secret", cmd.LogLevelFlag: "debug", cmd.PortFlag: 8181, + cmd.RepoConfigFlag: "atlantis.yaml", cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis", cmd.RequireApprovalFlag: true, cmd.SSLCertFileFlag: "cert-file", @@ -467,6 +469,7 @@ func TestExecute_Flags(t *testing.T) { Equals(t, "gitlab-secret", passedConfig.GitlabWebhookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) + Equals(t, "atlantis.yaml", passedConfig.RepoConfig) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) Equals(t, true, passedConfig.RequireApproval) Equals(t, "cert-file", passedConfig.SSLCertFile) @@ -494,6 +497,7 @@ gitlab-user: "gitlab-user" gitlab-webhook-secret: "gitlab-secret" log-level: "debug" port: 8181 +repo-config: "atlantis.yaml" repo-whitelist: "github.com/runatlantis/atlantis" require-approval: true ssl-cert-file: cert-file @@ -524,6 +528,7 @@ ssl-key-file: key-file Equals(t, "gitlab-secret", passedConfig.GitlabWebhookSecret) Equals(t, "debug", passedConfig.LogLevel) Equals(t, 8181, passedConfig.Port) + Equals(t, "atlantis.yaml", passedConfig.RepoConfig) Equals(t, "github.com/runatlantis/atlantis", passedConfig.RepoWhitelist) Equals(t, true, passedConfig.RequireApproval) Equals(t, "cert-file", passedConfig.SSLCertFile) @@ -551,6 +556,7 @@ gitlab-user: "gitlab-user" gitlab-webhook-secret: "gitlab-secret" log-level: "debug" port: 8181 +repo-config: "atlantis.yaml" repo-whitelist: "github.com/runatlantis/atlantis" require-approval: true ssl-cert-file: cert-file @@ -562,7 +568,7 @@ ssl-key-file: key-file for name, value := range map[string]string{ "ATLANTIS_URL": "override-url", "ALLOW_FORK_PRS": "false", - "ALLOW_REPO_CONFIG": "false", + "ALLOW_REPO_CONFIG": "true", "BITBUCKET_BASE_URL": "https://override-bitbucket-base-url", "BITBUCKET_TOKEN": "override-bitbucket-token", "BITBUCKET_USER": "override-bitbucket-user", @@ -592,7 +598,7 @@ ssl-key-file: key-file Ok(t, err) Equals(t, "override-url", passedConfig.AtlantisURL) Equals(t, false, passedConfig.AllowForkPRs) - Equals(t, false, passedConfig.AllowRepoConfig) + Equals(t, true, passedConfig.AllowRepoConfig) Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL) Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken) Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser) @@ -608,6 +614,7 @@ ssl-key-file: key-file Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) + Equals(t, "override-atlantis.yaml", passedConfig.RepoConfig) Equals(t, "override,override", passedConfig.RepoWhitelist) Equals(t, false, passedConfig.RequireApproval) Equals(t, "override-cert-file", passedConfig.SSLCertFile) @@ -619,7 +626,7 @@ func TestExecute_FlagConfigOverride(t *testing.T) { tmpFile := tempFile(t, `--- atlantis-url: "url" allow-fork-prs: true -allow-repo-config: true +allow-repo-config: false bitbucket-base-url: "https://bitbucket-base-url" bitbucket-token: "bitbucket-token" bitbucket-user: "bitbucket-user" @@ -635,6 +642,7 @@ gitlab-user: "gitlab-user" gitlab-webhook-secret: "gitlab-secret" log-level: "debug" port: 8181 +repo-config: "atlantis.yaml" repo-whitelist: "github.com/runatlantis/atlantis" require-approval: true ssl-cert-file: cert-file @@ -645,7 +653,7 @@ ssl-key-file: key-file c := setup(map[string]interface{}{ cmd.AtlantisURLFlag: "override-url", cmd.AllowForkPRsFlag: false, - cmd.AllowRepoConfigFlag: false, + cmd.AllowRepoConfigFlag: true, cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url", cmd.BitbucketTokenFlag: "override-bitbucket-token", cmd.BitbucketUserFlag: "override-bitbucket-user", @@ -661,6 +669,7 @@ ssl-key-file: key-file cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret", cmd.LogLevelFlag: "info", cmd.PortFlag: 8282, + cmd.RepoConfigFlag: "override-atlantis.yaml", cmd.RepoWhitelistFlag: "override,override", cmd.RequireApprovalFlag: false, cmd.SSLCertFileFlag: "override-cert-file", @@ -685,6 +694,7 @@ ssl-key-file: key-file Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) + Equals(t, "override-atlantis.yaml", passedConfig.RepoConfig) Equals(t, "override,override", passedConfig.RepoWhitelist) Equals(t, false, passedConfig.RequireApproval) Equals(t, "override-cert-file", passedConfig.SSLCertFile) @@ -697,7 +707,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { envVars := map[string]string{ "ATLANTIS_URL": "url", "ALLOW_FORK_PRS": "true", - "ALLOW_REPO_CONFIG": "true", + "ALLOW_REPO_CONFIG": "false", "BITBUCKET_BASE_URL": "https://bitbucket-base-url", "BITBUCKET_TOKEN": "bitbucket-token", "BITBUCKET_USER": "bitbucket-user", @@ -731,7 +741,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { c := setup(map[string]interface{}{ cmd.AtlantisURLFlag: "override-url", cmd.AllowForkPRsFlag: false, - cmd.AllowRepoConfigFlag: false, + cmd.AllowRepoConfigFlag: true, cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url", cmd.BitbucketTokenFlag: "override-bitbucket-token", cmd.BitbucketUserFlag: "override-bitbucket-user", @@ -747,6 +757,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret", cmd.LogLevelFlag: "info", cmd.PortFlag: 8282, + cmd.RepoConfigFlag: "override-atlantis.yaml", cmd.RepoWhitelistFlag: "override,override", cmd.RequireApprovalFlag: false, cmd.SSLCertFileFlag: "override-cert-file", @@ -757,7 +768,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-url", passedConfig.AtlantisURL) Equals(t, false, passedConfig.AllowForkPRs) - Equals(t, false, passedConfig.AllowRepoConfig) + Equals(t, true, passedConfig.AllowRepoConfig) Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL) Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken) Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser) @@ -773,6 +784,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-gitlab-webhook-secret", passedConfig.GitlabWebhookSecret) Equals(t, "info", passedConfig.LogLevel) Equals(t, 8282, passedConfig.Port) + Equals(t, "override-atlantis.yaml", passedConfig.RepoConfig) Equals(t, "override,override", passedConfig.RepoWhitelist) Equals(t, false, passedConfig.RequireApproval) Equals(t, "override-cert-file", passedConfig.SSLCertFile) @@ -822,6 +834,19 @@ func TestExecute_BitbucketServerBaseURLPort(t *testing.T) { Equals(t, "http://mydomain.com:7990", passedConfig.BitbucketBaseURL) } +func TestExecute_RepoConfigWithoutAllowRepoConfig(t *testing.T) { + t.Log("Should error when repo-config provided and allow-repo-config false.") + c := setup(map[string]interface{}{ + cmd.BitbucketUserFlag: "user", + cmd.BitbucketTokenFlag: "token", + cmd.RepoWhitelistFlag: "*", + cmd.AllowRepoConfigFlag: false, + cmd.RepoConfigFlag: "atlantis-stage.yaml", + }) + err := c.Execute() + ErrEquals(t, "custom --repo-config cannot be specified if --allow-repo-config is false", err) +} + func setup(flags map[string]interface{}) *cobra.Command { vipr := viper.New() for k, v := range flags { diff --git a/helm/atlantis/values.yaml b/helm/atlantis/values.yaml index 42feecb83d..6822ba5270 100644 --- a/helm/atlantis/values.yaml +++ b/helm/atlantis/values.yaml @@ -76,7 +76,7 @@ image: tag: v0.4.5 pullPolicy: IfNotPresent -## enable using atlantis.yaml file +## enable using atlantis repo config YAML files allowRepoConfig: false # We only need to check every 60s since Atlantis is not a high-throughput service. diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index cffd530788..a8952442b7 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -51,7 +51,7 @@ Atlantis injects 5 Terraform variables that can be used to dynamically name the Setting the `session_name` allows you to trace API calls made through Atlantis back to a specific user and repo via CloudWatch: -```bash +```hcl provider "aws" { assume_role { role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" @@ -72,7 +72,7 @@ Atlantis runs `terraform` with the following variables: If you want to use `assume_role` with Atlantis and you're also using the [S3 Backend](https://www.terraform.io/docs/backends/types/s3.html), make sure to add the `role_arn` option: -```bash +```hcl terraform { backend "s3" { bucket = "mybucket" @@ -85,3 +85,13 @@ terraform { } } ``` + +## Running Multiple Atlantis Servers Against The Same Repo + +A common use case is to have separate production and staging Atlantis servers. + +You can achieve this by using multiple atlantis.yaml config files in the same repo and setting the `--repo-config` flag on each server. + +This allows you to launch a staging Atlantis server pointing at a staging atlantis.yaml file (e.g. `--repo-config atlantis-staging.yaml`) and a production Atlantis server pointing at a production atlantis.yaml file in the same repo (e.g. `--repo-config atlantis-production.yaml`). + +This way you can use different credentials for staging and production and maintain cleaner separation between environments. diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 92b7310edd..9b151ba632 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/cloudposse/atlantis/server/events/models" - "github.com/cloudposse/atlantis/server/events/yaml" "github.com/spf13/pflag" ) @@ -162,7 +161,7 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen flagSet.SetOutput(ioutil.Discard) flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Switch to this Terraform workspace before planning.") flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Which directory to run plan in relative to root of repo, ex. 'child/dir'.") - flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to run plan for. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename)) + flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Which project to run plan for. Refers to the name of the project configured in the repos atlantis.yaml file. Cannot be used at same time as workspace or dir flags.")) flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") case ApplyCommand.String(): name = ApplyCommand @@ -170,7 +169,7 @@ func (e *CommentParser) Parse(comment string, vcsHost models.VCSHostType) Commen flagSet.SetOutput(ioutil.Discard) flagSet.StringVarP(&workspace, workspaceFlagLong, workspaceFlagShort, "", "Apply the plan for this Terraform workspace.") flagSet.StringVarP(&dir, dirFlagLong, dirFlagShort, "", "Apply the plan for this directory, relative to root of repo, ex. 'child/dir'.") - flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Apply the plan for this project. Refers to the name of the project configured in %s. Cannot be used at same time as workspace or dir flags.", yaml.AtlantisYAMLFilename)) + flagSet.StringVarP(&project, projectFlagLong, projectFlagShort, "", fmt.Sprintf("Apply the plan for this project. Refers to the name of the project configured in the repos atlantis.yaml file. Cannot be used at same time as workspace or dir flags.")) flagSet.BoolVarP(&verbose, verboseFlagLong, verboseFlagShort, false, "Append Atlantis log to comment.") default: return CommentParseResult{CommentResponse: fmt.Sprintf("Error: unknown command %q – this is a bug", command)} diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index b788455292..0bb166dc2d 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -632,8 +632,8 @@ var PlanUsage = `Usage of plan: -d, --dir string Which directory to run plan in relative to root of repo, ex. 'child/dir'. -p, --project string Which project to run plan for. Refers to the name of the - project configured in atlantis.yaml. Cannot be used at - same time as workspace or dir flags. + project configured in the repo's atlantis.yaml file. + Cannot be used at same time as workspace or dir flags. --verbose Append Atlantis log to comment. -w, --workspace string Switch to this Terraform workspace before planning. ` @@ -642,8 +642,8 @@ var ApplyUsage = `Usage of apply: -d, --dir string Apply the plan for this directory, relative to root of repo, ex. 'child/dir'. -p, --project string Apply the plan for this project. Refers to the name of - the project configured in atlantis.yaml. Cannot be used - at same time as workspace or dir flags. + the project configured in the repo's atlantis.yaml file. + Cannot be used at same time as workspace or dir flags. --verbose Append Atlantis log to comment. -w, --workspace string Apply the plan for this Terraform workspace. ` diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 4fcd4bec5e..0b804900c6 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -49,6 +49,7 @@ type DefaultProjectCommandBuilder struct { WorkingDirLocker WorkingDirLocker AllowRepoConfig bool AllowRepoConfigFlag string + RepoConfig string PendingPlanFinder *PendingPlanFinder CommentBuilder CommentBuilder } @@ -96,21 +97,21 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *CommandContext, // Parse config file if it exists. var config valid.Config - hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir) + hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir, p.RepoConfig) if err != nil { - return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir) + return nil, errors.Wrapf(err, "looking for %s file in %q", p.RepoConfig, repoDir) } if hasConfigFile { if !p.AllowRepoConfig { - return nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag) + return nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", p.RepoConfig, p.AllowRepoConfigFlag) } - config, err = p.ParserValidator.ReadConfig(repoDir) + config, err = p.ParserValidator.ReadConfig(repoDir, p.RepoConfig) if err != nil { return nil, err } - ctx.Log.Info("successfully parsed %s file", yaml.AtlantisYAMLFilename) + ctx.Log.Info("successfully parsed %s file", p.RepoConfig) } else { - ctx.Log.Info("found no %s file", yaml.AtlantisYAMLFilename) + ctx.Log.Info("found no %s file", p.RepoConfig) } // We'll need the list of modified files. @@ -319,22 +320,22 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex } func (p *DefaultProjectCommandBuilder) getCfg(projectName string, dir string, workspace string, repoDir string) (*valid.Project, *valid.Config, error) { - hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir) + hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir, p.RepoConfig) if err != nil { - return nil, nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir) + return nil, nil, errors.Wrapf(err, "looking for %s file in %q", p.RepoConfig, repoDir) } if !hasConfigFile { if projectName != "" { - return nil, nil, fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename) + return nil, nil, fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", p.RepoConfig) } return nil, nil, nil } if !p.AllowRepoConfig { - return nil, nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag) + return nil, nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", p.RepoConfig, p.AllowRepoConfigFlag) } - globalCfg, err := p.ParserValidator.ReadConfig(repoDir) + globalCfg, err := p.ParserValidator.ReadConfig(repoDir, p.RepoConfig) if err != nil { return nil, nil, err } @@ -344,7 +345,7 @@ func (p *DefaultProjectCommandBuilder) getCfg(projectName string, dir string, wo if projectName != "" { projCfg := globalCfg.FindProjectByName(projectName) if projCfg == nil { - return nil, nil, fmt.Errorf("no project with name %q is defined in %s", projectName, yaml.AtlantisYAMLFilename) + return nil, nil, fmt.Errorf("no project with name %q is defined in %s", projectName, p.RepoConfig) } return projCfg, &globalCfg, nil } @@ -354,7 +355,7 @@ func (p *DefaultProjectCommandBuilder) getCfg(projectName string, dir string, wo return nil, nil, nil } if len(projCfgs) > 1 { - return nil, nil, fmt.Errorf("must specify project name: more than one project defined in %s matched dir: %q workspace: %q", yaml.AtlantisYAMLFilename, dir, workspace) + return nil, nil, fmt.Errorf("must specify project name: more than one project defined in %s matched dir: %q workspace: %q", p.RepoConfig, dir, workspace) } return &projCfgs[0], &globalCfg, nil } diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go index dafc052ba4..f656ca3fcb 100644 --- a/server/events/project_command_builder_test.go +++ b/server/events/project_command_builder_test.go @@ -172,6 +172,7 @@ projects: t.Run(c.Description, func(t *testing.T) { RegisterMockTestingT(t) tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() baseRepo := models.Repo{} @@ -181,7 +182,7 @@ projects: workingDir := mocks.NewMockWorkingDir() When(workingDir.Clone(logger, baseRepo, headRepo, pull, "default")).ThenReturn(tmpDir, nil) if c.AtlantisYAML != "" { - err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(c.AtlantisYAML), 0600) Ok(t, err) } err := ioutil.WriteFile(filepath.Join(tmpDir, "main.tf"), nil, 0600) @@ -197,6 +198,7 @@ projects: VCSClient: vcsClient, ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, + RepoConfig: repoConfig, PendingPlanFinder: &events.PendingPlanFinder{}, AllowRepoConfigFlag: "allow-repo-config", CommentBuilder: &events.CommentParser{}, @@ -393,6 +395,7 @@ projects: t.Run(c.Description, func(t *testing.T) { RegisterMockTestingT(t) tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() baseRepo := models.Repo{} @@ -410,7 +413,7 @@ projects: When(workingDir.GetWorkingDir(baseRepo, pull, expWorkspace)).ThenReturn(tmpDir, nil) } if c.AtlantisYAML != "" { - err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(c.AtlantisYAML), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(c.AtlantisYAML), 0600) Ok(t, err) } err := ioutil.WriteFile(filepath.Join(tmpDir, "main.tf"), nil, 0600) @@ -427,6 +430,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } @@ -481,6 +485,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanNoAtlantisYAML(t *testing.T) }, }) defer cleanup() + repoConfig := "atlantis.yaml" workingDir := mocks.NewMockWorkingDir() When(workingDir.Clone( matchers.AnyPtrToLoggingSimpleLogger(), @@ -499,6 +504,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanNoAtlantisYAML(t *testing.T) ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } @@ -534,6 +540,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanNoAtlantisYAMLNoModified(t * RegisterMockTestingT(t) tmpDir, cleanup := TempDir(t) defer cleanup() + repoConfig := "atlantis.yaml" workingDir := mocks.NewMockWorkingDir() When(workingDir.Clone( matchers.AnyPtrToLoggingSimpleLogger(), @@ -552,6 +559,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanNoAtlantisYAMLNoModified(t * ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } @@ -590,6 +598,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanWithAtlantisYAML(t *testing. }, }) defer cleanup() + repoConfig := "atlantis.yaml" yamlCfg := `version: 2 projects: - dir: project1 # project1 uses the defaults @@ -601,7 +610,7 @@ projects: enabled: false when_modified: [] ` - err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(yamlCfg), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(yamlCfg), 0600) Ok(t, err) workingDir := mocks.NewMockWorkingDir() @@ -624,6 +633,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } @@ -658,6 +668,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiPlanWithAtlantisYAMLWorkspaces(t "main.tf": nil, }) defer cleanup() + repoConfig := "atlantis.yaml" yamlCfg := `version: 2 projects: - dir: . @@ -665,7 +676,7 @@ projects: - dir: . workspace: production ` - err := ioutil.WriteFile(filepath.Join(tmpDir, yaml.AtlantisYAMLFilename), []byte(yamlCfg), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(yamlCfg), 0600) Ok(t, err) workingDir := mocks.NewMockWorkingDir() @@ -686,6 +697,7 @@ projects: ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } @@ -739,6 +751,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { }, }) defer cleanup() + repoConfig := "atlantis.yaml" // Initialize git repos in each workspace so that the .tfplan files get // picked up. runCmd(t, filepath.Join(tmpDir, "workspace1"), "git", "init") @@ -758,6 +771,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) { ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: true, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, PendingPlanFinder: &events.PendingPlanFinder{}, CommentBuilder: &events.CommentParser{}, } @@ -801,7 +815,8 @@ func TestDefaultProjectCommandBuilder_RepoConfigDisabled(t *testing.T) { }) defer cleanup() repoDir := filepath.Join(tmpDir, "pulldir/workspace") - err := ioutil.WriteFile(filepath.Join(repoDir, yaml.AtlantisYAMLFilename), nil, 0600) + repoConfig := "atlantis.yaml" + err := ioutil.WriteFile(filepath.Join(repoDir, repoConfig), nil, 0600) Ok(t, err) When(workingDir.Clone( @@ -823,6 +838,7 @@ func TestDefaultProjectCommandBuilder_RepoConfigDisabled(t *testing.T) { ProjectFinder: &events.DefaultProjectFinder{}, AllowRepoConfig: false, AllowRepoConfigFlag: "allow-repo-config", + RepoConfig: repoConfig, CommentBuilder: &events.CommentParser{}, } diff --git a/server/events/yaml/parser_validator.go b/server/events/yaml/parser_validator.go index 42fc51dea9..dbfb3fede0 100644 --- a/server/events/yaml/parser_validator.go +++ b/server/events/yaml/parser_validator.go @@ -13,17 +13,14 @@ import ( "gopkg.in/yaml.v2" ) -// AtlantisYAMLFilename is the name of the config file for each repo. -const AtlantisYAMLFilename = "atlantis.yaml" - type ParserValidator struct{} -// ReadConfig returns the parsed and validated atlantis.yaml config for repoDir. +// ReadConfig returns the parsed and validated atlantis yaml repoConfig for repoDir. // If there was no config file, then this can be detected by checking the type // of error: os.IsNotExist(error) but it's instead preferred to check with // HasConfigFile. -func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) { - configFile := p.configFilePath(repoDir) +func (p *ParserValidator) ReadConfig(repoDir string, repoConfig string) (valid.Config, error) { + configFile := p.configFilePath(repoDir, repoConfig) configData, err := ioutil.ReadFile(configFile) // nolint: gosec // NOTE: the error we return here must also be os.IsNotExist since that's @@ -34,19 +31,19 @@ func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) { // If it exists but we couldn't read it return an error. if err != nil { - return valid.Config{}, errors.Wrapf(err, "unable to read %s file", AtlantisYAMLFilename) + return valid.Config{}, errors.Wrapf(err, "unable to read %s file", repoConfig) } // If the config file exists, parse it. config, err := p.parseAndValidate(configData) if err != nil { - return valid.Config{}, errors.Wrapf(err, "parsing %s", AtlantisYAMLFilename) + return valid.Config{}, errors.Wrapf(err, "parsing %s", repoConfig) } return config, err } -func (p *ParserValidator) HasConfigFile(repoDir string) (bool, error) { - _, err := os.Stat(p.configFilePath(repoDir)) +func (p *ParserValidator) HasConfigFile(repoDir string, repoConfig string) (bool, error) { + _, err := os.Stat(p.configFilePath(repoDir, repoConfig)) if os.IsNotExist(err) { return false, nil } @@ -56,8 +53,8 @@ func (p *ParserValidator) HasConfigFile(repoDir string) (bool, error) { return false, err } -func (p *ParserValidator) configFilePath(repoDir string) string { - return filepath.Join(repoDir, AtlantisYAMLFilename) +func (p *ParserValidator) configFilePath(repoDir string, repoConfig string) string { + return filepath.Join(repoDir, repoConfig) } func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, error) { diff --git a/server/events/yaml/parser_validator_test.go b/server/events/yaml/parser_validator_test.go index 0e13bcadf1..dcb90607c1 100644 --- a/server/events/yaml/parser_validator_test.go +++ b/server/events/yaml/parser_validator_test.go @@ -14,41 +14,45 @@ import ( func TestReadConfig_DirDoesNotExist(t *testing.T) { r := yaml.ParserValidator{} - _, err := r.ReadConfig("/not/exist") + _, err := r.ReadConfig("/not/exist", "atlantis.yaml") Assert(t, os.IsNotExist(err), "exp nil ptr") - exists, err := r.HasConfigFile("/not/exist") + exists, err := r.HasConfigFile("/not/exist", "atlantis.yaml") Ok(t, err) Equals(t, false, exists) } func TestReadConfig_FileDoesNotExist(t *testing.T) { tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() r := yaml.ParserValidator{} - _, err := r.ReadConfig(tmpDir) + _, err := r.ReadConfig(tmpDir, repoConfig) Assert(t, os.IsNotExist(err), "exp nil ptr") - exists, err := r.HasConfigFile(tmpDir) + exists, err := r.HasConfigFile(tmpDir, repoConfig) Ok(t, err) Equals(t, false, exists) } func TestReadConfig_BadPermissions(t *testing.T) { tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() - err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), nil, 0000) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), nil, 0000) Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ReadConfig(tmpDir) - ErrContains(t, "unable to read atlantis.yaml file: ", err) + _, err = r.ReadConfig(tmpDir, repoConfig) + ErrContains(t, "unable to read "+repoConfig+" file: ", err) } func TestReadConfig_UnmarshalErrors(t *testing.T) { // We only have a few cases here because we assume the YAML library to be // well tested. See https://github.com/go-yaml/yaml/blob/v2/decode_test.go#L810. + repoConfig := "atlantis.yaml" + cases := []struct { description string input string @@ -57,24 +61,25 @@ func TestReadConfig_UnmarshalErrors(t *testing.T) { { "random characters", "slkjds", - "parsing atlantis.yaml: yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `slkjds` into raw.Config", + "parsing " + repoConfig + ": yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `slkjds` into raw.Config", }, { "just a colon", ":", - "parsing atlantis.yaml: yaml: did not find expected key", + "parsing " + repoConfig + ": yaml: did not find expected key", }, } tmpDir, cleanup := TempDir(t) + defer cleanup() for _, c := range cases { t.Run(c.description, func(t *testing.T) { - err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.input), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(c.input), 0600) Ok(t, err) r := yaml.ParserValidator{} - _, err = r.ReadConfig(tmpDir) + _, err = r.ReadConfig(tmpDir, repoConfig) ErrEquals(t, c.expErr, err) }) } @@ -95,7 +100,7 @@ func TestReadConfig(t *testing.T) { projects: - dir: "." `, - expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 2. See www.runatlantis.io/docs/upgrading-atlantis-yaml-to-version-2.html.", + expErr: "version: is required. If you've just upgraded Atlantis you need to rewrite your atlantis.yaml for version 2. See www.cloudposse.io/docs/upgrading-atlantis-yaml-to-version-2.html.", }, { description: "unsupported version", @@ -345,17 +350,18 @@ projects: } tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() for _, c := range cases { t.Run(c.description, func(t *testing.T) { - err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.input), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(c.input), 0600) Ok(t, err) r := yaml.ParserValidator{} - act, err := r.ReadConfig(tmpDir) + act, err := r.ReadConfig(tmpDir, repoConfig) if c.expErr != "" { - ErrEquals(t, "parsing atlantis.yaml: "+c.expErr, err) + ErrEquals(t, "parsing "+repoConfig+": "+c.expErr, err) return } Ok(t, err) @@ -609,15 +615,16 @@ workflows: } tmpDir, cleanup := TempDir(t) + repoConfig := "atlantis.yaml" defer cleanup() for _, c := range cases { t.Run(c.description, func(t *testing.T) { - err := ioutil.WriteFile(filepath.Join(tmpDir, "atlantis.yaml"), []byte(c.input), 0600) + err := ioutil.WriteFile(filepath.Join(tmpDir, repoConfig), []byte(c.input), 0600) Ok(t, err) r := yaml.ParserValidator{} - act, err := r.ReadConfig(tmpDir) + act, err := r.ReadConfig(tmpDir, repoConfig) Ok(t, err) Equals(t, c.expOutput, act) }) diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index b22ad1ace4..c5980b97a0 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -328,6 +328,7 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClientProxy, WorkingDirLocker: locker, AllowRepoConfigFlag: "allow-repo-config", AllowRepoConfig: true, + RepoConfig: "atlantis.yaml", PendingPlanFinder: &events.PendingPlanFinder{}, CommentBuilder: commentParser, }, @@ -414,16 +415,16 @@ func GitHubPullRequestParsed(headSHA string) *github.PullRequest { HTMLURL: github.String("htmlurl"), Head: &github.PullRequestBranch{ Repo: &github.Repository{ - FullName: github.String("runatlantis/atlantis-tests"), - CloneURL: github.String("/runatlantis/atlantis-tests.git"), + FullName: github.String("cloudposse/atlantis-tests"), + CloneURL: github.String("/cloudposse/atlantis-tests.git"), }, SHA: github.String(headSHA), Ref: github.String("branch"), }, Base: &github.PullRequestBranch{ Repo: &github.Repository{ - FullName: github.String("runatlantis/atlantis-tests"), - CloneURL: github.String("/runatlantis/atlantis-tests.git"), + FullName: github.String("cloudposse/atlantis-tests"), + CloneURL: github.String("/cloudposse/atlantis-tests.git"), }, }, User: &github.User{ @@ -456,7 +457,7 @@ func initializeRepo(t *testing.T, repoDir string) (string, string, func()) { runCmd(t, destDir, "git", "init") runCmd(t, destDir, "touch", ".gitkeep") runCmd(t, destDir, "git", "add", ".gitkeep") - runCmd(t, destDir, "git", "config", "--local", "user.email", "atlantisbot@runatlantis.io") + runCmd(t, destDir, "git", "config", "--local", "user.email", "atlantisbot@cloudposse.io") runCmd(t, destDir, "git", "config", "--local", "user.name", "atlantisbot") runCmd(t, destDir, "git", "commit", "-m", "initial commit") runCmd(t, destDir, "git", "checkout", "-b", "branch") diff --git a/server/server.go b/server/server.go index c46ce3f0ca..86ff5cac12 100644 --- a/server/server.go +++ b/server/server.go @@ -100,6 +100,7 @@ type UserConfig struct { GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"` LogLevel string `mapstructure:"log-level"` Port int `mapstructure:"port"` + RepoConfig string `mapstructure:"repo-config"` RepoWhitelist string `mapstructure:"repo-whitelist"` // RequireApproval is whether to require pull request approval before // allowing terraform apply's to be run. @@ -276,6 +277,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { WorkingDirLocker: workingDirLocker, AllowRepoConfig: userConfig.AllowRepoConfig, AllowRepoConfigFlag: config.AllowRepoConfigFlag, + RepoConfig: userConfig.RepoConfig, PendingPlanFinder: &events.PendingPlanFinder{}, CommentBuilder: commentParser, },