diff --git a/command/apply.go b/command/apply.go index 611f2f250..33ff38c3b 100644 --- a/command/apply.go +++ b/command/apply.go @@ -68,7 +68,10 @@ func (c *ApplyCommand) Run(args []string) int { } // Build the context based on the arguments given - ctx, planned, err := c.Context(configPath, statePath) + ctx, planned, err := c.Context(contextOpts{ + Path: configPath, + StatePath: statePath, + }) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/get.go b/command/get.go new file mode 100644 index 000000000..394a9931c --- /dev/null +++ b/command/get.go @@ -0,0 +1,75 @@ +package command + +import ( + "flag" + "fmt" + "os" + "strings" +) + +// GetCommand is a Command implementation that takes a Terraform +// configuration and downloads all the modules. +type GetCommand struct { + Meta +} + +func (c *GetCommand) Run(args []string) int { + args = c.Meta.process(args, false) + + cmdFlags := flag.NewFlagSet("get", 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) > 1 { + c.Ui.Error("The graph command expects one argument.\n") + cmdFlags.Usage() + return 1 + } else if len(args) == 1 { + path = args[0] + } else { + var err error + path, err = os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + } + } + + _, _, err := c.Context(contextOpts{ + Path: path, + }) + if err != nil { + c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) + return 1 + } + + return 0 +} + +func (c *GetCommand) Help() string { + helpText := ` +Usage: terraform get [options] PATH + + Downloads and installs modules needed for the configuration given by + PATH. + + This recursively downloads all modules needed, such as modules + imported by modules imported by the root and so on. If a module is + already downloaded, it will not be redownloaded or checked for updates + unless the -update flag is specified. + +Options: + + -update=false If true, modules already downloaded will be checked + for updates and updated if necessary. + +` + return strings.TrimSpace(helpText) +} + +func (c *GetCommand) Synopsis() string { + return "Download and install modules for the configuration" +} diff --git a/command/get_test.go b/command/get_test.go new file mode 100644 index 000000000..dcde6d6b0 --- /dev/null +++ b/command/get_test.go @@ -0,0 +1,67 @@ +package command + +import ( + "os" + "testing" + + "github.com/mitchellh/cli" +) + +func TestGet(t *testing.T) { + ui := new(cli.MockUi) + c := &GetCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{ + testFixturePath("graph"), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} + +func TestGet_multipleArgs(t *testing.T) { + ui := new(cli.MockUi) + c := &GetCommand{ + 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 TestGet_noArgs(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("graph")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &GraphCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(testProvider()), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } +} diff --git a/command/graph.go b/command/graph.go index f974f1668..f28f4a82e 100644 --- a/command/graph.go +++ b/command/graph.go @@ -40,7 +40,10 @@ func (c *GraphCommand) Run(args []string) int { } } - ctx, _, err := c.Context(path, "") + ctx, _, err := c.Context(contextOpts{ + Path: path, + StatePath: "", + }) if err != nil { c.Ui.Error(fmt.Sprintf("Error loading Terraform: %s", err)) return 1 diff --git a/command/meta.go b/command/meta.go index ed1669ee2..e2708dcd8 100644 --- a/command/meta.go +++ b/command/meta.go @@ -7,7 +7,7 @@ import ( "io" "os" - "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" @@ -46,11 +46,11 @@ func (m *Meta) Colorize() *colorstring.Colorize { // Context returns a Terraform Context taking into account the context // options used to initialize this meta configuration. -func (m *Meta) Context(path, statePath string) (*terraform.Context, bool, error) { +func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { opts := m.contextOpts() // First try to just read the plan directly from the path given. - f, err := os.Open(path) + f, err := os.Open(copts.Path) if err == nil { plan, err := terraform.ReadPlan(f) f.Close() @@ -69,8 +69,8 @@ func (m *Meta) Context(path, statePath string) (*terraform.Context, bool, error) // Load up the state var state *terraform.State - if statePath != "" { - f, err := os.Open(statePath) + if copts.StatePath != "" { + f, err := os.Open(copts.StatePath) if err != nil && os.IsNotExist(err) { // If the state file doesn't exist, it is okay, since it // is probably a new infrastructure. @@ -88,15 +88,13 @@ func (m *Meta) Context(path, statePath string) (*terraform.Context, bool, error) // Store the loaded state m.state = state - config, err := config.LoadDir(path) + // Load the root module + mod, err := module.NewTreeModule("", copts.Path) if err != nil { return nil, false, fmt.Errorf("Error loading config: %s", err) } - if err := config.Validate(); err != nil { - return nil, false, fmt.Errorf("Error validating config: %s", err) - } - opts.Config = config + opts.Config = mod.Config() opts.State = state ctx := terraform.NewContext(opts) return ctx, false, nil @@ -207,3 +205,18 @@ func (m *Meta) uiHook() *UiHook { Ui: m.Ui, } } + +// contextOpts are the options used to load a context from a command. +type contextOpts struct { + // Path to the directory where the root module is. + Path string + + // StatePath is the path to the state file. If this is empty, then + // no state will be loaded. It is also okay for this to be a path to + // a file that doesn't exist; it is assumed that this means that there + // is simply no state. + StatePath string + + // GetMode is the module.GetMode to use when loading the module tree. + GetMode module.GetMode +} diff --git a/command/plan.go b/command/plan.go index 42106e2a1..62d364a26 100644 --- a/command/plan.go +++ b/command/plan.go @@ -65,7 +65,10 @@ func (c *PlanCommand) Run(args []string) int { backupPath = statePath + DefaultBackupExtention } - ctx, _, err := c.Context(path, statePath) + ctx, _, err := c.Context(contextOpts{ + Path: path, + StatePath: statePath, + }) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/command/refresh.go b/command/refresh.go index 2d45108bd..b4a331528 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -84,7 +84,10 @@ func (c *RefreshCommand) Run(args []string) int { } // Build the context based on the arguments given - ctx, _, err := c.Context(configPath, statePath) + ctx, _, err := c.Context(contextOpts{ + Path: configPath, + StatePath: statePath, + }) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/commands.go b/commands.go index 51a4f6eaa..fa9294df3 100644 --- a/commands.go +++ b/commands.go @@ -40,6 +40,12 @@ func init() { }, nil }, + "get": func() (cli.Command, error) { + return &command.GetCommand{ + Meta: meta, + }, nil + }, + "graph": func() (cli.Command, error) { return &command.GraphCommand{ Meta: meta, diff --git a/config/module/tree.go b/config/module/tree.go index 558dcce9d..6e6d30e0a 100644 --- a/config/module/tree.go +++ b/config/module/tree.go @@ -58,6 +58,11 @@ func NewTreeModule(name, dir string) (*Tree, error) { return NewTree(name, c), nil } +// Config returns the configuration for this module. +func (t *Tree) Config() *config.Config { + return t.config +} + // Children returns the children of this tree (the modules that are // imported by this root). //