Merge pull request #331 from hashicorp/f-init

Init Command
pull/334/head
Mitchell Hashimoto 12 years ago
commit c8ccc6f72e

@ -0,0 +1,108 @@
package command
import (
"flag"
"fmt"
"os"
"strings"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module"
)
// InitCommand is a Command implementation that takes a Terraform
// module and clones it to the working directory.
type InitCommand struct {
Meta
}
func (c *InitCommand) Run(args []string) int {
args = c.Meta.process(args, false)
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
}
var path string
args = cmdFlags.Args()
if len(args) > 2 {
c.Ui.Error("The init command expects at most two arguments.\n")
cmdFlags.Usage()
return 1
} else if len(args) < 1 {
c.Ui.Error("The init command expects at least one arguments.\n")
cmdFlags.Usage()
return 1
}
if len(args) == 2 {
path = args[1]
} else {
var err error
path, err = os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err))
}
}
source := args[0]
// Get our pwd since we need it
pwd, err := os.Getwd()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading working directory: %s", err))
return 1
}
// Verify the directory is empty
if empty, err := config.IsEmptyDir(path); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error checking on destination path: %s", err))
return 1
} else if !empty {
c.Ui.Error(
"The destination path has Terraform configuration files. The\n" +
"init command can only be used on a directory without existing Terraform\n" +
"files.")
return 1
}
// Detect
source, err = module.Detect(source, pwd)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error with module source: %s", err))
return 1
}
// Get it!
if err := module.GetCopy(path, source); err != nil {
c.Ui.Error(err.Error())
return 1
}
return 0
}
func (c *InitCommand) Help() string {
helpText := `
Usage: terraform init [options] SOURCE [PATH]
Downloads the module given by SOURCE into the PATH. The PATH defaults
to the working directory. PATH must be empty of any Terraform files.
Any conflicting non-Terraform files will be overwritten.
The module downloaded is a copy. If you're downloading a module from
Git, it will not preserve the Git history, it will only copy the
latest files.
`
return strings.TrimSpace(helpText)
}
func (c *InitCommand) Synopsis() string {
return "Initializes Terraform configuration from a module"
}

@ -0,0 +1,102 @@
package command
import (
"os"
"path/filepath"
"testing"
"github.com/mitchellh/cli"
)
func TestInit(t *testing.T) {
dir := tempDir(t)
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
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_cwd(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)
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
testFixturePath("init"),
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
if _, err := os.Stat("hello.tf"); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestInit_multipleArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"bad",
"bad",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}
func TestInit_noArgs(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: \n%s", ui.OutputWriter.String())
}
}

@ -52,6 +52,12 @@ func init() {
}, nil
},
"init": func() (cli.Command, error) {
return &command.InitCommand{
Meta: meta,
}, nil
},
"output": func() (cli.Command, error) {
return &command.OutputCommand{
Meta: meta,

@ -42,62 +42,10 @@ func Load(path string) (*Config, error) {
//
// Files are loaded in lexical order.
func LoadDir(root string) (*Config, error) {
var files, overrides []string
f, err := os.Open(root)
if err != nil {
return nil, err
}
fi, err := f.Stat()
files, overrides, err := dirFiles(root)
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, fmt.Errorf(
"configuration path must be a directory: %s",
root)
}
err = nil
for err != io.EOF {
var fis []os.FileInfo
fis, err = f.Readdir(128)
if err != nil && err != io.EOF {
f.Close()
return nil, err
}
for _, fi := range fis {
// Ignore directories
if fi.IsDir() {
continue
}
// Only care about files that are valid to load
name := fi.Name()
extValue := ext(name)
if extValue == "" {
continue
}
// Determine if we're dealing with an override
nameNoExt := name[:len(name)-len(extValue)]
override := nameNoExt == "override" ||
strings.HasSuffix(nameNoExt, "_override")
path := filepath.Join(root, name)
if override {
overrides = append(overrides, path)
} else {
files = append(files, path)
}
}
}
// Close the directory, we're done with it
f.Close()
if len(files) == 0 {
return nil, fmt.Errorf(
"No Terraform configuration files found in directory: %s",
@ -152,6 +100,21 @@ func LoadDir(root string) (*Config, error) {
return result, nil
}
// IsEmptyDir returns true if the directory given has no Terraform
// configuration files.
func IsEmptyDir(root string) (bool, error) {
if _, err := os.Stat(root); err != nil && os.IsNotExist(err) {
return true, nil
}
fs, os, err := dirFiles(root)
if err != nil {
return false, err
}
return len(fs) == 0 && len(os) == 0, nil
}
// Ext returns the Terraform configuration extension of the given
// path, or a blank string if it is an invalid function.
func ext(path string) string {
@ -163,3 +126,59 @@ func ext(path string) string {
return ""
}
}
func dirFiles(dir string) ([]string, []string, error) {
f, err := os.Open(dir)
if err != nil {
return nil, nil, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, nil, err
}
if !fi.IsDir() {
return nil, nil, fmt.Errorf(
"configuration path must be a directory: %s",
dir)
}
var files, overrides []string
err = nil
for err != io.EOF {
var fis []os.FileInfo
fis, err = f.Readdir(128)
if err != nil && err != io.EOF {
return nil, nil, err
}
for _, fi := range fis {
// Ignore directories
if fi.IsDir() {
continue
}
// Only care about files that are valid to load
name := fi.Name()
extValue := ext(name)
if extValue == "" {
continue
}
// Determine if we're dealing with an override
nameNoExt := name[:len(name)-len(extValue)]
override := nameNoExt == "override" ||
strings.HasSuffix(nameNoExt, "_override")
path := filepath.Join(dir, name)
if override {
overrides = append(overrides, path)
} else {
files = append(files, path)
}
}
}
return files, overrides, nil
}

@ -6,6 +6,36 @@ import (
"testing"
)
func TestIsEmptyDir(t *testing.T) {
val, err := IsEmptyDir(fixtureDir)
if err != nil {
t.Fatalf("err: %s", err)
}
if val {
t.Fatal("should not be empty")
}
}
func TestIsEmptyDir_noExist(t *testing.T) {
val, err := IsEmptyDir(filepath.Join(fixtureDir, "nopenopenope"))
if err != nil {
t.Fatalf("err: %s", err)
}
if !val {
t.Fatal("should be empty")
}
}
func TestIsEmptyDir_noConfigs(t *testing.T) {
val, err := IsEmptyDir(filepath.Join(fixtureDir, "dir-empty"))
if err != nil {
t.Fatalf("err: %s", err)
}
if !val {
t.Fatal("should be empty")
}
}
func TestLoad_badType(t *testing.T) {
_, err := Load(filepath.Join(fixtureDir, "bad_type.tf.nope"))
if err == nil {

@ -4,17 +4,32 @@ import (
"io"
"os"
"path/filepath"
"strings"
)
// copyDir copies the src directory contents into dst. Both directories
// should already exist.
func copyDir(dst, src string) error {
src, err := filepath.EvalSymlinks(src)
if err != nil {
return err
}
walkFn := func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if path == src {
return nil
}
basePath := filepath.Base(path)
if strings.HasPrefix(basePath, ".") {
// Skip any dot files
return nil
}
dstPath := filepath.Join(dst, filepath.Base(path))
dstPath := filepath.Join(dst, basePath)
// If we have a directory, make that subdirectory, then continue
// the walk.

@ -3,6 +3,7 @@ package module
import (
"fmt"
"net/url"
"path/filepath"
)
// Detector defines the interface that an invalid URL or a URL with a blank
@ -34,6 +35,9 @@ func init() {
func Detect(src string, pwd string) (string, error) {
getForce, getSrc := getForcedGetter(src)
// Separate out the subdir if there is one, we don't pass that to detect
getSrc, subDir := getDirSubdir(getSrc)
u, err := url.Parse(getSrc)
if err == nil && u.Scheme != "" {
// Valid URL
@ -51,6 +55,25 @@ func Detect(src string, pwd string) (string, error) {
var detectForce string
detectForce, result = getForcedGetter(result)
result, detectSubdir := getDirSubdir(result)
// If we have a subdir from the detection, then prepend it to our
// requested subdir.
if detectSubdir != "" {
if subDir != "" {
subDir = filepath.Join(detectSubdir, subDir)
} else {
subDir = detectSubdir
}
}
if subDir != "" {
u, err := url.Parse(result)
if err != nil {
return "", fmt.Errorf("Error parsing URL: %s", err)
}
u.Path += "//" + subDir
result = u.String()
}
// Preserve the forced getter if it exists. We try to use the
// original set force first, followed by any force set by the

@ -13,7 +13,24 @@ func TestDetect(t *testing.T) {
}{
{"./foo", "/foo", "file:///foo/foo", false},
{"git::./foo", "/foo", "git::file:///foo/foo", false},
{"git::github.com/hashicorp/foo", "", "git::https://github.com/hashicorp/foo.git", false},
{
"git::github.com/hashicorp/foo",
"",
"git::https://github.com/hashicorp/foo.git",
false,
},
{
"./foo//bar",
"/foo",
"file:///foo/foo//bar",
false,
},
{
"git::github.com/hashicorp/foo//bar",
"",
"git::https://github.com/hashicorp/foo.git//bar",
false,
},
}
for i, tc := range cases {

@ -3,8 +3,11 @@ package module
import (
"bytes"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"syscall"
@ -51,6 +54,24 @@ func Get(dst, src string) error {
var force string
force, src = getForcedGetter(src)
// If there is a subdir component, then we download the root separately
// and then copy over the proper subdir.
var realDst string
src, subDir := getDirSubdir(src)
if subDir != "" {
tmpDir, err := ioutil.TempDir("", "tf")
if err != nil {
return err
}
if err := os.RemoveAll(tmpDir); err != nil {
return err
}
defer os.RemoveAll(tmpDir)
realDst = dst
dst = subDir
}
u, err := url.Parse(src)
if err != nil {
return err
@ -68,9 +89,52 @@ func Get(dst, src string) error {
err = g.Get(dst, u)
if err != nil {
err = fmt.Errorf("error downloading module '%s': %s", src, err)
return err
}
// If we have a subdir, copy that over
if subDir != "" {
if err := os.RemoveAll(realDst); err != nil {
return err
}
if err := os.MkdirAll(realDst, 0755); err != nil {
return err
}
return copyDir(realDst, filepath.Join(dst, subDir))
}
return nil
}
// GetCopy is the same as Get except that it downloads a copy of the
// module represented by source.
//
// This copy will omit and dot-prefixed files (such as .git/, .hg/) and
// can't be updated on its own.
func GetCopy(dst, src string) error {
// Create the temporary directory to do the real Get to
tmpDir, err := ioutil.TempDir("", "tf")
if err != nil {
return err
}
if err := os.RemoveAll(tmpDir); err != nil {
return err
}
defer os.RemoveAll(tmpDir)
// Get to that temporary dir
if err := Get(tmpDir, src); err != nil {
return err
}
// Make sure the destination exists
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
return err
// Copy to the final location
return copyDir(dst, tmpDir)
}
// getRunCommand is a helper that will run a command and capture the output

@ -46,6 +46,34 @@ func TestGet_fileForced(t *testing.T) {
}
}
func TestGet_fileSubdir(t *testing.T) {
dst := tempDir(t)
u := testModule("basic//subdir")
if err := Get(dst, u); err != nil {
t.Fatalf("err: %s", err)
}
mainPath := filepath.Join(dst, "sub.tf")
if _, err := os.Stat(mainPath); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestGetCopy_file(t *testing.T) {
dst := tempDir(t)
u := testModule("basic")
if err := GetCopy(dst, u); err != nil {
t.Fatalf("err: %s", err)
}
mainPath := filepath.Join(dst, "main.tf")
if _, err := os.Stat(mainPath); err != nil {
t.Fatalf("err: %s", err)
}
}
func TestGetDirSubdir(t *testing.T) {
cases := []struct {
Input string

@ -0,0 +1,24 @@
---
layout: "docs"
page_title: "Command: init"
sidebar_current: "docs-commands-init"
---
# Command: init
The `terraform init` command is used to initialize a Terraform configuration
using another
[module](/docs/modules/index.html)
as a skeleton.
## Usage
Usage: `terraform init [options] SOURCE [DIR]`
Init will download the module from SOURCE and copy it into the DIR
(which defaults to the current working directory). Version control
information from the module (such as Git history) will not be copied.
The directory being initialized must be empty of all Terraform configurations.
If the module has other files which conflict with what is already in the
directory, they _will be overwritten_.

@ -63,6 +63,10 @@
<a href="/docs/commands/graph.html">graph</a>
</li>
<li<%= sidebar_current("docs-commands-init") %>>
<a href="/docs/commands/init.html">init</a>
</li>
<li<%= sidebar_current("docs-commands-output") %>>
<a href="/docs/commands/output.html">output</a>
</li>

Loading…
Cancel
Save