diff --git a/command/apply.go b/command/apply.go index f924c65ca..232b44b82 100644 --- a/command/apply.go +++ b/command/apply.go @@ -39,6 +39,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") @@ -94,9 +95,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()) @@ -278,6 +280,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 052bd592c..c5379c4f3 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 2544cf531..2b9f93dd1 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 4c1c09afe..af4a52302 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 + DefaultBackupExtension + // + // 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 @@ -430,4 +435,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 15c2b505f..e1b43babe 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") @@ -57,9 +58,10 @@ func (c *PlanCommand) Run(args []string) int { c.Meta.extraHooks = []terraform.Hook{countHook} 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 ee3cd7007..99190bf87 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 000000000..7708209c1 --- /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" {} +}