Skip to content

Commit

Permalink
Merge pull request #54 from gruntwork-io/update-config
Browse files Browse the repository at this point in the history
Ensure Terragrunt picks up changes to remote config
  • Loading branch information
brikis98 authored Nov 22, 2016
2 parents 53395ba + 4d9b1c1 commit f1a13d8
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 38 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ remote_state = {
different key/value pairs, so consult the [Terraform remote state docs](https://www.terraform.io/docs/state/remote/)
for details.

## CLI Options

Terragrunt forwards all arguments and options to Terraform. The only exceptions are the options that start with the
prefix `--terragrunt-`. The currently available options are:

* `--terragrunt-config`: A custom path to the `.terragrunt` file. May also be specified via the `TERRAGRUNT_CONFIG`
environment variable. The default path is `.terragrunt` in the current directory.
* `--terragrunt-non-interactive`: Don't show interactive user prompts. This will default the answer for all prompts to
'yes'. Useful if you need to run Terragrunt in an automated setting (e.g. from a script).

## Developing terragrunt

#### Running locally
Expand Down
70 changes: 52 additions & 18 deletions cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cli

import (
"fmt"
"os"
"regexp"

"github.com/gruntwork-io/terragrunt/config"
Expand All @@ -12,6 +11,8 @@ import (
"github.com/gruntwork-io/terragrunt/shell"
"github.com/gruntwork-io/terragrunt/util"
"github.com/urfave/cli"
"github.com/gruntwork-io/terragrunt/options"
"strings"
)

// Since Terragrunt is just a thin wrapper for Terraform, and we don't want to repeat every single Terraform command
Expand Down Expand Up @@ -47,6 +48,9 @@ var MODULE_REGEX = regexp.MustCompile(`module ".+"`)

const TERRAFORM_EXTENSION_GLOB = "*.tf"

const OPT_TERRAGRUNT_CONFIG = "terragrunt-config"
const OPT_NON_INTERACTIVE = "terragrunt-non-interactive"

// Create the Terragrunt CLI App
func CreateTerragruntCli(version string) *cli.App {
cli.AppHelpTemplate = CUSTOM_USAGE_TEXT
Expand All @@ -65,17 +69,16 @@ func CreateTerragruntCli(version string) *cli.App {
Moreover, for the apply and destroy commands, Terragrunt will first try to acquire a lock using DynamoDB. For
documentation, see https://github.com/gruntwork-io/terragrunt/.`

var defaultConfigFilePath = config.ConfigFilePath
if os.Getenv("TERRAGRUNT_CONFIG") != "" {
defaultConfigFilePath = os.Getenv("TERRAGRUNT_CONFIG")
}

app.Flags = []cli.Flag{
cli.StringFlag{
Name: "terragrunt-config",
Value: defaultConfigFilePath,
Name: OPT_TERRAGRUNT_CONFIG,
EnvVar: "TERRAGRUNT_CONFIG",
Usage: ".terragrunt file to use",
},
cli.BoolFlag{
Name: OPT_NON_INTERACTIVE,
Usage: "Don't show interactive user prompts. This will default the answer for all prompts to 'yes'.",
},
}

return app
Expand All @@ -92,7 +95,9 @@ func runApp(cliContext *cli.Context) (finalErr error) {
return nil
}

conf, err := config.ReadTerragruntConfig(cliContext.String("terragrunt-config"))
terragruntOptions := parseTerragruntOptions(cliContext)

conf, err := config.ReadTerragruntConfig(terragruntOptions)
if err != nil {
return err
}
Expand All @@ -102,7 +107,7 @@ func runApp(cliContext *cli.Context) (finalErr error) {
}

if conf.RemoteState != nil {
if err := configureRemoteState(cliContext, conf.RemoteState); err != nil {
if err := configureRemoteState(cliContext, conf.RemoteState, terragruntOptions); err != nil {
return err
}
}
Expand All @@ -112,7 +117,22 @@ func runApp(cliContext *cli.Context) (finalErr error) {
return runTerraformCommand(cliContext)
}

return runTerraformCommandWithLock(cliContext, conf.Lock)
return runTerraformCommandWithLock(cliContext, conf.Lock, terragruntOptions)
}

// Parse command line options that are passed in for Terragrunt
func parseTerragruntOptions(cliContext *cli.Context) options.TerragruntOptions {
terragruntConfigPath := cliContext.String(OPT_TERRAGRUNT_CONFIG)
if terragruntConfigPath == "" {
terragruntConfigPath = config.DefaultTerragruntConfigPath
}

nonInteractive := cliContext.Bool(OPT_NON_INTERACTIVE)

return options.TerragruntOptions{
TerragruntConfigPath: terragruntConfigPath,
NonInteractive: nonInteractive,
}
}

// A quick sanity check that calls `terraform get` to download modules, if they aren't already downloaded.
Expand Down Expand Up @@ -145,12 +165,12 @@ func shouldDownloadModules() (bool, error) {

// If the user entered a Terraform command that uses state (e.g. plan, apply), make sure remote state is configured
// before running the command.
func configureRemoteState(cliContext *cli.Context, remoteState *remote.RemoteState) error {
func configureRemoteState(cliContext *cli.Context, remoteState *remote.RemoteState, terragruntOptions options.TerragruntOptions) error {
// We only configure remote state for the commands that use the tfstate files. We do not configure it for
// commands such as "get" or "version".
switch cliContext.Args().First() {
case "apply", "destroy", "import", "graph", "output", "plan", "push", "refresh", "show", "taint", "untaint", "validate":
return remoteState.ConfigureRemoteState()
return remoteState.ConfigureRemoteState(terragruntOptions)
case "remote":
if cliContext.Args().Get(1) == "config" {
// Encourage the user to configure remote state by defining it in .terragrunt and letting
Expand All @@ -167,7 +187,7 @@ func configureRemoteState(cliContext *cli.Context, remoteState *remote.RemoteSta
}

// Run the given Terraform command with the given lock (if the command requires locking)
func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock) error {
func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock, terragruntOptions options.TerragruntOptions) error {
switch cliContext.Args().First() {
case "apply", "destroy", "import", "refresh":
return locks.WithLock(lock, func() error { return runTerraformCommand(cliContext) })
Expand All @@ -178,20 +198,34 @@ func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock) error
return runTerraformCommand(cliContext)
}
case "release-lock":
return runReleaseLockCommand(cliContext, lock)
return runReleaseLockCommand(cliContext, lock, terragruntOptions)
default:
return runTerraformCommand(cliContext)
}
}

// Run the given Terraform command
func runTerraformCommand(cliContext *cli.Context) error {
return shell.RunShellCommand("terraform", cliContext.Args()...)
return shell.RunShellCommand("terraform", filterOutTerragruntArgs(cliContext)...)
}

// Return the args in teh given CLI Context object, filtering any args that are only meant for Terragrunt itself
func filterOutTerragruntArgs(cliContext *cli.Context) []string {
args := []string{}

for _, arg := range cliContext.Args() {
if !strings.HasPrefix(arg, "--terragrunt") {
args = append(args, arg)
}
}

return args
}

// Release a lock, prompting the user for confirmation first
func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock) error {
proceed, err := shell.PromptUserForYesNo(fmt.Sprintf("Are you sure you want to release %s?", lock))
func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock, terragruntOptions options.TerragruntOptions) error {
prompt := fmt.Sprintf("Are you sure you want to release %s?", lock)
proceed, err := shell.PromptUserForYesNo(prompt, terragruntOptions)
if err != nil {
return err
}
Expand Down
7 changes: 4 additions & 3 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import (
"github.com/gruntwork-io/terragrunt/locks"
"github.com/gruntwork-io/terragrunt/remote"
"github.com/hashicorp/hcl"
"github.com/gruntwork-io/terragrunt/options"
)

const ConfigFilePath = ".terragrunt"
const DefaultTerragruntConfigPath = ".terragrunt"

// TerragruntConfig represents a parsed and expanded configuration
type TerragruntConfig struct {
Expand All @@ -30,8 +31,8 @@ type LockConfig struct {
}

// ReadTerragruntConfig the Terragrunt config file from its default location
func ReadTerragruntConfig(filePath string) (*TerragruntConfig, error) {
return parseConfigFile(filePath)
func ReadTerragruntConfig(terragruntOptions options.TerragruntOptions) (*TerragruntConfig, error) {
return parseConfigFile(terragruntOptions.TerragruntConfigPath)
}

// Parse the Terragrunt config file at the given path
Expand Down
8 changes: 8 additions & 0 deletions options/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package options

// TerragruntOptions represents command-line options that are read by Terragrunt
type TerragruntOptions struct {
TerragruntConfigPath string
NonInteractive bool
}

28 changes: 18 additions & 10 deletions remote/remote_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/shell"
"github.com/gruntwork-io/terragrunt/util"
"github.com/gruntwork-io/terragrunt/options"
"reflect"
)

// Configuration for Terraform remote state
Expand All @@ -32,8 +34,8 @@ func (remoteState *RemoteState) Validate() error {
}

// Configure Terraform remote state
func (remoteState RemoteState) ConfigureRemoteState() error {
shouldConfigure, err := shouldConfigureRemoteState(remoteState)
func (remoteState RemoteState) ConfigureRemoteState(terragruntOptions options.TerragruntOptions) error {
shouldConfigure, err := shouldConfigureRemoteState(remoteState, terragruntOptions)
if err != nil {
return err
}
Expand All @@ -50,14 +52,14 @@ func (remoteState RemoteState) ConfigureRemoteState() error {
//
// 1. Remote state has not already been configured
// 2. Remote state has been configured, but for a different backend type, and the user confirms it's OK to overwrite it.
func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState) (bool, error) {
func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState, terragruntOptions options.TerragruntOptions) (bool, error) {
state, err := ParseTerraformStateFileFromDefaultLocations()
if err != nil {
return false, err
}

if state != nil && state.IsRemote() {
return shouldOverrideExistingRemoteState(state.Remote, remoteStateFromTerragruntConfig)
return shouldOverrideExistingRemoteState(state.Remote, remoteStateFromTerragruntConfig, terragruntOptions)
} else {
return true, nil
}
Expand All @@ -66,13 +68,19 @@ func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState) (bo
// Check if the remote state that is already configured matches the one specified in the Terragrunt config. If it does,
// return false to indicate remote state does not need to be configured again. If it doesn't, prompt the user whether
// we should override the existing remote state setting.
func shouldOverrideExistingRemoteState(existingRemoteState *TerraformStateRemote, remoteStateFromTerragruntConfig RemoteState) (bool, error) {
if existingRemoteState.Type == remoteStateFromTerragruntConfig.Backend {
util.Logger.Printf("Remote state is already configured for backend %s", existingRemoteState.Type)
return false, nil
} else {
return shell.PromptUserForYesNo(fmt.Sprintf("WARNING: Terraform remote state is already configured, but for backend %s, whereas your Terragrunt configuration specifies %s. Overwrite?", existingRemoteState.Type, remoteStateFromTerragruntConfig.Backend))
func shouldOverrideExistingRemoteState(existingRemoteState *TerraformStateRemote, remoteStateFromTerragruntConfig RemoteState, terragruntOptions options.TerragruntOptions) (bool, error) {
if existingRemoteState.Type != remoteStateFromTerragruntConfig.Backend {
prompt := fmt.Sprintf("WARNING: Terraform remote state is already configured, but for backend %s, whereas your .terragrunt file specifies %s. Overwrite?", existingRemoteState.Type, remoteStateFromTerragruntConfig.Backend)
return shell.PromptUserForYesNo(prompt, terragruntOptions)
}

if !reflect.DeepEqual(existingRemoteState.Config, remoteStateFromTerragruntConfig.Config) {
prompt := fmt.Sprintf("WARNING: Terraform remote state is already configured for backend %s with config %v, but your .terragrunt file specifies config %v. Overwrite?", existingRemoteState.Type, existingRemoteState.Config, remoteStateFromTerragruntConfig.Config)
return shell.PromptUserForYesNo(prompt, terragruntOptions)
}

util.Logger.Printf("Remote state is already configured for backend %s", existingRemoteState.Type)
return false, nil
}

// Convert the RemoteState config into the format used by Terraform
Expand Down
64 changes: 64 additions & 0 deletions remote/remote_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/gruntwork-io/terragrunt/options"
)

func TestToTerraformRemoteConfigArgs(t *testing.T) {
Expand Down Expand Up @@ -33,6 +34,69 @@ func TestToTerraformRemoteConfigArgsNoBackendConfigs(t *testing.T) {
assertRemoteConfigArgsEqual(t, args, "remote config -backend s3")
}

func TestShouldOverrideExistingRemoteState(t *testing.T) {
t.Parallel()

terragruntOptions := options.TerragruntOptions{NonInteractive: true}

testCases := []struct {
existingState TerraformStateRemote
stateFromConfig RemoteState
shouldOverride bool
}{
{TerraformStateRemote{}, RemoteState{}, false},
{TerraformStateRemote{Type: "s3"}, RemoteState{Backend: "s3"}, false},
{TerraformStateRemote{Type: "s3"}, RemoteState{Backend: "atlas"}, true},
{
TerraformStateRemote{
Type: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
},
RemoteState{
Backend: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
},
false,
},{
TerraformStateRemote{
Type: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
},
RemoteState{
Backend: "s3",
Config: map[string]string{"bucket": "different", "key": "bar", "region": "us-east-1"},
},
true,
},{
TerraformStateRemote{
Type: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
},
RemoteState{
Backend: "s3",
Config: map[string]string{"bucket": "foo", "key": "different", "region": "us-east-1"},
},
true,
},{
TerraformStateRemote{
Type: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
},
RemoteState{
Backend: "s3",
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "different"},
},
true,
},
}

for _, testCase := range testCases {
shouldOverride, err := shouldOverrideExistingRemoteState(&testCase.existingState, testCase.stateFromConfig, terragruntOptions)
assert.Nil(t, err, "Unexpected error: %v", err)
assert.Equal(t, testCase.shouldOverride, shouldOverride, "Expect shouldOverrideExistingRemoteState to return %t but got %t for existingRemoteState %v and remoteStateFromTerragruntConfig %v", testCase.shouldOverride, shouldOverride, testCase.existingState, testCase.stateFromConfig)
}
}

func assertRemoteConfigArgsEqual(t *testing.T, actualArgs []string, expectedArgs string) {
expected := strings.Split(expectedArgs, " ")
assert.Len(t, actualArgs, len(expected))
Expand Down
2 changes: 1 addition & 1 deletion remote/terraform_state_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type TerraformState struct {
// The structure of the "remote" section of the Terraform .tfstate file
type TerraformStateRemote struct {
Type string
Config map[string]interface{}
Config map[string]string
}

// The structure of a "module" section of the Terraform .tfstate file
Expand Down
4 changes: 2 additions & 2 deletions remote/terraform_state_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func TestParseTerraformStateRemote(t *testing.T) {
Serial: 12,
Remote: &TerraformStateRemote{
Type: "s3",
Config: map[string]interface{}{
Config: map[string]string{
"bucket": "bucket",
"encrypt": "true",
"key": "experiment-1.tfstate",
Expand Down Expand Up @@ -211,7 +211,7 @@ func TestParseTerraformStateRemoteFull(t *testing.T) {
Serial: 51,
Remote: &TerraformStateRemote{
Type: "s3",
Config: map[string]interface{}{
Config: map[string]string{
"bucket": "bucket",
"encrypt": "true",
"key": "terraform.tfstate",
Expand Down
Loading

0 comments on commit f1a13d8

Please sign in to comment.