mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 04:18:32 +00:00
Various fixes
- delete working dir when lock deleted - check if dir still exists when looking at modified - check if atlantis.yaml is allowed before parsing
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
@@ -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
|
||||
@@ -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
|
||||
hasConfigFile, err := p.ParserValidator.HasConfigFile(repoDir)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "looking for %s file in %q", yaml.AtlantisYAMLFilename, repoDir)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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 != "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
const workspacePrefix = "repos"
|
||||
const workingDirPrefix = "repos"
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_working_dir.go WorkingDir
|
||||
|
||||
@@ -39,6 +39,7 @@ type WorkingDir interface {
|
||||
GetWorkingDir(r models.Repo, p models.PullRequest, workspace string) (string, error)
|
||||
// Delete deletes the workspace for this repo and pull.
|
||||
Delete(r models.Repo, p models.PullRequest) error
|
||||
DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error
|
||||
}
|
||||
|
||||
// FileWorkspace implements WorkingDir with the file system.
|
||||
@@ -64,11 +65,13 @@ func (w *FileWorkspace) Clone(
|
||||
// If the directory already exists, check if it's at the right commit.
|
||||
// If so, then we do nothing.
|
||||
if _, err := os.Stat(cloneDir); err == nil {
|
||||
log.Debug("clone directory %q already exists, checking if it's at the right commit", cloneDir)
|
||||
revParseCmd := exec.Command("git", "rev-parse", "HEAD") // #nosec
|
||||
revParseCmd.Dir = cloneDir
|
||||
output, err := revParseCmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "running git rev-parse HEAD: %s", string(output))
|
||||
log.Err("will re-clone repo, could not determine if was at correct commit: git rev-parse HEAD: %s: %s", err, string(output))
|
||||
return w.forceClone(log, cloneDir, headRepo, p)
|
||||
}
|
||||
currCommit := strings.Trim(string(output), "\n")
|
||||
if currCommit == p.HeadCommit {
|
||||
@@ -76,12 +79,21 @@ func (w *FileWorkspace) Clone(
|
||||
return cloneDir, nil
|
||||
}
|
||||
log.Debug("repo was already cloned but is not at correct commit, wanted %q got %q", p.HeadCommit, currCommit)
|
||||
// We'll fall through to re-clone.
|
||||
}
|
||||
|
||||
// It's okay to delete all plans now since they're out of date.
|
||||
log.Info("cleaning clone directory %q", cloneDir)
|
||||
if err := os.RemoveAll(cloneDir); err != nil {
|
||||
return "", errors.Wrap(err, "deleting old workspace")
|
||||
}
|
||||
// Otherwise we clone the repo.
|
||||
return w.forceClone(log, cloneDir, headRepo, p)
|
||||
}
|
||||
|
||||
func (w *FileWorkspace) forceClone(log *logging.SimpleLogger,
|
||||
cloneDir string,
|
||||
headRepo models.Repo,
|
||||
p models.PullRequest) (string, error) {
|
||||
|
||||
err := os.RemoveAll(cloneDir)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "deleting dir %q before cloning", cloneDir)
|
||||
}
|
||||
|
||||
// Create the directory and parents if necessary.
|
||||
@@ -124,8 +136,13 @@ func (w *FileWorkspace) Delete(r models.Repo, p models.PullRequest) error {
|
||||
return os.RemoveAll(w.repoPullDir(r, p))
|
||||
}
|
||||
|
||||
// Delete deletes the working dir for this workspace.
|
||||
func (w *FileWorkspace) DeleteForWorkspace(r models.Repo, p models.PullRequest, workspace string) error {
|
||||
return os.RemoveAll(w.cloneDir(r, p, workspace))
|
||||
}
|
||||
|
||||
func (w *FileWorkspace) repoPullDir(r models.Repo, p models.PullRequest) string {
|
||||
return filepath.Join(w.DataDir, workspacePrefix, r.FullName, strconv.Itoa(p.Num))
|
||||
return filepath.Join(w.DataDir, workingDirPrefix, r.FullName, strconv.Itoa(p.Num))
|
||||
}
|
||||
|
||||
func (w *FileWorkspace) cloneDir(r models.Repo, p models.PullRequest, workspace string) string {
|
||||
|
||||
@@ -20,9 +20,10 @@ type ParserValidator struct{}
|
||||
|
||||
// ReadConfig returns the parsed and validated atlantis.yaml config for repoDir.
|
||||
// If there was no config file, then this can be detected by checking the type
|
||||
// of error: os.IsNotExist(error).
|
||||
// of error: os.IsNotExist(error) but it's instead preferred to check with
|
||||
// HasConfigFile.
|
||||
func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) {
|
||||
configFile := filepath.Join(repoDir, AtlantisYAMLFilename)
|
||||
configFile := p.configFilePath(repoDir)
|
||||
configData, err := ioutil.ReadFile(configFile)
|
||||
|
||||
// NOTE: the error we return here must also be os.IsNotExist since that's
|
||||
@@ -44,6 +45,21 @@ func (p *ParserValidator) ReadConfig(repoDir string) (valid.Config, error) {
|
||||
return config, err
|
||||
}
|
||||
|
||||
func (p *ParserValidator) HasConfigFile(repoDir string) (bool, error) {
|
||||
_, err := os.Stat(p.configFilePath(repoDir))
|
||||
if os.IsNotExist(err) {
|
||||
return false, nil
|
||||
}
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (p *ParserValidator) configFilePath(repoDir string) string {
|
||||
return filepath.Join(repoDir, AtlantisYAMLFilename)
|
||||
}
|
||||
|
||||
func (p *ParserValidator) parseAndValidate(configData []byte) (valid.Config, error) {
|
||||
var rawConfig raw.Config
|
||||
if err := yaml.UnmarshalStrict(configData, &rawConfig); err != nil {
|
||||
|
||||
@@ -16,6 +16,10 @@ func TestReadConfig_DirDoesNotExist(t *testing.T) {
|
||||
r := yaml.ParserValidator{}
|
||||
_, err := r.ReadConfig("/not/exist")
|
||||
Assert(t, os.IsNotExist(err), "exp nil ptr")
|
||||
|
||||
exists, err := r.HasConfigFile("/not/exist")
|
||||
Ok(t, err)
|
||||
Equals(t, false, exists)
|
||||
}
|
||||
|
||||
func TestReadConfig_FileDoesNotExist(t *testing.T) {
|
||||
@@ -25,6 +29,10 @@ func TestReadConfig_FileDoesNotExist(t *testing.T) {
|
||||
r := yaml.ParserValidator{}
|
||||
_, err := r.ReadConfig(tmpDir)
|
||||
Assert(t, os.IsNotExist(err), "exp nil ptr")
|
||||
|
||||
exists, err := r.HasConfigFile(tmpDir)
|
||||
Ok(t, err)
|
||||
Equals(t, false, exists)
|
||||
}
|
||||
|
||||
func TestReadConfig_BadPermissions(t *testing.T) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/events/locking"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
@@ -20,6 +21,8 @@ type LocksController struct {
|
||||
Logger *logging.SimpleLogger
|
||||
VCSClient vcs.ClientProxy
|
||||
LockDetailTemplate TemplateWriter
|
||||
WorkingDir events.WorkingDir
|
||||
WorkingDirLocker events.WorkingDirLocker
|
||||
}
|
||||
|
||||
// GetLock is the GET /locks/{id} route. It renders the lock detail view.
|
||||
@@ -84,20 +87,29 @@ func (l *LocksController) DeleteLock(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Once the lock has been deleted, comment back on the pull request.
|
||||
comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+
|
||||
"To `apply` you must run `plan` again.", lock.Project.Path, lock.Workspace)
|
||||
// NOTE: Because BaseRepo was added to the PullRequest model later, previous
|
||||
// installations of Atlantis will have locks in their DB that do not have
|
||||
// this field on PullRequest. We skip commenting in this case.
|
||||
// this field on PullRequest. We skip commenting and deleting the working dir in this case.
|
||||
if lock.Pull.BaseRepo != (models.Repo{}) {
|
||||
unlock, err := l.WorkingDirLocker.TryLock(lock.Pull.BaseRepo.FullName, lock.Workspace, lock.Pull.Num)
|
||||
if err != nil {
|
||||
l.Logger.Err("unable to obtain working dir lock when trying to delete old plans: %s", err)
|
||||
} else {
|
||||
defer unlock()
|
||||
err = l.WorkingDir.DeleteForWorkspace(lock.Pull.BaseRepo, lock.Pull, lock.Workspace)
|
||||
l.Logger.Err("unable to delete workspace: %s", err)
|
||||
}
|
||||
|
||||
// Once the lock has been deleted, comment back on the pull request.
|
||||
comment := fmt.Sprintf("**Warning**: The plan for dir: `%s` workspace: `%s` was **discarded** via the Atlantis UI.\n\n"+
|
||||
"To `apply` you must run `plan` again.", lock.Project.Path, lock.Workspace)
|
||||
err = l.VCSClient.CreateComment(lock.Pull.BaseRepo, lock.Pull.Num, comment)
|
||||
if err != nil {
|
||||
l.respond(w, logging.Error, http.StatusInternalServerError, "Failed commenting on pull request: %s", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
l.Logger.Debug("skipping commenting on pull request that lock was deleted because BaseRepo field is empty")
|
||||
l.Logger.Debug("skipping commenting on pull request and deleting workspace because BaseRepo field is empty")
|
||||
}
|
||||
l.respond(w, logging.Info, http.StatusOK, "Deleted lock id %q", id)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
. "github.com/petergtz/pegomock"
|
||||
"github.com/runatlantis/atlantis/server"
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/events/locking/mocks"
|
||||
mocks2 "github.com/runatlantis/atlantis/server/events/mocks"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
@@ -187,6 +189,8 @@ func TestDeleteLock_CommentFailed(t *testing.T) {
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cp := vcsmocks.NewMockClientProxy()
|
||||
workingDir := mocks2.NewMockWorkingDir()
|
||||
workingDirLocker := events.NewDefaultWorkingDirLocker()
|
||||
When(cp.CreateComment(AnyRepo(), AnyInt(), AnyString())).ThenReturn(errors.New("err"))
|
||||
l := mocks.NewMockLocker()
|
||||
When(l.Unlock("id")).ThenReturn(&models.ProjectLock{
|
||||
@@ -195,9 +199,11 @@ func TestDeleteLock_CommentFailed(t *testing.T) {
|
||||
},
|
||||
}, nil)
|
||||
lc := server.LocksController{
|
||||
Locker: l,
|
||||
Logger: logging.NewNoopLogger(),
|
||||
VCSClient: cp,
|
||||
Locker: l,
|
||||
Logger: logging.NewNoopLogger(),
|
||||
VCSClient: cp,
|
||||
WorkingDir: workingDir,
|
||||
WorkingDirLocker: workingDirLocker,
|
||||
}
|
||||
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
|
||||
req = mux.SetURLVars(req, map[string]string{"id": "id"})
|
||||
@@ -212,6 +218,8 @@ func TestDeleteLock_CommentSuccess(t *testing.T) {
|
||||
|
||||
cp := vcsmocks.NewMockClientProxy()
|
||||
l := mocks.NewMockLocker()
|
||||
workingDir := mocks2.NewMockWorkingDir()
|
||||
workingDirLocker := events.NewDefaultWorkingDirLocker()
|
||||
pull := models.PullRequest{
|
||||
BaseRepo: models.Repo{FullName: "owner/repo"},
|
||||
}
|
||||
@@ -224,9 +232,11 @@ func TestDeleteLock_CommentSuccess(t *testing.T) {
|
||||
},
|
||||
}, nil)
|
||||
lc := server.LocksController{
|
||||
Locker: l,
|
||||
Logger: logging.NewNoopLogger(),
|
||||
VCSClient: cp,
|
||||
Locker: l,
|
||||
Logger: logging.NewNoopLogger(),
|
||||
VCSClient: cp,
|
||||
WorkingDirLocker: workingDirLocker,
|
||||
WorkingDir: workingDir,
|
||||
}
|
||||
req, _ := http.NewRequest("GET", "", bytes.NewBuffer(nil))
|
||||
req = mux.SetURLVars(req, map[string]string{"id": "id"})
|
||||
@@ -236,4 +246,5 @@ func TestDeleteLock_CommentSuccess(t *testing.T) {
|
||||
cp.VerifyWasCalled(Once()).CreateComment(pull.BaseRepo, pull.Num,
|
||||
"**Warning**: The plan for dir: `path` workspace: `workspace` was **discarded** via the Atlantis UI.\n\n"+
|
||||
"To `apply` you must run `plan` again.")
|
||||
workingDir.VerifyWasCalledOnce().DeleteForWorkspace(pull.BaseRepo, pull, "workspace")
|
||||
}
|
||||
|
||||
@@ -275,6 +275,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
Logger: logger,
|
||||
VCSClient: vcsClient,
|
||||
LockDetailTemplate: lockTemplate,
|
||||
WorkingDir: workingDir,
|
||||
WorkingDirLocker: workingDirLocker,
|
||||
}
|
||||
eventsController := &EventsController{
|
||||
CommandRunner: commandRunner,
|
||||
|
||||
@@ -3,6 +3,7 @@ package testing
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -17,3 +18,26 @@ func TempDir(t *testing.T) (string, func()) {
|
||||
os.RemoveAll(tmpDir) // nolint: errcheck
|
||||
}
|
||||
}
|
||||
func DirStructure(t *testing.T, structure map[string]interface{}) (string, func()) {
|
||||
tmpDir, cleanup := TempDir(t)
|
||||
dirStructureGo(t, tmpDir, structure)
|
||||
return tmpDir, cleanup
|
||||
}
|
||||
|
||||
func dirStructureGo(t *testing.T, parentDir string, structure map[string]interface{}) {
|
||||
for key, val := range structure {
|
||||
// If val is nil then key is a filename and we just create it
|
||||
if val == nil {
|
||||
_, err := os.Create(filepath.Join(parentDir, key))
|
||||
Ok(t, err)
|
||||
continue
|
||||
}
|
||||
// If val is another map then key is a dir
|
||||
if dirContents, ok := val.(map[string]interface{}); ok {
|
||||
subDir := filepath.Join(parentDir, key)
|
||||
Ok(t, os.Mkdir(subDir, 0700))
|
||||
// Recurse and create contents.
|
||||
dirStructureGo(t, subDir, dirContents)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user