Skip to content

Commit f1a13d8

Browse files
authored
Merge pull request #54 from gruntwork-io/update-config
Ensure Terragrunt picks up changes to remote config
2 parents 53395ba + 4d9b1c1 commit f1a13d8

10 files changed

Lines changed: 172 additions & 38 deletions

File tree

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ remote_state = {
229229
different key/value pairs, so consult the [Terraform remote state docs](https://www.terraform.io/docs/state/remote/)
230230
for details.
231231

232+
## CLI Options
233+
234+
Terragrunt forwards all arguments and options to Terraform. The only exceptions are the options that start with the
235+
prefix `--terragrunt-`. The currently available options are:
236+
237+
* `--terragrunt-config`: A custom path to the `.terragrunt` file. May also be specified via the `TERRAGRUNT_CONFIG`
238+
environment variable. The default path is `.terragrunt` in the current directory.
239+
* `--terragrunt-non-interactive`: Don't show interactive user prompts. This will default the answer for all prompts to
240+
'yes'. Useful if you need to run Terragrunt in an automated setting (e.g. from a script).
241+
232242
## Developing terragrunt
233243

234244
#### Running locally

cli/cli_app.go

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cli
22

33
import (
44
"fmt"
5-
"os"
65
"regexp"
76

87
"github.com/gruntwork-io/terragrunt/config"
@@ -12,6 +11,8 @@ import (
1211
"github.com/gruntwork-io/terragrunt/shell"
1312
"github.com/gruntwork-io/terragrunt/util"
1413
"github.com/urfave/cli"
14+
"github.com/gruntwork-io/terragrunt/options"
15+
"strings"
1516
)
1617

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

4849
const TERRAFORM_EXTENSION_GLOB = "*.tf"
4950

51+
const OPT_TERRAGRUNT_CONFIG = "terragrunt-config"
52+
const OPT_NON_INTERACTIVE = "terragrunt-non-interactive"
53+
5054
// Create the Terragrunt CLI App
5155
func CreateTerragruntCli(version string) *cli.App {
5256
cli.AppHelpTemplate = CUSTOM_USAGE_TEXT
@@ -65,17 +69,16 @@ func CreateTerragruntCli(version string) *cli.App {
6569
Moreover, for the apply and destroy commands, Terragrunt will first try to acquire a lock using DynamoDB. For
6670
documentation, see https://github.com/gruntwork-io/terragrunt/.`
6771

68-
var defaultConfigFilePath = config.ConfigFilePath
69-
if os.Getenv("TERRAGRUNT_CONFIG") != "" {
70-
defaultConfigFilePath = os.Getenv("TERRAGRUNT_CONFIG")
71-
}
72-
7372
app.Flags = []cli.Flag{
7473
cli.StringFlag{
75-
Name: "terragrunt-config",
76-
Value: defaultConfigFilePath,
74+
Name: OPT_TERRAGRUNT_CONFIG,
75+
EnvVar: "TERRAGRUNT_CONFIG",
7776
Usage: ".terragrunt file to use",
7877
},
78+
cli.BoolFlag{
79+
Name: OPT_NON_INTERACTIVE,
80+
Usage: "Don't show interactive user prompts. This will default the answer for all prompts to 'yes'.",
81+
},
7982
}
8083

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

95-
conf, err := config.ReadTerragruntConfig(cliContext.String("terragrunt-config"))
98+
terragruntOptions := parseTerragruntOptions(cliContext)
99+
100+
conf, err := config.ReadTerragruntConfig(terragruntOptions)
96101
if err != nil {
97102
return err
98103
}
@@ -102,7 +107,7 @@ func runApp(cliContext *cli.Context) (finalErr error) {
102107
}
103108

104109
if conf.RemoteState != nil {
105-
if err := configureRemoteState(cliContext, conf.RemoteState); err != nil {
110+
if err := configureRemoteState(cliContext, conf.RemoteState, terragruntOptions); err != nil {
106111
return err
107112
}
108113
}
@@ -112,7 +117,22 @@ func runApp(cliContext *cli.Context) (finalErr error) {
112117
return runTerraformCommand(cliContext)
113118
}
114119

115-
return runTerraformCommandWithLock(cliContext, conf.Lock)
120+
return runTerraformCommandWithLock(cliContext, conf.Lock, terragruntOptions)
121+
}
122+
123+
// Parse command line options that are passed in for Terragrunt
124+
func parseTerragruntOptions(cliContext *cli.Context) options.TerragruntOptions {
125+
terragruntConfigPath := cliContext.String(OPT_TERRAGRUNT_CONFIG)
126+
if terragruntConfigPath == "" {
127+
terragruntConfigPath = config.DefaultTerragruntConfigPath
128+
}
129+
130+
nonInteractive := cliContext.Bool(OPT_NON_INTERACTIVE)
131+
132+
return options.TerragruntOptions{
133+
TerragruntConfigPath: terragruntConfigPath,
134+
NonInteractive: nonInteractive,
135+
}
116136
}
117137

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

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

169189
// Run the given Terraform command with the given lock (if the command requires locking)
170-
func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock) error {
190+
func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock, terragruntOptions options.TerragruntOptions) error {
171191
switch cliContext.Args().First() {
172192
case "apply", "destroy", "import", "refresh":
173193
return locks.WithLock(lock, func() error { return runTerraformCommand(cliContext) })
@@ -178,20 +198,34 @@ func runTerraformCommandWithLock(cliContext *cli.Context, lock locks.Lock) error
178198
return runTerraformCommand(cliContext)
179199
}
180200
case "release-lock":
181-
return runReleaseLockCommand(cliContext, lock)
201+
return runReleaseLockCommand(cliContext, lock, terragruntOptions)
182202
default:
183203
return runTerraformCommand(cliContext)
184204
}
185205
}
186206

187207
// Run the given Terraform command
188208
func runTerraformCommand(cliContext *cli.Context) error {
189-
return shell.RunShellCommand("terraform", cliContext.Args()...)
209+
return shell.RunShellCommand("terraform", filterOutTerragruntArgs(cliContext)...)
210+
}
211+
212+
// Return the args in teh given CLI Context object, filtering any args that are only meant for Terragrunt itself
213+
func filterOutTerragruntArgs(cliContext *cli.Context) []string {
214+
args := []string{}
215+
216+
for _, arg := range cliContext.Args() {
217+
if !strings.HasPrefix(arg, "--terragrunt") {
218+
args = append(args, arg)
219+
}
220+
}
221+
222+
return args
190223
}
191224

192225
// Release a lock, prompting the user for confirmation first
193-
func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock) error {
194-
proceed, err := shell.PromptUserForYesNo(fmt.Sprintf("Are you sure you want to release %s?", lock))
226+
func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock, terragruntOptions options.TerragruntOptions) error {
227+
prompt := fmt.Sprintf("Are you sure you want to release %s?", lock)
228+
proceed, err := shell.PromptUserForYesNo(prompt, terragruntOptions)
195229
if err != nil {
196230
return err
197231
}

config/config.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
"github.com/gruntwork-io/terragrunt/locks"
88
"github.com/gruntwork-io/terragrunt/remote"
99
"github.com/hashicorp/hcl"
10+
"github.com/gruntwork-io/terragrunt/options"
1011
)
1112

12-
const ConfigFilePath = ".terragrunt"
13+
const DefaultTerragruntConfigPath = ".terragrunt"
1314

1415
// TerragruntConfig represents a parsed and expanded configuration
1516
type TerragruntConfig struct {
@@ -30,8 +31,8 @@ type LockConfig struct {
3031
}
3132

3233
// ReadTerragruntConfig the Terragrunt config file from its default location
33-
func ReadTerragruntConfig(filePath string) (*TerragruntConfig, error) {
34-
return parseConfigFile(filePath)
34+
func ReadTerragruntConfig(terragruntOptions options.TerragruntOptions) (*TerragruntConfig, error) {
35+
return parseConfigFile(terragruntOptions.TerragruntConfigPath)
3536
}
3637

3738
// Parse the Terragrunt config file at the given path

options/options.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package options
2+
3+
// TerragruntOptions represents command-line options that are read by Terragrunt
4+
type TerragruntOptions struct {
5+
TerragruntConfigPath string
6+
NonInteractive bool
7+
}
8+

remote/remote_state.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"github.com/gruntwork-io/terragrunt/errors"
77
"github.com/gruntwork-io/terragrunt/shell"
88
"github.com/gruntwork-io/terragrunt/util"
9+
"github.com/gruntwork-io/terragrunt/options"
10+
"reflect"
911
)
1012

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

3436
// Configure Terraform remote state
35-
func (remoteState RemoteState) ConfigureRemoteState() error {
36-
shouldConfigure, err := shouldConfigureRemoteState(remoteState)
37+
func (remoteState RemoteState) ConfigureRemoteState(terragruntOptions options.TerragruntOptions) error {
38+
shouldConfigure, err := shouldConfigureRemoteState(remoteState, terragruntOptions)
3739
if err != nil {
3840
return err
3941
}
@@ -50,14 +52,14 @@ func (remoteState RemoteState) ConfigureRemoteState() error {
5052
//
5153
// 1. Remote state has not already been configured
5254
// 2. Remote state has been configured, but for a different backend type, and the user confirms it's OK to overwrite it.
53-
func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState) (bool, error) {
55+
func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState, terragruntOptions options.TerragruntOptions) (bool, error) {
5456
state, err := ParseTerraformStateFileFromDefaultLocations()
5557
if err != nil {
5658
return false, err
5759
}
5860

5961
if state != nil && state.IsRemote() {
60-
return shouldOverrideExistingRemoteState(state.Remote, remoteStateFromTerragruntConfig)
62+
return shouldOverrideExistingRemoteState(state.Remote, remoteStateFromTerragruntConfig, terragruntOptions)
6163
} else {
6264
return true, nil
6365
}
@@ -66,13 +68,19 @@ func shouldConfigureRemoteState(remoteStateFromTerragruntConfig RemoteState) (bo
6668
// Check if the remote state that is already configured matches the one specified in the Terragrunt config. If it does,
6769
// return false to indicate remote state does not need to be configured again. If it doesn't, prompt the user whether
6870
// we should override the existing remote state setting.
69-
func shouldOverrideExistingRemoteState(existingRemoteState *TerraformStateRemote, remoteStateFromTerragruntConfig RemoteState) (bool, error) {
70-
if existingRemoteState.Type == remoteStateFromTerragruntConfig.Backend {
71-
util.Logger.Printf("Remote state is already configured for backend %s", existingRemoteState.Type)
72-
return false, nil
73-
} else {
74-
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))
71+
func shouldOverrideExistingRemoteState(existingRemoteState *TerraformStateRemote, remoteStateFromTerragruntConfig RemoteState, terragruntOptions options.TerragruntOptions) (bool, error) {
72+
if existingRemoteState.Type != remoteStateFromTerragruntConfig.Backend {
73+
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)
74+
return shell.PromptUserForYesNo(prompt, terragruntOptions)
75+
}
76+
77+
if !reflect.DeepEqual(existingRemoteState.Config, remoteStateFromTerragruntConfig.Config) {
78+
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)
79+
return shell.PromptUserForYesNo(prompt, terragruntOptions)
7580
}
81+
82+
util.Logger.Printf("Remote state is already configured for backend %s", existingRemoteState.Type)
83+
return false, nil
7684
}
7785

7886
// Convert the RemoteState config into the format used by Terraform

remote/remote_state_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66

77
"github.com/stretchr/testify/assert"
8+
"github.com/gruntwork-io/terragrunt/options"
89
)
910

1011
func TestToTerraformRemoteConfigArgs(t *testing.T) {
@@ -33,6 +34,69 @@ func TestToTerraformRemoteConfigArgsNoBackendConfigs(t *testing.T) {
3334
assertRemoteConfigArgsEqual(t, args, "remote config -backend s3")
3435
}
3536

37+
func TestShouldOverrideExistingRemoteState(t *testing.T) {
38+
t.Parallel()
39+
40+
terragruntOptions := options.TerragruntOptions{NonInteractive: true}
41+
42+
testCases := []struct {
43+
existingState TerraformStateRemote
44+
stateFromConfig RemoteState
45+
shouldOverride bool
46+
}{
47+
{TerraformStateRemote{}, RemoteState{}, false},
48+
{TerraformStateRemote{Type: "s3"}, RemoteState{Backend: "s3"}, false},
49+
{TerraformStateRemote{Type: "s3"}, RemoteState{Backend: "atlas"}, true},
50+
{
51+
TerraformStateRemote{
52+
Type: "s3",
53+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
54+
},
55+
RemoteState{
56+
Backend: "s3",
57+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
58+
},
59+
false,
60+
},{
61+
TerraformStateRemote{
62+
Type: "s3",
63+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
64+
},
65+
RemoteState{
66+
Backend: "s3",
67+
Config: map[string]string{"bucket": "different", "key": "bar", "region": "us-east-1"},
68+
},
69+
true,
70+
},{
71+
TerraformStateRemote{
72+
Type: "s3",
73+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
74+
},
75+
RemoteState{
76+
Backend: "s3",
77+
Config: map[string]string{"bucket": "foo", "key": "different", "region": "us-east-1"},
78+
},
79+
true,
80+
},{
81+
TerraformStateRemote{
82+
Type: "s3",
83+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "us-east-1"},
84+
},
85+
RemoteState{
86+
Backend: "s3",
87+
Config: map[string]string{"bucket": "foo", "key": "bar", "region": "different"},
88+
},
89+
true,
90+
},
91+
}
92+
93+
for _, testCase := range testCases {
94+
shouldOverride, err := shouldOverrideExistingRemoteState(&testCase.existingState, testCase.stateFromConfig, terragruntOptions)
95+
assert.Nil(t, err, "Unexpected error: %v", err)
96+
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)
97+
}
98+
}
99+
36100
func assertRemoteConfigArgsEqual(t *testing.T, actualArgs []string, expectedArgs string) {
37101
expected := strings.Split(expectedArgs, " ")
38102
assert.Len(t, actualArgs, len(expected))

remote/terraform_state_file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type TerraformState struct {
2929
// The structure of the "remote" section of the Terraform .tfstate file
3030
type TerraformStateRemote struct {
3131
Type string
32-
Config map[string]interface{}
32+
Config map[string]string
3333
}
3434

3535
// The structure of a "module" section of the Terraform .tfstate file

remote/terraform_state_file_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func TestParseTerraformStateRemote(t *testing.T) {
8181
Serial: 12,
8282
Remote: &TerraformStateRemote{
8383
Type: "s3",
84-
Config: map[string]interface{}{
84+
Config: map[string]string{
8585
"bucket": "bucket",
8686
"encrypt": "true",
8787
"key": "experiment-1.tfstate",
@@ -211,7 +211,7 @@ func TestParseTerraformStateRemoteFull(t *testing.T) {
211211
Serial: 51,
212212
Remote: &TerraformStateRemote{
213213
Type: "s3",
214-
Config: map[string]interface{}{
214+
Config: map[string]string{
215215
"bucket": "bucket",
216216
"encrypt": "true",
217217
"key": "terraform.tfstate",

0 commit comments

Comments
 (0)