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.
This commit is contained in:
Martin Atkins 2017-07-28 15:23:29 -07:00
parent 870617d22d
commit 8a7a0a7459
3 changed files with 168 additions and 6 deletions

View File

@ -7,6 +7,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/hashicorp/go-getter"
multierror "github.com/hashicorp/go-multierror" multierror "github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/backend" "github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
@ -33,6 +35,7 @@ type InitCommand struct {
} }
func (c *InitCommand) Run(args []string) int { func (c *InitCommand) Run(args []string) int {
var flagFromModule string
var flagBackend, flagGet, flagUpgrade bool var flagBackend, flagGet, flagUpgrade bool
var flagConfigExtra map[string]interface{} var flagConfigExtra map[string]interface{}
var flagPluginPath FlagStringSlice var flagPluginPath FlagStringSlice
@ -45,6 +48,7 @@ func (c *InitCommand) Run(args []string) int {
cmdFlags := c.flagSet("init") cmdFlags := c.flagSet("init")
cmdFlags.BoolVar(&flagBackend, "backend", true, "") cmdFlags.BoolVar(&flagBackend, "backend", true, "")
cmdFlags.Var((*variables.FlagAny)(&flagConfigExtra), "backend-config", "") 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(&flagGet, "get", true, "")
cmdFlags.BoolVar(&c.getPlugins, "get-plugins", true, "") cmdFlags.BoolVar(&c.getPlugins, "get-plugins", true, "")
cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") 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 return 1
} }
// Get the path and source module to copy // If an argument is provided then it overrides our working directory.
path := pwd path := pwd
if len(args) == 1 { if len(args) == 1 {
path = args[0] path = args[0]
@ -105,6 +109,30 @@ func (c *InitCommand) Run(args []string) int {
// to output a newline before the success message // to output a newline before the success message
var header bool 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 // If our directory is empty, then we're done. We can't get or setup
// the backend with an empty directory. // the backend with an empty directory.
if empty, err := config.IsEmptyDir(path); err != nil { if empty, err := config.IsEmptyDir(path); err != nil {
@ -232,6 +260,19 @@ func (c *InitCommand) Run(args []string) int {
return 0 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. // Load the complete module tree, and fetch any missing providers.
// This method outputs its own Ui. // This method outputs its own Ui.
func (c *InitCommand) getProviders(path string, state *terraform.State, upgrade bool) error { 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 equivalent to providing a "yes" to all confirmation
prompts. 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=true Download any modules for this configuration.
-get-plugins=true Download any missing plugins for this configuration. -get-plugins=true Download any missing plugins for this configuration.
@ -462,15 +506,15 @@ Options:
} }
func (c *InitCommand) Synopsis() string { func (c *InitCommand) Synopsis() string {
return "Initialize a new or existing Terraform configuration" return "Initialize a Terraform working directory"
} }
const errInitCopyNotEmpty = ` const errInitCopyNotEmpty = `
The destination path contains Terraform configuration files. The init command The working directory already contains files. The -from-module option requires
with a SOURCE parameter can only be used on a directory without existing an empty directory into which a copy of the referenced module will be placed.
Terraform files.
Please resolve this issue and try again. To initialize the configuration already in this working directory, omit the
-from-module option.
` `
const outputInitEmpty = ` const outputInitEmpty = `

View File

@ -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) { func TestInit_get(t *testing.T) {
// Create a temporary working directory that is empty // Create a temporary working directory that is empty
td := tempDir(t) td := tempDir(t)

View File

@ -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 * `-upgrade` Opt to upgrade modules and plugins as part of their respective
installation steps. See the seconds below for more details. 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 ## Backend Initialization
During init, the root configuration directory is consulted for During init, the root configuration directory is consulted for