From 8a7a0a74597ce2edde783d706d8402ccfbb7101e Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 28 Jul 2017 15:23:29 -0700 Subject: [PATCH] command: terraform init -from-module=... This restores the earlier behavior of the first positional argument to terraform init in 0.9, but as a command line option. The positional argument was removed to improve consistency with other commands that take a working directory as their first positional argument. It was originally intended that this functionality would return in a later release along with some other general improvements to Terraform's module handling, but we're introducing here an interim solution that uses the existing module source concept, to allow for easier porting of workflows that previously depended on the automatic copy behavior. In a future release this feature may change again as the module improvements design firms up, but we expect it to be broadly compatible with this temporary state. --- command/init.go | 56 ++++++++++++-- command/init_test.go | 93 ++++++++++++++++++++++++ website/docs/commands/init.html.markdown | 25 +++++++ 3 files changed, 168 insertions(+), 6 deletions(-) diff --git a/command/init.go b/command/init.go index 7c070f9df..427a8ce52 100644 --- a/command/init.go +++ b/command/init.go @@ -7,6 +7,8 @@ import ( "sort" "strings" + "github.com/hashicorp/go-getter" + multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/config" @@ -33,6 +35,7 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { + var flagFromModule string var flagBackend, flagGet, flagUpgrade bool var flagConfigExtra map[string]interface{} var flagPluginPath FlagStringSlice @@ -45,6 +48,7 @@ func (c *InitCommand) Run(args []string) int { cmdFlags := c.flagSet("init") cmdFlags.BoolVar(&flagBackend, "backend", true, "") cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "") + cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init") cmdFlags.BoolVar(&flagGet, "get", true, "") cmdFlags.BoolVar(&c.getPlugins, "get-plugins", true, "") cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") @@ -95,7 +99,7 @@ func (c *InitCommand) Run(args []string) int { return 1 } - // Get the path and source module to copy + // If an argument is provided then it overrides our working directory. path := pwd if len(args) == 1 { path = args[0] @@ -105,6 +109,30 @@ func (c *InitCommand) Run(args []string) int { // to output a newline before the success message var header bool + if flagFromModule != "" { + src := flagFromModule + + empty, err := config.IsEmptyDir(path) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error validating destination directory: %s", err)) + return 1 + } + if !empty { + c.Ui.Error(strings.TrimSpace(errInitCopyNotEmpty)) + return 1 + } + + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + "[reset][bold]Copying configuration[reset] from %q...", src, + ))) + header = true + + if err := c.copyConfigFromModule(path, src, pwd); err != nil { + c.Ui.Error(fmt.Sprintf("Error copying source module: %s", err)) + return 1 + } + } + // If our directory is empty, then we're done. We can't get or setup // the backend with an empty directory. if empty, err := config.IsEmptyDir(path); err != nil { @@ -232,6 +260,19 @@ func (c *InitCommand) Run(args []string) int { return 0 } +func (c *InitCommand) copyConfigFromModule(dst, src, pwd string) error { + // errors from this function will be prefixed with "Error copying source module: " + // when returned to the user. + var err error + + src, err = getter.Detect(src, pwd, getter.Detectors) + if err != nil { + return fmt.Errorf("invalid module source: %s", err) + } + + return module.GetCopy(dst, src) +} + // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade bool) error { @@ -430,6 +471,9 @@ Options: equivalent to providing a "yes" to all confirmation prompts. + -from-module=SOURCE Copy the contents of the given module into the target + directory before initialization. + -get=true Download any modules for this configuration. -get-plugins=true Download any missing plugins for this configuration. @@ -462,15 +506,15 @@ Options: } func (c *InitCommand) Synopsis() string { - return "Initialize a new or existing Terraform configuration" + return "Initialize a Terraform working directory" } const errInitCopyNotEmpty = ` -The destination path contains Terraform configuration files. The init command -with a SOURCE parameter can only be used on a directory without existing -Terraform files. +The working directory already contains files. The -from-module option requires +an empty directory into which a copy of the referenced module will be placed. -Please resolve this issue and try again. +To initialize the configuration already in this working directory, omit the +-from-module option. ` const outputInitEmpty = ` diff --git a/command/init_test.go b/command/init_test.go index e9403e1a5..ebfa397f1 100644 --- a/command/init_test.go +++ b/command/init_test.go @@ -55,6 +55,99 @@ func TestInit_multipleArgs(t *testing.T) { } } +func TestInit_fromModule_explicitDest(t *testing.T) { + dir := tempDir(t) + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-from-module=" + testFixturePath("init"), + dir, + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + if _, err := os.Stat(filepath.Join(dir, "hello.tf")); err != nil { + t.Fatalf("err: %s", err) + } +} + +func TestInit_fromModule_cwdDest(t *testing.T) { + // Create a temporary working directory that is empty + td := tempDir(t) + os.MkdirAll(td, os.ModePerm) + defer os.RemoveAll(td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-from-module=" + testFixturePath("init"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { + t.Fatalf("err: %s", err) + } +} + +// https://github.com/hashicorp/terraform/issues/518 +func TestInit_fromModule_dstInSrc(t *testing.T) { + dir := tempDir(t) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("err: %s", err) + } + + // Change to the temporary directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + if _, err := os.Create("issue518.tf"); err != nil { + t.Fatalf("err: %s", err) + } + + ui := new(cli.MockUi) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + } + + args := []string{ + "-from-module=.", + "foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + + if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { + t.Fatalf("err: %s", err) + } +} + func TestInit_get(t *testing.T) { // Create a temporary working directory that is empty td := tempDir(t) diff --git a/website/docs/commands/init.html.markdown b/website/docs/commands/init.html.markdown index 035eaabf2..851aa8e3d 100644 --- a/website/docs/commands/init.html.markdown +++ b/website/docs/commands/init.html.markdown @@ -50,6 +50,31 @@ The following options apply to all of (or several of) the initialization steps: * `-upgrade` Opt to upgrade modules and plugins as part of their respective installation steps. See the seconds below for more details. +## Copy a Source Module + +By default, `terraform init` assumes that the working directory already +contains a configuration and will attempt to initialize that configuration. + +Optionally, init can be run against an empty directory with the +`-with-module=MODULE-SOURCE` option, in which case the given module will be +copied into the target directory before any other initialization steps are +run. + +This special mode of operation supports two use-cases: + +* Given a version control source, it can serve as a shorthand for checking out + a configuration from version control and then initializing the work directory + for it. + +* If the source refers to an _example_ configuration, it can be copied into + a local directory to be used as a basis for a new configuration. + +For routine use it's recommended to check out configuration from version +control separately, using the version control system's own commands. This way +it's possible to pass extra flags to the version control system when necessary, +and to perform other preparation steps (such as configuration generation, or +activating credentials) before running `terraform init`. + ## Backend Initialization During init, the root configuration directory is consulted for