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