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

Fix apply all with project name bug #565

Merged
merged 1 commit into from
Apr 2, 2019
Merged
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
16 changes: 11 additions & 5 deletions server/events/pending_plan_finder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package events

import (
"github.com/runatlantis/atlantis/server/events/runtime"
"io/ioutil"
"os"
"os/exec"
Expand Down Expand Up @@ -29,7 +30,8 @@ type PendingPlan struct {
// the plan is for.
RepoRelDir string
// Workspace is the workspace this plan should execute in.
Workspace string
Workspace string
ProjectName string
}

// Find finds all pending plans in pullDir. pullDir should be the working
Expand Down Expand Up @@ -67,11 +69,15 @@ func (p *DefaultPendingPlanFinder) findWithAbsPaths(pullDir string) ([]PendingPl
continue
}

repoRelDir := filepath.Dir(file)
projectName, err := runtime.ProjectNameFromPlanfile(workspace, filepath.Base(file))
if err != nil {
return nil, nil, err
}
plans = append(plans, PendingPlan{
RepoDir: repoDir,
RepoRelDir: repoRelDir,
Workspace: workspace,
RepoDir: repoDir,
RepoRelDir: filepath.Dir(file),
Workspace: workspace,
ProjectName: projectName,
})
absPaths = append(absPaths, filepath.Join(repoDir, file))
}
Expand Down
23 changes: 20 additions & 3 deletions server/events/pending_plan_finder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,26 @@ func TestPendingPlanFinder_Find(t *testing.T) {
},
[]events.PendingPlan{
{
RepoDir: "???/default",
RepoRelDir: ".",
Workspace: "default",
RepoDir: "???/default",
RepoRelDir: ".",
Workspace: "default",
ProjectName: "projectname",
},
},
},
{
"root dir project plan with slashes",
map[string]interface{}{
"default": map[string]interface{}{
"project::name-default.tfplan": nil,
},
},
[]events.PendingPlan{
{
RepoDir: "???/default",
RepoRelDir: ".",
Workspace: "default",
ProjectName: "project/name",
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (p *DefaultProjectCommandBuilder) buildApplyAllCommands(ctx *CommandContext

var cmds []models.ProjectCommandContext
for _, plan := range plans {
cmd, err := p.buildProjectCommandCtx(ctx, models.ApplyCommand, commentCmd.ProjectName, commentCmd.Flags, plan.RepoDir, plan.RepoRelDir, plan.Workspace, commentCmd.Verbose)
cmd, err := p.buildProjectCommandCtx(ctx, models.ApplyCommand, plan.ProjectName, commentCmd.Flags, plan.RepoDir, plan.RepoRelDir, plan.Workspace, commentCmd.Verbose)
if err != nil {
return nil, errors.Wrapf(err, "building command for dir %q", plan.RepoRelDir)
}
Expand Down
42 changes: 28 additions & 14 deletions server/events/runtime/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,22 @@ package runtime

import (
"fmt"
"github.com/pkg/errors"
"regexp"
"strings"

version "github.com/hashicorp/go-version"
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/terraform"
"github.com/runatlantis/atlantis/server/logging"
)

// lineBeforeRunURL is the line output during a remote operation right before
// a link to the run url will be output.
const lineBeforeRunURL = "To view this run in a browser, visit:"
const (
// lineBeforeRunURL is the line output during a remote operation right before
// a link to the run url will be output.
lineBeforeRunURL = "To view this run in a browser, visit:"
planfileSlashReplace = "::"
)

// TerraformExec brings the interface from TerraformClient into this package
// without causing circular imports.
Expand Down Expand Up @@ -51,19 +56,28 @@ func MustConstraint(constraint string) version.Constraints {
return c
}

// invalidFilenameChars matches chars that are invalid for linux and windows
// filenames.
// From https://www.oreilly.com/library/view/regular-expressions-cookbook/9781449327453/ch08s25.html
var invalidFilenameChars = regexp.MustCompile(`[\\/:"*?<>|]`)

// GetPlanFilename returns the filename (not the path) of the generated tf plan
// given a workspace and project name.
func GetPlanFilename(workspace string, projName string) string {
var unescapedFilename string
if projName == "" {
unescapedFilename = fmt.Sprintf("%s.tfplan", workspace)
} else {
unescapedFilename = fmt.Sprintf("%s-%s.tfplan", projName, workspace)
return fmt.Sprintf("%s.tfplan", workspace)
}
projName = strings.Replace(projName, "/", planfileSlashReplace, -1)
return fmt.Sprintf("%s-%s.tfplan", projName, workspace)
}

// ProjectNameFromPlanfile returns the project name that a planfile with name
// filename is for. If filename is for a project without a name then it will
// return an empty string. workspace is the workspace this project is in.
func ProjectNameFromPlanfile(workspace string, filename string) (string, error) {
r, err := regexp.Compile(fmt.Sprintf(`(.*?)-%s\.tfplan`, workspace))
if err != nil {
return "", errors.Wrap(err, "compiling project name regex, this is a bug")
}
projMatch := r.FindAllStringSubmatch(filename, 1)
if projMatch == nil {
return "", nil
}
return invalidFilenameChars.ReplaceAllLiteralString(unescapedFilename, "-")
rawProjName := projMatch[0][1]
return strings.Replace(rawProjName, planfileSlashReplace, "/", -1), nil
}
50 changes: 43 additions & 7 deletions server/events/runtime/runtime_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ func TestGetPlanFilename(t *testing.T) {
"",
"workspace.tfplan",
},
{
"workspace",
"",
"workspace.tfplan",
},
{
"workspace",
"project",
Expand All @@ -32,7 +27,7 @@ func TestGetPlanFilename(t *testing.T) {
{
"workspace",
"project/with/slash",
"project-with-slash-workspace.tfplan",
"project::with::slash-workspace.tfplan",
},
{
"workspace",
Expand All @@ -44,10 +39,14 @@ func TestGetPlanFilename(t *testing.T) {
"project😀",
"project😀-workspace😀.tfplan",
},
// Previously we replaced invalid chars with -'s, however we now
// rely on validation of the atlantis.yaml file to ensure the name's
// don't contain chars that need to be url encoded. So now these
// chars shouldn't get replaced.
{
"default",
`all.invalid.chars \/"*?<>`,
"all.invalid.chars --------default.tfplan",
"all.invalid.chars \\::\"*?<>-default.tfplan",
},
}

Expand All @@ -57,3 +56,40 @@ func TestGetPlanFilename(t *testing.T) {
})
}
}

func TestProjectNameFromPlanfile(t *testing.T) {
cases := []struct {
workspace string
filename string
exp string
}{
{
"workspace",
"workspace.tfplan",
"",
},
{
"workspace",
"project-workspace.tfplan",
"project",
},
{
"workspace",
"project-workspace-workspace.tfplan",
"project-workspace",
},
{
"workspace",
"project::with::slashes::-workspace.tfplan",
"project/with/slashes/",
},
}

for i, c := range cases {
t.Run(fmt.Sprintf("case %d", i), func(t *testing.T) {
act, err := runtime.ProjectNameFromPlanfile(c.workspace, c.filename)
Ok(t, err)
Equals(t, c.exp, act)
})
}
}
4 changes: 2 additions & 2 deletions server/events/yaml/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ type RepoCfg struct {
Automerge bool
}

func (r RepoCfg) FindProjectsByDirWorkspace(dir string, workspace string) []Project {
func (r RepoCfg) FindProjectsByDirWorkspace(repoRelDir string, workspace string) []Project {
var ps []Project
for _, p := range r.Projects {
if p.Dir == dir && p.Workspace == workspace {
if p.Dir == repoRelDir && p.Workspace == workspace {
ps = append(ps, p)
}
}
Expand Down