diff --git a/server/events/event_parser.go b/server/events/event_parser.go index 3d8dc6550a..dbd91255dc 100644 --- a/server/events/event_parser.go +++ b/server/events/event_parser.go @@ -112,6 +112,10 @@ func (c CommentCommand) IsForSpecificProject() bool { return c.RepoRelDir != "" || c.Workspace != "" || c.ProjectName != "" } +func (c CommentCommand) HasDirPatternMatching() bool { + return strings.Contains(c.RepoRelDir, "*") || strings.Contains(c.RepoRelDir, "?") || strings.Contains(c.RepoRelDir, "[") || strings.Contains(c.RepoRelDir, "]") +} + // CommandName returns the name of this command. func (c CommentCommand) CommandName() command.Name { return c.Name diff --git a/server/events/project_command_builder.go b/server/events/project_command_builder.go index 2d23513232..3afc94ccd6 100644 --- a/server/events/project_command_builder.go +++ b/server/events/project_command_builder.go @@ -3,7 +3,9 @@ package events import ( "fmt" "os" + "path/filepath" "sort" + "strings" "github.com/uber-go/tally" @@ -14,6 +16,7 @@ import ( "github.com/runatlantis/atlantis/server/core/config" "github.com/runatlantis/atlantis/server/events/command" + "github.com/runatlantis/atlantis/server/events/models" "github.com/runatlantis/atlantis/server/events/vcs" ) @@ -170,7 +173,7 @@ type DefaultProjectCommandBuilder struct { // See ProjectCommandBuilder.BuildAutoplanCommands. func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Context) ([]command.ProjectContext, error) { - projCtxs, err := p.buildPlanAllCommands(ctx, nil, false) + projCtxs, err := p.buildPlanAllCommands(ctx, &CommentCommand{}) if err != nil { return nil, err } @@ -187,8 +190,8 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *command.Contex // See ProjectCommandBuilder.BuildPlanCommands. func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { - if !cmd.IsForSpecificProject() { - return p.buildPlanAllCommands(ctx, cmd.Flags, cmd.Verbose) + if !cmd.IsForSpecificProject() || (p.EnableRegExpCmd && cmd.HasDirPatternMatching()) { + return p.buildPlanAllCommands(ctx, cmd) } pcc, err := p.buildProjectPlanCommand(ctx, cmd) return pcc, err @@ -196,7 +199,7 @@ func (p *DefaultProjectCommandBuilder) BuildPlanCommands(ctx *command.Context, c // See ProjectCommandBuilder.BuildApplyCommands. func (p *DefaultProjectCommandBuilder) BuildApplyCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { - if !cmd.IsForSpecificProject() { + if !cmd.IsForSpecificProject() || (p.EnableRegExpCmd && cmd.HasDirPatternMatching()) { return p.buildAllProjectCommands(ctx, cmd) } pac, err := p.buildProjectApplyCommand(ctx, cmd) @@ -217,7 +220,7 @@ func (p *DefaultProjectCommandBuilder) BuildVersionCommands(ctx *command.Context // buildPlanAllCommands builds plan contexts for all projects we determine were // modified in this ctx. -func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context, commentFlags []string, verbose bool) ([]command.ProjectContext, error) { +func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context, cmd *CommentCommand) ([]command.ProjectContext, error) { // We'll need the list of modified files. modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.Pull.BaseRepo, ctx.Pull) if err != nil { @@ -296,7 +299,14 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context if err != nil { return nil, err } - ctx.Log.Info("%d projects are to be planned based on their when_modified config", len(matchingProjects)) + + if p.EnableRegExpCmd && cmd.RepoRelDir != "" { + filteredProjects, err := filterValidProjects(matchingProjects, cmd.RepoRelDir) + if err != nil { + return nil, err + } + matchingProjects = filteredProjects + } for _, mp := range matchingProjects { ctx.Log.Debug("determining config for project at dir: %q workspace: %q", mp.Dir, mp.Workspace) @@ -307,13 +317,13 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context ctx, command.Plan, mergedCfg, - commentFlags, + cmd.Flags, repoDir, repoCfg.Automerge, mergedCfg.DeleteSourceBranchOnMerge, repoCfg.ParallelApply, repoCfg.ParallelPlan, - verbose, + cmd.IsVerbose(), )...) } } else { @@ -332,6 +342,15 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context ctx.Log.Debug("moduleInfo for %s (matching %q) = %v", repoDir, p.AutoDetectModuleFiles, moduleInfo) modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.Pull.BaseRepo.FullName, repoDir, p.AutoplanFileList, moduleInfo) ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects) + + if p.EnableRegExpCmd && cmd.RepoRelDir != "" { + filteredProjects, err := filterProjects(modifiedProjects, cmd.RepoRelDir) + if err != nil { + return nil, err + } + modifiedProjects = filteredProjects + } + for _, mp := range modifiedProjects { ctx.Log.Debug("determining config for project at dir: %q", mp.Path) pWorkspace, err := p.ProjectFinder.DetermineWorkspaceFromHCL(ctx.Log, repoDir) @@ -345,13 +364,13 @@ func (p *DefaultProjectCommandBuilder) buildPlanAllCommands(ctx *command.Context ctx, command.Plan, pCfg, - commentFlags, + cmd.Flags, repoDir, DefaultAutomergeEnabled, pCfg.DeleteSourceBranchOnMerge, DefaultParallelApplyEnabled, DefaultParallelPlanEnabled, - verbose, + cmd.IsVerbose(), )...) } } @@ -482,6 +501,14 @@ func (p *DefaultProjectCommandBuilder) buildAllProjectCommands(ctx *command.Cont return nil, err } + if p.EnableRegExpCmd && commentCmd.RepoRelDir != "" { + filteredPlans, err := filterPlans(plans, commentCmd.RepoRelDir) + if err != nil { + return nil, err + } + plans = filteredPlans + } + // use the default repository workspace because it is the only one guaranteed to have an atlantis.yaml, // other workspaces will not have the file if they are using pre_workflow_hooks to generate it dynamically defaultRepoDir, err := p.WorkingDir.GetWorkingDir(ctx.Pull.BaseRepo, ctx.Pull, DefaultWorkspace) @@ -673,3 +700,48 @@ func (p *DefaultProjectCommandBuilder) validateWorkspaceAllowed(repoCfg *valid.R return repoCfg.ValidateWorkspaceAllowed(repoRelDir, workspace) } + +func filterProjects(projects []models.Project, filter string) ([]models.Project, error) { + trimmedFilter := strings.TrimPrefix(filter, "./") + filteredProjects := make([]models.Project, 0, len(projects)) + for _, proj := range projects { + match, err := filepath.Match(trimmedFilter, proj.Path) + if err != nil { + return nil, err + } + if match { + filteredProjects = append(filteredProjects, proj) + } + } + return filteredProjects, nil +} + +func filterValidProjects(projects []valid.Project, filter string) ([]valid.Project, error) { + trimmedFilter := strings.TrimPrefix(filter, "./") + filteredProjects := make([]valid.Project, 0, len(projects)) + for _, proj := range projects { + match, err := filepath.Match(trimmedFilter, proj.Dir) + if err != nil { + return nil, err + } + if match { + filteredProjects = append(filteredProjects, proj) + } + } + return filteredProjects, nil +} + +func filterPlans(plans []PendingPlan, filter string) ([]PendingPlan, error) { + trimmedFilter := strings.TrimPrefix(filter, "./") + filteredPlans := make([]PendingPlan, 0, len(plans)) + for _, plan := range plans { + match, err := filepath.Match(trimmedFilter, plan.RepoRelDir) + if err != nil { + return nil, err + } + if match { + filteredPlans = append(filteredPlans, plan) + } + } + return filteredPlans, nil +}