diff --git a/.gitignore b/.gitignore index 14349d26b5..f44f40e2d2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ output .terraform/ node_modules/ **/.vuepress/dist -helm/test-values.yaml \ No newline at end of file +helm/test-values.yaml +server/testfixtures/test-repos/*/*.txt.act diff --git a/cmd/server.go b/cmd/server.go index e983d5de2e..ddf61e0ed4 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -53,6 +53,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" @@ -65,6 +66,7 @@ const ( DefaultGitlabHostname = "gitlab.com" DefaultLogLevel = "info" DefaultPort = 4141 + DefaultRepoConfig = "atlantis.yaml" ) const redTermStart = "\033[31m" @@ -151,6 +153,12 @@ var stringFlags = []stringFlag{ description: "Log level. Either debug, info, warn, or error.", 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. " + @@ -175,7 +183,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, @@ -365,6 +373,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) { if c.Port == 0 { c.Port = DefaultPort } + if c.RepoConfig == "" { + c.RepoConfig = DefaultRepoConfig + } } func (s *ServerCmd) validate(userConfig server.UserConfig) error { @@ -410,6 +421,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 428ea34591..63f9c67b16 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", @@ -578,6 +584,7 @@ ssl-key-file: key-file "GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret", "LOG_LEVEL": "info", "PORT": "8282", + "REPO_CONFIG": "override-atlantis.yaml", "REPO_WHITELIST": "override,override", "REQUIRE_APPROVAL": "false", "SSL_CERT_FILE": "override-cert-file", @@ -592,7 +599,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 +615,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 +627,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 +643,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 +654,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 +670,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 +695,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 +708,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", @@ -713,6 +724,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { "GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret", "LOG_LEVEL": "debug", "PORT": "8181", + "REPO_CONFIG": "atlantis.yaml", "REPO_WHITELIST": "*", "REQUIRE_APPROVAL": "true", "SSL_CERT_FILE": "cert-file", @@ -731,7 +743,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 +759,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 +770,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 +786,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 +836,20 @@ 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..f270d855c3 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -85,3 +85,10 @@ 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 c4c5743ae0..7057ce8d7d 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -22,7 +22,6 @@ import ( "strings" "github.com/runatlantis/atlantis/server/events/models" - "github.com/runatlantis/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 ff6250b7f4..7d78fa2dea 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 repos 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 repos 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 9160588ea8..5105c8f738 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 a73b6692cb..1f2712bc8c 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 14917753af..63135fee18 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 a47db640b5..c6d15b3310 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) }) } @@ -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 c5c899699c..6a3e97bb0f 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, }, diff --git a/server/server.go b/server/server.go index 947af013e9..17ae91ea30 100644 --- a/server/server.go +++ b/server/server.go @@ -99,6 +99,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. @@ -275,6 +276,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, },