Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add pattern matching support to atlantis plan/apply -d flag #2742

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions server/events/event_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ func (c CommentCommand) IsForSpecificProject() bool {
return c.RepoRelDir != "" || c.Workspace != "" || c.ProjectName != ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please re-word the PR summary and title to reflect that this is no longer using the -f flag and instead will add wildcard filtering using the existing -d flag

}

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add tests for the new functionality

return c.Name
Expand Down
92 changes: 82 additions & 10 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package events
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/uber-go/tally"

Expand All @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand All @@ -187,16 +190,16 @@ 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
}

// 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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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 != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code looks to be the same as lines 303 to 309.

Can we encapsulate this into a function to keep the code as DRY as possible?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or is there a better way to structure this so we can run this block for both if and else blocks?

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)
Expand All @@ -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(),
)...)
}
}
Expand Down Expand Up @@ -482,6 +501,14 @@ func (p *DefaultProjectCommandBuilder) buildAllProjectCommands(ctx *command.Cont
return nil, err
}

if p.EnableRegExpCmd && commentCmd.RepoRelDir != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another duplicated block here

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)
Expand Down Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we combine both filterProjects and filterValidProjects ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only difference I see is models.Project and proj.Path vs valid.Project and proj.Dir

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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be combined with the above 2 functions. Only thing I see here that's different is using PendingPlan and plan.RepoRelDir

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
}