diff --git a/builtin/provisioners/file/resource_provisioner.go b/builtin/provisioners/file/resource_provisioner.go index 91970b215..bb95c0860 100644 --- a/builtin/provisioners/file/resource_provisioner.go +++ b/builtin/provisioners/file/resource_provisioner.go @@ -13,7 +13,9 @@ import ( type ResourceProvisioner struct{} -func (p *ResourceProvisioner) Apply(s *terraform.InstanceState, +func (p *ResourceProvisioner) Apply( + o terraform.UIOutput, + s *terraform.InstanceState, c *terraform.ResourceConfig) error { // Ensure the connection type is SSH if err := helper.VerifySSH(s); err != nil { diff --git a/builtin/provisioners/local-exec/resource_provisioner.go b/builtin/provisioners/local-exec/resource_provisioner.go index 234b8ee3e..544dabe86 100644 --- a/builtin/provisioners/local-exec/resource_provisioner.go +++ b/builtin/provisioners/local-exec/resource_provisioner.go @@ -20,6 +20,7 @@ const ( type ResourceProvisioner struct{} func (p *ResourceProvisioner) Apply( + o terraform.UIOutput, s *terraform.InstanceState, c *terraform.ResourceConfig) error { diff --git a/builtin/provisioners/local-exec/resource_provisioner_test.go b/builtin/provisioners/local-exec/resource_provisioner_test.go index e31641561..9158c333e 100644 --- a/builtin/provisioners/local-exec/resource_provisioner_test.go +++ b/builtin/provisioners/local-exec/resource_provisioner_test.go @@ -20,8 +20,9 @@ func TestResourceProvider_Apply(t *testing.T) { "command": "echo foo > test_out", }) + output := new(terraform.MockUIOutput) p := new(ResourceProvisioner) - if err := p.Apply(nil, c); err != nil { + if err := p.Apply(output, nil, c); err != nil { t.Fatalf("err: %v", err) } diff --git a/builtin/provisioners/remote-exec/resource_provisioner.go b/builtin/provisioners/remote-exec/resource_provisioner.go index fc694d8aa..7868f32ac 100644 --- a/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/builtin/provisioners/remote-exec/resource_provisioner.go @@ -22,7 +22,9 @@ const ( type ResourceProvisioner struct{} -func (p *ResourceProvisioner) Apply(s *terraform.InstanceState, +func (p *ResourceProvisioner) Apply( + o terraform.UIOutput, + s *terraform.InstanceState, c *terraform.ResourceConfig) error { // Ensure the connection type is SSH if err := helper.VerifySSH(s); err != nil { diff --git a/command/hook_ui.go b/command/hook_ui.go index b86701f5e..80c7fea28 100644 --- a/command/hook_ui.go +++ b/command/hook_ui.go @@ -164,6 +164,18 @@ func (h *UiHook) PreProvision( return terraform.HookActionContinue, nil } +func (h *UiHook) ProvisionOutput( + n *terraform.InstanceInfo, + provId string, + msg string) { + id := n.HumanId() + var buf bytes.Buffer + buf.WriteString(h.Colorize.Color(fmt.Sprintf( + "[reset]%s (%s): ", id, provId))) + buf.WriteString(msg) + h.ui.Output(buf.String()) +} + func (h *UiHook) PreRefresh( n *terraform.InstanceInfo, s *terraform.InstanceState) (terraform.HookAction, error) { diff --git a/rpc/client.go b/rpc/client.go index f6d20e3a6..0c80385ee 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -101,6 +101,7 @@ func (c *Client) ResourceProvisioner() (terraform.ResourceProvisioner, error) { } return &ResourceProvisioner{ + Broker: c.broker, Client: rpc.NewClient(conn), Name: "ResourceProvisioner", }, nil diff --git a/rpc/client_test.go b/rpc/client_test.go index c4479cfd1..f8c286fe8 100644 --- a/rpc/client_test.go +++ b/rpc/client_test.go @@ -60,9 +60,10 @@ func TestClient_ResourceProvisioner(t *testing.T) { } // Apply + output := &terraform.MockUIOutput{} state := &terraform.InstanceState{} conf := &terraform.ResourceConfig{} - err = provisioner.Apply(state, conf) + err = provisioner.Apply(output, state, conf) if !p.ApplyCalled { t.Fatal("apply should be called") } diff --git a/rpc/resource_provisioner.go b/rpc/resource_provisioner.go index 5fc45a98c..cf8c00812 100644 --- a/rpc/resource_provisioner.go +++ b/rpc/resource_provisioner.go @@ -9,6 +9,7 @@ import ( // ResourceProvisioner is an implementation of terraform.ResourceProvisioner // that communicates over RPC. type ResourceProvisioner struct { + Broker *muxBroker Client *rpc.Client Name string } @@ -36,12 +37,19 @@ func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, [ } func (p *ResourceProvisioner) Apply( + output terraform.UIOutput, s *terraform.InstanceState, c *terraform.ResourceConfig) error { + id := p.Broker.NextId() + go acceptAndServe(p.Broker, id, "UIOutput", &UIOutputServer{ + UIOutput: output, + }) + var resp ResourceProvisionerApplyResponse args := &ResourceProvisionerApplyArgs{ - State: s, - Config: c, + OutputId: id, + State: s, + Config: c, } err := p.Client.Call(p.Name+".Apply", args, &resp) @@ -65,8 +73,9 @@ type ResourceProvisionerValidateResponse struct { } type ResourceProvisionerApplyArgs struct { - State *terraform.InstanceState - Config *terraform.ResourceConfig + OutputId uint32 + State *terraform.InstanceState + Config *terraform.ResourceConfig } type ResourceProvisionerApplyResponse struct { @@ -76,13 +85,29 @@ type ResourceProvisionerApplyResponse struct { // ResourceProvisionerServer is a net/rpc compatible structure for serving // a ResourceProvisioner. This should not be used directly. type ResourceProvisionerServer struct { + Broker *muxBroker Provisioner terraform.ResourceProvisioner } func (s *ResourceProvisionerServer) Apply( args *ResourceProvisionerApplyArgs, result *ResourceProvisionerApplyResponse) error { - err := s.Provisioner.Apply(args.State, args.Config) + conn, err := s.Broker.Dial(args.OutputId) + if err != nil { + *result = ResourceProvisionerApplyResponse{ + Error: NewBasicError(err), + } + return nil + } + client := rpc.NewClient(conn) + defer client.Close() + + output := &UIOutput{ + Client: client, + Name: "UIOutput", + } + + err = s.Provisioner.Apply(output, args.State, args.Config) *result = ResourceProvisionerApplyResponse{ Error: NewBasicError(err), } diff --git a/rpc/resource_provisioner_test.go b/rpc/resource_provisioner_test.go index b91252648..32b6c06f3 100644 --- a/rpc/resource_provisioner_test.go +++ b/rpc/resource_provisioner_test.go @@ -13,18 +13,21 @@ func TestResourceProvisioner_impl(t *testing.T) { } func TestResourceProvisioner_apply(t *testing.T) { - p := new(terraform.MockResourceProvisioner) - client, server := testClientServer(t) - name, err := Register(server, p) + client, server := testNewClientServer(t) + defer client.Close() + + p := server.ProvisionerFunc().(*terraform.MockResourceProvisioner) + + provisioner, err := client.ResourceProvisioner() if err != nil { t.Fatalf("err: %s", err) } - provisioner := &ResourceProvisioner{Client: client, Name: name} // Apply + output := &terraform.MockUIOutput{} state := &terraform.InstanceState{} conf := &terraform.ResourceConfig{} - err = provisioner.Apply(state, conf) + err = provisioner.Apply(output, state, conf) if !p.ApplyCalled { t.Fatal("apply should be called") } diff --git a/rpc/server.go b/rpc/server.go index 5ca6c6746..dd1e9b7b0 100644 --- a/rpc/server.go +++ b/rpc/server.go @@ -118,6 +118,7 @@ func (d *dispenseServer) ResourceProvisioner( } serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{ + Broker: d.broker, Provisioner: d.ProvisionerFunc(), }) }() diff --git a/rpc/ui_output.go b/rpc/ui_output.go new file mode 100644 index 000000000..a997b943b --- /dev/null +++ b/rpc/ui_output.go @@ -0,0 +1,30 @@ +package rpc + +import ( + "net/rpc" + + "github.com/hashicorp/terraform/terraform" +) + +// UIOutput is an implementatin of terraform.UIOutput that communicates +// over RPC. +type UIOutput struct { + Client *rpc.Client + Name string +} + +func (o *UIOutput) Output(v string) { + o.Client.Call(o.Name+".Output", v, new(interface{})) +} + +// UIOutputServer is the RPC server for serving UIOutput. +type UIOutputServer struct { + UIOutput terraform.UIOutput +} + +func (s *UIOutputServer) Output( + v string, + reply *interface{}) error { + s.UIOutput.Output(v) + return nil +} diff --git a/rpc/ui_output_test.go b/rpc/ui_output_test.go new file mode 100644 index 000000000..0113a0903 --- /dev/null +++ b/rpc/ui_output_test.go @@ -0,0 +1,34 @@ +package rpc + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestUIOutput_impl(t *testing.T) { + var _ terraform.UIOutput = new(UIOutput) +} + +func TestUIOutput_input(t *testing.T) { + client, server := testClientServer(t) + defer client.Close() + + o := new(terraform.MockUIOutput) + + err := server.RegisterName("UIOutput", &UIOutputServer{ + UIOutput: o, + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + output := &UIOutput{Client: client, Name: "UIOutput"} + output.Output("foo") + if !o.OutputCalled { + t.Fatal("output should be called") + } + if o.OutputMessage != "foo" { + t.Fatalf("bad: %#v", o.OutputMessage) + } +} diff --git a/terraform/context.go b/terraform/context.go index 146152a0e..fa3ff1065 100644 --- a/terraform/context.go +++ b/terraform/context.go @@ -54,7 +54,7 @@ type ContextOpts struct { Provisioners map[string]ResourceProvisionerFactory Variables map[string]string - UIInput UIInput + UIInput UIInput } // NewContext creates a new context. @@ -1309,7 +1309,13 @@ func (c *walkContext) applyProvisioners(r *Resource, is *InstanceState) error { handleHook(h.PreProvision(r.Info, prov.Type)) } - if err := prov.Provisioner.Apply(is, prov.Config); err != nil { + output := ProvisionerUIOutput{ + Info: r.Info, + Type: prov.Type, + Hooks: c.Context.hooks, + } + err := prov.Provisioner.Apply(&output, is, prov.Config) + if err != nil { return err } diff --git a/terraform/hook.go b/terraform/hook.go index 8d4c83d4c..e4ad42016 100644 --- a/terraform/hook.go +++ b/terraform/hook.go @@ -33,10 +33,17 @@ type Hook interface { PostDiff(*InstanceInfo, *InstanceDiff) (HookAction, error) // Provisioning hooks + // + // All should be self-explanatory. ProvisionOutput is called with + // output sent back by the provisioners. This will be called multiple + // times as output comes in, but each call should represent a line of + // output. The ProvisionOutput method cannot control whether the + // hook continues running. PreProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) PostProvisionResource(*InstanceInfo, *InstanceState) (HookAction, error) PreProvision(*InstanceInfo, string) (HookAction, error) PostProvision(*InstanceInfo, string) (HookAction, error) + ProvisionOutput(*InstanceInfo, string, string) // PreRefresh and PostRefresh are called before and after a single // resource state is refreshed, respectively. @@ -81,6 +88,10 @@ func (*NilHook) PostProvision(*InstanceInfo, string) (HookAction, error) { return HookActionContinue, nil } +func (*NilHook) ProvisionOutput( + *InstanceInfo, string, string) { +} + func (*NilHook) PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { return HookActionContinue, nil } diff --git a/terraform/hook_mock.go b/terraform/hook_mock.go index 63ae011b7..b2b6a6e6f 100644 --- a/terraform/hook_mock.go +++ b/terraform/hook_mock.go @@ -53,6 +53,11 @@ type MockHook struct { PostProvisionReturn HookAction PostProvisionError error + ProvisionOutputCalled bool + ProvisionOutputInfo *InstanceInfo + ProvisionOutputProvisionerId string + ProvisionOutputMessage string + PostRefreshCalled bool PostRefreshInfo *InstanceInfo PostRefreshState *InstanceState @@ -124,6 +129,16 @@ func (h *MockHook) PostProvision(n *InstanceInfo, provId string) (HookAction, er return h.PostProvisionReturn, h.PostProvisionError } +func (h *MockHook) ProvisionOutput( + n *InstanceInfo, + provId string, + msg string) { + h.ProvisionOutputCalled = true + h.ProvisionOutputInfo = n + h.ProvisionOutputProvisionerId = provId + h.ProvisionOutputMessage = msg +} + func (h *MockHook) PreRefresh(n *InstanceInfo, s *InstanceState) (HookAction, error) { h.PreRefreshCalled = true h.PreRefreshInfo = n diff --git a/terraform/hook_stop.go b/terraform/hook_stop.go index 148f9c63c..0dc1ad7b4 100644 --- a/terraform/hook_stop.go +++ b/terraform/hook_stop.go @@ -42,6 +42,9 @@ func (h *stopHook) PostProvision(*InstanceInfo, string) (HookAction, error) { return h.hook() } +func (h *stopHook) ProvisionOutput(*InstanceInfo, string, string) { +} + func (h *stopHook) PreRefresh(*InstanceInfo, *InstanceState) (HookAction, error) { return h.hook() } diff --git a/terraform/resource_provisioner.go b/terraform/resource_provisioner.go index 6a749e001..acc80d054 100644 --- a/terraform/resource_provisioner.go +++ b/terraform/resource_provisioner.go @@ -20,7 +20,7 @@ type ResourceProvisioner interface { // resource state along with an error. Instead of a diff, the ResourceConfig // is provided since provisioners only run after a resource has been // newly created. - Apply(*InstanceState, *ResourceConfig) error + Apply(UIOutput, *InstanceState, *ResourceConfig) error } // ResourceProvisionerFactory is a function type that creates a new instance diff --git a/terraform/resource_provisioner_mock.go b/terraform/resource_provisioner_mock.go index f1600784c..2ba7220cd 100644 --- a/terraform/resource_provisioner_mock.go +++ b/terraform/resource_provisioner_mock.go @@ -7,6 +7,7 @@ type MockResourceProvisioner struct { Meta interface{} ApplyCalled bool + ApplyOutput UIOutput ApplyState *InstanceState ApplyConfig *ResourceConfig ApplyFn func(*InstanceState, *ResourceConfig) error @@ -28,8 +29,12 @@ func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error return p.ValidateReturnWarns, p.ValidateReturnErrors } -func (p *MockResourceProvisioner) Apply(state *InstanceState, c *ResourceConfig) error { +func (p *MockResourceProvisioner) Apply( + output UIOutput, + state *InstanceState, + c *ResourceConfig) error { p.ApplyCalled = true + p.ApplyOutput = output p.ApplyState = state p.ApplyConfig = c if p.ApplyFn != nil { diff --git a/terraform/ui_output.go b/terraform/ui_output.go new file mode 100644 index 000000000..84427c63d --- /dev/null +++ b/terraform/ui_output.go @@ -0,0 +1,7 @@ +package terraform + +// UIOutput is the interface that must be implemented to output +// data to the end user. +type UIOutput interface { + Output(string) +} diff --git a/terraform/ui_output_callback.go b/terraform/ui_output_callback.go new file mode 100644 index 000000000..147515b95 --- /dev/null +++ b/terraform/ui_output_callback.go @@ -0,0 +1,5 @@ +package terraform + +type CallbackUIOutput struct { + OutputFun func(string) +} diff --git a/terraform/ui_output_mock.go b/terraform/ui_output_mock.go new file mode 100644 index 000000000..8e16ac9af --- /dev/null +++ b/terraform/ui_output_mock.go @@ -0,0 +1,16 @@ +package terraform + +// MockUIOutput is an implementation of UIOutput that can be used for tests. +type MockUIOutput struct { + OutputCalled bool + OutputMessage string + OutputFn func(string) +} + +func (o *MockUIOutput) Output(v string) { + o.OutputCalled = true + o.OutputMessage= v + if o.OutputFn != nil { + o.OutputFn(v) + } +} diff --git a/terraform/ui_output_mock_test.go b/terraform/ui_output_mock_test.go new file mode 100644 index 000000000..0a23c2e23 --- /dev/null +++ b/terraform/ui_output_mock_test.go @@ -0,0 +1,9 @@ +package terraform + +import ( + "testing" +) + +func TestMockUIOutput(t *testing.T) { + var _ UIOutput = new(MockUIOutput) +} diff --git a/terraform/ui_output_provisioner.go b/terraform/ui_output_provisioner.go new file mode 100644 index 000000000..878a03122 --- /dev/null +++ b/terraform/ui_output_provisioner.go @@ -0,0 +1,15 @@ +package terraform + +// ProvisionerUIOutput is an implementation of UIOutput that calls a hook +// for the output so that the hooks can handle it. +type ProvisionerUIOutput struct { + Info *InstanceInfo + Type string + Hooks []Hook +} + +func (o *ProvisionerUIOutput) Output(msg string) { + for _, h := range o.Hooks { + h.ProvisionOutput(o.Info, o.Type, msg) + } +} diff --git a/terraform/ui_output_provisioner_test.go b/terraform/ui_output_provisioner_test.go new file mode 100644 index 000000000..dc1d00c21 --- /dev/null +++ b/terraform/ui_output_provisioner_test.go @@ -0,0 +1,30 @@ +package terraform + +import ( + "testing" +) + +func TestProvisionerUIOutput_impl(t *testing.T) { + var _ UIOutput = new(ProvisionerUIOutput) +} + +func TestProvisionerUIOutputOutput(t *testing.T) { + hook := new(MockHook) + output := &ProvisionerUIOutput{ + Info: nil, + Type: "foo", + Hooks: []Hook{hook}, + } + + output.Output("bar") + + if !hook.ProvisionOutputCalled { + t.Fatal("should be called") + } + if hook.ProvisionOutputProvisionerId != "foo" { + t.Fatalf("bad: %#v", hook.ProvisionOutputProvisionerId) + } + if hook.ProvisionOutputMessage != "bar" { + t.Fatalf("bad: %#v", hook.ProvisionOutputMessage) + } +}