Merge pull request #10934 from hashicorp/f-provisioner-stop

core: stoppable provisioners, helper/schema for provisioners
This commit is contained in:
Mitchell Hashimoto 2017-01-30 12:53:15 -08:00 committed by GitHub
commit 61881d2795
37 changed files with 1386 additions and 319 deletions

View File

@ -3,13 +3,10 @@ package main
import ( import (
"github.com/hashicorp/terraform/builtin/provisioners/file" "github.com/hashicorp/terraform/builtin/provisioners/file"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
) )
func main() { func main() {
plugin.Serve(&plugin.ServeOpts{ plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner { ProvisionerFunc: file.Provisioner,
return new(file.ResourceProvisioner)
},
}) })
} }

View File

@ -3,13 +3,10 @@ package main
import ( import (
"github.com/hashicorp/terraform/builtin/provisioners/local-exec" "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
) )
func main() { func main() {
plugin.Serve(&plugin.ServeOpts{ plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner { ProvisionerFunc: localexec.Provisioner,
return new(localexec.ResourceProvisioner)
},
}) })
} }

View File

@ -3,13 +3,10 @@ package main
import ( import (
"github.com/hashicorp/terraform/builtin/provisioners/remote-exec" "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform"
) )
func main() { func main() {
plugin.Serve(&plugin.ServeOpts{ plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: func() terraform.ResourceProvisioner { ProvisionerFunc: remoteexec.Provisioner,
return new(remoteexec.ResourceProvisioner)
},
}) })
} }

View File

@ -132,6 +132,11 @@ type Provisioner struct {
// ResourceProvisioner represents a generic chef provisioner // ResourceProvisioner represents a generic chef provisioner
type ResourceProvisioner struct{} type ResourceProvisioner struct{}
func (r *ResourceProvisioner) Stop() error {
// Noop for now. TODO in the future.
return nil
}
// Apply executes the file provisioner // Apply executes the file provisioner
func (r *ResourceProvisioner) Apply( func (r *ResourceProvisioner) Apply(
o terraform.UIOutput, o terraform.UIOutput,

View File

@ -1,6 +1,7 @@
package file package file
import ( import (
"context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -8,26 +9,48 @@ import (
"time" "time"
"github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
// ResourceProvisioner represents a file provisioner func Provisioner() terraform.ResourceProvisioner {
type ResourceProvisioner struct{} 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 // Get a new communicator
comm, err := communicator.New(s) comm, err := communicator.New(connState)
if err != nil { if err != nil {
return err return err
} }
// Get the source // Get the source
src, deleteSource, err := p.getSrc(c) src, deleteSource, err := getSrc(data)
if err != nil { if err != nil {
return err return err
} }
@ -35,58 +58,35 @@ func (p *ResourceProvisioner) Apply(
defer os.Remove(src) defer os.Remove(src)
} }
// Get destination // Begin the file copy
dRaw := c.Config["destination"] dst := data.Get("destination").(string)
dst, ok := dRaw.(string) resultCh := make(chan error, 1)
if !ok { go func() {
return fmt.Errorf("Unsupported 'destination' type! Must be string.") resultCh <- copyFiles(comm, src, dst)
} }()
return p.copyFiles(comm, src, dst)
}
// Validate checks if the required arguments are configured // Allow the file copy to complete unless there is an interrupt.
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { // If there is an interrupt we make no attempt to cleanly close
numDst := 0 // the connection currently. We just abruptly exit. Because Terraform
numSrc := 0 // taints the resource, this is fine.
for name := range c.Raw { select {
switch name { case err := <-resultCh:
case "destination": return err
numDst++ case <-ctx.Done():
case "source", "content": return fmt.Errorf("file transfer interrupted")
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"))
}
return
} }
// getSrc returns the file to use as source // getSrc returns the file to use as source
func (p *ResourceProvisioner) getSrc(c *terraform.ResourceConfig) (string, bool, error) { func getSrc(data *schema.ResourceData) (string, bool, error) {
var src string src := data.Get("source").(string)
if content, ok := data.GetOk("content"); ok {
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 {
file, err := ioutil.TempFile("", "tf-file-content") file, err := ioutil.TempFile("", "tf-file-content")
if err != nil { if err != nil {
return "", true, err return "", true, err
} }
contentStr, ok := content.(string) if _, err = file.WriteString(content.(string)); err != nil {
if !ok {
return "", true, fmt.Errorf("Unsupported 'content' type! Must be string.")
}
if _, err = file.WriteString(contentStr); err != nil {
return "", true, err return "", true, err
} }
@ -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 // 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 // Wait and retry until we establish the connection
err := retryFunc(comm.Timeout(), func() error { err := retryFunc(comm.Timeout(), func() error {
err := comm.Connect(nil) err := comm.Connect(nil)

View File

@ -7,16 +7,12 @@ import (
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
func TestResourceProvider_Validate_good_source(t *testing.T) { func TestResourceProvider_Validate_good_source(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"source": "/tmp/foo", "source": "/tmp/foo",
"destination": "/tmp/bar", "destination": "/tmp/bar",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -31,7 +27,7 @@ func TestResourceProvider_Validate_good_content(t *testing.T) {
"content": "value to copy", "content": "value to copy",
"destination": "/tmp/bar", "destination": "/tmp/bar",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -45,7 +41,7 @@ func TestResourceProvider_Validate_bad_not_destination(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"source": "nope", "source": "nope",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -61,7 +57,7 @@ func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) {
"content": "value to copy", "content": "value to copy",
"destination": "/tmp/bar", "destination": "/tmp/bar",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)

View File

@ -1,13 +1,14 @@
package localexec package localexec
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os/exec" "os/exec"
"runtime" "runtime"
"github.com/armon/circbuf" "github.com/armon/circbuf"
"github.com/hashicorp/terraform/helper/config" "github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader" "github.com/mitchellh/go-linereader"
) )
@ -19,21 +20,26 @@ const (
maxBufSize = 8 * 1024 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( ApplyFunc: applyFn,
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'")
} }
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 // Execute the command using a shell
@ -49,7 +55,7 @@ func (p *ResourceProvisioner) Apply(
// Setup the reader that will read the lines from the command // Setup the reader that will read the lines from the command
pr, pw := io.Pipe() pr, pw := io.Pipe()
copyDoneCh := make(chan struct{}) copyDoneCh := make(chan struct{})
go p.copyOutput(o, pr, copyDoneCh) go copyOutput(o, pr, copyDoneCh)
// Setup the command // Setup the command
cmd := exec.Command(shell, flag, command) cmd := exec.Command(shell, flag, command)
@ -62,8 +68,23 @@ func (p *ResourceProvisioner) Apply(
"Executing: %s %s \"%s\"", "Executing: %s %s \"%s\"",
shell, flag, command)) shell, flag, command))
// Run the command to completion // Start the command
err := cmd.Run() 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 // Close the write-end of the pipe so that the goroutine mirroring output
// ends properly. // ends properly.
@ -78,15 +99,7 @@ func (p *ResourceProvisioner) Apply(
return nil return nil
} }
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) { func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
validator := config.Validator{
Required: []string{"command"},
}
return validator.Validate(c)
}
func (p *ResourceProvisioner) copyOutput(
o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh) defer close(doneCh)
lr := linereader.New(r) lr := linereader.New(r)
for line := range lr.Ch { for line := range lr.Ch {

View File

@ -5,15 +5,12 @@ import (
"os" "os"
"strings" "strings"
"testing" "testing"
"time"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
func TestResourceProvider_Apply(t *testing.T) { func TestResourceProvider_Apply(t *testing.T) {
defer os.Remove("test_out") defer os.Remove("test_out")
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
@ -21,7 +18,7 @@ func TestResourceProvider_Apply(t *testing.T) {
}) })
output := new(terraform.MockUIOutput) output := new(terraform.MockUIOutput)
p := new(ResourceProvisioner) p := Provisioner()
if err := p.Apply(output, nil, c); err != nil { if err := p.Apply(output, nil, c); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -39,11 +36,42 @@ func TestResourceProvider_Apply(t *testing.T) {
} }
} }
func TestResourceProvider_stop(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"command": "sleep 60",
})
output := new(terraform.MockUIOutput)
p := Provisioner()
var err error
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
err = p.Apply(output, nil, c)
}()
select {
case <-doneCh:
t.Fatal("should not finish quickly")
case <-time.After(10 * time.Millisecond):
}
// Stop it
p.Stop()
select {
case <-doneCh:
case <-time.After(100 * time.Millisecond):
t.Fatal("should finish")
}
}
func TestResourceProvider_Validate_good(t *testing.T) { func TestResourceProvider_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"command": "echo foo", "command": "echo foo",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -55,7 +83,7 @@ func TestResourceProvider_Validate_good(t *testing.T) {
func TestResourceProvider_Validate_missing(t *testing.T) { func TestResourceProvider_Validate_missing(t *testing.T) {
c := testConfig(t, map[string]interface{}{}) c := testConfig(t, map[string]interface{}{})
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)

View File

@ -2,35 +2,65 @@ package remoteexec
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"sync/atomic"
"time" "time"
"github.com/hashicorp/terraform/communicator" "github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote" "github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader" "github.com/mitchellh/go-linereader"
) )
// ResourceProvisioner represents a remote exec provisioner func Provisioner() terraform.ResourceProvisioner {
type ResourceProvisioner struct{} return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"inline": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
PromoteSingle: true,
Optional: true,
ConflictsWith: []string{"script", "scripts"},
},
"script": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"inline", "scripts"},
},
"scripts": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
ConflictsWith: []string{"script", "inline"},
},
},
ApplyFunc: applyFn,
}
}
// Apply executes the remote exec provisioner // Apply executes the remote exec provisioner
func (p *ResourceProvisioner) Apply( func applyFn(ctx context.Context) error {
o terraform.UIOutput, connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
s *terraform.InstanceState, data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
c *terraform.ResourceConfig) error { o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
// Get a new communicator // Get a new communicator
comm, err := communicator.New(s) comm, err := communicator.New(connState)
if err != nil { if err != nil {
return err return err
} }
// Collect the scripts // Collect the scripts
scripts, err := p.collectScripts(c) scripts, err := collectScripts(data)
if err != nil { if err != nil {
return err return err
} }
@ -39,67 +69,33 @@ func (p *ResourceProvisioner) Apply(
} }
// Copy and execute each script // Copy and execute each script
if err := p.runScripts(o, comm, scripts); err != nil { if err := runScripts(ctx, o, comm, scripts); err != nil {
return err return err
} }
return nil return nil
} }
// Validate checks if the required arguments are configured
func (p *ResourceProvisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) {
num := 0
for name := range c.Raw {
switch name {
case "scripts", "script", "inline":
num++
default:
es = append(es, fmt.Errorf("Unknown configuration '%s'", name))
}
}
if num != 1 {
es = append(es, fmt.Errorf("Must provide one of 'scripts', 'script' or 'inline' to remote-exec"))
}
return
}
// generateScripts takes the configuration and creates a script from each inline config // generateScripts takes the configuration and creates a script from each inline config
func (p *ResourceProvisioner) generateScripts(c *terraform.ResourceConfig) ([]string, error) { func generateScripts(d *schema.ResourceData) ([]string, error) {
var scripts []string var scripts []string
command, ok := c.Config["inline"] for _, l := range d.Get("inline").([]interface{}) {
if ok { scripts = append(scripts, l.(string))
switch cmd := command.(type) {
case string:
scripts = append(scripts, cmd)
case []string:
scripts = append(scripts, cmd...)
case []interface{}:
for _, l := range cmd {
lStr, ok := l.(string)
if ok {
scripts = append(scripts, lStr)
} else {
return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
}
}
default:
return nil, fmt.Errorf("Unsupported 'inline' type! Must be string, or list of strings.")
}
} }
return scripts, nil return scripts, nil
} }
// collectScripts is used to collect all the scripts we need // collectScripts is used to collect all the scripts we need
// to execute in preparation for copying them. // to execute in preparation for copying them.
func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.ReadCloser, error) { func collectScripts(d *schema.ResourceData) ([]io.ReadCloser, error) {
// Check if inline // Check if inline
_, ok := c.Config["inline"] if _, ok := d.GetOk("inline"); ok {
if ok { scripts, err := generateScripts(d)
scripts, err := p.generateScripts(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
r := []io.ReadCloser{} var r []io.ReadCloser
for _, script := range scripts { for _, script := range scripts {
r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script)))) r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
} }
@ -109,31 +105,13 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
// Collect scripts // Collect scripts
var scripts []string var scripts []string
s, ok := c.Config["script"] if script, ok := d.GetOk("script"); ok {
if ok { scripts = append(scripts, script.(string))
sStr, ok := s.(string)
if !ok {
return nil, fmt.Errorf("Unsupported 'script' type! Must be a string.")
}
scripts = append(scripts, sStr)
} }
sl, ok := c.Config["scripts"] if scriptList, ok := d.GetOk("scripts"); ok {
if ok { for _, script := range scriptList.([]interface{}) {
switch slt := sl.(type) { scripts = append(scripts, script.(string))
case []string:
scripts = append(scripts, slt...)
case []interface{}:
for _, l := range slt {
lStr, ok := l.(string)
if ok {
scripts = append(scripts, lStr)
} else {
return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
}
}
default:
return nil, fmt.Errorf("Unsupported 'scripts' type! Must be list of strings.")
} }
} }
@ -155,19 +133,30 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
} }
// runScripts is used to copy and execute a set of scripts // runScripts is used to copy and execute a set of scripts
func (p *ResourceProvisioner) runScripts( func runScripts(
ctx context.Context,
o terraform.UIOutput, o terraform.UIOutput,
comm communicator.Communicator, comm communicator.Communicator,
scripts []io.ReadCloser) error { scripts []io.ReadCloser) error {
// Wrap out context in a cancelation function that we use to
// kill the connection.
ctx, cancelFunc := context.WithCancel(ctx)
defer cancelFunc()
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
// Wait and retry until we establish the connection // Wait and retry until we establish the connection
err := retryFunc(comm.Timeout(), func() error { err := retryFunc(ctx, comm.Timeout(), func() error {
err := comm.Connect(o) err := comm.Connect(o)
return err return err
}) })
if err != nil { if err != nil {
return err return err
} }
defer comm.Disconnect()
for _, script := range scripts { for _, script := range scripts {
var cmd *remote.Cmd var cmd *remote.Cmd
@ -175,11 +164,11 @@ func (p *ResourceProvisioner) runScripts(
errR, errW := io.Pipe() errR, errW := io.Pipe()
outDoneCh := make(chan struct{}) outDoneCh := make(chan struct{})
errDoneCh := make(chan struct{}) errDoneCh := make(chan struct{})
go p.copyOutput(o, outR, outDoneCh) go copyOutput(o, outR, outDoneCh)
go p.copyOutput(o, errR, errDoneCh) go copyOutput(o, errR, errDoneCh)
remotePath := comm.ScriptPath() remotePath := comm.ScriptPath()
err = retryFunc(comm.Timeout(), func() error { err = retryFunc(ctx, comm.Timeout(), func() error {
if err := comm.UploadScript(remotePath, script); err != nil { if err := comm.UploadScript(remotePath, script); err != nil {
return fmt.Errorf("Failed to upload script: %v", err) return fmt.Errorf("Failed to upload script: %v", err)
} }
@ -202,6 +191,13 @@ func (p *ResourceProvisioner) runScripts(
} }
} }
// If we have an error, end our context so the disconnect happens.
// This has to happen before the output cleanup below since during
// an interrupt this will cause the outputs to end.
if err != nil {
cancelFunc()
}
// Wait for output to clean up // Wait for output to clean up
outW.Close() outW.Close()
errW.Close() errW.Close()
@ -225,7 +221,7 @@ func (p *ResourceProvisioner) runScripts(
return nil return nil
} }
func (p *ResourceProvisioner) copyOutput( func copyOutput(
o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
defer close(doneCh) defer close(doneCh)
lr := linereader.New(r) lr := linereader.New(r)
@ -235,19 +231,54 @@ func (p *ResourceProvisioner) copyOutput(
} }
// retryFunc is used to retry a function for a given duration // retryFunc is used to retry a function for a given duration
func retryFunc(timeout time.Duration, f func() error) error { func retryFunc(ctx context.Context, timeout time.Duration, f func() error) error {
finish := time.After(timeout) // Build a new context with the timeout
for { ctx, done := context.WithTimeout(ctx, timeout)
err := f() defer done()
if err == nil {
return nil
}
log.Printf("Retryable error: %v", err)
select { // Try the function in a goroutine
case <-finish: var errVal atomic.Value
return err doneCh := make(chan struct{})
case <-time.After(3 * time.Second): go func() {
defer close(doneCh)
for {
// If our context ended, we want to exit right away.
select {
case <-ctx.Done():
return
default:
}
// Try the function call
err := f()
if err == nil {
return
}
log.Printf("Retryable error: %v", err)
errVal.Store(err)
} }
}()
// Wait for completion
select {
case <-doneCh:
case <-ctx.Done():
} }
// Check if we have a context error to check if we're interrupted or timeout
switch ctx.Err() {
case context.Canceled:
return fmt.Errorf("interrupted")
case context.DeadlineExceeded:
return fmt.Errorf("timeout")
}
// Check if we got an error executing
if err, ok := errVal.Load().(error); ok {
return err
}
return nil
} }

View File

@ -9,18 +9,15 @@ import (
"reflect" "reflect"
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
}
func TestResourceProvider_Validate_good(t *testing.T) { func TestResourceProvider_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"inline": "echo foo", "inline": "echo foo",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -34,7 +31,7 @@ func TestResourceProvider_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{ c := testConfig(t, map[string]interface{}{
"invalid": "nope", "invalid": "nope",
}) })
p := new(ResourceProvisioner) p := Provisioner()
warn, errs := p.Validate(c) warn, errs := p.Validate(c)
if len(warn) > 0 { if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn) t.Fatalf("Warnings: %v", warn)
@ -51,16 +48,17 @@ exit 0
var expectedInlineScriptsOut = strings.Split(expectedScriptOut, "\n") var expectedInlineScriptsOut = strings.Split(expectedScriptOut, "\n")
func TestResourceProvider_generateScripts(t *testing.T) { func TestResourceProvider_generateScript(t *testing.T) {
p := new(ResourceProvisioner) p := Provisioner().(*schema.Provisioner)
conf := testConfig(t, map[string]interface{}{ conf := map[string]interface{}{
"inline": []interface{}{ "inline": []interface{}{
"cd /tmp", "cd /tmp",
"wget http://foobar", "wget http://foobar",
"exit 0", "exit 0",
}, },
}) }
out, err := p.generateScripts(conf) out, err := generateScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -71,16 +69,17 @@ func TestResourceProvider_generateScripts(t *testing.T) {
} }
func TestResourceProvider_CollectScripts_inline(t *testing.T) { func TestResourceProvider_CollectScripts_inline(t *testing.T) {
p := new(ResourceProvisioner) p := Provisioner().(*schema.Provisioner)
conf := testConfig(t, map[string]interface{}{ conf := map[string]interface{}{
"inline": []interface{}{ "inline": []interface{}{
"cd /tmp", "cd /tmp",
"wget http://foobar", "wget http://foobar",
"exit 0", "exit 0",
}, },
}) }
scripts, err := p.collectScripts(conf) scripts, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -103,12 +102,13 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) {
} }
func TestResourceProvider_CollectScripts_script(t *testing.T) { func TestResourceProvider_CollectScripts_script(t *testing.T) {
p := new(ResourceProvisioner) p := Provisioner().(*schema.Provisioner)
conf := testConfig(t, map[string]interface{}{ conf := map[string]interface{}{
"script": "test-fixtures/script1.sh", "script": "test-fixtures/script1.sh",
}) }
scripts, err := p.collectScripts(conf) scripts, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
@ -129,16 +129,17 @@ func TestResourceProvider_CollectScripts_script(t *testing.T) {
} }
func TestResourceProvider_CollectScripts_scripts(t *testing.T) { func TestResourceProvider_CollectScripts_scripts(t *testing.T) {
p := new(ResourceProvisioner) p := Provisioner().(*schema.Provisioner)
conf := testConfig(t, map[string]interface{}{ conf := map[string]interface{}{
"scripts": []interface{}{ "scripts": []interface{}{
"test-fixtures/script1.sh", "test-fixtures/script1.sh",
"test-fixtures/script1.sh", "test-fixtures/script1.sh",
"test-fixtures/script1.sh", "test-fixtures/script1.sh",
}, },
}) }
scripts, err := p.collectScripts(conf) scripts, err := collectScripts(schema.TestResourceDataRaw(
t, p.Schema, conf))
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -65,13 +65,15 @@ import (
vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault" vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault"
vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd" vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd"
vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere" vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere"
chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef" fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
fileresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file" localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
localexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec" remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
remoteexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
// Legacy, will remove once it conforms with new structure
chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
) )
var InternalProviders = map[string]plugin.ProviderFunc{ var InternalProviders = map[string]plugin.ProviderFunc{
@ -137,8 +139,13 @@ var InternalProviders = map[string]plugin.ProviderFunc{
} }
var InternalProvisioners = map[string]plugin.ProvisionerFunc{ var InternalProvisioners = map[string]plugin.ProvisionerFunc{
"chef": func() terraform.ResourceProvisioner { return new(chefresourceprovisioner.ResourceProvisioner) }, "file": fileprovisioner.Provisioner,
"file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) }, "local-exec": localexecprovisioner.Provisioner,
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) }, "remote-exec": remoteexecprovisioner.Provisioner,
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) }, }
func init() {
// Legacy provisioners that don't match our heuristics for auto-finding
// built-in provisioners.
InternalProvisioners["chef"] = func() terraform.ResourceProvisioner { return new(chefprovisioner.ResourceProvisioner) }
} }

View File

@ -42,6 +42,8 @@ type Communicator struct {
config *sshConfig config *sshConfig
conn net.Conn conn net.Conn
address string address string
lock sync.Mutex
} }
type sshConfig struct { type sshConfig struct {
@ -96,6 +98,10 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
// Connect implementation of communicator.Communicator interface // Connect implementation of communicator.Communicator interface
func (c *Communicator) Connect(o terraform.UIOutput) (err error) { func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
// Grab a lock so we can modify our internal attributes
c.lock.Lock()
defer c.lock.Unlock()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
} }
@ -190,8 +196,19 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
// Disconnect implementation of communicator.Communicator interface // Disconnect implementation of communicator.Communicator interface
func (c *Communicator) Disconnect() error { func (c *Communicator) Disconnect() error {
c.lock.Lock()
defer c.lock.Unlock()
if c.config.sshAgent != nil { if c.config.sshAgent != nil {
return c.config.sshAgent.Close() if err := c.config.sshAgent.Close(); err != nil {
return err
}
}
if c.conn != nil {
conn := c.conn
c.conn = nil
return conn.Close()
} }
return nil return nil

View File

@ -79,10 +79,35 @@ func (r *ConfigFieldReader) readField(
k := strings.Join(address, ".") k := strings.Join(address, ".")
schema := schemaList[len(schemaList)-1] schema := schemaList[len(schemaList)-1]
// If we're getting the single element of a promoted list, then
// check to see if we have a single element we need to promote.
if address[len(address)-1] == "0" && len(schemaList) > 1 {
lastSchema := schemaList[len(schemaList)-2]
if lastSchema.Type == TypeList && lastSchema.PromoteSingle {
k := strings.Join(address[:len(address)-1], ".")
result, err := r.readPrimitive(k, schema)
if err == nil {
return result, nil
}
}
}
switch schema.Type { switch schema.Type {
case TypeBool, TypeFloat, TypeInt, TypeString: case TypeBool, TypeFloat, TypeInt, TypeString:
return r.readPrimitive(k, schema) return r.readPrimitive(k, schema)
case TypeList: case TypeList:
// If we support promotion then we first check if we have a lone
// value that we must promote.
// a value that is alone.
if schema.PromoteSingle {
result, err := r.readPrimitive(k, schema.Elem.(*Schema))
if err == nil && result.Exists {
result.Value = []interface{}{result.Value}
return result, nil
}
}
return readListField(&nestedConfigFieldReader{r}, address, schema) return readListField(&nestedConfigFieldReader{r}, address, schema)
case TypeMap: case TypeMap:
return r.readMap(k, schema) return r.readMap(k, schema)

View File

@ -0,0 +1,180 @@
package schema
import (
"context"
"errors"
"fmt"
"sync"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
// Provisioner represents a resource provisioner in Terraform and properly
// implements all of the ResourceProvisioner API.
//
// This higher level structure makes it much easier to implement a new or
// custom provisioner for Terraform.
//
// The function callbacks for this structure are all passed a context object.
// This context object has a number of pre-defined values that can be accessed
// via the global functions defined in context.go.
type Provisioner struct {
// ConnSchema is the schema for the connection settings for this
// provisioner.
//
// The keys of this map are the configuration keys, and the value is
// the schema describing the value of the configuration.
//
// NOTE: The value of connection keys can only be strings for now.
ConnSchema map[string]*Schema
// Schema is the schema for the usage of this provisioner.
//
// The keys of this map are the configuration keys, and the value is
// the schema describing the value of the configuration.
Schema map[string]*Schema
// ApplyFunc is the function for executing the provisioner. This is required.
// It is given a context. See the Provisioner struct docs for more
// information.
ApplyFunc func(ctx context.Context) error
stopCtx context.Context
stopCtxCancel context.CancelFunc
stopOnce sync.Once
}
// These constants are the keys that can be used to access data in
// the context parameters for Provisioners.
const (
connDataInvalid int = iota
// This returns a *ResourceData for the connection information.
// Guaranteed to never be nil.
ProvConnDataKey
// This returns a *ResourceData for the config information.
// Guaranteed to never be nil.
ProvConfigDataKey
// This returns a terraform.UIOutput. Guaranteed to never be nil.
ProvOutputKey
// This returns the raw InstanceState passed to Apply. Guaranteed to
// be set, but may be nil.
ProvRawStateKey
)
// InternalValidate should be called to validate the structure
// of the provisioner.
//
// This should be called in a unit test to verify before release that this
// structure is properly configured for use.
func (p *Provisioner) InternalValidate() error {
if p == nil {
return errors.New("provisioner is nil")
}
var validationErrors error
{
sm := schemaMap(p.ConnSchema)
if err := sm.InternalValidate(sm); err != nil {
validationErrors = multierror.Append(validationErrors, err)
}
}
{
sm := schemaMap(p.Schema)
if err := sm.InternalValidate(sm); err != nil {
validationErrors = multierror.Append(validationErrors, err)
}
}
if p.ApplyFunc == nil {
validationErrors = multierror.Append(validationErrors, fmt.Errorf(
"ApplyFunc must not be nil"))
}
return validationErrors
}
// StopContext returns a context that checks whether a provisioner is stopped.
func (p *Provisioner) StopContext() context.Context {
p.stopOnce.Do(p.stopInit)
return p.stopCtx
}
func (p *Provisioner) stopInit() {
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
}
// Stop implementation of terraform.ResourceProvisioner interface.
func (p *Provisioner) Stop() error {
p.stopOnce.Do(p.stopInit)
p.stopCtxCancel()
return nil
}
func (p *Provisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {
return schemaMap(p.Schema).Validate(c)
}
// Apply implementation of terraform.ResourceProvisioner interface.
func (p *Provisioner) Apply(
o terraform.UIOutput,
s *terraform.InstanceState,
c *terraform.ResourceConfig) error {
var connData, configData *ResourceData
{
// We first need to turn the connection information into a
// terraform.ResourceConfig so that we can use that type to more
// easily build a ResourceData structure. We do this by simply treating
// the conn info as configuration input.
raw := make(map[string]interface{})
if s != nil {
for k, v := range s.Ephemeral.ConnInfo {
raw[k] = v
}
}
c, err := config.NewRawConfig(raw)
if err != nil {
return err
}
sm := schemaMap(p.ConnSchema)
diff, err := sm.Diff(nil, terraform.NewResourceConfig(c))
if err != nil {
return err
}
connData, err = sm.Data(nil, diff)
if err != nil {
return err
}
}
{
// Build the configuration data. Doing this requires making a "diff"
// even though that's never used. We use that just to get the correct types.
configMap := schemaMap(p.Schema)
diff, err := configMap.Diff(nil, c)
if err != nil {
return err
}
configData, err = configMap.Data(nil, diff)
if err != nil {
return err
}
}
// Build the context and call the function
ctx := p.StopContext()
ctx = context.WithValue(ctx, ProvConnDataKey, connData)
ctx = context.WithValue(ctx, ProvConfigDataKey, configData)
ctx = context.WithValue(ctx, ProvOutputKey, o)
ctx = context.WithValue(ctx, ProvRawStateKey, s)
return p.ApplyFunc(ctx)
}

View File

@ -0,0 +1,277 @@
package schema
import (
"context"
"fmt"
"testing"
"time"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
func TestProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(Provisioner)
}
func TestProvisionerValidate(t *testing.T) {
cases := []struct {
Name string
P *Provisioner
Config map[string]interface{}
Err bool
}{
{
"Basic required field",
&Provisioner{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
},
nil,
true,
},
{
"Basic required field set",
&Provisioner{
Schema: map[string]*Schema{
"foo": &Schema{
Required: true,
Type: TypeString,
},
},
},
map[string]interface{}{
"foo": "bar",
},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
_, es := tc.P.Validate(terraform.NewResourceConfig(c))
if len(es) > 0 != tc.Err {
t.Fatalf("%d: %#v", i, es)
}
})
}
}
func TestProvisionerApply(t *testing.T) {
cases := []struct {
Name string
P *Provisioner
Conn map[string]string
Config map[string]interface{}
Err bool
}{
{
"Basic config",
&Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
cd := ctx.Value(ProvConnDataKey).(*ResourceData)
d := ctx.Value(ProvConfigDataKey).(*ResourceData)
if d.Get("foo").(int) != 42 {
return fmt.Errorf("bad config data")
}
if cd.Get("foo").(string) != "bar" {
return fmt.Errorf("bad conn data")
}
return nil
},
},
map[string]string{
"foo": "bar",
},
map[string]interface{}{
"foo": 42,
},
false,
},
}
for i, tc := range cases {
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
c, err := config.NewRawConfig(tc.Config)
if err != nil {
t.Fatalf("err: %s", err)
}
state := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: tc.Conn,
},
}
err = tc.P.Apply(
nil, state, terraform.NewResourceConfig(c))
if err != nil != tc.Err {
t.Fatalf("%d: %s", i, err)
}
})
}
}
func TestProvisionerApply_nilState(t *testing.T) {
p := &Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
return nil
},
}
conf := map[string]interface{}{
"foo": 42,
}
c, err := config.NewRawConfig(conf)
if err != nil {
t.Fatalf("err: %s", err)
}
err = p.Apply(nil, nil, terraform.NewResourceConfig(c))
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerStop(t *testing.T) {
var p Provisioner
// Verify stopch blocks
ch := p.StopContext().Done()
select {
case <-ch:
t.Fatal("should not be stopped")
case <-time.After(10 * time.Millisecond):
}
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
select {
case <-ch:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}
func TestProvisionerStop_apply(t *testing.T) {
p := &Provisioner{
ConnSchema: map[string]*Schema{
"foo": &Schema{
Type: TypeString,
Optional: true,
},
},
Schema: map[string]*Schema{
"foo": &Schema{
Type: TypeInt,
Optional: true,
},
},
ApplyFunc: func(ctx context.Context) error {
<-ctx.Done()
return nil
},
}
conn := map[string]string{
"foo": "bar",
}
conf := map[string]interface{}{
"foo": 42,
}
c, err := config.NewRawConfig(conf)
if err != nil {
t.Fatalf("err: %s", err)
}
state := &terraform.InstanceState{
Ephemeral: terraform.EphemeralState{
ConnInfo: conn,
},
}
// Run the apply in a goroutine
doneCh := make(chan struct{})
go func() {
p.Apply(nil, state, terraform.NewResourceConfig(c))
close(doneCh)
}()
// Should block
select {
case <-doneCh:
t.Fatal("should not be done")
case <-time.After(10 * time.Millisecond):
}
// Stop!
p.Stop()
select {
case <-doneCh:
case <-time.After(10 * time.Millisecond):
t.Fatal("should be done")
}
}
func TestProvisionerStop_stopFirst(t *testing.T) {
var p Provisioner
// Stop it
if err := p.Stop(); err != nil {
t.Fatalf("err: %s", err)
}
select {
case <-p.StopContext().Done():
case <-time.After(10 * time.Millisecond):
t.Fatal("should be stopped")
}
}

View File

@ -118,9 +118,16 @@ type Schema struct {
// TypeSet or TypeList. Specific use cases would be if a TypeSet is being // TypeSet or TypeList. Specific use cases would be if a TypeSet is being
// used to wrap a complex structure, however less than one instance would // used to wrap a complex structure, however less than one instance would
// cause instability. // cause instability.
Elem interface{} //
MaxItems int // PromoteSingle, if true, will allow single elements to be standalone
MinItems int // and promote them to a list. For example "foo" would be promoted to
// ["foo"] automatically. This is primarily for legacy reasons and the
// ambiguity is not recommended for new usage. Promotion is only allowed
// for primitive element types.
Elem interface{}
MaxItems int
MinItems int
PromoteSingle bool
// The following fields are only valid for a TypeSet type. // The following fields are only valid for a TypeSet type.
// //
@ -1163,6 +1170,14 @@ func (m schemaMap) validateList(
// We use reflection to verify the slice because you can't // We use reflection to verify the slice because you can't
// case to []interface{} unless the slice is exactly that type. // case to []interface{} unless the slice is exactly that type.
rawV := reflect.ValueOf(raw) rawV := reflect.ValueOf(raw)
// If we support promotion and the raw value isn't a slice, wrap
// it in []interface{} and check again.
if schema.PromoteSingle && rawV.Kind() != reflect.Slice {
raw = []interface{}{raw}
rawV = reflect.ValueOf(raw)
}
if rawV.Kind() != reflect.Slice { if rawV.Kind() != reflect.Slice {
return nil, []error{fmt.Errorf( return nil, []error{fmt.Errorf(
"%s: should be a list", k)} "%s: should be a list", k)}

View File

@ -582,6 +582,72 @@ func TestSchemaMap_Diff(t *testing.T) {
Err: false, Err: false,
}, },
{
Name: "List decode with promotion",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
},
},
State: nil,
Config: map[string]interface{}{
"ports": "5",
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{
Name: "List decode with promotion with list",
Schema: map[string]*Schema{
"ports": &Schema{
Type: TypeList,
Required: true,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
},
},
State: nil,
Config: map[string]interface{}{
"ports": []interface{}{"5"},
},
Diff: &terraform.InstanceDiff{
Attributes: map[string]*terraform.ResourceAttrDiff{
"ports.#": &terraform.ResourceAttrDiff{
Old: "0",
New: "1",
},
"ports.0": &terraform.ResourceAttrDiff{
Old: "",
New: "5",
},
},
},
Err: false,
},
{ {
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ports": &Schema{ "ports": &Schema{
@ -3853,6 +3919,40 @@ func TestSchemaMap_Validate(t *testing.T) {
Err: true, Err: true,
}, },
"List with promotion": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
Optional: true,
},
},
Config: map[string]interface{}{
"ingress": "5",
},
Err: false,
},
"List with promotion set as list": {
Schema: map[string]*Schema{
"ingress": &Schema{
Type: TypeList,
Elem: &Schema{Type: TypeInt},
PromoteSingle: true,
Optional: true,
},
},
Config: map[string]interface{}{
"ingress": []interface{}{"5"},
},
Err: false,
},
"Optional sub-resource": { "Optional sub-resource": {
Schema: map[string]*Schema{ Schema: map[string]*Schema{
"ingress": &Schema{ "ingress": &Schema{

30
helper/schema/testing.go Normal file
View File

@ -0,0 +1,30 @@
package schema
import (
"testing"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/terraform"
)
// TestResourceDataRaw creates a ResourceData from a raw configuration map.
func TestResourceDataRaw(
t *testing.T, schema map[string]*Schema, raw map[string]interface{}) *ResourceData {
c, err := config.NewRawConfig(raw)
if err != nil {
t.Fatalf("err: %s", err)
}
sm := schemaMap(schema)
diff, err := sm.Diff(nil, terraform.NewResourceConfig(c))
if err != nil {
t.Fatalf("err: %s", err)
}
result, err := sm.Data(nil, diff)
if err != nil {
t.Fatalf("err: %s", err)
}
return result
}

View File

@ -77,6 +77,19 @@ func (p *ResourceProvisioner) Apply(
return err return err
} }
func (p *ResourceProvisioner) Stop() error {
var resp ResourceProvisionerStopResponse
err := p.Client.Call("Plugin.Stop", new(interface{}), &resp)
if err != nil {
return err
}
if resp.Error != nil {
err = resp.Error
}
return err
}
func (p *ResourceProvisioner) Close() error { func (p *ResourceProvisioner) Close() error {
return p.Client.Close() return p.Client.Close()
} }
@ -100,6 +113,10 @@ type ResourceProvisionerApplyResponse struct {
Error *plugin.BasicError Error *plugin.BasicError
} }
type ResourceProvisionerStopResponse struct {
Error *plugin.BasicError
}
// ResourceProvisionerServer is a net/rpc compatible structure for serving // ResourceProvisionerServer is a net/rpc compatible structure for serving
// a ResourceProvisioner. This should not be used directly. // a ResourceProvisioner. This should not be used directly.
type ResourceProvisionerServer struct { type ResourceProvisionerServer struct {
@ -143,3 +160,14 @@ func (s *ResourceProvisionerServer) Validate(
} }
return nil return nil
} }
func (s *ResourceProvisionerServer) Stop(
_ interface{},
reply *ResourceProvisionerStopResponse) error {
err := s.Provisioner.Stop()
*reply = ResourceProvisionerStopResponse{
Error: plugin.NewBasicError(err),
}
return nil
}

View File

@ -14,6 +14,61 @@ func TestResourceProvisioner_impl(t *testing.T) {
var _ terraform.ResourceProvisioner = new(ResourceProvisioner) var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
} }
func TestResourceProvisioner_stop(t *testing.T) {
// Create a mock provider
p := new(terraform.MockResourceProvisioner)
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvisioner)
// Stop
e := provider.Stop()
if !p.StopCalled {
t.Fatal("stop should be called")
}
if e != nil {
t.Fatalf("bad: %#v", e)
}
}
func TestResourceProvisioner_stopErrors(t *testing.T) {
p := new(terraform.MockResourceProvisioner)
p.StopReturnError = errors.New("foo")
// Create a mock provider
client, _ := plugin.TestPluginRPCConn(t, pluginMap(&ServeOpts{
ProvisionerFunc: testProvisionerFixed(p),
}))
defer client.Close()
// Request the provider
raw, err := client.Dispense(ProvisionerPluginName)
if err != nil {
t.Fatalf("err: %s", err)
}
provider := raw.(terraform.ResourceProvisioner)
// Stop
e := provider.Stop()
if !p.StopCalled {
t.Fatal("stop should be called")
}
if e == nil {
t.Fatal("should have error")
}
if e.Error() != "foo" {
t.Fatalf("bad: %s", e)
}
}
func TestResourceProvisioner_apply(t *testing.T) { func TestResourceProvisioner_apply(t *testing.T) {
// Create a mock provider // Create a mock provider
p := new(terraform.MockResourceProvisioner) p := new(terraform.MockResourceProvisioner)

View File

@ -19,7 +19,7 @@ var Handshake = plugin.HandshakeConfig{
// one or the other that makes it so that they can't safely communicate. // one or the other that makes it so that they can't safely communicate.
// This could be adding a new interface value, it could be how // This could be adding a new interface value, it could be how
// helper/schema computes diffs, etc. // helper/schema computes diffs, etc.
ProtocolVersion: 2, ProtocolVersion: 3,
// The magic cookie values should NEVER be changed. // The magic cookie values should NEVER be changed.
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE", MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",

View File

@ -91,7 +91,7 @@ func makeProviderMap(items []plugin) string {
func makeProvisionerMap(items []plugin) string { func makeProvisionerMap(items []plugin) string {
output := "" output := ""
for _, item := range items { for _, item := range items {
output += fmt.Sprintf("\t\"%s\": func() terraform.ResourceProvisioner { return new(%s.%s) },\n", item.PluginName, item.ImportName, item.TypeName) output += fmt.Sprintf("\t\"%s\": %s.%s,\n", item.PluginName, item.ImportName, item.TypeName)
} }
return output return output
} }
@ -254,8 +254,8 @@ func discoverProviders() ([]plugin, error) {
func discoverProvisioners() ([]plugin, error) { func discoverProvisioners() ([]plugin, error) {
path := "./builtin/provisioners" path := "./builtin/provisioners"
typeID := "ResourceProvisioner" typeID := "terraform.ResourceProvisioner"
typeName := "" typeName := "Provisioner"
return discoverTypesInPath(path, typeID, typeName) return discoverTypesInPath(path, typeID, typeName)
} }
@ -270,6 +270,9 @@ import (
IMPORTS IMPORTS
"github.com/hashicorp/terraform/plugin" "github.com/hashicorp/terraform/plugin"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
// Legacy, will remove once it conforms with new structure
chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
) )
var InternalProviders = map[string]plugin.ProviderFunc{ var InternalProviders = map[string]plugin.ProviderFunc{
@ -280,4 +283,10 @@ var InternalProvisioners = map[string]plugin.ProvisionerFunc{
PROVISIONERS PROVISIONERS
} }
func init() {
// Legacy provisioners that don't match our heuristics for auto-finding
// built-in provisioners.
InternalProvisioners["chef"] = func() terraform.ResourceProvisioner { return new(chefprovisioner.ResourceProvisioner) }
}
` `

View File

@ -7,29 +7,29 @@ func TestMakeProvisionerMap(t *testing.T) {
{ {
Package: "file", Package: "file",
PluginName: "file", PluginName: "file",
TypeName: "ResourceProvisioner", TypeName: "Provisioner",
Path: "builtin/provisioners/file", Path: "builtin/provisioners/file",
ImportName: "fileresourceprovisioner", ImportName: "fileprovisioner",
}, },
{ {
Package: "localexec", Package: "localexec",
PluginName: "local-exec", PluginName: "local-exec",
TypeName: "ResourceProvisioner", TypeName: "Provisioner",
Path: "builtin/provisioners/local-exec", Path: "builtin/provisioners/local-exec",
ImportName: "localexecresourceprovisioner", ImportName: "localexecprovisioner",
}, },
{ {
Package: "remoteexec", Package: "remoteexec",
PluginName: "remote-exec", PluginName: "remote-exec",
TypeName: "ResourceProvisioner", TypeName: "Provisioner",
Path: "builtin/provisioners/remote-exec", Path: "builtin/provisioners/remote-exec",
ImportName: "remoteexecresourceprovisioner", ImportName: "remoteexecprovisioner",
}, },
}) })
expected := ` "file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) }, expected := ` "file": fileprovisioner.Provisioner,
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) }, "local-exec": localexecprovisioner.Provisioner,
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) }, "remote-exec": remoteexecprovisioner.Provisioner,
` `
if p != expected { if p != expected {
@ -86,13 +86,10 @@ func TestDiscoverTypesProviders(t *testing.T) {
} }
func TestDiscoverTypesProvisioners(t *testing.T) { func TestDiscoverTypesProvisioners(t *testing.T) {
plugins, err := discoverTypesInPath("../builtin/provisioners", "ResourceProvisioner", "") plugins, err := discoverTypesInPath("../builtin/provisioners", "terraform.ResourceProvisioner", "Provisioner")
if err != nil { if err != nil {
t.Fatalf(err.Error()) t.Fatalf(err.Error())
} }
if !contains(plugins, "chef") {
t.Errorf("Expected to find chef provisioner")
}
if !contains(plugins, "remote-exec") { if !contains(plugins, "remote-exec") {
t.Errorf("Expected to find remote-exec provisioner") t.Errorf("Expected to find remote-exec provisioner")
} }

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"sort" "sort"
@ -91,8 +92,10 @@ type Context struct {
l sync.Mutex // Lock acquired during any task l sync.Mutex // Lock acquired during any task
parallelSem Semaphore parallelSem Semaphore
providerInputConfig map[string]map[string]interface{} providerInputConfig map[string]map[string]interface{}
runCh <-chan struct{} runLock sync.Mutex
stopCh chan struct{} runCond *sync.Cond
runContext context.Context
runContextCancel context.CancelFunc
shadowErr error shadowErr error
} }
@ -320,8 +323,7 @@ func (c *Context) Interpolater() *Interpolater {
// This modifies the configuration in-place, so asking for Input twice // This modifies the configuration in-place, so asking for Input twice
// may result in different UI output showing different current values. // may result in different UI output showing different current values.
func (c *Context) Input(mode InputMode) error { func (c *Context) Input(mode InputMode) error {
v := c.acquireRun("input") defer c.acquireRun("input")()
defer c.releaseRun(v)
if mode&InputModeVar != 0 { if mode&InputModeVar != 0 {
// Walk the variables first for the root module. We walk them in // Walk the variables first for the root module. We walk them in
@ -440,8 +442,7 @@ func (c *Context) Input(mode InputMode) error {
// In addition to returning the resulting state, this context is updated // In addition to returning the resulting state, this context is updated
// with the latest state. // with the latest state.
func (c *Context) Apply() (*State, error) { func (c *Context) Apply() (*State, error) {
v := c.acquireRun("apply") defer c.acquireRun("apply")()
defer c.releaseRun(v)
// Copy our own state // Copy our own state
c.state = c.state.DeepCopy() c.state = c.state.DeepCopy()
@ -478,8 +479,7 @@ func (c *Context) Apply() (*State, error) {
// Plan also updates the diff of this context to be the diff generated // Plan also updates the diff of this context to be the diff generated
// by the plan, so Apply can be called after. // by the plan, so Apply can be called after.
func (c *Context) Plan() (*Plan, error) { func (c *Context) Plan() (*Plan, error) {
v := c.acquireRun("plan") defer c.acquireRun("plan")()
defer c.releaseRun(v)
p := &Plan{ p := &Plan{
Module: c.module, Module: c.module,
@ -569,8 +569,7 @@ func (c *Context) Plan() (*Plan, error) {
// Even in the case an error is returned, the state will be returned and // Even in the case an error is returned, the state will be returned and
// will potentially be partially updated. // will potentially be partially updated.
func (c *Context) Refresh() (*State, error) { func (c *Context) Refresh() (*State, error) {
v := c.acquireRun("refresh") defer c.acquireRun("refresh")()
defer c.releaseRun(v)
// Copy our own state // Copy our own state
c.state = c.state.DeepCopy() c.state = c.state.DeepCopy()
@ -596,30 +595,34 @@ func (c *Context) Refresh() (*State, error) {
// //
// Stop will block until the task completes. // Stop will block until the task completes.
func (c *Context) Stop() { func (c *Context) Stop() {
c.l.Lock() log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence")
ch := c.runCh
// If we aren't running, then just return c.l.Lock()
if ch == nil { defer c.l.Unlock()
c.l.Unlock()
return // If we're running, then stop
if c.runContextCancel != nil {
log.Printf("[WARN] terraform: run context exists, stopping")
// Tell the hook we want to stop
c.sh.Stop()
// Stop the context
c.runContextCancel()
c.runContextCancel = nil
} }
// Tell the hook we want to stop // Grab the condition var before we exit
c.sh.Stop() if cond := c.runCond; cond != nil {
cond.Wait()
}
// Close the stop channel log.Printf("[WARN] terraform: stop complete")
close(c.stopCh)
// Wait for us to stop
c.l.Unlock()
<-ch
} }
// Validate validates the configuration and returns any warnings or errors. // Validate validates the configuration and returns any warnings or errors.
func (c *Context) Validate() ([]string, []error) { func (c *Context) Validate() ([]string, []error) {
v := c.acquireRun("validate") defer c.acquireRun("validate")()
defer c.releaseRun(v)
var errs error var errs error
@ -680,26 +683,25 @@ func (c *Context) SetVariable(k string, v interface{}) {
c.variables[k] = v c.variables[k] = v
} }
func (c *Context) acquireRun(phase string) chan<- struct{} { func (c *Context) acquireRun(phase string) func() {
// With the run lock held, grab the context lock to make changes
// to the run context.
c.l.Lock() c.l.Lock()
defer c.l.Unlock() defer c.l.Unlock()
dbug.SetPhase(phase) // Wait until we're no longer running
for c.runCond != nil {
// Wait for no channel to exist c.runCond.Wait()
for c.runCh != nil {
c.l.Unlock()
ch := c.runCh
<-ch
c.l.Lock()
} }
// Create the new channel // Build our lock
ch := make(chan struct{}) c.runCond = sync.NewCond(&c.l)
c.runCh = ch
// Reset the stop channel so we can watch that // Setup debugging
c.stopCh = make(chan struct{}) dbug.SetPhase(phase)
// Create a new run context
c.runContext, c.runContextCancel = context.WithCancel(context.Background())
// Reset the stop hook so we're not stopped // Reset the stop hook so we're not stopped
c.sh.Reset() c.sh.Reset()
@ -707,10 +709,11 @@ func (c *Context) acquireRun(phase string) chan<- struct{} {
// Reset the shadow errors // Reset the shadow errors
c.shadowErr = nil c.shadowErr = nil
return ch return c.releaseRun
} }
func (c *Context) releaseRun(ch chan<- struct{}) { func (c *Context) releaseRun() {
// Grab the context lock so that we can make modifications to fields
c.l.Lock() c.l.Lock()
defer c.l.Unlock() defer c.l.Unlock()
@ -719,9 +722,19 @@ func (c *Context) releaseRun(ch chan<- struct{}) {
// phase // phase
dbug.SetPhase("INVALID") dbug.SetPhase("INVALID")
close(ch) // End our run. We check if runContext is non-nil because it can be
c.runCh = nil // set to nil if it was cancelled via Stop()
c.stopCh = nil if c.runContextCancel != nil {
c.runContextCancel()
}
// Unlock all waiting our condition
cond := c.runCond
c.runCond = nil
cond.Broadcast()
// Unset the context
c.runContext = nil
} }
func (c *Context) walk( func (c *Context) walk(
@ -753,13 +766,15 @@ func (c *Context) walk(
log.Printf("[DEBUG] Starting graph walk: %s", operation.String()) log.Printf("[DEBUG] Starting graph walk: %s", operation.String())
walker := &ContextGraphWalker{ walker := &ContextGraphWalker{
Context: realCtx, Context: realCtx,
Operation: operation, Operation: operation,
StopContext: c.runContext,
} }
// Watch for a stop so we can call the provider Stop() API. // Watch for a stop so we can call the provider Stop() API.
doneCh := make(chan struct{}) doneCh := make(chan struct{})
go c.watchStop(walker, c.stopCh, doneCh) stopCh := c.runContext.Done()
go c.watchStop(walker, doneCh, stopCh)
// Walk the real graph, this will block until it completes // Walk the real graph, this will block until it completes
realErr := graph.Walk(walker) realErr := graph.Walk(walker)
@ -854,7 +869,7 @@ func (c *Context) walk(
return walker, realErr return walker, realErr
} }
func (c *Context) watchStop(walker *ContextGraphWalker, stopCh, doneCh <-chan struct{}) { func (c *Context) watchStop(walker *ContextGraphWalker, doneCh, stopCh <-chan struct{}) {
// Wait for a stop or completion // Wait for a stop or completion
select { select {
case <-stopCh: case <-stopCh:
@ -866,20 +881,39 @@ func (c *Context) watchStop(walker *ContextGraphWalker, stopCh, doneCh <-chan st
// If we're here, we're stopped, trigger the call. // If we're here, we're stopped, trigger the call.
// Copy the providers so that a misbehaved blocking Stop doesn't {
// completely hang Terraform. // Copy the providers so that a misbehaved blocking Stop doesn't
walker.providerLock.Lock() // completely hang Terraform.
ps := make([]ResourceProvider, 0, len(walker.providerCache)) walker.providerLock.Lock()
for _, p := range walker.providerCache { ps := make([]ResourceProvider, 0, len(walker.providerCache))
ps = append(ps, p) for _, p := range walker.providerCache {
} ps = append(ps, p)
defer walker.providerLock.Unlock() }
defer walker.providerLock.Unlock()
for _, p := range ps { for _, p := range ps {
// We ignore the error for now since there isn't any reasonable // We ignore the error for now since there isn't any reasonable
// action to take if there is an error here, since the stop is still // action to take if there is an error here, since the stop is still
// advisory: Terraform will exit once the graph node completes. // advisory: Terraform will exit once the graph node completes.
p.Stop() p.Stop()
}
}
{
// Call stop on all the provisioners
walker.provisionerLock.Lock()
ps := make([]ResourceProvisioner, 0, len(walker.provisionerCache))
for _, p := range walker.provisionerCache {
ps = append(ps, p)
}
defer walker.provisionerLock.Unlock()
for _, p := range ps {
// We ignore the error for now since there isn't any reasonable
// action to take if there is an error here, since the stop is still
// advisory: Terraform will exit once the graph node completes.
p.Stop()
}
} }
} }

View File

@ -1720,6 +1720,145 @@ func TestContext2Apply_cancel(t *testing.T) {
} }
} }
func TestContext2Apply_cancelBlock(t *testing.T) {
m := testModule(t, "apply-cancel-block")
p := testProvider("aws")
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
})
applyCh := make(chan struct{})
p.DiffFn = testDiffFn
p.ApplyFn = func(*InstanceInfo, *InstanceState, *InstanceDiff) (*InstanceState, error) {
close(applyCh)
for !ctx.sh.Stopped() {
// Wait for stop to be called
}
// Sleep
time.Sleep(100 * time.Millisecond)
return &InstanceState{
ID: "foo",
}, nil
}
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
// Start the Apply in a goroutine
var applyErr error
stateCh := make(chan *State)
go func() {
state, err := ctx.Apply()
if err != nil {
applyErr = err
}
stateCh <- state
}()
stopDone := make(chan struct{})
go func() {
defer close(stopDone)
<-applyCh
ctx.Stop()
}()
// Make sure that stop blocks
select {
case <-stopDone:
t.Fatal("stop should block")
case <-time.After(10 * time.Millisecond):
}
// Wait for stop
select {
case <-stopDone:
case <-time.After(500 * time.Millisecond):
t.Fatal("stop should be done")
}
// Wait for apply to complete
state := <-stateCh
if applyErr != nil {
t.Fatalf("err: %s", applyErr)
}
checkStateString(t, state, `
aws_instance.foo:
ID = foo
`)
}
func TestContext2Apply_cancelProvisioner(t *testing.T) {
m := testModule(t, "apply-cancel-provisioner")
p := testProvider("aws")
p.ApplyFn = testApplyFn
p.DiffFn = testDiffFn
pr := testProvisioner()
ctx := testContext2(t, &ContextOpts{
Module: m,
Providers: map[string]ResourceProviderFactory{
"aws": testProviderFuncFixed(p),
},
Provisioners: map[string]ResourceProvisionerFactory{
"shell": testProvisionerFuncFixed(pr),
},
})
prStopped := make(chan struct{})
pr.ApplyFn = func(rs *InstanceState, c *ResourceConfig) error {
// Start the stop process
go ctx.Stop()
<-prStopped
return nil
}
pr.StopFn = func() error {
close(prStopped)
return nil
}
if _, err := ctx.Plan(); err != nil {
t.Fatalf("err: %s", err)
}
// Start the Apply in a goroutine
var applyErr error
stateCh := make(chan *State)
go func() {
state, err := ctx.Apply()
if err != nil {
applyErr = err
}
stateCh <- state
}()
// Wait for completion
state := <-stateCh
if applyErr != nil {
t.Fatalf("err: %s", applyErr)
}
checkStateString(t, state, `
aws_instance.foo: (tainted)
ID = foo
num = 2
type = aws_instance
`)
if !pr.StopCalled {
t.Fatal("stop should be called")
}
}
func TestContext2Apply_compute(t *testing.T) { func TestContext2Apply_compute(t *testing.T) {
m := testModule(t, "apply-compute") m := testModule(t, "apply-compute")
p := testProvider("aws") p := testProvider("aws")

View File

@ -40,8 +40,7 @@ type ImportTarget struct {
// imported. // imported.
func (c *Context) Import(opts *ImportOpts) (*State, error) { func (c *Context) Import(opts *ImportOpts) (*State, error) {
// Hold a lock since we can modify our own state here // Hold a lock since we can modify our own state here
v := c.acquireRun("import") defer c.acquireRun("import")()
defer c.releaseRun(v)
// Copy our own state // Copy our own state
c.state = c.state.DeepCopy() c.state = c.state.DeepCopy()

View File

@ -928,6 +928,7 @@ func TestContext2Validate_PlanGraphBuilder(t *testing.T) {
Targets: c.targets, Targets: c.targets,
}).Build(RootModulePath) }).Build(RootModulePath)
defer c.acquireRun("validate-test")()
walker, err := c.walk(graph, graph, walkValidate) walker, err := c.walk(graph, graph, walkValidate)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -8,6 +8,10 @@ import (
// EvalContext is the interface that is given to eval nodes to execute. // EvalContext is the interface that is given to eval nodes to execute.
type EvalContext interface { type EvalContext interface {
// Stopped returns a channel that is closed when evaluation is stopped
// via Terraform.Context.Stop()
Stopped() <-chan struct{}
// Path is the current module path. // Path is the current module path.
Path() []string Path() []string

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"strings" "strings"
@ -12,6 +13,9 @@ import (
// BuiltinEvalContext is an EvalContext implementation that is used by // BuiltinEvalContext is an EvalContext implementation that is used by
// Terraform by default. // Terraform by default.
type BuiltinEvalContext struct { type BuiltinEvalContext struct {
// StopContext is the context used to track whether we're complete
StopContext context.Context
// PathValue is the Path that this context is operating within. // PathValue is the Path that this context is operating within.
PathValue []string PathValue []string
@ -43,6 +47,15 @@ type BuiltinEvalContext struct {
once sync.Once once sync.Once
} }
func (ctx *BuiltinEvalContext) Stopped() <-chan struct{} {
// This can happen during tests. During tests, we just block forever.
if ctx.StopContext == nil {
return nil
}
return ctx.StopContext.Done()
}
func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error {
for _, h := range ctx.Hooks { for _, h := range ctx.Hooks {
action, err := fn(h) action, err := fn(h)

View File

@ -9,6 +9,9 @@ import (
// MockEvalContext is a mock version of EvalContext that can be used // MockEvalContext is a mock version of EvalContext that can be used
// for tests. // for tests.
type MockEvalContext struct { type MockEvalContext struct {
StoppedCalled bool
StoppedValue <-chan struct{}
HookCalled bool HookCalled bool
HookHook Hook HookHook Hook
HookError error HookError error
@ -85,6 +88,11 @@ type MockEvalContext struct {
StateLock *sync.RWMutex StateLock *sync.RWMutex
} }
func (c *MockEvalContext) Stopped() <-chan struct{} {
c.StoppedCalled = true
return c.StoppedValue
}
func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error {
c.HookCalled = true c.HookCalled = true
if c.HookHook != nil { if c.HookHook != nil {

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"context"
"fmt" "fmt"
"log" "log"
"sync" "sync"
@ -15,8 +16,9 @@ type ContextGraphWalker struct {
NullGraphWalker NullGraphWalker
// Configurable values // Configurable values
Context *Context Context *Context
Operation walkOperation Operation walkOperation
StopContext context.Context
// Outputs, do not set these. Do not read these while the graph // Outputs, do not set these. Do not read these while the graph
// is being walked. // is being walked.
@ -65,6 +67,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
w.interpolaterVarLock.Unlock() w.interpolaterVarLock.Unlock()
ctx := &BuiltinEvalContext{ ctx := &BuiltinEvalContext{
StopContext: w.StopContext,
PathValue: path, PathValue: path,
Hooks: w.Context.hooks, Hooks: w.Context.hooks,
InputValue: w.Context.uiInput, InputValue: w.Context.uiInput,

View File

@ -21,6 +21,26 @@ type ResourceProvisioner interface {
// is provided since provisioners only run after a resource has been // is provided since provisioners only run after a resource has been
// newly created. // newly created.
Apply(UIOutput, *InstanceState, *ResourceConfig) error Apply(UIOutput, *InstanceState, *ResourceConfig) error
// Stop is called when the provisioner should halt any in-flight actions.
//
// This can be used to make a nicer Ctrl-C experience for Terraform.
// Even if this isn't implemented to do anything (just returns nil),
// Terraform will still cleanly stop after the currently executing
// graph node is complete. However, this API can be used to make more
// efficient halts.
//
// Stop doesn't have to and shouldn't block waiting for in-flight actions
// to complete. It should take any action it wants and return immediately
// acknowledging it has received the stop request. Terraform core will
// automatically not make any further API calls to the provider soon
// after Stop is called (technically exactly once the currently executing
// graph nodes are complete).
//
// The error returned, if non-nil, is assumed to mean that signaling the
// stop somehow failed and that the user should expect potentially waiting
// a longer period of time.
Stop() error
} }
// ResourceProvisionerCloser is an interface that provisioners that can close // ResourceProvisionerCloser is an interface that provisioners that can close

View File

@ -21,6 +21,10 @@ type MockResourceProvisioner struct {
ValidateFn func(c *ResourceConfig) ([]string, []error) ValidateFn func(c *ResourceConfig) ([]string, []error)
ValidateReturnWarns []string ValidateReturnWarns []string
ValidateReturnErrors []error ValidateReturnErrors []error
StopCalled bool
StopFn func() error
StopReturnError error
} }
func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) { func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) {
@ -40,14 +44,29 @@ func (p *MockResourceProvisioner) Apply(
state *InstanceState, state *InstanceState,
c *ResourceConfig) error { c *ResourceConfig) error {
p.Lock() p.Lock()
defer p.Unlock()
p.ApplyCalled = true p.ApplyCalled = true
p.ApplyOutput = output p.ApplyOutput = output
p.ApplyState = state p.ApplyState = state
p.ApplyConfig = c p.ApplyConfig = c
if p.ApplyFn != nil { if p.ApplyFn != nil {
return p.ApplyFn(state, c) fn := p.ApplyFn
p.Unlock()
return fn(state, c)
} }
defer p.Unlock()
return p.ApplyReturnError return p.ApplyReturnError
} }
func (p *MockResourceProvisioner) Stop() error {
p.Lock()
defer p.Unlock()
p.StopCalled = true
if p.StopFn != nil {
return p.StopFn()
}
return p.StopReturnError
}

View File

@ -88,7 +88,8 @@ func newShadowContext(c *Context) (*Context, *Context, Shadow) {
// l - no copy // l - no copy
parallelSem: c.parallelSem, parallelSem: c.parallelSem,
providerInputConfig: c.providerInputConfig, providerInputConfig: c.providerInputConfig,
runCh: c.runCh, runContext: c.runContext,
runContextCancel: c.runContextCancel,
shadowErr: c.shadowErr, shadowErr: c.shadowErr,
} }

View File

@ -112,6 +112,10 @@ func (p *shadowResourceProvisionerReal) Apply(
return err return err
} }
func (p *shadowResourceProvisionerReal) Stop() error {
return p.ResourceProvisioner.Stop()
}
// shadowResourceProvisionerShadow is the shadow resource provisioner. Function // shadowResourceProvisionerShadow is the shadow resource provisioner. Function
// calls never affect real resources. This is paired with the "real" side // calls never affect real resources. This is paired with the "real" side
// which must be called properly to enable recording. // which must be called properly to enable recording.
@ -228,6 +232,13 @@ func (p *shadowResourceProvisionerShadow) Apply(
return result.ResultErr return result.ResultErr
} }
func (p *shadowResourceProvisionerShadow) Stop() error {
// For the shadow, we always just return nil since a Stop indicates
// that we were interrupted and shadows are disabled during interrupts
// anyways.
return nil
}
// The structs for the various function calls are put below. These structs // The structs for the various function calls are put below. These structs
// are used to carry call information across the real/shadow boundaries. // are used to carry call information across the real/shadow boundaries.

View File

@ -0,0 +1,3 @@
resource "aws_instance" "foo" {
num = "2"
}

View File

@ -0,0 +1,7 @@
resource "aws_instance" "foo" {
num = "2"
provisioner "shell" {
foo = "bar"
}
}