diff --git a/command/apply.go b/command/apply.go index 529d6e701a87..a31676a36d6f 100644 --- a/command/apply.go +++ b/command/apply.go @@ -38,6 +38,7 @@ func (c *ApplyCommand) Run(args []string) int { cmdFlags.BoolVar(&destroyForce, "force", false, "force") } cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") + cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") @@ -93,9 +94,10 @@ func (c *ApplyCommand) Run(args []string) int { // Build the context based on the arguments given ctx, planned, err := c.Context(contextOpts{ - Destroy: c.Destroy, - Path: configPath, - StatePath: c.Meta.statePath, + Destroy: c.Destroy, + Path: configPath, + StatePath: c.Meta.statePath, + Parallelism: c.Meta.parallelism, }) if err != nil { c.Ui.Error(err.Error()) @@ -305,6 +307,8 @@ Options: -no-color If specified, output won't contain any color. + -parallelism=# Limit the number of concurrent operations. + -refresh=true Update state prior to checking for differences. This has no effect if a plan file is given to apply. diff --git a/command/apply_test.go b/command/apply_test.go index e9ff25fe8ce9..339db05cf8d8 100644 --- a/command/apply_test.go +++ b/command/apply_test.go @@ -58,6 +58,82 @@ func TestApply(t *testing.T) { } } +func TestApply_parallelism1(t *testing.T) { + statePath := testTempFile(t) + + ui := new(cli.MockUi) + p := testProvider() + pr := new(terraform.MockResourceProvisioner) + + pr.ApplyFn = func(*terraform.InstanceState, *terraform.ResourceConfig) error { + time.Sleep(time.Second) + return nil + } + + args := []string{ + "-state", statePath, + "-parallelism=1", + testFixturePath("parallelism"), + } + + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfigWithShell(p, pr), + Ui: ui, + }, + } + + start := time.Now() + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + elapsed := time.Since(start).Seconds() + + // This test should take exactly two seconds, plus some minor amount of execution time. + if elapsed < 2 || elapsed > 2.2 { + t.Fatalf("bad: %f\n\n%s", elapsed, ui.ErrorWriter.String()) + } + +} + +func TestApply_parallelism2(t *testing.T) { + statePath := testTempFile(t) + + ui := new(cli.MockUi) + p := testProvider() + pr := new(terraform.MockResourceProvisioner) + + pr.ApplyFn = func(*terraform.InstanceState, *terraform.ResourceConfig) error { + time.Sleep(time.Second) + return nil + } + + args := []string{ + "-state", statePath, + "-parallelism=2", + testFixturePath("parallelism"), + } + + c := &ApplyCommand{ + Meta: Meta{ + ContextOpts: testCtxConfigWithShell(p, pr), + Ui: ui, + }, + } + + start := time.Now() + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + elapsed := time.Since(start).Seconds() + + // This test should take exactly one second, plus some minor amount of execution time. + if elapsed < 1 || elapsed > 1.2 { + t.Fatalf("bad: %f\n\n%s", elapsed, ui.ErrorWriter.String()) + } + +} + func TestApply_configInvalid(t *testing.T) { p := testProvider() ui := new(cli.MockUi) diff --git a/command/command_test.go b/command/command_test.go index 2544cf531844..2b9f93dd1d37 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -52,6 +52,21 @@ func testCtxConfig(p terraform.ResourceProvider) *terraform.ContextOpts { } } +func testCtxConfigWithShell(p terraform.ResourceProvider, pr terraform.ResourceProvisioner) *terraform.ContextOpts { + return &terraform.ContextOpts{ + Providers: map[string]terraform.ResourceProviderFactory{ + "test": func() (terraform.ResourceProvider, error) { + return p, nil + }, + }, + Provisioners: map[string]terraform.ResourceProvisionerFactory{ + "shell": func() (terraform.ResourceProvisioner, error) { + return pr, nil + }, + }, + } +} + func testModule(t *testing.T, name string) *module.Tree { mod, err := module.NewTreeModule("", filepath.Join(fixtureDir, name)) if err != nil { diff --git a/command/meta.go b/command/meta.go index 7237a8d617d7..f8f925447a9e 100644 --- a/command/meta.go +++ b/command/meta.go @@ -59,9 +59,13 @@ type Meta struct { // // backupPath is used to backup the state file before writing a modified // version. It defaults to stateOutPath + DefaultBackupExtention + // + // parallelism is used to control the number of concurrent operations + // allowed when walking the graph statePath string stateOutPath string backupPath string + parallelism int } // initStatePaths is used to initialize the default values for @@ -151,6 +155,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { } opts.Module = mod + opts.Parallelism = copts.Parallelism opts.State = state.State() ctx := terraform.NewContext(opts) return ctx, false, nil @@ -429,4 +434,7 @@ type contextOpts struct { // Set to true when running a destroy plan/apply. Destroy bool + + // Number of concurrent operations allowed + Parallelism int } diff --git a/command/plan.go b/command/plan.go index 083373c3aebb..34f1d7ecdb27 100644 --- a/command/plan.go +++ b/command/plan.go @@ -27,6 +27,7 @@ func (c *PlanCommand) Run(args []string) int { cmdFlags.BoolVar(&refresh, "refresh", true, "refresh") c.addModuleDepthFlag(cmdFlags, &moduleDepth) cmdFlags.StringVar(&outPath, "out", "", "path") + cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.BoolVar(&detailed, "detailed-exitcode", false, "detailed-exitcode") @@ -54,9 +55,10 @@ func (c *PlanCommand) Run(args []string) int { } ctx, _, err := c.Context(contextOpts{ - Destroy: destroy, - Path: path, - StatePath: c.Meta.statePath, + Destroy: destroy, + Path: path, + StatePath: c.Meta.statePath, + Parallelism: c.Meta.parallelism, }) if err != nil { c.Ui.Error(err.Error()) diff --git a/command/refresh.go b/command/refresh.go index 32e7950474fa..37bbc754830d 100644 --- a/command/refresh.go +++ b/command/refresh.go @@ -18,6 +18,7 @@ func (c *RefreshCommand) Run(args []string) int { cmdFlags := c.Meta.flagSet("refresh") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.IntVar(&c.Meta.parallelism, "parallelism", 0, "parallelism") cmdFlags.StringVar(&c.Meta.stateOutPath, "state-out", "", "path") cmdFlags.StringVar(&c.Meta.backupPath, "backup", "", "path") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } @@ -78,8 +79,9 @@ func (c *RefreshCommand) Run(args []string) int { // Build the context based on the arguments given ctx, _, err := c.Context(contextOpts{ - Path: configPath, - StatePath: c.Meta.statePath, + Path: configPath, + StatePath: c.Meta.statePath, + Parallelism: c.Meta.parallelism, }) if err != nil { c.Ui.Error(err.Error()) diff --git a/command/test-fixtures/parallelism/main.tf b/command/test-fixtures/parallelism/main.tf new file mode 100644 index 000000000000..7708209c178a --- /dev/null +++ b/command/test-fixtures/parallelism/main.tf @@ -0,0 +1,13 @@ +resource "test_instance" "foo1" { + ami = "bar" + + // shell has been configured to sleep for one second + provisioner "shell" {} +} + +resource "test_instance" "foo2" { + ami = "bar" + + // shell has been configured to sleep for one second + provisioner "shell" {} +}