Merge pull request #7501 from wandel/limit-parallel
limit number of builds running in parallel & test BuildCommand more
This commit is contained in:
commit
7e91a3c8ea
|
@ -27,7 +27,7 @@ build_script:
|
||||||
- git rev-parse HEAD
|
- git rev-parse HEAD
|
||||||
# go test $(go list ./... | grep -v vendor)
|
# go test $(go list ./... | grep -v vendor)
|
||||||
- ps: |
|
- ps: |
|
||||||
go.exe test -v -timeout=2m (go.exe list ./... `
|
go.exe test -timeout=2m (go.exe list ./... `
|
||||||
|? { -not $_.Contains('/vendor/') } `
|
|? { -not $_.Contains('/vendor/') } `
|
||||||
|? { $_ -ne 'github.com/hashicorp/packer/builder/parallels/common' } `
|
|? { $_ -ne 'github.com/hashicorp/packer/builder/parallels/common' } `
|
||||||
|? { $_ -ne 'github.com/hashicorp/packer/provisioner/ansible' })
|
|? { $_ -ne 'github.com/hashicorp/packer/provisioner/ansible' })
|
||||||
|
|
173
command/build.go
173
command/build.go
|
@ -5,6 +5,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -15,6 +16,7 @@ import (
|
||||||
"github.com/hashicorp/packer/helper/enumflag"
|
"github.com/hashicorp/packer/helper/enumflag"
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
"github.com/hashicorp/packer/template"
|
"github.com/hashicorp/packer/template"
|
||||||
|
"golang.org/x/sync/semaphore"
|
||||||
|
|
||||||
"github.com/posener/complete"
|
"github.com/posener/complete"
|
||||||
)
|
)
|
||||||
|
@ -24,31 +26,82 @@ type BuildCommand struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *BuildCommand) Run(args []string) int {
|
func (c *BuildCommand) Run(args []string) int {
|
||||||
var cfgColor, cfgDebug, cfgForce, cfgTimestamp, cfgParallel bool
|
buildCtx, cancelBuildCtx := context.WithCancel(context.Background())
|
||||||
var cfgOnError string
|
// Handle interrupts for this build
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer func() {
|
||||||
|
cancelBuildCtx()
|
||||||
|
signal.Stop(sigCh)
|
||||||
|
close(sigCh)
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case sig := <-sigCh:
|
||||||
|
if sig == nil {
|
||||||
|
// context got cancelled and this closed chan probably
|
||||||
|
// triggered first
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Ui.Error(fmt.Sprintf("Cancelling build after receiving %s", sig))
|
||||||
|
cancelBuildCtx()
|
||||||
|
case <-buildCtx.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c.RunContext(buildCtx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Color, Debug, Force, Timestamp bool
|
||||||
|
ParallelBuilds int64
|
||||||
|
OnError string
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BuildCommand) ParseArgs(args []string) (Config, int) {
|
||||||
|
var cfg Config
|
||||||
|
var parallel bool
|
||||||
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
|
flags := c.Meta.FlagSet("build", FlagSetBuildFilter|FlagSetVars)
|
||||||
flags.Usage = func() { c.Ui.Say(c.Help()) }
|
flags.Usage = func() { c.Ui.Say(c.Help()) }
|
||||||
flags.BoolVar(&cfgColor, "color", true, "")
|
flags.BoolVar(&cfg.Color, "color", true, "")
|
||||||
flags.BoolVar(&cfgDebug, "debug", false, "")
|
flags.BoolVar(&cfg.Debug, "debug", false, "")
|
||||||
flags.BoolVar(&cfgForce, "force", false, "")
|
flags.BoolVar(&cfg.Force, "force", false, "")
|
||||||
flags.BoolVar(&cfgTimestamp, "timestamp-ui", false, "")
|
flags.BoolVar(&cfg.Timestamp, "timestamp-ui", false, "")
|
||||||
flagOnError := enumflag.New(&cfgOnError, "cleanup", "abort", "ask")
|
flagOnError := enumflag.New(&cfg.OnError, "cleanup", "abort", "ask")
|
||||||
flags.Var(flagOnError, "on-error", "")
|
flags.Var(flagOnError, "on-error", "")
|
||||||
flags.BoolVar(&cfgParallel, "parallel", true, "")
|
flags.BoolVar(¶llel, "parallel", true, "")
|
||||||
|
flags.Int64Var(&cfg.ParallelBuilds, "parallel-builds", 0, "")
|
||||||
if err := flags.Parse(args); err != nil {
|
if err := flags.Parse(args); err != nil {
|
||||||
return 1
|
return cfg, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if parallel == false && cfg.ParallelBuilds == 0 {
|
||||||
|
cfg.ParallelBuilds = 1
|
||||||
|
}
|
||||||
|
if cfg.ParallelBuilds < 1 {
|
||||||
|
cfg.ParallelBuilds = math.MaxInt64
|
||||||
}
|
}
|
||||||
|
|
||||||
args = flags.Args()
|
args = flags.Args()
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
flags.Usage()
|
flags.Usage()
|
||||||
return 1
|
return cfg, 1
|
||||||
|
}
|
||||||
|
cfg.Path = args[0]
|
||||||
|
return cfg, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BuildCommand) RunContext(buildCtx context.Context, args []string) int {
|
||||||
|
cfg, ret := c.ParseArgs(args)
|
||||||
|
if ret != 0 {
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the template
|
// Parse the template
|
||||||
var tpl *template.Template
|
var tpl *template.Template
|
||||||
var err error
|
var err error
|
||||||
tpl, err = template.ParseFile(args[0])
|
tpl, err = template.ParseFile(cfg.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||||
return 1
|
return 1
|
||||||
|
@ -76,7 +129,7 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
builds = append(builds, b)
|
builds = append(builds, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfgDebug {
|
if cfg.Debug {
|
||||||
c.Ui.Say("Debug mode enabled. Builds will not be parallelized.")
|
c.Ui.Say("Debug mode enabled. Builds will not be parallelized.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +145,7 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
for i, b := range buildNames {
|
for i, b := range buildNames {
|
||||||
var ui packer.Ui
|
var ui packer.Ui
|
||||||
ui = c.Ui
|
ui = c.Ui
|
||||||
if cfgColor {
|
if cfg.Color {
|
||||||
ui = &packer.ColoredUi{
|
ui = &packer.ColoredUi{
|
||||||
Color: colors[i%len(colors)],
|
Color: colors[i%len(colors)],
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
|
@ -104,7 +157,7 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
c.Ui.Say("")
|
c.Ui.Say("")
|
||||||
}
|
}
|
||||||
// Now add timestamps if requested
|
// Now add timestamps if requested
|
||||||
if cfgTimestamp {
|
if cfg.Timestamp {
|
||||||
ui = &packer.TimestampedUi{
|
ui = &packer.TimestampedUi{
|
||||||
Ui: ui,
|
Ui: ui,
|
||||||
}
|
}
|
||||||
|
@ -115,16 +168,16 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
buildUis[b] = ui
|
buildUis[b] = ui
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Build debug mode: %v", cfgDebug)
|
log.Printf("Build debug mode: %v", cfg.Debug)
|
||||||
log.Printf("Force build: %v", cfgForce)
|
log.Printf("Force build: %v", cfg.Force)
|
||||||
log.Printf("On error: %v", cfgOnError)
|
log.Printf("On error: %v", cfg.OnError)
|
||||||
|
|
||||||
// Set the debug and force mode and prepare all the builds
|
// Set the debug and force mode and prepare all the builds
|
||||||
for _, b := range builds {
|
for _, b := range builds {
|
||||||
log.Printf("Preparing build: %s", b.Name())
|
log.Printf("Preparing build: %s", b.Name())
|
||||||
b.SetDebug(cfgDebug)
|
b.SetDebug(cfg.Debug)
|
||||||
b.SetForce(cfgForce)
|
b.SetForce(cfg.Force)
|
||||||
b.SetOnError(cfgOnError)
|
b.SetOnError(cfg.OnError)
|
||||||
|
|
||||||
warnings, err := b.Prepare()
|
warnings, err := b.Prepare()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -142,68 +195,68 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run all the builds in parallel and wait for them to complete
|
// Run all the builds in parallel and wait for them to complete
|
||||||
var interruptWg, wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
interrupted := false
|
|
||||||
var artifacts = struct {
|
var artifacts = struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
m map[string][]packer.Artifact
|
m map[string][]packer.Artifact
|
||||||
}{m: make(map[string][]packer.Artifact)}
|
}{m: make(map[string][]packer.Artifact)}
|
||||||
errors := make(map[string]error)
|
var errors = struct {
|
||||||
// ctx := context.Background()
|
sync.RWMutex
|
||||||
for _, b := range builds {
|
m map[string]error
|
||||||
|
}{m: make(map[string]error)}
|
||||||
|
|
||||||
|
limitParallel := semaphore.NewWeighted(cfg.ParallelBuilds)
|
||||||
|
for i := range builds {
|
||||||
|
if err := buildCtx.Err(); err != nil {
|
||||||
|
log.Println("Interrupted, not going to start any more builds.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
b := builds[i]
|
||||||
|
name := b.Name()
|
||||||
|
ui := buildUis[name]
|
||||||
|
if err := limitParallel.Acquire(buildCtx, 1); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Build '%s' failed to acquire semaphore: %s", name, err))
|
||||||
|
errors.Lock()
|
||||||
|
errors.m[name] = err
|
||||||
|
errors.Unlock()
|
||||||
|
break
|
||||||
|
}
|
||||||
// Increment the waitgroup so we wait for this item to finish properly
|
// Increment the waitgroup so we wait for this item to finish properly
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
buildCtx, cancelCtx := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
// Handle interrupts for this build
|
|
||||||
sigCh := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
|
|
||||||
defer signal.Stop(sigCh)
|
|
||||||
go func(b packer.Build) {
|
|
||||||
sig := <-sigCh
|
|
||||||
interruptWg.Add(1)
|
|
||||||
defer interruptWg.Done()
|
|
||||||
interrupted = true
|
|
||||||
|
|
||||||
log.Printf("Stopping build: %s after receiving %s", b.Name(), sig)
|
|
||||||
cancelCtx()
|
|
||||||
log.Printf("Build cancelled: %s", b.Name())
|
|
||||||
}(b)
|
|
||||||
|
|
||||||
// Run the build in a goroutine
|
// Run the build in a goroutine
|
||||||
go func(b packer.Build) {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
name := b.Name()
|
defer limitParallel.Release(1)
|
||||||
|
|
||||||
log.Printf("Starting build run: %s", name)
|
log.Printf("Starting build run: %s", name)
|
||||||
ui := buildUis[name]
|
|
||||||
runArtifacts, err := b.Run(buildCtx, ui)
|
runArtifacts, err := b.Run(buildCtx, ui)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ui.Error(fmt.Sprintf("Build '%s' errored: %s", name, err))
|
ui.Error(fmt.Sprintf("Build '%s' errored: %s", name, err))
|
||||||
errors[name] = err
|
errors.Lock()
|
||||||
|
errors.m[name] = err
|
||||||
|
errors.Unlock()
|
||||||
} else {
|
} else {
|
||||||
ui.Say(fmt.Sprintf("Build '%s' finished.", name))
|
ui.Say(fmt.Sprintf("Build '%s' finished.", name))
|
||||||
artifacts.Lock()
|
artifacts.Lock()
|
||||||
artifacts.m[name] = runArtifacts
|
artifacts.m[name] = runArtifacts
|
||||||
artifacts.Unlock()
|
artifacts.Unlock()
|
||||||
}
|
}
|
||||||
}(b)
|
}()
|
||||||
|
|
||||||
if cfgDebug {
|
if cfg.Debug {
|
||||||
log.Printf("Debug enabled, so waiting for build to finish: %s", b.Name())
|
log.Printf("Debug enabled, so waiting for build to finish: %s", b.Name())
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfgParallel {
|
if cfg.ParallelBuilds == 1 {
|
||||||
log.Printf("Parallelization disabled, waiting for build to finish: %s", b.Name())
|
log.Printf("Parallelization disabled, waiting for build to finish: %s", b.Name())
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
if interrupted {
|
|
||||||
log.Println("Interrupted, not going to start any more builds.")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for both the builds to complete and the interrupt handler,
|
// Wait for both the builds to complete and the interrupt handler,
|
||||||
|
@ -211,19 +264,16 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
log.Printf("Waiting on builds to complete...")
|
log.Printf("Waiting on builds to complete...")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
log.Printf("Builds completed. Waiting on interrupt barrier...")
|
if err := buildCtx.Err(); err != nil {
|
||||||
interruptWg.Wait()
|
|
||||||
|
|
||||||
if interrupted {
|
|
||||||
c.Ui.Say("Cleanly cancelled builds after being interrupted.")
|
c.Ui.Say("Cleanly cancelled builds after being interrupted.")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errors) > 0 {
|
if len(errors.m) > 0 {
|
||||||
c.Ui.Machine("error-count", strconv.FormatInt(int64(len(errors)), 10))
|
c.Ui.Machine("error-count", strconv.FormatInt(int64(len(errors.m)), 10))
|
||||||
|
|
||||||
c.Ui.Error("\n==> Some builds didn't complete successfully and had errors:")
|
c.Ui.Error("\n==> Some builds didn't complete successfully and had errors:")
|
||||||
for name, err := range errors {
|
for name, err := range errors.m {
|
||||||
// Create a UI for the machine readable stuff to be targeted
|
// Create a UI for the machine readable stuff to be targeted
|
||||||
ui := &packer.TargetedUI{
|
ui := &packer.TargetedUI{
|
||||||
Target: name,
|
Target: name,
|
||||||
|
@ -284,7 +334,7 @@ func (c *BuildCommand) Run(args []string) int {
|
||||||
c.Ui.Say("\n==> Builds finished but no artifacts were created.")
|
c.Ui.Say("\n==> Builds finished but no artifacts were created.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errors) > 0 {
|
if len(errors.m) > 0 {
|
||||||
// If any errors occurred, exit with a non-zero exit status
|
// If any errors occurred, exit with a non-zero exit status
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -308,7 +358,8 @@ Options:
|
||||||
-force Force a build to continue if artifacts exist, deletes existing artifacts.
|
-force Force a build to continue if artifacts exist, deletes existing artifacts.
|
||||||
-machine-readable Produce machine-readable output.
|
-machine-readable Produce machine-readable output.
|
||||||
-on-error=[cleanup|abort|ask] If the build fails do: clean up (default), abort, or ask.
|
-on-error=[cleanup|abort|ask] If the build fails do: clean up (default), abort, or ask.
|
||||||
-parallel=false Disable parallelization. (Default: parallel)
|
-parallel=false Disable parallelization. (Default: true)
|
||||||
|
-parallel-builds=1 Number of builds to run in parallel. 0 means no limit (Default: 0)
|
||||||
-timestamp-ui Enable prefixing of each ui output with an RFC3339 timestamp.
|
-timestamp-ui Enable prefixing of each ui output with an RFC3339 timestamp.
|
||||||
-var 'key=value' Variable for templates, can be used multiple times.
|
-var 'key=value' Variable for templates, can be used multiple times.
|
||||||
-var-file=path JSON file containing user variables.
|
-var-file=path JSON file containing user variables.
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildCommand_RunContext_CtxCancel(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
parallelPassingTests int
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"cancel 1 pending build - parallel=true",
|
||||||
|
[]string{"-parallel=true", filepath.Join(testFixture("parallel"), "1lock-5wg.json")},
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{"cancel in the middle with 2 pending builds - parallel=true",
|
||||||
|
[]string{"-parallel=true", filepath.Join(testFixture("parallel"), "2lock-4wg.json")},
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{"cancel 1 locked build - debug - parallel=true",
|
||||||
|
[]string{"-parallel=true", "-debug=true", filepath.Join(testFixture("parallel"), "1lock.json")},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{"cancel 2 locked builds - debug - parallel=true",
|
||||||
|
[]string{"-parallel=true", "-debug=true", filepath.Join(testFixture("parallel"), "2lock.json")},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{"cancel 1 locked build - debug - parallel=false",
|
||||||
|
[]string{"-parallel=false", "-debug=true", filepath.Join(testFixture("parallel"), "1lock.json")},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
{"cancel 2 locked builds - debug - parallel=false",
|
||||||
|
[]string{"-parallel=false", "-debug=true", filepath.Join(testFixture("parallel"), "2lock.json")},
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
b := NewParallelTestBuilder(tt.parallelPassingTests)
|
||||||
|
locked := &LockedBuilder{unlock: make(chan interface{})}
|
||||||
|
c := &BuildCommand{
|
||||||
|
Meta: testMetaParallel(t, b, locked),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||||
|
codeC := make(chan int)
|
||||||
|
go func() {
|
||||||
|
defer close(codeC)
|
||||||
|
codeC <- c.RunContext(ctx, tt.args)
|
||||||
|
}()
|
||||||
|
t.Logf("waiting for passing tests if any")
|
||||||
|
b.wg.Wait() // ran `tt.parallelPassingTests` times
|
||||||
|
t.Logf("cancelling context")
|
||||||
|
cancelCtx()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case code := <-codeC:
|
||||||
|
if code != tt.expected {
|
||||||
|
t.Logf("wrong code: %s", cmp.Diff(code, tt.expected))
|
||||||
|
fatalCommand(t, c.Meta)
|
||||||
|
}
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
t.Fatal("deadlock")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
package command
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer/builder/file"
|
||||||
|
"github.com/hashicorp/packer/packer"
|
||||||
|
"github.com/hashicorp/packer/provisioner/sleep"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewParallelTestBuilder will return a New ParallelTestBuilder that will
|
||||||
|
// unlock after `runs` builds
|
||||||
|
func NewParallelTestBuilder(runs int) *ParallelTestBuilder {
|
||||||
|
pb := &ParallelTestBuilder{}
|
||||||
|
pb.wg.Add(runs)
|
||||||
|
return pb
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ParallelTestBuilder's first run will lock
|
||||||
|
type ParallelTestBuilder struct {
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ParallelTestBuilder) Prepare(raws ...interface{}) ([]string, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (b *ParallelTestBuilder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||||
|
ui.Say("building")
|
||||||
|
b.wg.Done()
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockedBuilder wont run until unlock is called
|
||||||
|
type LockedBuilder struct{ unlock chan interface{} }
|
||||||
|
|
||||||
|
func (b *LockedBuilder) Prepare(raws ...interface{}) ([]string, error) { return nil, nil }
|
||||||
|
|
||||||
|
func (b *LockedBuilder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||||
|
ui.Say("locking build")
|
||||||
|
select {
|
||||||
|
case <-b.unlock:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMetaFile creates a Meta object that includes a file builder
|
||||||
|
func testMetaParallel(t *testing.T, builder *ParallelTestBuilder, locked *LockedBuilder) Meta {
|
||||||
|
var out, err bytes.Buffer
|
||||||
|
return Meta{
|
||||||
|
CoreConfig: &packer.CoreConfig{
|
||||||
|
Components: packer.ComponentFinder{
|
||||||
|
Builder: func(n string) (packer.Builder, error) {
|
||||||
|
switch n {
|
||||||
|
case "parallel-test":
|
||||||
|
return builder, nil
|
||||||
|
case "file":
|
||||||
|
return &file.Builder{}, nil
|
||||||
|
case "lock":
|
||||||
|
return locked, nil
|
||||||
|
default:
|
||||||
|
panic(n)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Provisioner: func(n string) (packer.Provisioner, error) {
|
||||||
|
switch n {
|
||||||
|
case "sleep":
|
||||||
|
return &sleep.Provisioner{}, nil
|
||||||
|
default:
|
||||||
|
panic(n)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ui: &packer.BasicUi{
|
||||||
|
Writer: &out,
|
||||||
|
ErrorWriter: &err,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildParallel_1(t *testing.T) {
|
||||||
|
// testfile has 6 builds, with first one locks 'forever', other builds
|
||||||
|
// should go through.
|
||||||
|
b := NewParallelTestBuilder(5)
|
||||||
|
locked := &LockedBuilder{unlock: make(chan interface{})}
|
||||||
|
|
||||||
|
c := &BuildCommand{
|
||||||
|
Meta: testMetaParallel(t, b, locked),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
fmt.Sprintf("-parallel=true"),
|
||||||
|
filepath.Join(testFixture("parallel"), "1lock-5wg.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := errgroup.Group{}
|
||||||
|
|
||||||
|
wg.Go(func() error {
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
fatalCommand(t, c.Meta)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
b.wg.Wait() // ran 5 times
|
||||||
|
close(locked.unlock) // unlock locking one
|
||||||
|
wg.Wait() // wait for termination
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildParallel_2(t *testing.T) {
|
||||||
|
// testfile has 6 builds, 2 of them lock 'forever', other builds
|
||||||
|
// should go through.
|
||||||
|
b := NewParallelTestBuilder(4)
|
||||||
|
locked := &LockedBuilder{unlock: make(chan interface{})}
|
||||||
|
|
||||||
|
c := &BuildCommand{
|
||||||
|
Meta: testMetaParallel(t, b, locked),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
fmt.Sprintf("-parallel-builds=3"),
|
||||||
|
filepath.Join(testFixture("parallel"), "2lock-4wg.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := errgroup.Group{}
|
||||||
|
|
||||||
|
wg.Go(func() error {
|
||||||
|
if code := c.Run(args); code != 0 {
|
||||||
|
fatalCommand(t, c.Meta)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
b.wg.Wait() // ran 4 times
|
||||||
|
close(locked.unlock) // unlock locking one
|
||||||
|
wg.Wait() // wait for termination
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildParallel_Timeout(t *testing.T) {
|
||||||
|
// testfile has 6 builds, 1 of them locks 'forever', one locks and times
|
||||||
|
// out other builds should go through.
|
||||||
|
b := NewParallelTestBuilder(4)
|
||||||
|
locked := &LockedBuilder{unlock: make(chan interface{})}
|
||||||
|
|
||||||
|
c := &BuildCommand{
|
||||||
|
Meta: testMetaParallel(t, b, locked),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
fmt.Sprintf("-parallel-builds=3"),
|
||||||
|
filepath.Join(testFixture("parallel"), "2lock-timeout.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := errgroup.Group{}
|
||||||
|
|
||||||
|
wg.Go(func() error {
|
||||||
|
if code := c.Run(args); code == 0 {
|
||||||
|
fatalCommand(t, c.Meta)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
b.wg.Wait() // ran 4 times
|
||||||
|
close(locked.unlock) // unlock locking one
|
||||||
|
wg.Wait() // wait for termination
|
||||||
|
}
|
|
@ -2,10 +2,13 @@ package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/packer/builder/file"
|
"github.com/hashicorp/packer/builder/file"
|
||||||
"github.com/hashicorp/packer/packer"
|
"github.com/hashicorp/packer/packer"
|
||||||
shell_local "github.com/hashicorp/packer/post-processor/shell-local"
|
shell_local "github.com/hashicorp/packer/post-processor/shell-local"
|
||||||
|
@ -210,3 +213,79 @@ func cleanup() {
|
||||||
os.RemoveAll("lilas.txt")
|
os.RemoveAll("lilas.txt")
|
||||||
os.RemoveAll("campanules.txt")
|
os.RemoveAll("campanules.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCommand_ParseArgs(t *testing.T) {
|
||||||
|
defaultMeta := testMetaFile(t)
|
||||||
|
type fields struct {
|
||||||
|
Meta Meta
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantCfg Config
|
||||||
|
wantExitCode int
|
||||||
|
}{
|
||||||
|
{fields{defaultMeta},
|
||||||
|
args{[]string{"file.json"}},
|
||||||
|
Config{
|
||||||
|
Path: "file.json",
|
||||||
|
ParallelBuilds: math.MaxInt64,
|
||||||
|
Color: true,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{fields{defaultMeta},
|
||||||
|
args{[]string{"-parallel=true", "file.json"}},
|
||||||
|
Config{
|
||||||
|
Path: "file.json",
|
||||||
|
ParallelBuilds: math.MaxInt64,
|
||||||
|
Color: true,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{fields{defaultMeta},
|
||||||
|
args{[]string{"-parallel=false", "file.json"}},
|
||||||
|
Config{
|
||||||
|
Path: "file.json",
|
||||||
|
ParallelBuilds: 1,
|
||||||
|
Color: true,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{fields{defaultMeta},
|
||||||
|
args{[]string{"-parallel-builds=5", "file.json"}},
|
||||||
|
Config{
|
||||||
|
Path: "file.json",
|
||||||
|
ParallelBuilds: 5,
|
||||||
|
Color: true,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
{fields{defaultMeta},
|
||||||
|
args{[]string{"-parallel=false", "-parallel-builds=5", "otherfile.json"}},
|
||||||
|
Config{
|
||||||
|
Path: "otherfile.json",
|
||||||
|
ParallelBuilds: 5,
|
||||||
|
Color: true,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%s", tt.args.args), func(t *testing.T) {
|
||||||
|
c := &BuildCommand{
|
||||||
|
Meta: tt.fields.Meta,
|
||||||
|
}
|
||||||
|
gotCfg, gotExitCode := c.ParseArgs(tt.args.args)
|
||||||
|
if diff := cmp.Diff(gotCfg, tt.wantCfg); diff != "" {
|
||||||
|
t.Fatalf("BuildCommand.ParseArgs() unexpected cfg %s", diff)
|
||||||
|
}
|
||||||
|
if gotExitCode != tt.wantExitCode {
|
||||||
|
t.Fatalf("BuildCommand.ParseArgs() gotExitCode = %v, want %v", gotExitCode, tt.wantExitCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{"type": "lock", "name": "build0"},
|
||||||
|
{"type": "parallel-test", "name": "build1"},
|
||||||
|
{"type": "parallel-test", "name": "build2"},
|
||||||
|
{"type": "parallel-test", "name": "build3"},
|
||||||
|
{"type": "parallel-test", "name": "build4"},
|
||||||
|
{"type": "parallel-test", "name": "build5"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{"type": "lock", "name": "build0"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{"type": "lock", "name": "build0"},
|
||||||
|
{"type": "parallel-test", "name": "build1"},
|
||||||
|
{"type": "parallel-test", "name": "build2"},
|
||||||
|
{"type": "lock", "name": "build3"},
|
||||||
|
{"type": "parallel-test", "name": "build4"},
|
||||||
|
{"type": "parallel-test", "name": "build5"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{"type": "lock", "name": "build0"},
|
||||||
|
{"type": "parallel-test", "name": "build1"},
|
||||||
|
{"type": "parallel-test", "name": "build2"},
|
||||||
|
{"type": "file", "name": "timeout-build", "target": "roses.txt"},
|
||||||
|
{"type": "parallel-test", "name": "build4"},
|
||||||
|
{"type": "parallel-test", "name": "build5"}
|
||||||
|
],
|
||||||
|
"provisioners": [
|
||||||
|
{
|
||||||
|
"only": ["timeout-build"],
|
||||||
|
"type": "sleep",
|
||||||
|
"duration": "2m",
|
||||||
|
|
||||||
|
"timeout": "1ns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"builders": [
|
||||||
|
{"type": "lock", "name": "build0"},
|
||||||
|
{"type": "lock", "name": "build1"}
|
||||||
|
]
|
||||||
|
}
|
|
@ -17,7 +17,8 @@ _packer () {
|
||||||
'-except=[(foo,bar,baz) Run all builds and post-procesors other than these.]'
|
'-except=[(foo,bar,baz) Run all builds and post-procesors other than these.]'
|
||||||
'-on-error=[(cleanup,abort,ask) If the build fails do: clean up (default), abort, or ask.]'
|
'-on-error=[(cleanup,abort,ask) If the build fails do: clean up (default), abort, or ask.]'
|
||||||
'-only=[(foo,bar,baz) Only build the given builds by name.]'
|
'-only=[(foo,bar,baz) Only build the given builds by name.]'
|
||||||
'-parallel=[(false) Disable parallelization. (Default: parallel)]'
|
'-parallel=[(false) Disable parallelization. (Default: false)]'
|
||||||
|
'-parallel-builds=[(0) Number of builds to run in parallel. (Defaults to infinite: 0)]'
|
||||||
'-var[("key=value") Variable for templates, can be used multiple times.]'
|
'-var[("key=value") Variable for templates, can be used multiple times.]'
|
||||||
'-var-file=[(path) JSON file containing user variables.]'
|
'-var-file=[(path) JSON file containing user variables.]'
|
||||||
'(-)*:files:_files -g "*.json"'
|
'(-)*:files:_files -g "*.json"'
|
||||||
|
|
|
@ -526,8 +526,8 @@ golang.org/x/oauth2/jwt
|
||||||
golang.org/x/oauth2/internal
|
golang.org/x/oauth2/internal
|
||||||
golang.org/x/oauth2/jws
|
golang.org/x/oauth2/jws
|
||||||
# golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
# golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||||
golang.org/x/sync/errgroup
|
|
||||||
golang.org/x/sync/semaphore
|
golang.org/x/sync/semaphore
|
||||||
|
golang.org/x/sync/errgroup
|
||||||
# golang.org/x/sys v0.0.0-20190425145619-16072639606e
|
# golang.org/x/sys v0.0.0-20190425145619-16072639606e
|
||||||
golang.org/x/sys/unix
|
golang.org/x/sys/unix
|
||||||
golang.org/x/sys/cpu
|
golang.org/x/sys/cpu
|
||||||
|
|
|
@ -52,8 +52,13 @@ artifacts that are created will be outputted at the end of the build.
|
||||||
attribute is specified within the configuration. `-only` does not apply to
|
attribute is specified within the configuration. `-only` does not apply to
|
||||||
post-processors.
|
post-processors.
|
||||||
|
|
||||||
- `-parallel=false` - Disable parallelization of multiple builders (on by
|
- `-parallel=false` - /!\ Deprecated, use `-parallel-builds=1` instead,
|
||||||
default).
|
setting `-parallel-builds=N` to more that 0 will ignore the `-parallel`
|
||||||
|
setting. Set `-parallel=false` to disable parallelization of multiple
|
||||||
|
builders (on by default).
|
||||||
|
|
||||||
|
- `-parallel-builds=N` - Limit the number of builds to run in parallel, 0
|
||||||
|
means no limit (defaults to 0).
|
||||||
|
|
||||||
- `-timestamp-ui` - Enable prefixing of each ui output with an RFC3339
|
- `-timestamp-ui` - Enable prefixing of each ui output with an RFC3339
|
||||||
timestamp.
|
timestamp.
|
||||||
|
|
Loading…
Reference in New Issue