Skip to content

Commit

Permalink
Merge pull request #56 from gruntwork-io/lock-command
Browse files Browse the repository at this point in the history
Add an acquire-lock command. Fixes #19.
  • Loading branch information
brikis98 authored Nov 23, 2016
2 parents cea2a11 + c004974 commit 6f7b3d3
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 27 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.idea
vendor
.terraform
.vscode
.vscode
*.tfstate
*.tfstate.backup
19 changes: 16 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,23 @@ When you run `terragrunt apply` or `terragrunt destroy`, Terragrunt does the fol
1. Run `terraform apply` or `terraform destroy`.
1. When Terraform is done, delete the item from the `terragrunt_locks` table to release the lock.

## Cleaning up old locks
## Acquiring a long-term lock

Occasionally, you may want to lock a set of Terraform files and not allow further changes, perhaps during maintenance
work or as a precaution for templates that rarely change. To do that, you can use the `acquire-lock` command:

```
terragrunt acquire-lock
Are you sure you want to acquire a long-term lock? (y/n): y
```

See the next section for how to release this lock.

## Manually releasing a lock

If Terragrunt is shut down before it releases a lock (e.g. via `CTRL+C` or a crash), the lock might not be deleted, and
will prevent future changes to your state files. To clean up old locks, you can use the `release-lock` command:
You can use the `release-lock` command to manually release a lock. This is useful if you used the `acquire-lock`
command to create a long-term lock or if Terragrunt shut down before it released a lock (e.g. because of `CTRL+C` or a
crash).

```
terragrunt release-lock
Expand Down
20 changes: 19 additions & 1 deletion cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ COMMANDS:
import Acquire a lock and run 'terraform import'
refresh Acquire a lock and run 'terraform refresh'
remote push Acquire a lock and run 'terraform remote push'
release-lock Release a lock that is left over from some previous command
acquire-lock Acquire a long-term long for these templates
release-lock Release a long-term lock or a lock that failed to clean up
* Terragrunt forwards all other commands directly to Terraform
{{if .VisibleFlags}}
GLOBAL OPTIONS:
Expand Down Expand Up @@ -222,13 +223,30 @@ func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock, terra
} else {
return runTerraformCommand(terragruntOptions)
}
case "acquire-lock":
return acquireLock(lock, terragruntOptions)
case "release-lock":
return runReleaseLockCommand(cliContext, lock, terragruntOptions)
default:
return runTerraformCommand(terragruntOptions)
}
}

// Acquire a lock. This can be useful for locking down a deploy for a long time, such as during a major deployment.
func acquireLock(lock locks.Lock, terragruntOptions *options.TerragruntOptions) error {
shouldAcquireLock, err := shell.PromptUserForYesNo("Are you sure you want to acquire a long-term lock?", terragruntOptions)
if err != nil {
return err
}

if shouldAcquireLock {
util.Logger.Printf("Acquiring long-term lock. To release the lock, use the release-lock command.")
return lock.AcquireLock()
}

return nil
}

// Run the given Terraform command
func runTerraformCommand(terragruntOptions *options.TerragruntOptions) error {
return shell.RunShellCommand("terraform", terragruntOptions.NonTerragruntArgs...)
Expand Down
19 changes: 11 additions & 8 deletions locks/dynamodb/dynamo_lock_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func writeItemToLockTable(itemId string, tableName string, client *dynamodb.Dyna
// the lock, so display their metadata, sleep for the given amount of time, and try again, up to a maximum of
// maxRetries retries.
func writeItemToLockTableUntilSuccess(itemId string, tableName string, client *dynamodb.DynamoDB, maxRetries int, sleepBetweenRetries time.Duration) error {
for i := 0; i < maxRetries; i++ {
for retries := 1; ; retries++ {
util.Logger.Printf("Attempting to create lock item for state file %s in DynamoDB table %s", itemId, tableName)

err := writeItemToLockTable(itemId, tableName, client)
Expand All @@ -143,16 +143,19 @@ func writeItemToLockTableUntilSuccess(itemId string, tableName string, client *d
return nil
}

if isItemAlreadyExistsErr(err) {
displayLockMetadata(itemId, tableName, client)
util.Logger.Printf("Will try to acquire lock again in %s.", sleepBetweenRetries)
time.Sleep(sleepBetweenRetries)
} else {
if !isItemAlreadyExistsErr(err) {
return err
}
}

return errors.WithStackTrace(AcquireLockRetriesExceeded{ItemId: itemId, Retries: maxRetries})
displayLockMetadata(itemId, tableName, client)

if retries >= maxRetries {
return errors.WithStackTrace(AcquireLockRetriesExceeded{ItemId: itemId, Retries: maxRetries})
}

util.Logger.Printf("Will try to acquire lock again in %s.", sleepBetweenRetries)
time.Sleep(sleepBetweenRetries)
}
}

// Return true if the given error is the error returned by AWS when a conditional check fails. This is usually
Expand Down
10 changes: 10 additions & 0 deletions test/fixture-lock/.terragrunt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Configure Terragrunt to use DynamoDB for locking
lock = {
backend = "dynamodb"
config {
state_file_id = "terragrunt-test-fixture-lock"
aws_region = "us-east-1"
table_name = "terragrunt_locks"
max_lock_retries = 1
}
}
4 changes: 4 additions & 0 deletions test/fixture-lock/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Create an arbitrary local resource
data "template_file" "test" {
template = "Hello, I am a template."
}
6 changes: 3 additions & 3 deletions test/fixture/.terragrunt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ lock = {
backend = "dynamodb"
config {
state_file_id = "terragrunt-test-fixture"
awsRegion = "us-east-1"
tableName = "terragrunt_locks"
maxLockRetries = 360
aws_region = "us-east-1"
table_name = "terragrunt_locks"
max_lock_retries = 360
}
}

Expand Down
45 changes: 34 additions & 11 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package test

import (
"fmt"
"os"
"os/exec"
"strings"
"testing"
Expand All @@ -23,30 +22,54 @@ import (
const (
TERRAFORM_REMOTE_STATE_S3_REGION = "us-west-2"
TEST_FIXTURE_PATH = "fixture/"
TEST_FIXTURE_LOCK_PATH = "fixture-lock/"
)

func TestTerragruntWorksWithLocalTerraformVersion(t *testing.T) {
validateCommandInstalled(t, "terraform")
t.Parallel()

s3BucketName := fmt.Sprintf("terragrunt-test-bucket-%s", strings.ToLower(uniqueId()))
tmpTerragruntConfigPath := createTmpTerragruntConfig(t, TEST_FIXTURE_PATH, s3BucketName)

defer deleteS3Bucket(t, TERRAFORM_REMOTE_STATE_S3_REGION, s3BucketName)
runTerragruntApply(t, TEST_FIXTURE_PATH, tmpTerragruntConfigPath)
runTerragrunt(t, fmt.Sprintf("terragrunty apply --terragrunt-non-interactive --terragrunt-config %s %s", tmpTerragruntConfigPath, TEST_FIXTURE_PATH))
validateS3BucketExists(t, TERRAFORM_REMOTE_STATE_S3_REGION, s3BucketName)
}

// Run Terragrunt Apply directory in the test fixture path
func runTerragruntApply(t *testing.T, templatesPath string, terragruntConfigPath string) {
os.Chdir(templatesPath)
func TestAcquireAndReleaseLock(t *testing.T) {
t.Parallel()

app := cli.CreateTerragruntCli("TEST")
terragruntConfigPath := path.Join(TEST_FIXTURE_LOCK_PATH, ".terragrunt")

// Acquire a long-term lock
runTerragrunt(t, fmt.Sprintf("terragrunt acquire-lock --terragrunt-non-interactive --terragrunt-config %s", terragruntConfigPath))

// Try to apply the templates. Since a lock has been acquired, and max_lock_retries is set to 1, this should
// fail quickly.
err := runTerragruntCommand(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s %s", terragruntConfigPath, TEST_FIXTURE_LOCK_PATH))

if assert.NotNil(t, err, "Expected to get an error when trying to apply templates after a long-term lock has already been acquired, but got nil") {
assert.Contains(t, err.Error(), "Unable to acquire lock")
}

// Release the lock
runTerragrunt(t, fmt.Sprintf("terragrunt release-lock --terragrunt-non-interactive --terragrunt-config %s", terragruntConfigPath))

cmd := fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s", terragruntConfigPath)
args := strings.Split(cmd, " ")
// Try to apply the templates. Since the lock has been released, this should work without errors.
runTerragrunt(t, fmt.Sprintf("terragrunt apply --terragrunt-non-interactive --terragrunt-config %s %s", terragruntConfigPath, TEST_FIXTURE_LOCK_PATH))
}

func runTerragruntCommand(t *testing.T, command string) error {
validateCommandInstalled(t, "terraform")
args := strings.Split(command, " ")

app := cli.CreateTerragruntCli("TEST")
return app.Run(args)
}

if err := app.Run(args); err != nil {
t.Fatalf("Failed to run terragrunt: %s", err)
func runTerragrunt(t *testing.T, command string) {
if err := runTerragruntCommand(t, command); err != nil {
t.Fatalf("Failed to run Terragrunt command '%s' due to error: %s", command, err)
}
}

Expand Down

0 comments on commit 6f7b3d3

Please sign in to comment.