Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core: stoppable provisioners, helper/schema for provisioners #10934

Merged
merged 23 commits into from
Jan 30, 2017
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
f8c7b63
terraform: switch to Context for stop, Stoppable provisioners
mitchellh Dec 22, 2016
2e894c4
plugin: add ResourceProvisioner.Stop API
mitchellh Dec 22, 2016
96884ec
plugin: bump the protocol version due to Provisioner change
mitchellh Dec 22, 2016
b2891bc
helper/schema: Provisioner support
mitchellh Dec 22, 2016
a1da59a
helper/schema: provisioner allows for nil state
mitchellh Dec 22, 2016
c5b784c
provisioners/local-exec: switch to helper/schema
mitchellh Dec 22, 2016
0fb87cd
provisioners/local-exec: stoppable
mitchellh Dec 23, 2016
02a4adc
provisioners/file: convert to helper/schema
mitchellh Dec 23, 2016
a2e0448
provisioners/file: use the old communicator.New just to minimize risk
mitchellh Dec 23, 2016
27c19af
provisioners/file: support Stop
mitchellh Dec 23, 2016
3c0c819
provisioners/remote-exec: switch to helper/schema
mitchellh Dec 23, 2016
f29845e
update privisioner bins to use new functions
mitchellh Dec 23, 2016
487a37b
helper/schema: PromoteSingle for legacy support of "maybe list" types
mitchellh Dec 23, 2016
447a5c8
scripts: update internal plugin gen to support new provisioner
mitchellh Dec 23, 2016
cde458d
scripts: update tests for generate plugins to pass new style
mitchellh Dec 23, 2016
b486354
communicator/ssh: Disconnect() should also kill the actual connection
mitchellh Dec 27, 2016
142df65
provisioners/remote-exec: listen to Stop
mitchellh Dec 27, 2016
a8f64cb
terraform: make sure Stop blocks until full completion
mitchellh Dec 28, 2016
83cc54b
updated generate output
mitchellh Jan 26, 2017
5b42781
terraform: defer unlock of lock in Stop to enure it always unlocks
mitchellh Jan 30, 2017
00232f0
terraform: acquireRun during test to avoid special case logic
mitchellh Jan 30, 2017
3e771a6
terraform: acquire stopCh outside goroutine to ensure in lock
mitchellh Jan 30, 2017
3776d31
provisioners/local-exec: remove data race by setting err only once
mitchellh Jan 30, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
66 changes: 40 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,24 @@ 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 struct{})
go func() {
defer close(doneCh)
err = cmd.Wait()
}()

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this err assignment races with the one in the goroutine above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed!

}
}

// Close the write-end of the pipe so that the goroutine mirroring output
// ends properly.
Expand All @@ -78,15 +100,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