diff --git a/command/apply.go b/command/apply.go index 859d0515f..780becf5c 100644 --- a/command/apply.go +++ b/command/apply.go @@ -8,6 +8,7 @@ import ( "sort" "strings" + "github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/terraform" ) @@ -35,6 +36,12 @@ func (c *ApplyCommand) Run(args []string) int { return 1 } + pwd, err := os.Getwd() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) + return 1 + } + var configPath string args = cmdFlags.Args() if len(args) > 1 { @@ -44,11 +51,7 @@ func (c *ApplyCommand) Run(args []string) int { } else if len(args) == 1 { configPath = args[0] } else { - var err error - configPath, err = os.Getwd() - if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting pwd: %s", err)) - } + configPath = pwd } // Prepare the extra hooks to count resources @@ -67,6 +70,24 @@ func (c *ApplyCommand) Run(args []string) int { backupPath = stateOutPath + DefaultBackupExtention } + // Do a detect to determine if we need to do an init + apply. + if detected, err := module.Detect(configPath, pwd); err != nil { + c.Ui.Error(fmt.Sprintf( + "Invalid path: %s", err)) + return 1 + } else if !strings.HasPrefix(detected, "file") { + // If this isn't a file URL then we're doing an init + + // apply. + var init InitCommand + init.Meta = c.Meta + if code := init.Run([]string{detected}); code != 0 { + return code + } + + // Change the config path to be the cwd + configPath = pwd + } + // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ Path: configPath, @@ -229,10 +250,16 @@ func (c *ApplyCommand) Run(args []string) int { func (c *ApplyCommand) Help() string { helpText := ` -Usage: terraform apply [options] [dir] +Usage: terraform apply [options] [DIR] Builds or changes infrastructure according to Terraform configuration - files . + files in DIR. + + DIR can also be a SOURCE as given to the "init" command. In this case, + apply behaves as though "init" was called followed by "apply". This only + works for sources that aren't files, and only if the current working + directory is empty of Terraform files. This is a shortcut for getting + started. Options: diff --git a/command/apply_test.go b/command/apply_test.go index f745016d2..4423b3b2e 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -3,6 +3,9 @@ package command import ( "fmt" "io/ioutil" + "net" + "net/http" + "net/url" "os" "path/filepath" "reflect" @@ -196,6 +199,73 @@ func TestApply_error(t *testing.T) { } } +func TestApply_init(t *testing.T) { + // Change to the temporary directory + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + dir := tempDir(t) + if err := os.MkdirAll(dir, 0755); err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(dir); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + // Create the test fixtures + statePath := testTempFile(t) + ln := testHttpServer(t) + defer ln.Close() + + // Initialize the command + p := testProvider() + ui := new(cli.MockUi) + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfig(p), + Ui: ui, + }, + } + + // Build the URL to the init + var u url.URL + u.Scheme = "http" + u.Host = ln.Addr().String() + u.Path = "/header" + + args := []string{ + "-state", statePath, + u.String(), + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + if _, err := os.Stat("hello.tf"); err != nil { + t.Fatalf("err: %s", err) + } + + if _, err := os.Stat(statePath); err != nil { + t.Fatalf("err: %s", err) + } + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("err: %s", err) + } + defer f.Close() + + state, err := terraform.ReadState(f) + if err != nil { + t.Fatalf("err: %s", err) + } + if state == nil { + t.Fatal("state should not be nil") + } +} + func TestApply_noArgs(t *testing.T) { cwd, err := os.Getwd() if err != nil { @@ -942,6 +1012,31 @@ func TestApply_disableBackup(t *testing.T) { } } +func testHttpServer(t *testing.T) net.Listener { + ln, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("err: %s", err) + } + + mux := http.NewServeMux() + mux.HandleFunc("/header", testHttpHandlerHeader) + + var server http.Server + server.Handler = mux + go server.Serve(ln) + + return ln +} + +func testHttpHandlerHeader(w http.ResponseWriter, r *http.Request) { + var url url.URL + url.Scheme = "file" + url.Path = testFixturePath("init") + + w.Header().Add("X-Terraform-Get", url.String()) + w.WriteHeader(200) +} + const applyVarFile = ` foo = "bar" ` diff --git a/website/source/docs/commands/apply.html.markdown b/website/source/docs/commands/apply.html.markdown index 58adc9104..bfd563d75 100644 --- a/website/source/docs/commands/apply.html.markdown +++ b/website/source/docs/commands/apply.html.markdown @@ -19,6 +19,11 @@ and applies the changes appropriately. However, a path to another configuration or an execution plan can be provided. Execution plans can be used to only execute a pre-determined set of actions. +The `dir` argument can also be a [module source](/docs/modules/index.html). +In this case, `apply` behaves as though `init` were called with that +argument followed by an `apply` in the current directory. This is meant +as a shortcut for getting started. + The command-line flags are all optional. The list of available flags are: * `-backup=path` - Path to the backup file. Defaults to `-state-out` with