Merge pull request #361 from hashicorp/f-provisioner-ui

Foundational support for UI output from provisioners
This commit is contained in:
Mitchell Hashimoto 2014-10-05 08:29:18 -07:00
commit 28acb7baa8
24 changed files with 253 additions and 18 deletions

View File

@ -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 {

View File

@ -20,6 +20,7 @@ const (
type ResourceProvisioner struct{}
func (p *ResourceProvisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {

View File

@ -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)
}

View File

@ -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 {

View File

@ -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) {

View File

@ -101,6 +101,7 @@ func (c *Client) ResourceProvisioner() (terraform.ResourceProvisioner, error) {
}
return &ResourceProvisioner{
Broker: c.broker,
Client: rpc.NewClient(conn),
Name: "ResourceProvisioner",
}, nil

View File

@ -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")
}

View File

@ -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,10 +37,17 @@ 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{
OutputId: id,
State: s,
Config: c,
}
@ -65,6 +73,7 @@ type ResourceProvisionerValidateResponse struct {
}
type ResourceProvisionerApplyArgs struct {
OutputId uint32
State *terraform.InstanceState
Config *terraform.ResourceConfig
}
@ -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),
}

View File

@ -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")
}

View File

@ -118,6 +118,7 @@ func (d *dispenseServer) ResourceProvisioner(
}
serve(conn, "ResourceProvisioner", &ResourceProvisionerServer{
Broker: d.broker,
Provisioner: d.ProvisionerFunc(),
})
}()

30
rpc/ui_output.go Normal file
View File

@ -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
}

34
rpc/ui_output_test.go Normal file
View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -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 {

7
terraform/ui_output.go Normal file
View File

@ -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)
}

View File

@ -0,0 +1,5 @@
package terraform
type CallbackUIOutput struct {
OutputFun func(string)
}

View File

@ -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)
}
}

View File

@ -0,0 +1,9 @@
package terraform
import (
"testing"
)
func TestMockUIOutput(t *testing.T) {
var _ UIOutput = new(MockUIOutput)
}

View File

@ -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)
}
}

View File

@ -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)
}
}