Skip to content

Commit

Permalink
Merge pull request #10934 from hashicorp/f-provisioner-stop
Browse files Browse the repository at this point in the history
core: stoppable provisioners, helper/schema for provisioners
  • Loading branch information
mitchellh authored Jan 30, 2017
2 parents 3c0fdc4 + 3776d31 commit 61881d2
Show file tree
Hide file tree
Showing 37 changed files with 1,387 additions and 320 deletions.
5 changes: 1 addition & 4 deletions builtin/bins/provisioner-file/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/file"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner {
return new(file.ResourceProvisioner)
},
ProvisionerFunc: file.Provisioner,
})
}
5 changes: 1 addition & 4 deletions builtin/bins/provisioner-local-exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/local-exec"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner {
return new(localexec.ResourceProvisioner)
},
ProvisionerFunc: localexec.Provisioner,
})
}
5 changes: 1 addition & 4 deletions builtin/bins/provisioner-remote-exec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
)

func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner {
return new(remoteexec.ResourceProvisioner)
},
ProvisionerFunc: remoteexec.Provisioner,
})
}
5 changes: 5 additions & 0 deletions builtin/provisioners/chef/resource_provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ type Provisioner struct {
// ResourceProvisioner represents a generic chef provisioner
type ResourceProvisioner struct{}

func (r *ResourceProvisioner) Stop() error {
// Noop for now. TODO in the future.
return nil
}

// Apply executes the file provisioner
func (r *ResourceProvisioner) Apply(
o terraform.UIOutput,
Expand Down
106 changes: 53 additions & 53 deletions builtin/provisioners/file/resource_provisioner.go
Original file line number Diff line number Diff line change
@@ -1,92 +1,92 @@
package file

import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"time"

"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
)

// ResourceProvisioner represents a file provisioner
type ResourceProvisioner struct{}
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"source": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"content"},
},

"content": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"source"},
},

"destination": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},

ApplyFunc: applyFn,
}
}

func applyFn(ctx context.Context) error {
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)

// Apply executes the file provisioner
func (p *ResourceProvisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
// Get a new communicator
comm, err := communicator.New(s)
comm, err := communicator.New(connState)
if err != nil {
return err
}

// Get the source
src, deleteSource, err := p.getSrc(c)
src, deleteSource, err := getSrc(data)
if err != nil {
return err
}
if deleteSource {
defer os.Remove(src)
}

// Get destination
dRaw := c.Config["destination"]
dst, ok := dRaw.(string)
if !ok {
return fmt.Errorf("Unsupported 'destination' type! Must be string.")
}
return p.copyFiles(comm, src, dst)
}

// Validate checks if the required arguments are configured
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
numDst := 0
numSrc := 0
for name := range c.Raw {
switch name {
case "destination":
numDst++
case "source", "content":
numSrc++
default:
es = append(es, fmt.Errorf("Unknown configuration '%s'", name))
}
}
if numSrc != 1 || numDst != 1 {
es = append(es, fmt.Errorf("Must provide one of 'content' or 'source' and 'destination' to file"))
// Begin the file copy
dst := data.Get("destination").(string)
resultCh := make(chan error, 1)
go func() {
resultCh <- copyFiles(comm, src, dst)
}()

// Allow the file copy to complete unless there is an interrupt.
// If there is an interrupt we make no attempt to cleanly close
// the connection currently. We just abruptly exit. Because Terraform
// taints the resource, this is fine.
select {
case err := <-resultCh:
return err
case <-ctx.Done():
return fmt.Errorf("file transfer interrupted")
}
return
}

// getSrc returns the file to use as source
func (p *ResourceProvisioner) getSrc(c *terraform.ResourceConfig) (string, bool, error) {
var src string

sRaw, ok := c.Config["source"]
if ok {
if src, ok = sRaw.(string); !ok {
return "", false, fmt.Errorf("Unsupported 'source' type! Must be string.")
}
}

content, ok := c.Config["content"]
if ok {
func getSrc(data *schema.ResourceData) (string, bool, error) {
src := data.Get("source").(string)
if content, ok := data.GetOk("content"); ok {
file, err := ioutil.TempFile("", "tf-file-content")
if err != nil {
return "", true, err
}

contentStr, ok := content.(string)
if !ok {
return "", true, fmt.Errorf("Unsupported 'content' type! Must be string.")
}
if _, err = file.WriteString(contentStr); err != nil {
if _, err = file.WriteString(content.(string)); err != nil {
return "", true, err
}

Expand All @@ -98,7 +98,7 @@ func (p *ResourceProvisioner) getSrc(c *terraform.ResourceConfig) (string, bool,
}

// copyFiles is used to copy the files from a source to a destination
func (p *ResourceProvisioner) copyFiles(comm communicator.Communicator, src, dst string) error {
func copyFiles(comm communicator.Communicator, src, dst string) error {
// Wait and retry until we establish the connection
err := retryFunc(comm.Timeout(), func() error {
err := comm.Connect(nil)
Expand Down
12 changes: 4 additions & 8 deletions builtin/provisioners/file/resource_provisioner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@ import (
"github.com/hashicorp/terraform/terraform"
)

func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}

func TestResourceProvider_Validate_good_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"source": "/tmp/foo",
"destination": "/tmp/bar",
})
p := new(ResourceProvisioner)
p := Provisioner()
warn, errs := p.Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
Expand All @@ -31,7 +27,7 @@ func TestResourceProvider_Validate_good_content(t *testing.T) {
"content": "value to copy",
"destination": "/tmp/bar",
})
p := new(ResourceProvisioner)
p := Provisioner()
warn, errs := p.Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
Expand All @@ -45,7 +41,7 @@ func TestResourceProvider_Validate_bad_not_destination(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"source": "nope",
})
p := new(ResourceProvisioner)
p := Provisioner()
warn, errs := p.Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
Expand All @@ -61,7 +57,7 @@ func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) {
"content": "value to copy",
"destination": "/tmp/bar",
})
p := new(ResourceProvisioner)
p := Provisioner()
warn, errs := p.Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
Expand Down
65 changes: 39 additions & 26 deletions builtin/provisioners/local-exec/resource_provisioner.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package localexec

import (
"context"
"fmt"
"io"
"os/exec"
"runtime"

"github.com/armon/circbuf"
"github.com/hashicorp/terraform/helper/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader"
)
Expand All @@ -19,21 +20,26 @@ const (
maxBufSize = 8 * 1024
)

type ResourceProvisioner struct{}
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"command": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},

func (p *ResourceProvisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {

// Get the command
commandRaw, ok := c.Config["command"]
if !ok {
return fmt.Errorf("local-exec provisioner missing 'command'")
ApplyFunc: applyFn,
}
command, ok := commandRaw.(string)
if !ok {
return fmt.Errorf("local-exec provisioner command must be a string")
}

func applyFn(ctx context.Context) error {
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)

command := data.Get("command").(string)
if command == "" {
return fmt.Errorf("local-exec provisioner command must be a non-empty string")
}

// Execute the command using a shell
Expand All @@ -49,7 +55,7 @@ func (p *ResourceProvisioner) Apply(
// Setup the reader that will read the lines from the command
pr, pw := io.Pipe()
copyDoneCh := make(chan struct{})
go p.copyOutput(o, pr, copyDoneCh)
go copyOutput(o, pr, copyDoneCh)

// Setup the command
cmd := exec.Command(shell, flag, command)
Expand All @@ -62,8 +68,23 @@ func (p *ResourceProvisioner) Apply(
"Executing: %s %s \"%s\"",
shell, flag, command))

// Run the command to completion
err := cmd.Run()
// Start the command
err := cmd.Start()
if err == nil {
// Wait for the command to complete in a goroutine
doneCh := make(chan error, 1)
go func() {
doneCh <- cmd.Wait()
}()

// Wait for the command to finish or for us to be interrupted
select {
case err = <-doneCh:
case <-ctx.Done():
cmd.Process.Kill()
err = cmd.Wait()
}
}

// Close the write-end of the pipe so that the goroutine mirroring output
// ends properly.
Expand All @@ -78,15 +99,7 @@ func (p *ResourceProvisioner) Apply(
return nil
}

func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {
validator := config.Validator{
Required: []string{"command"},
}
return validator.Validate(c)
}

func (p *ResourceProvisioner) copyOutput(
o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh)
lr := linereader.New(r)
for line := range lr.Ch {
Expand Down
Loading

0 comments on commit 61881d2

Please sign in to comment.