Skip to content

Commit

Permalink
Merge pull request #55 from gruntwork-io/auto-create-s3-bucket
Browse files Browse the repository at this point in the history
Terragrunt will now create remote state S3 bucket if it doesn't exist
  • Loading branch information
brikis98 authored Nov 22, 2016
2 parents f1a13d8 + 50bcb40 commit cea2a11
Show file tree
Hide file tree
Showing 14 changed files with 597 additions and 163 deletions.
19 changes: 19 additions & 0 deletions aws_helper/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package aws_helper

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/gruntwork-io/terragrunt/errors"
)

// Returns an AWS config object for the given region, ensuring that the config has credentials
func CreateAwsConfig(awsRegion string) (*aws.Config, error) {
config := defaults.Get().Config.WithRegion(awsRegion)

_, err := config.Credentials.Get()
if err != nil {
return nil, errors.WithStackTraceAndPrefix(err, "Error finding AWS credentials (did you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables?)")
}

return config, nil
}
99 changes: 56 additions & 43 deletions cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/gruntwork-io/terragrunt/util"
"github.com/urfave/cli"
"github.com/gruntwork-io/terragrunt/options"
"strings"
"os"
)

// Since Terragrunt is just a thin wrapper for Terraform, and we don't want to repeat every single Terraform command
Expand Down Expand Up @@ -69,18 +69,6 @@ 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/.`

app.Flags = []cli.Flag{
cli.StringFlag{
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 @@ -95,7 +83,10 @@ func runApp(cliContext *cli.Context) (finalErr error) {
return nil
}

terragruntOptions := parseTerragruntOptions(cliContext)
terragruntOptions, err := parseTerragruntOptions(cliContext)
if err != nil {
return err
}

conf, err := config.ReadTerragruntConfig(terragruntOptions)
if err != nil {
Expand All @@ -114,25 +105,59 @@ func runApp(cliContext *cli.Context) (finalErr error) {

if conf.Lock == nil {
util.Logger.Printf("WARNING: you have not configured locking in your .terragrunt file. Concurrent changes to your .tfstate files may cause conflicts!")
return runTerraformCommand(cliContext)
return runTerraformCommand(terragruntOptions)
}

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)
func parseTerragruntOptions(cliContext *cli.Context) (*options.TerragruntOptions, error) {
return parseTerragruntOptionsFromArgs(cliContext.Args())
}

// TODO: replace the urfave CLI library with something else.
//
// EXPLANATION: The normal way to parse flags with the urfave CLI library would be to define the flags in the
// CreateTerragruntCLI method and to read the values of those flags using cliContext.String(...),
// cliContext.Bool(...), etc. Unfortunately, this does not work here due to a limitation in the urfave
// CLI library: if the user passes in any "command" whatsever, (e.g. the "apply" in "terragrunt apply"), then
// any flags that come after it are not parsed (e.g. the "--foo" is not parsed in "terragrunt apply --foo").
// Therefore, we have to parse options ourselves, which is infuriating. For more details on this limitation,
// see: https://github.com/urfave/cli/issues/533. For now, our workaround is to dumbly loop over the arguments
// and look for the ones we need, but in the future, we should change to a different CLI library to avoid this
// limitation.
func parseTerragruntOptionsFromArgs(args []string) (*options.TerragruntOptions, error) {
nonInteractive := false
terragruntConfigPath := os.Getenv("TERRAGRUNT_CONFIG")
if terragruntConfigPath == "" {
terragruntConfigPath = config.DefaultTerragruntConfigPath
}
nonTerragruntArgs := []string{}
skipArg := false

for i, arg := range args {
if arg == fmt.Sprintf("--%s", OPT_NON_INTERACTIVE) {
nonInteractive = true
} else if arg == fmt.Sprintf("--%s", OPT_TERRAGRUNT_CONFIG) {
if (i + 1) < len(args) {
terragruntConfigPath = args[i + 1]
skipArg = true
} else {
return nil, errors.WithStackTrace(MissingTerragruntConfigValue)
}
} else if skipArg {
skipArg = false
} else {
nonTerragruntArgs = append(nonTerragruntArgs, arg)
}
}

nonInteractive := cliContext.Bool(OPT_NON_INTERACTIVE)

return options.TerragruntOptions{
return &options.TerragruntOptions{
TerragruntConfigPath: terragruntConfigPath,
NonInteractive: nonInteractive,
}
NonTerragruntArgs: nonTerragruntArgs,
}, nil
}

// A quick sanity check that calls `terraform get` to download modules, if they aren't already downloaded.
Expand Down Expand Up @@ -165,7 +190,7 @@ 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, terragruntOptions options.TerragruntOptions) 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() {
Expand All @@ -187,43 +212,30 @@ 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, terragruntOptions options.TerragruntOptions) 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) })
return locks.WithLock(lock, func() error { return runTerraformCommand(terragruntOptions) })
case "remote":
if cliContext.Args().Get(1) == "push" {
return locks.WithLock(lock, func() error { return runTerraformCommand(cliContext) })
return locks.WithLock(lock, func() error { return runTerraformCommand(terragruntOptions) })
} else {
return runTerraformCommand(cliContext)
return runTerraformCommand(terragruntOptions)
}
case "release-lock":
return runReleaseLockCommand(cliContext, lock, terragruntOptions)
default:
return runTerraformCommand(cliContext)
return runTerraformCommand(terragruntOptions)
}
}

// Run the given Terraform command
func runTerraformCommand(cliContext *cli.Context) error {
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
func runTerraformCommand(terragruntOptions *options.TerragruntOptions) error {
return shell.RunShellCommand("terraform", terragruntOptions.NonTerragruntArgs...)
}

// Release a lock, prompting the user for confirmation first
func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock, terragruntOptions options.TerragruntOptions) error {
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 {
Expand All @@ -238,3 +250,4 @@ func runReleaseLockCommand(cliContext *cli.Context, lock locks.Lock, terragruntO
}

var DontManuallyConfigureRemoteState = fmt.Errorf("Instead of manually using the 'remote config' command, define your remote state settings in .terragrunt and Terragrunt will automatically configure it for you (and all your team members) next time you run it.")
var MissingTerragruntConfigValue = fmt.Errorf("You must specify a value for the --%s option", OPT_TERRAGRUNT_CONFIG)
121 changes: 121 additions & 0 deletions cli/cli_app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package cli

import (
"testing"
"github.com/gruntwork-io/terragrunt/options"
"github.com/stretchr/testify/assert"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/config"
)

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

testCases := []struct {
args []string
expectedOptions *options.TerragruntOptions
expectedErr error
}{
{
[]string{},
&options.TerragruntOptions{
TerragruntConfigPath: config.DefaultTerragruntConfigPath,
NonInteractive: false,
NonTerragruntArgs: []string{},
},
nil,
},

{
[]string{"foo", "bar"},
&options.TerragruntOptions{
TerragruntConfigPath: config.DefaultTerragruntConfigPath,
NonInteractive: false,
NonTerragruntArgs: []string{"foo", "bar"},
},
nil,
},

{
[]string{"--foo", "--bar"},
&options.TerragruntOptions{
TerragruntConfigPath: config.DefaultTerragruntConfigPath,
NonInteractive: false,
NonTerragruntArgs: []string{"--foo", "--bar"},
},
nil,
},

{
[]string{"--foo", "apply", "--bar"},
&options.TerragruntOptions{
TerragruntConfigPath: config.DefaultTerragruntConfigPath,
NonInteractive: false,
NonTerragruntArgs: []string{"--foo", "apply", "--bar"},
},
nil,
},

{
[]string{"--terragrunt-non-interactive"},
&options.TerragruntOptions{
TerragruntConfigPath: config.DefaultTerragruntConfigPath,
NonInteractive: true,
NonTerragruntArgs: []string{},
},
nil,
},

{
[]string{"--terragrunt-config", "/some/path/.terragrunt"},
&options.TerragruntOptions{
TerragruntConfigPath: "/some/path/.terragrunt",
NonInteractive: false,
NonTerragruntArgs: []string{},
},
nil,
},

{
[]string{"--terragrunt-config", "/some/path/.terragrunt", "--terragrunt-non-interactive"},
&options.TerragruntOptions{
TerragruntConfigPath: "/some/path/.terragrunt",
NonInteractive: true,
NonTerragruntArgs: []string{},
},
nil,
},

{
[]string{"--foo", "--terragrunt-config", "/some/path/.terragrunt", "bar", "--terragrunt-non-interactive", "--baz"},
&options.TerragruntOptions{
TerragruntConfigPath: "/some/path/.terragrunt",
NonInteractive: true,
NonTerragruntArgs: []string{"--foo", "bar", "--baz"},
},
nil,
},

{
[]string{"--terragrunt-config"},
nil,
MissingTerragruntConfigValue,
},

{
[]string{"--foo", "bar", "--terragrunt-config"},
nil,
MissingTerragruntConfigValue,
},
}

for _, testCase := range testCases {
actualOptions, actualErr := parseTerragruntOptionsFromArgs(testCase.args)
if testCase.expectedErr != nil {
assert.True(t, errors.IsError(actualErr, testCase.expectedErr), "Expected error %v but got error %v", testCase.expectedErr, actualErr)
} else {
assert.Nil(t, actualErr, "Unexpected error: %v", actualErr)
assert.Equal(t, testCase.expectedOptions, actualOptions, "Expected options %v but got %v", testCase.expectedOptions, actualOptions)
}
}
}
4 changes: 3 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gruntwork-io/terragrunt/remote"
"github.com/hashicorp/hcl"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/util"
)

const DefaultTerragruntConfigPath = ".terragrunt"
Expand All @@ -31,7 +32,8 @@ type LockConfig struct {
}

// ReadTerragruntConfig the Terragrunt config file from its default location
func ReadTerragruntConfig(terragruntOptions options.TerragruntOptions) (*TerragruntConfig, error) {
func ReadTerragruntConfig(terragruntOptions *options.TerragruntOptions) (*TerragruntConfig, error) {
util.Logger.Printf("Reading Terragrunt config file at %s", terragruntOptions.TerragruntConfigPath)
return parseConfigFile(terragruntOptions.TerragruntConfigPath)
}

Expand Down
Loading

0 comments on commit cea2a11

Please sign in to comment.