Skip to content

Commit

Permalink
Local locking with BoltDB or remote with DynamoDB
Browse files Browse the repository at this point in the history
  • Loading branch information
lkysow authored May 31, 2017
1 parent 88fc0d0 commit 4e20cc1
Show file tree
Hide file tree
Showing 27 changed files with 1,162 additions and 516 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# atlantis
A [terraform](https://www.terraform.io/) collaboration tool that enables you to collaborate on infrastructure safely and securely.

## Locking
When a **Run** is triggered, the set of infrastructure that is being modified is locked against any modification or planning until the **Run** has been
completed by an `apply` and the pull request has been merged

```
{
"data_dir": "/var/lib/atlantis",
"locking": {
"backend": "file"
}
}
{
"locking": {
"backend": "dynamodb"
}
}
```
58 changes: 27 additions & 31 deletions apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/google/go-github/github"
"github.com/hootsuite/atlantis/locking"
"time"
)

type ApplyExecutor struct {
Expand Down Expand Up @@ -60,7 +61,6 @@ func (a *ApplyExecutor) execute(ctx *ExecutionContext, prCtx *PullRequestContext
}

func (a *ApplyExecutor) setupAndApply(ctx *ExecutionContext, prCtx *PullRequestContext) ExecutionResult {
stashCtx := a.stashContext(ctx)
a.github.UpdateStatus(prCtx, PendingStatus, "Applying...")

if a.requireApproval {
Expand All @@ -78,8 +78,6 @@ func (a *ApplyExecutor) setupAndApply(ctx *ExecutionContext, prCtx *PullRequestC
}
}

//runLog = append(runLog, "-> Confirmed pull request was plus one'd")

planPaths, err := a.downloadPlans(ctx.repoOwner, ctx.repoName, ctx.pullNum, ctx.command.environment, a.scratchDir, a.awsConfig, a.s3Bucket)
if err != nil {
errMsg := fmt.Sprintf("failed to download plans: %v", err)
Expand All @@ -99,15 +97,15 @@ func (a *ApplyExecutor) setupAndApply(ctx *ExecutionContext, prCtx *PullRequestC
//runLog = append(runLog, fmt.Sprintf("-> Downloaded plans: %v", planPaths))
applyOutputs := []PathResult{}
for _, planPath := range planPaths {
output := a.apply(ctx, stashCtx, planPath)
output := a.apply(ctx, prCtx, planPath)
output.Path = planPath
applyOutputs = append(applyOutputs, output)
}
a.updateGithubStatus(prCtx, applyOutputs)
return ExecutionResult{PathResults: applyOutputs}
}

func (a *ApplyExecutor) apply(ctx *ExecutionContext, stashCtx *StashPullRequestContext, planPath string) PathResult {
func (a *ApplyExecutor) apply(ctx *ExecutionContext, prCtx *PullRequestContext, planPath string) PathResult {
//runLog = append(runLog, fmt.Sprintf("-> Running apply %s", planPath))
planName := path.Base(planPath)
planSubDir := a.determinePlanSubDir(planName, ctx.pullNum)
Expand Down Expand Up @@ -164,18 +162,33 @@ func (a *ApplyExecutor) apply(ctx *ExecutionContext, stashCtx *StashPullRequestC
}

if remoteStatePath != "" {
//runLog = append(runLog, "-> Remote state configured")
// now lock the state prior to applying
stashLockResponse := a.stash.LockState(ctx.log, stashCtx, remoteStatePath)
if !stashLockResponse.Success {
msg := fmt.Sprintf("failed to lock state: %s", stashLockResponse.Message)
ctx.log.Err(msg)
tfEnv := ctx.command.environment
if tfEnv == "" {
tfEnv = "default"
}
run := locking.Run{
RepoOwner: prCtx.owner,
RepoName: prCtx.repoName,
Path: execPath.Relative,
Env: tfEnv,
PullID: prCtx.number,
User: prCtx.terraformApplier,
Timestamp: time.Now(),
}

lockAttempt, err := a.lockManager.TryLock(run)
if err != nil {
return PathResult{
Status: "error",
Result: GeneralError{errors.New(msg)},
Result: GeneralError{fmt.Errorf("failed to acquire lock: %s", err)},
}
}
if lockAttempt.LockAcquired != true && lockAttempt.LockingRun.PullID != prCtx.number {
return PathResult{
Status: "error",
Result: GeneralError{fmt.Errorf("failed to acquire lock: lock held by pull request #%d", lockAttempt.LockingRun.PullID)},
}
}
//runLog = append(runLog, "-> Stash lock aquired")
}

// need to get auth data from assumed role
Expand Down Expand Up @@ -226,23 +239,6 @@ func (a *ApplyExecutor) apply(ctx *ExecutionContext, stashCtx *StashPullRequestC
}
}

func (a *ApplyExecutor) validatePlusOne(prSubmitter string) func(*github.IssueComment) bool {
return func(c *github.IssueComment) bool {
if c == nil || c.Body == nil {
return false
}
body := *c.Body
if !(strings.Contains(body, ":+1:") || strings.Contains(body, "+1") || strings.ContainsRune(body, '\U0001f44d')) {
return false
}
if c.User == nil || c.User.Login == nil {
return false
}
// the plus-oner can't be the user that submitted the PR or ourselves (otherwise our comment telling the user they're missing a +1 would count as approval)
return *c.User.Login != prSubmitter && *c.User.Login != a.atlantisGithubUser
}
}

func (a *ApplyExecutor) downloadPlans(repoOwner string, repoName string, pullNum int, env string, outputDir string, awsConfig *AWSConfig, s3Bucket string) (planPaths []string, err error) {
awsSession, err := awsConfig.CreateAWSSession()
if err != nil {
Expand Down
29 changes: 15 additions & 14 deletions atlantis_config_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,34 @@ import (
"io/ioutil"
"os"
"testing"
. "github.com/hootsuite/atlantis/testing_util"
)

var tempConfigFile = "/tmp/" + AtlantisConfigFile

func TestConfigFileExists_invalid_path(t *testing.T) {
var c Config
equals(t, c.Exists("/invalid/path"), false)
Equals(t, c.Exists("/invalid/path"), false)
}

func TestConfigFileExists_valid_path(t *testing.T) {
var c Config
var str = `
---
---
terraform_version: "0.0.1"
pre_apply:
commands:
pre_apply:
commands:
- "echo"
- "date"
pre_plan:
commands:
pre_plan:
commands:
- "echo"
- "date"
stash_path: "file/path"
`
writeAtlantisConfigFile([]byte(str))
defer os.Remove(tempConfigFile)
equals(t, c.Exists("/tmp"), true)
Equals(t, c.Exists("/tmp"), true)
}

func TestConfigFileRead_invalid_config(t *testing.T) {
Expand All @@ -39,28 +40,28 @@ func TestConfigFileRead_invalid_config(t *testing.T) {
writeAtlantisConfigFile(str)
defer os.Remove(tempConfigFile)
err := c.Read("/tmp")
assert(t, err != nil, "expect an error")
Assert(t, err != nil, "expect an error")
}

func TestConfigFileRead_valid_config(t *testing.T) {
var c Config
var str = `
---
---
terraform_version: "0.0.1"
pre_apply:
commands:
pre_apply:
commands:
- "echo"
- "date"
pre_plan:
commands:
pre_plan:
commands:
- "echo"
- "date"
stash_path: "file/path"
`
writeAtlantisConfigFile([]byte(str))
defer os.Remove(tempConfigFile)
err := c.Read("/tmp")
assert(t, err == nil, "should be valid yaml")
Assert(t, err == nil, "should be valid json")
}

func writeAtlantisConfigFile(s []byte) error {
Expand Down
18 changes: 5 additions & 13 deletions base_executor.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package main

import "path/filepath"
import (
"path/filepath"
"github.com/hootsuite/atlantis/locking"
)

type BaseExecutor struct {
github *GithubClient
awsConfig *AWSConfig
scratchDir string
s3Bucket string
sshKey string
stash *StashPRClient
ghComments *GithubCommentRenderer
terraform *TerraformClient
githubCommentRenderer *GithubCommentRenderer
lockManager locking.LockManager
}

type PullRequestContext struct {
Expand Down Expand Up @@ -75,17 +78,6 @@ func (b *BaseExecutor) worstResult(results []PathResult) string {
return worst
}

func (b *BaseExecutor) stashContext(ctx *ExecutionContext) *StashPullRequestContext {
return &StashPullRequestContext{
owner: ctx.repoOwner,
repoName: ctx.repoName,
number: ctx.pullNum,
pullRequestLink: ctx.pullLink,
terraformApplier: ctx.requesterUsername,
terraformApplierEmail: ctx.requesterEmail,
}
}

func (b *BaseExecutor) Exec(f func(*ExecutionContext, *PullRequestContext) ExecutionResult, ctx *ExecutionContext, github *GithubClient) {
prCtx := b.githubContext(ctx)
result := f(ctx, prCtx)
Expand Down
Loading

0 comments on commit 4e20cc1

Please sign in to comment.