From fb6d2754da7de8bcf7f5a556dae39b130872910d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 18:16:00 -0700 Subject: [PATCH 01/14] packer: Add Machine func to Ui --- packer/rpc/ui.go | 24 ++++++++++++++++++++++++ packer/rpc/ui_test.go | 9 +++++++++ packer/ui.go | 15 +++++++++++++++ provisioner/file/provisioner_test.go | 3 +++ 4 files changed, 51 insertions(+) diff --git a/packer/rpc/ui.go b/packer/rpc/ui.go index 6d69ca244..2a3807528 100644 --- a/packer/rpc/ui.go +++ b/packer/rpc/ui.go @@ -17,6 +17,12 @@ type UiServer struct { ui packer.Ui } +// The arguments sent to Ui.Machine +type UiMachineArgs struct { + category string + args []string +} + func (u *Ui) Ask(query string) (result string, err error) { err = u.client.Call("Ui.Ask", query, &result) return @@ -28,6 +34,17 @@ func (u *Ui) Error(message string) { } } +func (u *Ui) Machine(t string, args ...string) { + rpcArgs := &UiMachineArgs{ + category: t, + args: args, + } + + if err := u.client.Call("Ui.Message", rpcArgs, new(interface{})); err != nil { + panic(err) + } +} + func (u *Ui) Message(message string) { if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil { panic(err) @@ -52,6 +69,13 @@ func (u *UiServer) Error(message *string, reply *interface{}) error { return nil } +func (u *UiServer) Machine(args *UiMachineArgs, reply *interface{}) error { + u.ui.Machine(args.category, args.args...) + + *reply = nil + return nil +} + func (u *UiServer) Message(message *string, reply *interface{}) error { u.ui.Message(*message) *reply = nil diff --git a/packer/rpc/ui_test.go b/packer/rpc/ui_test.go index 78c203540..7dae40763 100644 --- a/packer/rpc/ui_test.go +++ b/packer/rpc/ui_test.go @@ -11,6 +11,9 @@ type testUi struct { askQuery string errorCalled bool errorMessage string + machineCalled bool + machineType string + machineArgs []string messageCalled bool messageMessage string sayCalled bool @@ -28,6 +31,12 @@ func (u *testUi) Error(message string) { u.errorMessage = message } +func (u *testUi) Machine(t string, args ...string) { + u.machineCalled = true + u.machineType = t + u.machineArgs = args +} + func (u *testUi) Message(message string) { u.messageCalled = true u.messageMessage = message diff --git a/packer/ui.go b/packer/ui.go index f19ebc04b..5d5ca1065 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -33,6 +33,7 @@ type Ui interface { Say(string) Message(string) Error(string) + Machine(string, ...string) } // ColoredUi is a UI that is colored using terminal colors. @@ -80,6 +81,11 @@ func (u *ColoredUi) Error(message string) { u.Ui.Error(u.colorize(message, color, true)) } +func (u *ColoredUi) Machine(t string, args ...string) { + // Don't colorize machine-readable output + u.Ui.Machine(t, args...) +} + func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string { if !u.supportsColors() { return message @@ -123,6 +129,11 @@ func (u *PrefixedUi) Error(message string) { u.Ui.Error(u.prefixLines(u.SayPrefix, message)) } +func (u *PrefixedUi) Machine(t string, args ...string) { + // Just pass it through for now. + u.Ui.Machine(t, args...) +} + func (u *PrefixedUi) prefixLines(prefix, message string) string { var result bytes.Buffer @@ -209,3 +220,7 @@ func (rw *ReaderWriterUi) Error(message string) { panic(err) } } + +func (rw *ReaderWriterUi) Machine(t string, args ...string) { + // TODO +} diff --git a/provisioner/file/provisioner_test.go b/provisioner/file/provisioner_test.go index 9c57ac452..c35f67cdb 100644 --- a/provisioner/file/provisioner_test.go +++ b/provisioner/file/provisioner_test.go @@ -106,6 +106,9 @@ func (su *stubUi) Ask(string) (string, error) { func (su *stubUi) Error(string) { } +func (su *stubUi) Machine(string, ...string) { +} + func (su *stubUi) Message(string) { } From 02edc7579b32af7517c7f9fc789577af87f92be8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 18:20:27 -0700 Subject: [PATCH 02/14] packer: Rename ReaderWriterUi to BasicUi --- command/build/command_test.go | 2 +- packer/communicator_test.go | 2 +- packer/environment.go | 2 +- packer/environment_test.go | 10 +++++----- packer/ui.go | 19 +++++++++++-------- packer/ui_test.go | 16 ++++++++-------- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/command/build/command_test.go b/command/build/command_test.go index 433f0ef46..33559cfc5 100644 --- a/command/build/command_test.go +++ b/command/build/command_test.go @@ -9,7 +9,7 @@ import ( func testEnvironment() packer.Environment { config := packer.DefaultEnvironmentConfig() - config.Ui = &packer.ReaderWriterUi{ + config.Ui = &packer.BasicUi{ Reader: new(bytes.Buffer), Writer: new(bytes.Buffer), } diff --git a/packer/communicator_test.go b/packer/communicator_test.go index d9f31c2bd..ac4c5b3e8 100644 --- a/packer/communicator_test.go +++ b/packer/communicator_test.go @@ -47,7 +47,7 @@ func TestRemoteCmd_StartWithUi(t *testing.T) { Stdout: rcOutput, } - testUi := &ReaderWriterUi{ + testUi := &BasicUi{ Reader: new(bytes.Buffer), Writer: uiOutput, } diff --git a/packer/environment.go b/packer/environment.go index b0ee2579f..267db5826 100644 --- a/packer/environment.go +++ b/packer/environment.go @@ -73,7 +73,7 @@ type EnvironmentConfig struct { func DefaultEnvironmentConfig() *EnvironmentConfig { config := &EnvironmentConfig{} config.Commands = make([]string, 0) - config.Ui = &ReaderWriterUi{ + config.Ui = &BasicUi{ Reader: os.Stdin, Writer: os.Stdout, } diff --git a/packer/environment_test.go b/packer/environment_test.go index 125058bf2..3a15edea7 100644 --- a/packer/environment_test.go +++ b/packer/environment_test.go @@ -19,7 +19,7 @@ func init() { func testEnvironment() Environment { config := DefaultEnvironmentConfig() - config.Ui = &ReaderWriterUi{ + config.Ui = &BasicUi{ Reader: new(bytes.Buffer), Writer: new(bytes.Buffer), } @@ -45,8 +45,8 @@ func TestEnvironment_DefaultConfig_Ui(t *testing.T) { config := DefaultEnvironmentConfig() assert.NotNil(config.Ui, "default UI should not be nil") - rwUi, ok := config.Ui.(*ReaderWriterUi) - assert.True(ok, "default UI should be ReaderWriterUi") + rwUi, ok := config.Ui.(*BasicUi) + assert.True(ok, "default UI should be BasicUi") assert.Equal(rwUi.Writer, os.Stdout, "default UI should go to stdout") assert.Equal(rwUi.Reader, os.Stdin, "default UI should read from stdin") } @@ -175,7 +175,7 @@ func TestEnvironment_DefaultCli_Help(t *testing.T) { // A little lambda to help us test the output actually contains help testOutput := func() { - buffer := defaultEnv.Ui().(*ReaderWriterUi).Writer.(*bytes.Buffer) + buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer) output := buffer.String() buffer.Reset() assert.True(strings.Contains(output, "usage: packer"), "should print help") @@ -341,7 +341,7 @@ func TestEnvironmentProvisioner_Error(t *testing.T) { func TestEnvironment_SettingUi(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) - ui := &ReaderWriterUi{ + ui := &BasicUi{ Reader: new(bytes.Buffer), Writer: new(bytes.Buffer), } diff --git a/packer/ui.go b/packer/ui.go index 5d5ca1065..d6c97f355 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -51,9 +51,12 @@ type PrefixedUi struct { Ui Ui } -// The ReaderWriterUi is a UI that writes and reads from standard Go -// io.Reader and io.Writer. -type ReaderWriterUi struct { +// The BasicUI is a UI that reads and writes from a standard Go reader +// and writer. It is safe to be called from multiple goroutines. The +// target for machine-readable output can be configured by prefixing the +// type of the machine readable output with the target and separating it +// with a comma. +type BasicUi struct { Reader io.Reader Writer io.Writer l sync.Mutex @@ -144,7 +147,7 @@ func (u *PrefixedUi) prefixLines(prefix, message string) string { return strings.TrimRightFunc(result.String(), unicode.IsSpace) } -func (rw *ReaderWriterUi) Ask(query string) (string, error) { +func (rw *BasicUi) Ask(query string) (string, error) { rw.l.Lock() defer rw.l.Unlock() @@ -188,7 +191,7 @@ func (rw *ReaderWriterUi) Ask(query string) (string, error) { } } -func (rw *ReaderWriterUi) Say(message string) { +func (rw *BasicUi) Say(message string) { rw.l.Lock() defer rw.l.Unlock() @@ -199,7 +202,7 @@ func (rw *ReaderWriterUi) Say(message string) { } } -func (rw *ReaderWriterUi) Message(message string) { +func (rw *BasicUi) Message(message string) { rw.l.Lock() defer rw.l.Unlock() @@ -210,7 +213,7 @@ func (rw *ReaderWriterUi) Message(message string) { } } -func (rw *ReaderWriterUi) Error(message string) { +func (rw *BasicUi) Error(message string) { rw.l.Lock() defer rw.l.Unlock() @@ -221,6 +224,6 @@ func (rw *ReaderWriterUi) Error(message string) { } } -func (rw *ReaderWriterUi) Machine(t string, args ...string) { +func (rw *BasicUi) Machine(t string, args ...string) { // TODO } diff --git a/packer/ui_test.go b/packer/ui_test.go index 6a48a80a3..b42255f15 100644 --- a/packer/ui_test.go +++ b/packer/ui_test.go @@ -6,8 +6,8 @@ import ( "testing" ) -func testUi() *ReaderWriterUi { - return &ReaderWriterUi{ +func testUi() *BasicUi { + return &BasicUi{ Reader: new(bytes.Buffer), Writer: new(bytes.Buffer), } @@ -71,15 +71,15 @@ func TestPrefixedUi_ImplUi(t *testing.T) { } } -func TestReaderWriterUi_ImplUi(t *testing.T) { +func TestBasicUi_ImplUi(t *testing.T) { var raw interface{} - raw = &ReaderWriterUi{} + raw = &BasicUi{} if _, ok := raw.(Ui); !ok { - t.Fatalf("ReaderWriterUi must implement Ui") + t.Fatalf("BasicUi must implement Ui") } } -func TestReaderWriterUi_Error(t *testing.T) { +func TestBasicUi_Error(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) bufferUi := testUi() @@ -91,7 +91,7 @@ func TestReaderWriterUi_Error(t *testing.T) { assert.Equal(readWriter(bufferUi), "5\n", "formatting") } -func TestReaderWriterUi_Say(t *testing.T) { +func TestBasicUi_Say(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) bufferUi := testUi() @@ -105,7 +105,7 @@ func TestReaderWriterUi_Say(t *testing.T) { // This reads the output from the bytes.Buffer in our test object // and then resets the buffer. -func readWriter(ui *ReaderWriterUi) (result string) { +func readWriter(ui *BasicUi) (result string) { buffer := ui.Writer.(*bytes.Buffer) result = buffer.String() buffer.Reset() From 03c10a9aa83882103efbf76f0265f3c71c2b7cce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 18:31:28 -0700 Subject: [PATCH 03/14] packer: renamed PrefixedUi to TargettedUi --- packer/build.go | 16 +++++++--------- packer/ui.go | 41 ++++++++++++++++++++++++----------------- packer/ui_test.go | 29 ++++++++++++++++------------- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/packer/build.go b/packer/build.go index bff34767e..39e54a559 100644 --- a/packer/build.go +++ b/packer/build.go @@ -213,11 +213,10 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) { hook := &DispatchHook{hooks} artifacts := make([]Artifact, 0, 1) - // The builder just has a normal Ui, but prefixed - builderUi := &PrefixedUi{ - fmt.Sprintf("==> %s", b.Name()), - fmt.Sprintf(" %s", b.Name()), - originalUi, + // The builder just has a normal Ui, but targetted + builderUi := &TargettedUi{ + Target: b.Name(), + Ui: originalUi, } log.Printf("Running builder: %s", b.builderType) @@ -240,10 +239,9 @@ PostProcessorRunSeqLoop: for _, ppSeq := range b.postProcessors { priorArtifact := builderArtifact for i, corePP := range ppSeq { - ppUi := &PrefixedUi{ - fmt.Sprintf("==> %s (%s)", b.Name(), corePP.processorType), - fmt.Sprintf(" %s (%s)", b.Name(), corePP.processorType), - originalUi, + ppUi := &TargettedUi{ + Target: fmt.Sprintf("%s (%s)", b.Name(), corePP.processorType), + Ui: originalUi, } builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType)) diff --git a/packer/ui.go b/packer/ui.go index d6c97f355..62d44a630 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -43,12 +43,14 @@ type ColoredUi struct { Ui Ui } -// PrefixedUi is a UI that wraps another UI implementation and adds a -// prefix to all the messages going out. -type PrefixedUi struct { - SayPrefix string - MessagePrefix string - Ui Ui +// TargettedUi is a UI that wraps another UI implementation and modifies +// the output to indicate a specific target. Specifically, all Say output +// is prefixed with the target name. Message output is not prefixed but +// is offset by the length of the target so that output is lined up properly +// with Say output. Machine-readable output has the proper target set. +type TargettedUi struct { + Target string + Ui Ui } // The BasicUI is a UI that reads and writes from a standard Go reader @@ -116,32 +118,37 @@ func (u *ColoredUi) supportsColors() bool { return cygwin } -func (u *PrefixedUi) Ask(query string) (string, error) { - return u.Ui.Ask(u.prefixLines(u.SayPrefix, query)) +func (u *TargettedUi) Ask(query string) (string, error) { + return u.Ui.Ask(u.prefixLines(true, query)) } -func (u *PrefixedUi) Say(message string) { - u.Ui.Say(u.prefixLines(u.SayPrefix, message)) +func (u *TargettedUi) Say(message string) { + u.Ui.Say(u.prefixLines(true, message)) } -func (u *PrefixedUi) Message(message string) { - u.Ui.Message(u.prefixLines(u.MessagePrefix, message)) +func (u *TargettedUi) Message(message string) { + u.Ui.Message(u.prefixLines(false, message)) } -func (u *PrefixedUi) Error(message string) { - u.Ui.Error(u.prefixLines(u.SayPrefix, message)) +func (u *TargettedUi) Error(message string) { + u.Ui.Error(u.prefixLines(true, message)) } -func (u *PrefixedUi) Machine(t string, args ...string) { +func (u *TargettedUi) Machine(t string, args ...string) { // Just pass it through for now. u.Ui.Machine(t, args...) } -func (u *PrefixedUi) prefixLines(prefix, message string) string { +func (u *TargettedUi) prefixLines(arrow bool, message string) string { + arrowText := "==>" + if !arrow { + arrowText = strings.Repeat(" ", len(arrowText)) + } + var result bytes.Buffer for _, line := range strings.Split(message, "\n") { - result.WriteString(fmt.Sprintf("%s: %s\n", prefix, line)) + result.WriteString(fmt.Sprintf("%s %s: %s\n", arrowText, u.Target, line)) } return strings.TrimRightFunc(result.String(), unicode.IsSpace) diff --git a/packer/ui_test.go b/packer/ui_test.go index b42255f15..13ed53a18 100644 --- a/packer/ui_test.go +++ b/packer/ui_test.go @@ -36,23 +36,26 @@ func TestColoredUi(t *testing.T) { } } -func TestPrefixedUi(t *testing.T) { +func TestTargettedUi(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) bufferUi := testUi() - prefixUi := &PrefixedUi{"mitchell", "bar", bufferUi} + TargettedUi := &TargettedUi{ + Target: "foo", + Ui: bufferUi, + } - prefixUi.Say("foo") - assert.Equal(readWriter(bufferUi), "mitchell: foo\n", "should have prefix") + TargettedUi.Say("foo") + assert.Equal(readWriter(bufferUi), "==> foo: foo\n", "should have prefix") - prefixUi.Message("foo") - assert.Equal(readWriter(bufferUi), "bar: foo\n", "should have prefix") + TargettedUi.Message("foo") + assert.Equal(readWriter(bufferUi), " foo: foo\n", "should have prefix") - prefixUi.Error("bar") - assert.Equal(readWriter(bufferUi), "mitchell: bar\n", "should have prefix") + TargettedUi.Error("bar") + assert.Equal(readWriter(bufferUi), "==> foo: bar\n", "should have prefix") - prefixUi.Say("foo\nbar") - assert.Equal(readWriter(bufferUi), "mitchell: foo\nmitchell: bar\n", "should multiline") + TargettedUi.Say("foo\nbar") + assert.Equal(readWriter(bufferUi), "==> foo: foo\n==> foo: bar\n", "should multiline") } func TestColoredUi_ImplUi(t *testing.T) { @@ -63,11 +66,11 @@ func TestColoredUi_ImplUi(t *testing.T) { } } -func TestPrefixedUi_ImplUi(t *testing.T) { +func TestTargettedUi_ImplUi(t *testing.T) { var raw interface{} - raw = &PrefixedUi{} + raw = &TargettedUi{} if _, ok := raw.(Ui); !ok { - t.Fatalf("PrefixedUi must implement Ui") + t.Fatalf("TargettedUi must implement Ui") } } From edc59499e7ace93e359683676eb829de1ab8c11a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 18:38:24 -0700 Subject: [PATCH 04/14] packer: BasicUi doesn't do machine-readable, just log --- packer/ui.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packer/ui.go b/packer/ui.go index 62d44a630..ef8dc5572 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -54,10 +54,8 @@ type TargettedUi struct { } // The BasicUI is a UI that reads and writes from a standard Go reader -// and writer. It is safe to be called from multiple goroutines. The -// target for machine-readable output can be configured by prefixing the -// type of the machine readable output with the target and separating it -// with a comma. +// and writer. It is safe to be called from multiple goroutines. Machine +// readable output is simply logged for this UI. type BasicUi struct { Reader io.Reader Writer io.Writer @@ -232,5 +230,5 @@ func (rw *BasicUi) Error(message string) { } func (rw *BasicUi) Machine(t string, args ...string) { - // TODO + log.Printf("machine readable: %s %#v", t, args) } From 89be12ae21ea3b843c69655dbc6c988276c943dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 19:05:07 -0700 Subject: [PATCH 05/14] packer: Add MachineReadableUi --- packer/ui.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ packer/ui_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/packer/ui.go b/packer/ui.go index ef8dc5572..b2b10293a 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -11,6 +11,7 @@ import ( "runtime" "strings" "sync" + "time" "unicode" ) @@ -63,6 +64,12 @@ type BasicUi struct { interrupted bool } +// MachineReadableUi is a UI that only outputs machine-readable output +// to the given Writer. +type MachineReadableUi struct { + Writer io.Writer +} + func (u *ColoredUi) Ask(query string) (string, error) { return u.Ui.Ask(u.colorize(query, u.Color, true)) } @@ -232,3 +239,42 @@ func (rw *BasicUi) Error(message string) { func (rw *BasicUi) Machine(t string, args ...string) { log.Printf("machine readable: %s %#v", t, args) } + +func (u *MachineReadableUi) Ask(query string) (string, error) { + return "", errors.New("machine-readable UI can't ask") +} + +func (u *MachineReadableUi) Say(message string) { + u.Machine("ui", "say", message) +} + +func (u *MachineReadableUi) Message(message string) { + u.Machine("ui", "message", message) +} + +func (u *MachineReadableUi) Error(message string) { + u.Machine("ui", "error", message) +} + +func (u *MachineReadableUi) Machine(category string, args ...string) { + now := time.Now().UTC() + + // Determine if we have a target, and set it + target := "" + commaIdx := strings.Index(category, ",") + if commaIdx > -1 { + target = category[0:commaIdx] + category = category[commaIdx+1:] + } + + // Prepare the args + for i, v := range args { + args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1) + } + argsString := strings.Join(args, ",") + + _, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s", now.Unix(), target, category, argsString) + if err != nil { + panic(err) + } +} diff --git a/packer/ui_test.go b/packer/ui_test.go index 13ed53a18..b5cb0f345 100644 --- a/packer/ui_test.go +++ b/packer/ui_test.go @@ -3,6 +3,7 @@ package packer import ( "bytes" "cgl.tideland.biz/asserts" + "strings" "testing" ) @@ -106,6 +107,44 @@ func TestBasicUi_Say(t *testing.T) { assert.Equal(readWriter(bufferUi), "5\n", "formatting") } +func TestMachineReadableUi_ImplUi(t *testing.T) { + var raw interface{} + raw = &MachineReadableUi{} + if _, ok := raw.(Ui); !ok { + t.Fatalf("MachineReadableUi must implement Ui") + } +} + +func TestMachineReadableUi(t *testing.T) { + var data, expected string + + buf := new(bytes.Buffer) + ui := &MachineReadableUi{Writer: buf} + + ui.Machine("foo", "bar", "baz") + data = strings.SplitN(buf.String(), ",", 2)[1] + expected = ",foo,bar,baz" + if data != expected { + t.Fatalf("bad: %s", data) + } + + buf.Reset() + ui.Machine("mitchellh,foo", "bar", "baz") + data = strings.SplitN(buf.String(), ",", 2)[1] + expected = "mitchellh,foo,bar,baz" + if data != expected { + t.Fatalf("bad: %s", data) + } + + buf.Reset() + ui.Machine("foo", "foo,bar") + data = strings.SplitN(buf.String(), ",", 2)[1] + expected = ",foo,foo%!(PACKER_COMMA)bar" + if data != expected { + t.Fatalf("bad: %s", data) + } +} + // This reads the output from the bytes.Buffer in our test object // and then resets the buffer. func readWriter(ui *BasicUi) (result string) { From aa143a330b5d6c0a99436da885954ec46ad70c41 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 19:08:08 -0700 Subject: [PATCH 06/14] packer: TargettedUi properly targets machine-readable data --- packer/ui.go | 4 ++-- packer/ui_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packer/ui.go b/packer/ui.go index b2b10293a..db521bfa1 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -140,8 +140,8 @@ func (u *TargettedUi) Error(message string) { } func (u *TargettedUi) Machine(t string, args ...string) { - // Just pass it through for now. - u.Ui.Machine(t, args...) + // Prefix in the target, then pass through + u.Ui.Machine(fmt.Sprintf("%s,%s", u.Target, t), args...) } func (u *TargettedUi) prefixLines(arrow bool, message string) string { diff --git a/packer/ui_test.go b/packer/ui_test.go index b5cb0f345..a9ce5dc26 100644 --- a/packer/ui_test.go +++ b/packer/ui_test.go @@ -41,21 +41,21 @@ func TestTargettedUi(t *testing.T) { assert := asserts.NewTestingAsserts(t, true) bufferUi := testUi() - TargettedUi := &TargettedUi{ + targettedUi := &TargettedUi{ Target: "foo", Ui: bufferUi, } - TargettedUi.Say("foo") + targettedUi.Say("foo") assert.Equal(readWriter(bufferUi), "==> foo: foo\n", "should have prefix") - TargettedUi.Message("foo") + targettedUi.Message("foo") assert.Equal(readWriter(bufferUi), " foo: foo\n", "should have prefix") - TargettedUi.Error("bar") + targettedUi.Error("bar") assert.Equal(readWriter(bufferUi), "==> foo: bar\n", "should have prefix") - TargettedUi.Say("foo\nbar") + targettedUi.Say("foo\nbar") assert.Equal(readWriter(bufferUi), "==> foo: foo\n==> foo: bar\n", "should multiline") } From 46f5a9b1f6785487f57edc4f29c3b4752a8c65f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:12:20 -0700 Subject: [PATCH 07/14] parse --machine-readable and set it --- packer.go | 29 ++++++++++++++++++++++++++++- packer_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/packer.go b/packer.go index e1049b231..aa53fe57d 100644 --- a/packer.go +++ b/packer.go @@ -76,8 +76,13 @@ func main() { log.Printf("Setting cache directory: %s", cacheDir) cache := &packer.FileCache{CacheDir: cacheDir} + // Determine if we're in machine-readable mode by mucking around with + // the arguments... + args, machineReadable := extractMachineReadable(os.Args[1:]) + defer plugin.CleanupClients() + // Create the environment configuration envConfig := packer.DefaultEnvironmentConfig() envConfig.Cache = cache envConfig.Commands = config.CommandNames() @@ -86,6 +91,11 @@ func main() { envConfig.Components.Hook = config.LoadHook envConfig.Components.PostProcessor = config.LoadPostProcessor envConfig.Components.Provisioner = config.LoadProvisioner + if machineReadable { + envConfig.Ui = &packer.MachineReadableUi{ + Writer: os.Stdout, + } + } env, err := packer.NewEnvironment(envConfig) if err != nil { @@ -96,7 +106,7 @@ func main() { setupSignalHandlers(env) - exitCode, err := env.Cli(os.Args[1:]) + exitCode, err := env.Cli(args) if err != nil { fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error()) plugin.CleanupClients() @@ -107,6 +117,23 @@ func main() { os.Exit(exitCode) } +// extractMachineReadable checks the args for the machine readable +// flag and returns whether or not it is on. It modifies the args +// to remove this flag. +func extractMachineReadable(args []string) ([]string, bool) { + for i, arg := range args { + if arg == "--machine-readable" { + // We found it. Slice it out. + result := make([]string, len(args)-1) + copy(result, args[:i]) + copy(result[i:], args[i+1:]) + return result, true + } + } + + return args, false +} + func loadConfig() (*config, error) { var config config if err := decodeConfig(bytes.NewBufferString(defaultConfig), &config); err != nil { diff --git a/packer_test.go b/packer_test.go index 06ab7d0f9..5a08f8431 100644 --- a/packer_test.go +++ b/packer_test.go @@ -1 +1,35 @@ package main + +import ( + "reflect" + "testing" +) + +func TestExtractMachineReadable(t *testing.T) { + var args, expected, result []string + var mr bool + + // Not + args = []string{"foo", "bar", "baz"} + result, mr = extractMachineReadable(args) + expected = []string{"foo", "bar", "baz"} + if !reflect.DeepEqual(result, expected) { + t.Fatalf("bad: %#v", result) + } + + if mr { + t.Fatal("should not be mr") + } + + // Yes + args = []string{"foo", "--machine-readable", "baz"} + result, mr = extractMachineReadable(args) + expected = []string{"foo", "baz"} + if !reflect.DeepEqual(result, expected) { + t.Fatalf("bad: %#v", result) + } + + if !mr { + t.Fatal("should be mr") + } +} From 7b4b5d23fc1426aae56a3aa063007866540d4e38 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:14:42 -0700 Subject: [PATCH 08/14] packer: output a newline after each MR line --- packer/ui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packer/ui.go b/packer/ui.go index db521bfa1..8abaffa55 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -273,7 +273,7 @@ func (u *MachineReadableUi) Machine(category string, args ...string) { } argsString := strings.Join(args, ",") - _, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s", now.Unix(), target, category, argsString) + _, err := fmt.Fprintf(u.Writer, "%d,%s,%s,%s\n", now.Unix(), target, category, argsString) if err != nil { panic(err) } From ba3344a32253d830ff3b42df8d37f64912233dc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:18:14 -0700 Subject: [PATCH 09/14] packer: replace new lines with literal \n --- packer/ui.go | 1 + packer/ui_test.go | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packer/ui.go b/packer/ui.go index 8abaffa55..8a869aab6 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -270,6 +270,7 @@ func (u *MachineReadableUi) Machine(category string, args ...string) { // Prepare the args for i, v := range args { args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1) + args[i] = strings.Replace(args[i], "\n", "\\n", -1) } argsString := strings.Join(args, ",") diff --git a/packer/ui_test.go b/packer/ui_test.go index a9ce5dc26..87091f189 100644 --- a/packer/ui_test.go +++ b/packer/ui_test.go @@ -121,28 +121,40 @@ func TestMachineReadableUi(t *testing.T) { buf := new(bytes.Buffer) ui := &MachineReadableUi{Writer: buf} + // No target ui.Machine("foo", "bar", "baz") data = strings.SplitN(buf.String(), ",", 2)[1] - expected = ",foo,bar,baz" + expected = ",foo,bar,baz\n" if data != expected { t.Fatalf("bad: %s", data) } + // Target buf.Reset() ui.Machine("mitchellh,foo", "bar", "baz") data = strings.SplitN(buf.String(), ",", 2)[1] - expected = "mitchellh,foo,bar,baz" + expected = "mitchellh,foo,bar,baz\n" if data != expected { t.Fatalf("bad: %s", data) } + // Commas buf.Reset() ui.Machine("foo", "foo,bar") data = strings.SplitN(buf.String(), ",", 2)[1] - expected = ",foo,foo%!(PACKER_COMMA)bar" + expected = ",foo,foo%!(PACKER_COMMA)bar\n" if data != expected { t.Fatalf("bad: %s", data) } + + // New lines + buf.Reset() + ui.Machine("foo", "foo\n") + data = strings.SplitN(buf.String(), ",", 2)[1] + expected = ",foo,foo\\n\n" + if data != expected { + t.Fatalf("bad: %#v", data) + } } // This reads the output from the bytes.Buffer in our test object From c77d26e46c1ad6e45bd2be0bf42f260559b57666 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:19:13 -0700 Subject: [PATCH 10/14] packer: replace \r with literal on Ui for MR --- packer/ui.go | 1 + 1 file changed, 1 insertion(+) diff --git a/packer/ui.go b/packer/ui.go index 8a869aab6..3d82b281a 100644 --- a/packer/ui.go +++ b/packer/ui.go @@ -270,6 +270,7 @@ func (u *MachineReadableUi) Machine(category string, args ...string) { // Prepare the args for i, v := range args { args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1) + args[i] = strings.Replace(args[i], "\r", "\\r", -1) args[i] = strings.Replace(args[i], "\n", "\\n", -1) } argsString := strings.Join(args, ",") From b3e8a419541eced84ca003a69ab36a1dd98b734a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:24:57 -0700 Subject: [PATCH 11/14] packer: document machine-readable --- packer/environment.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packer/environment.go b/packer/environment.go index 267db5826..4a14b48ed 100644 --- a/packer/environment.go +++ b/packer/environment.go @@ -299,6 +299,9 @@ func (e *coreEnvironment) printHelp() { // Output the command and the synopsis e.ui.Say(fmt.Sprintf(" %v %v", key, synopsis)) } + + e.ui.Say("\nGlobally recognized options:") + e.ui.Say(" --machine-readable Machine-readable output format.") } // Returns the UI for the environment. The UI is the interface that should From a675b89b0ec530e76f976ba9b8311201e4106da2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:26:24 -0700 Subject: [PATCH 12/14] packer: -machine-readable not double hyphen --- command/build/help.go | 1 + packer.go | 2 +- packer/environment.go | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/command/build/help.go b/command/build/help.go index 07c247c23..65ec5eb65 100644 --- a/command/build/help.go +++ b/command/build/help.go @@ -10,6 +10,7 @@ Options: -debug Debug mode enabled for builds -force Force a build to continue if artifacts exist, deletes existing artifacts + -machine-readable Machine-readable output -except=foo,bar,baz Build all builds other than these -only=foo,bar,baz Only build the given builds by name -var 'key=value' Variable for templates, can be used multiple times. diff --git a/packer.go b/packer.go index aa53fe57d..c14d778f5 100644 --- a/packer.go +++ b/packer.go @@ -122,7 +122,7 @@ func main() { // to remove this flag. func extractMachineReadable(args []string) ([]string, bool) { for i, arg := range args { - if arg == "--machine-readable" { + if arg == "-machine-readable" { // We found it. Slice it out. result := make([]string, len(args)-1) copy(result, args[:i]) diff --git a/packer/environment.go b/packer/environment.go index 4a14b48ed..1ef6c5c2a 100644 --- a/packer/environment.go +++ b/packer/environment.go @@ -301,7 +301,7 @@ func (e *coreEnvironment) printHelp() { } e.ui.Say("\nGlobally recognized options:") - e.ui.Say(" --machine-readable Machine-readable output format.") + e.ui.Say(" -machine-readable Machine-readable output format.") } // Returns the UI for the environment. The UI is the interface that should From ec31453820c0df58dff233dc7b79c757ea3d5b70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:56:39 -0700 Subject: [PATCH 13/14] website: document machine-readable --- .../docs/command-line/build.html.markdown | 3 +- .../docs/command-line/fix.html.markdown | 2 +- .../machine-readable.html.markdown | 83 +++++++++++++++++++ .../docs/command-line/validate.html.markdown | 1 + website/source/layouts/docs.erb | 1 + 5 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 website/source/docs/command-line/machine-readable.html.markdown diff --git a/website/source/docs/command-line/build.html.markdown b/website/source/docs/command-line/build.html.markdown index 09a271af8..198bd85b4 100644 --- a/website/source/docs/command-line/build.html.markdown +++ b/website/source/docs/command-line/build.html.markdown @@ -1,5 +1,6 @@ --- layout: "docs" +page_title: "Build - Command-Line" --- # Command-Line: Build @@ -22,7 +23,7 @@ artifacts that are created will be outputted at the end of the build. In general, a builder supporting the forced build will remove the artifacts from the previous build. This will allow the user to repeat a build without having to manually clean these artifacts beforehand. - + * `-except=foo,bar,baz` - Builds all the builds except those with the given comma-separated names. Build names by default are the names of their builders, unless a specific `name` attribute is specified within the configuration. diff --git a/website/source/docs/command-line/fix.html.markdown b/website/source/docs/command-line/fix.html.markdown index 614777d4c..fa96f394c 100644 --- a/website/source/docs/command-line/fix.html.markdown +++ b/website/source/docs/command-line/fix.html.markdown @@ -1,6 +1,6 @@ --- layout: "docs" -page_title: "Command-line: Fix" +page_title: "Fix - Command-Line" --- # Command-Line: Fix diff --git a/website/source/docs/command-line/machine-readable.html.markdown b/website/source/docs/command-line/machine-readable.html.markdown new file mode 100644 index 000000000..b5f5b5b71 --- /dev/null +++ b/website/source/docs/command-line/machine-readable.html.markdown @@ -0,0 +1,83 @@ +--- +layout: "docs" +page_title: "Machine-Readable Output - Command-Line" +--- + +# Machine-Readable Output + +By default, the output of Packer is very human-readable. It uses nice +formatting, spacing, and colors in order to make Packer a pleasure to use. +However, Packer was built with automation in mind. To that end, Packer +supports a fully machine-readable output setting, allowing you to use +Packer in automated environments. + +The machine-readable output format is easy to use and read and was made +with Unix tools in mind, so it is awk/sed/grep/etc. friendly. + +## Enabling + +The machine-readable output format can be enabled by passing the +`-machine-readable` flag to any Packer command. This immediately enables +all output to become machine-readable on stdout. Logging, if enabled, +continues to appear on stderr. An example of the output is shown +below: + +``` +$ packer -machine-readable version +1376289459,,version,0.2.4 +1376289459,,version-prerelease, +1376289459,,version-commit,eed6ece +1376289459,,ui,say,Packer v0.2.4.dev (eed6ece+CHANGES) +``` + +The format will be covered in more detail later. But as you can see, +the output immediately becomes machine-friendly. Try some other commands +with the `-machine-readable` flag to see! + +## Format + +The machine readable format is a line-oriented, comma-delimeted text +format. This makes it extremely to parse using standard Unix tools such +as awk or grep in addition to full programming languages like Ruby or +Python. + +The format is: + +``` +timestamp,target,type,data... +``` + +Each component is explained below: + +* **timestamp** is a Unix timestamp in UTC of when the message was + printed. + +* **target** is the target of the following output. This is empty if + the message is related to Packer globally. Otherwise, this is generally + a build name so you can relate output to a specific build while parallel + builds are running. + +* **type** is the type of machine-readable message being outputted. There + are a set of standard types which are covered later, but each component + of Packer (builders, provisioners, etc.) may output their own custom types + as well, allowing the machine-readable output to be infinitely flexible. + +* **data** is zero or more comma-seperated values associated with the prior + type. The exact amount and meaning of this data is type-dependent, so you + must read the documentation associated with the type to understand fully. + +Within the format, if data contains a comma, it is replaced with +`%!(PACKER_COMMA)`. This was preferred over an escape character such as +`\'` because it is more friendly to tools like awk. + +Newlines within the format are replaced with their respective standard +escape sequence. Newlines become a literal `\n` within the output. Carriage +returns become a literal `\r`. + +## Message Types + +The set of machine-readable message types can be found in the +[machine-readable format](#) +complete documentation section. This section contains documentation +on all the message types exposed by Packer core as well as all the +components that ship with Packer by default. diff --git a/website/source/docs/command-line/validate.html.markdown b/website/source/docs/command-line/validate.html.markdown index fcf992f1f..e05c9410a 100644 --- a/website/source/docs/command-line/validate.html.markdown +++ b/website/source/docs/command-line/validate.html.markdown @@ -1,5 +1,6 @@ --- layout: "docs" +page_title: "Validate - Command-Line" --- # Command-Line: Validate diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb index 8b3ef5ddd..2ab4b4a8e 100644 --- a/website/source/layouts/docs.erb +++ b/website/source/layouts/docs.erb @@ -13,6 +13,7 @@
  • Build
  • Fix
  • Validate
  • +
  • Machine-Readable Output
    • From d9bfe58c935c76efe25f237286046c525640f043 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 11 Aug 2013 23:56:47 -0700 Subject: [PATCH 14/14] packer: version machine-readable output --- packer/version.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packer/version.go b/packer/version.go index 4c2413b7c..bb2ca744f 100644 --- a/packer/version.go +++ b/packer/version.go @@ -27,6 +27,10 @@ command-line flags for this command.` } func (versionCommand) Run(env Environment, args []string) int { + env.Ui().Machine("version", Version) + env.Ui().Machine("version-prelease", VersionPrerelease) + env.Ui().Machine("version-commit", GitCommit) + var versionString bytes.Buffer fmt.Fprintf(&versionString, "Packer v%s", Version) if VersionPrerelease != "" {