Skip to content

Commit

Permalink
Various fixes
Browse files Browse the repository at this point in the history
- delete working dir when lock deleted
- check if dir still exists when looking at modified
- check if atlantis.yaml is allowed before parsing
  • Loading branch information
lkysow committed Jul 3, 2018
1 parent 8effd01 commit b2bd90d
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 43 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# v0.4.0-alpha

## Features
* Autoplanning - Atlantis will automatically run `plan` on new pull requests and
when new commits are pushed to the pull request.
* New repository `atlantis.yaml` format that supports:
* Arbitrary step ordering
* Single config file for whole repository
* Controlling autoplanning
* Moved docs to standalone website from the README.

## Bugfixes
* Won't attempt to run plan in a directory that was deleted.

## Backwards Incompatibilities / Notes:

Expand Down
47 changes: 47 additions & 0 deletions server/events/mocks/mock_working_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ func (mock *MockWorkingDir) Delete(r models.Repo, p models.PullRequest) error {
return ret0
}

func (mock *MockWorkingDir) DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error {
params := []pegomock.Param{r, p, workspace}
result := pegomock.GetGenericMockFrom(mock).Invoke("DeleteForWorkspace", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var ret0 error
if len(result) != 0 {
if result[0] != nil {
ret0 = result[0].(error)
}
}
return ret0
}

func (mock *MockWorkingDir) VerifyWasCalledOnce() *VerifierWorkingDir {
return &VerifierWorkingDir{mock, pegomock.Times(1), nil}
}
Expand Down Expand Up @@ -189,3 +201,38 @@ func (c *WorkingDir_Delete_OngoingVerification) GetAllCapturedArguments() (_para
}
return
}

func (verifier *VerifierWorkingDir) DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) *WorkingDir_DeleteForWorkspace_OngoingVerification {
params := []pegomock.Param{r, p, workspace}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DeleteForWorkspace", params)
return &WorkingDir_DeleteForWorkspace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}

type WorkingDir_DeleteForWorkspace_OngoingVerification struct {
mock *MockWorkingDir
methodInvocations []pegomock.MethodInvocation
}

func (c *WorkingDir_DeleteForWorkspace_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, string) {
r, p, workspace := c.GetAllCapturedArguments()
return r[len(r)-1], p[len(p)-1], workspace[len(workspace)-1]
}

func (c *WorkingDir_DeleteForWorkspace_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []string) {
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(params) > 0 {
_param0 = make([]models.Repo, len(params[0]))
for u, param := range params[0] {
_param0[u] = param.(models.Repo)
}
_param1 = make([]models.PullRequest, len(params[1]))
for u, param := range params[1] {
_param1[u] = param.(models.PullRequest)
}
_param2 = make([]string, len(params[2]))
for u, param := range params[2] {
_param2[u] = param.(string)
}
}
return
}
48 changes: 29 additions & 19 deletions server/events/project_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ package events

import (
"fmt"
"os"

"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
"github.com/runatlantis/atlantis/server/events/yaml"
Expand Down Expand Up @@ -51,19 +51,22 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext
}

// Parse config file if it exists.
ctx.Log.Debug("parsing config file")
config, err := p.ParserValidator.ReadConfig(repoDir)
if err != nil && !os.IsNotExist(err) {
return nil, err
var config valid.Config
hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir)
if err != nil {
return nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
}
noAtlantisYAML := os.IsNotExist(err)
if noAtlantisYAML {
ctx.Log.Info("found no %s file", yaml.AtlantisYAMLFilename)
} else {
ctx.Log.Info("successfully parsed %s file", yaml.AtlantisYAMLFilename)
if hasConfigFile {
if !p.AllowRepoConfig {
return nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag)
}
config, err = p.ParserValidator.ReadConfig(repoDir)
if err != nil {
return nil, err
}
ctx.Log.Info("successfully parsed %s file", yaml.AtlantisYAMLFilename)
} else {
ctx.Log.Info("found no %s file", yaml.AtlantisYAMLFilename)
}

// We'll need the list of modified files.
Expand All @@ -78,7 +81,7 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext

// If there is no config file, then we try to plan for each project that
// was modified in the pull request.
if noAtlantisYAML {
if !hasConfigFile {
modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.BaseRepo.FullName, repoDir)
ctx.Log.Info("automatically determined that there were %d projects modified in this pull request: %s", len(modifiedProjects), modifiedProjects)
for _, mp := range modifiedProjects {
Expand Down Expand Up @@ -128,12 +131,14 @@ func (p *DefaultProjectCommandBuilder) BuildAutoplanCommands(ctx *CommandContext
func (p *DefaultProjectCommandBuilder) BuildPlanCommand(ctx *CommandContext, cmd *CommentCommand) (models.ProjectCommandContext, error) {
var projCtx models.ProjectCommandContext

ctx.Log.Debug("building plan command")
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.BaseRepo.FullName, cmd.Workspace, ctx.Pull.Num)
if err != nil {
return projCtx, err
}
defer unlockFn()

ctx.Log.Debug("cloning repository")
repoDir, err := p.WorkingDir.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, cmd.Workspace)
if err != nil {
return projCtx, err
Expand Down Expand Up @@ -190,21 +195,26 @@ func (p *DefaultProjectCommandBuilder) buildProjectCommandCtx(ctx *CommandContex
}

func (p *DefaultProjectCommandBuilder) getCfg(projectName string, dir string, workspace string, repoDir string) (*valid.Project, *valid.Config, error) {
globalCfg, err := p.ParserValidator.ReadConfig(repoDir)
if err != nil && !os.IsNotExist(err) {
return nil, nil, err
}
hasAtlantisYAML := !os.IsNotExist(err)
if !hasAtlantisYAML && projectName != "" {
return nil, nil, fmt.Errorf("cannot specify a project name unless an %s file exists to configure projects", yaml.AtlantisYAMLFilename)
hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir)
if err != nil {
return nil, nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
}
if !hasAtlantisYAML {
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, nil
}

if !p.AllowRepoConfig {
return nil, nil, fmt.Errorf("%s files not allowed because Atlantis is not running with --%s", yaml.AtlantisYAMLFilename, p.AllowRepoConfigFlag)
}

globalCfg, err := p.ParserValidator.ReadConfig(repoDir)
if err != nil {
return nil, nil, err
}

// If they've specified a project by name we look it up. Otherwise we
// use the dir and workspace.
if projectName != "" {
Expand Down
8 changes: 6 additions & 2 deletions server/events/project_finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,16 @@ func (p *DefaultProjectFinder) DetermineProjectsViaConfig(log *logging.SimpleLog
}
if match {
log.Debug("file %q matched pattern", file)
projects = append(projects, project)
_, err := os.Stat(filepath.Join(repoDir, project.Dir))
if err == nil {
projects = append(projects, project)
} else {
log.Debug("project at dir %q not included because dir does not exist", project.Dir)
}
break
}
}
}
// todo: check if dir is deleted though
return projects, nil
}

Expand Down
160 changes: 160 additions & 0 deletions server/events/project_finder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"testing"

"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/yaml/valid"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
)
Expand Down Expand Up @@ -219,3 +220,162 @@ func TestDetermineProjects(t *testing.T) {
})
}
}

func TestDefaultProjectFinder_DetermineProjectsViaConfig(t *testing.T) {
/*
Create dir structure:
main.tf
project1/
main.tf
project2/
main.tf
modules/
module/
main.tf
*/
tmpDir, cleanup := DirStructure(t, map[string]interface{}{
"main.tf": nil,
"project1": map[string]interface{}{
"main.tf": nil,
},
"project2": map[string]interface{}{
"main.tf": nil,
},
"modules": map[string]interface{}{
"module": map[string]interface{}{
"main.tf": nil,
},
},
})
defer cleanup()

cases := []struct {
description string
config valid.Config
modified []string
expProjPaths []string
}{
{
description: "autoplan disabled",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: false,
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: nil,
},
{
description: "autoplan default",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: []string{"."},
},
{
description: "parent dir modified",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: nil,
},
{
description: "parent dir modified matches",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project1",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"../**/*.tf"},
},
},
},
},
modified: []string{"main.tf"},
expProjPaths: []string{"project1"},
},
{
description: "dir deleted",
config: valid.Config{
Projects: []valid.Project{
{
Dir: "project3",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"*.tf"},
},
},
},
},
modified: []string{"project3/main.tf"},
expProjPaths: nil,
},
{
description: "multiple projects",
config: valid.Config{
Projects: []valid.Project{
{
Dir: ".",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"*.tf"},
},
},
{
Dir: "project1",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"../modules/module/*.tf", "**/*.tf"},
},
},
{
Dir: "project2",
Autoplan: valid.Autoplan{
Enabled: true,
WhenModified: []string{"**/*.tf"},
},
},
},
},
modified: []string{"main.tf", "modules/module/another.tf", "project2/nontf.txt"},
expProjPaths: []string{".", "project1"},
},
}

for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
pf := events.DefaultProjectFinder{}
projects, err := pf.DetermineProjectsViaConfig(logging.NewNoopLogger(), c.modified, c.config, tmpDir)
Ok(t, err)
Equals(t, len(c.expProjPaths), len(projects))
for i, proj := range projects {
Equals(t, c.expProjPaths[i], proj.Dir)
}
})
}
}
Loading

0 comments on commit b2bd90d

Please sign in to comment.