mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
Merge pull request #361 from hashicorp/f-provisioner-ui
Foundational support for UI output from provisioners
This commit is contained in:
commit
28acb7baa8
@ -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 {
|
||||
|
@ -20,6 +20,7 @@ const (
|
||||
type ResourceProvisioner struct{}
|
||||
|
||||
func (p *ResourceProvisioner) Apply(
|
||||
o terraform.UIOutput,
|
||||
s *terraform.InstanceState,
|
||||
c *terraform.ResourceConfig) error {
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -101,6 +101,7 @@ func (c *Client) ResourceProvisioner() (terraform.ResourceProvisioner, error) {
|
||||
}
|
||||
|
||||
return &ResourceProvisioner{
|
||||
Broker: c.broker,
|
||||
Client: rpc.NewClient(conn),
|
||||
Name: "ResourceProvisioner",
|
||||
}, nil
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
30
rpc/ui_output.go
Normal 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
34
rpc/ui_output_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
7
terraform/ui_output.go
Normal 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)
|
||||
}
|
5
terraform/ui_output_callback.go
Normal file
5
terraform/ui_output_callback.go
Normal file
@ -0,0 +1,5 @@
|
||||
package terraform
|
||||
|
||||
type CallbackUIOutput struct {
|
||||
OutputFun func(string)
|
||||
}
|
16
terraform/ui_output_mock.go
Normal file
16
terraform/ui_output_mock.go
Normal 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)
|
||||
}
|
||||
}
|
9
terraform/ui_output_mock_test.go
Normal file
9
terraform/ui_output_mock_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package terraform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMockUIOutput(t *testing.T) {
|
||||
var _ UIOutput = new(MockUIOutput)
|
||||
}
|
15
terraform/ui_output_provisioner.go
Normal file
15
terraform/ui_output_provisioner.go
Normal 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)
|
||||
}
|
||||
}
|
30
terraform/ui_output_provisioner_test.go
Normal file
30
terraform/ui_output_provisioner_test.go
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user