Merge pull request #10934 from hashicorp/f-provisioner-stop
core: stoppable provisioners, helper/schema for provisioners
This commit is contained in:
commit
61881d2795
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
@ -8,26 +9,48 @@ import (
|
|||
"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
|
||||
}
|
||||
|
@ -35,58 +58,35 @@ func (p *ResourceProvisioner) Apply(
|
|||
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)
|
||||
}
|
||||
// Begin the file copy
|
||||
dst := data.Get("destination").(string)
|
||||
resultCh := make(chan error, 1)
|
||||
go func() {
|
||||
resultCh <- 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))
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
|
|
@ -5,15 +5,12 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceProvisioner_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
|
||||
}
|
||||
|
||||
func TestResourceProvider_Apply(t *testing.T) {
|
||||
defer os.Remove("test_out")
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
|
@ -21,7 +18,7 @@ func TestResourceProvider_Apply(t *testing.T) {
|
|||
})
|
||||
|
||||
output := new(terraform.MockUIOutput)
|
||||
p := new(ResourceProvisioner)
|
||||
p := Provisioner()
|
||||
if err := p.Apply(output, nil, c); err != nil {
|
||||
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) {
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"command": "echo foo",
|
||||
})
|
||||
p := new(ResourceProvisioner)
|
||||
p := Provisioner()
|
||||
warn, errs := p.Validate(c)
|
||||
if len(warn) > 0 {
|
||||
t.Fatalf("Warnings: %v", warn)
|
||||
|
@ -55,7 +83,7 @@ func TestResourceProvider_Validate_good(t *testing.T) {
|
|||
|
||||
func TestResourceProvider_Validate_missing(t *testing.T) {
|
||||
c := testConfig(t, map[string]interface{}{})
|
||||
p := new(ResourceProvisioner)
|
||||
p := Provisioner()
|
||||
warn, errs := p.Validate(c)
|
||||
if len(warn) > 0 {
|
||||
t.Fatalf("Warnings: %v", warn)
|
||||
|
|
|
@ -2,35 +2,65 @@ package remoteexec
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/communicator"
|
||||
"github.com/hashicorp/terraform/communicator/remote"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/mitchellh/go-linereader"
|
||||
)
|
||||
|
||||
// ResourceProvisioner represents a remote exec provisioner
|
||||
type ResourceProvisioner struct{}
|
||||
func Provisioner() terraform.ResourceProvisioner {
|
||||
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
|
||||
func (p *ResourceProvisioner) Apply(
|
||||
o terraform.UIOutput,
|
||||
s *terraform.InstanceState,
|
||||
c *terraform.ResourceConfig) error {
|
||||
func applyFn(ctx context.Context) error {
|
||||
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
|
||||
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
||||
|
||||
// Get a new communicator
|
||||
comm, err := communicator.New(s)
|
||||
comm, err := communicator.New(connState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Collect the scripts
|
||||
scripts, err := p.collectScripts(c)
|
||||
scripts, err := collectScripts(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -39,67 +69,33 @@ func (p *ResourceProvisioner) Apply(
|
|||
}
|
||||
|
||||
// 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 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
|
||||
func (p *ResourceProvisioner) generateScripts(c *terraform.ResourceConfig) ([]string, error) {
|
||||
func generateScripts(d *schema.ResourceData) ([]string, error) {
|
||||
var scripts []string
|
||||
command, ok := c.Config["inline"]
|
||||
if ok {
|
||||
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.")
|
||||
}
|
||||
for _, l := range d.Get("inline").([]interface{}) {
|
||||
scripts = append(scripts, l.(string))
|
||||
}
|
||||
return scripts, nil
|
||||
}
|
||||
|
||||
// collectScripts is used to collect all the scripts we need
|
||||
// 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
|
||||
_, ok := c.Config["inline"]
|
||||
if ok {
|
||||
scripts, err := p.generateScripts(c)
|
||||
if _, ok := d.GetOk("inline"); ok {
|
||||
scripts, err := generateScripts(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := []io.ReadCloser{}
|
||||
var r []io.ReadCloser
|
||||
for _, script := range scripts {
|
||||
r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
|
||||
}
|
||||
|
@ -109,31 +105,13 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
|
|||
|
||||
// Collect scripts
|
||||
var scripts []string
|
||||
s, ok := c.Config["script"]
|
||||
if ok {
|
||||
sStr, ok := s.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Unsupported 'script' type! Must be a string.")
|
||||
}
|
||||
scripts = append(scripts, sStr)
|
||||
if script, ok := d.GetOk("script"); ok {
|
||||
scripts = append(scripts, script.(string))
|
||||
}
|
||||
|
||||
sl, ok := c.Config["scripts"]
|
||||
if ok {
|
||||
switch slt := sl.(type) {
|
||||
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.")
|
||||
if scriptList, ok := d.GetOk("scripts"); ok {
|
||||
for _, script := range scriptList.([]interface{}) {
|
||||
scripts = append(scripts, script.(string))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,19 +133,30 @@ func (p *ResourceProvisioner) collectScripts(c *terraform.ResourceConfig) ([]io.
|
|||
}
|
||||
|
||||
// runScripts is used to copy and execute a set of scripts
|
||||
func (p *ResourceProvisioner) runScripts(
|
||||
func runScripts(
|
||||
ctx context.Context,
|
||||
o terraform.UIOutput,
|
||||
comm communicator.Communicator,
|
||||
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
|
||||
err := retryFunc(comm.Timeout(), func() error {
|
||||
err := retryFunc(ctx, comm.Timeout(), func() error {
|
||||
err := comm.Connect(o)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer comm.Disconnect()
|
||||
|
||||
for _, script := range scripts {
|
||||
var cmd *remote.Cmd
|
||||
|
@ -175,11 +164,11 @@ func (p *ResourceProvisioner) runScripts(
|
|||
errR, errW := io.Pipe()
|
||||
outDoneCh := make(chan struct{})
|
||||
errDoneCh := make(chan struct{})
|
||||
go p.copyOutput(o, outR, outDoneCh)
|
||||
go p.copyOutput(o, errR, errDoneCh)
|
||||
go copyOutput(o, outR, outDoneCh)
|
||||
go copyOutput(o, errR, errDoneCh)
|
||||
|
||||
remotePath := comm.ScriptPath()
|
||||
err = retryFunc(comm.Timeout(), func() error {
|
||||
err = retryFunc(ctx, comm.Timeout(), func() error {
|
||||
if err := comm.UploadScript(remotePath, script); err != nil {
|
||||
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
|
||||
outW.Close()
|
||||
errW.Close()
|
||||
|
@ -225,7 +221,7 @@ func (p *ResourceProvisioner) runScripts(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (p *ResourceProvisioner) copyOutput(
|
||||
func copyOutput(
|
||||
o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
|
||||
defer close(doneCh)
|
||||
lr := linereader.New(r)
|
||||
|
@ -235,19 +231,54 @@ func (p *ResourceProvisioner) copyOutput(
|
|||
}
|
||||
|
||||
// retryFunc is used to retry a function for a given duration
|
||||
func retryFunc(timeout time.Duration, f func() error) error {
|
||||
finish := time.After(timeout)
|
||||
func retryFunc(ctx context.Context, timeout time.Duration, f func() error) error {
|
||||
// Build a new context with the timeout
|
||||
ctx, done := context.WithTimeout(ctx, timeout)
|
||||
defer done()
|
||||
|
||||
// Try the function in a goroutine
|
||||
var errVal atomic.Value
|
||||
doneCh := make(chan struct{})
|
||||
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
|
||||
}
|
||||
log.Printf("Retryable error: %v", err)
|
||||
|
||||
select {
|
||||
case <-finish:
|
||||
return err
|
||||
case <-time.After(3 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,18 +9,15 @@ import (
|
|||
"reflect"
|
||||
|
||||
"github.com/hashicorp/terraform/config"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
)
|
||||
|
||||
func TestResourceProvisioner_impl(t *testing.T) {
|
||||
var _ terraform.ResourceProvisioner = new(ResourceProvisioner)
|
||||
}
|
||||
|
||||
func TestResourceProvider_Validate_good(t *testing.T) {
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"inline": "echo foo",
|
||||
})
|
||||
p := new(ResourceProvisioner)
|
||||
p := Provisioner()
|
||||
warn, errs := p.Validate(c)
|
||||
if len(warn) > 0 {
|
||||
t.Fatalf("Warnings: %v", warn)
|
||||
|
@ -34,7 +31,7 @@ func TestResourceProvider_Validate_bad(t *testing.T) {
|
|||
c := testConfig(t, map[string]interface{}{
|
||||
"invalid": "nope",
|
||||
})
|
||||
p := new(ResourceProvisioner)
|
||||
p := Provisioner()
|
||||
warn, errs := p.Validate(c)
|
||||
if len(warn) > 0 {
|
||||
t.Fatalf("Warnings: %v", warn)
|
||||
|
@ -51,16 +48,17 @@ exit 0
|
|||
|
||||
var expectedInlineScriptsOut = strings.Split(expectedScriptOut, "\n")
|
||||
|
||||
func TestResourceProvider_generateScripts(t *testing.T) {
|
||||
p := new(ResourceProvisioner)
|
||||
conf := testConfig(t, map[string]interface{}{
|
||||
func TestResourceProvider_generateScript(t *testing.T) {
|
||||
p := Provisioner().(*schema.Provisioner)
|
||||
conf := map[string]interface{}{
|
||||
"inline": []interface{}{
|
||||
"cd /tmp",
|
||||
"wget http://foobar",
|
||||
"exit 0",
|
||||
},
|
||||
})
|
||||
out, err := p.generateScripts(conf)
|
||||
}
|
||||
out, err := generateScripts(schema.TestResourceDataRaw(
|
||||
t, p.Schema, conf))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -71,16 +69,17 @@ func TestResourceProvider_generateScripts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourceProvider_CollectScripts_inline(t *testing.T) {
|
||||
p := new(ResourceProvisioner)
|
||||
conf := testConfig(t, map[string]interface{}{
|
||||
p := Provisioner().(*schema.Provisioner)
|
||||
conf := map[string]interface{}{
|
||||
"inline": []interface{}{
|
||||
"cd /tmp",
|
||||
"wget http://foobar",
|
||||
"exit 0",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
scripts, err := p.collectScripts(conf)
|
||||
scripts, err := collectScripts(schema.TestResourceDataRaw(
|
||||
t, p.Schema, conf))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -103,12 +102,13 @@ func TestResourceProvider_CollectScripts_inline(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourceProvider_CollectScripts_script(t *testing.T) {
|
||||
p := new(ResourceProvisioner)
|
||||
conf := testConfig(t, map[string]interface{}{
|
||||
p := Provisioner().(*schema.Provisioner)
|
||||
conf := map[string]interface{}{
|
||||
"script": "test-fixtures/script1.sh",
|
||||
})
|
||||
}
|
||||
|
||||
scripts, err := p.collectScripts(conf)
|
||||
scripts, err := collectScripts(schema.TestResourceDataRaw(
|
||||
t, p.Schema, conf))
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
@ -129,16 +129,17 @@ func TestResourceProvider_CollectScripts_script(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestResourceProvider_CollectScripts_scripts(t *testing.T) {
|
||||
p := new(ResourceProvisioner)
|
||||
conf := testConfig(t, map[string]interface{}{
|
||||
p := Provisioner().(*schema.Provisioner)
|
||||
conf := map[string]interface{}{
|
||||
"scripts": []interface{}{
|
||||
"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 {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
|
|
@ -65,13 +65,15 @@ import (
|
|||
vaultprovider "github.com/hashicorp/terraform/builtin/providers/vault"
|
||||
vcdprovider "github.com/hashicorp/terraform/builtin/providers/vcd"
|
||||
vsphereprovider "github.com/hashicorp/terraform/builtin/providers/vsphere"
|
||||
chefresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
|
||||
fileresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
||||
localexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
||||
remoteexecresourceprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
||||
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
||||
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
||||
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
||||
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
"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{
|
||||
|
@ -137,8 +139,13 @@ var InternalProviders = map[string]plugin.ProviderFunc{
|
|||
}
|
||||
|
||||
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
|
||||
"chef": func() terraform.ResourceProvisioner { return new(chefresourceprovisioner.ResourceProvisioner) },
|
||||
"file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) },
|
||||
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) },
|
||||
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) },
|
||||
"file": fileprovisioner.Provisioner,
|
||||
"local-exec": localexecprovisioner.Provisioner,
|
||||
"remote-exec": remoteexecprovisioner.Provisioner,
|
||||
}
|
||||
|
||||
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) }
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@ type Communicator struct {
|
|||
config *sshConfig
|
||||
conn net.Conn
|
||||
address string
|
||||
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
type sshConfig struct {
|
||||
|
@ -96,6 +98,10 @@ func New(s *terraform.InstanceState) (*Communicator, error) {
|
|||
|
||||
// Connect implementation of communicator.Communicator interface
|
||||
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 {
|
||||
c.conn.Close()
|
||||
}
|
||||
|
@ -190,8 +196,19 @@ func (c *Communicator) Connect(o terraform.UIOutput) (err error) {
|
|||
|
||||
// Disconnect implementation of communicator.Communicator interface
|
||||
func (c *Communicator) Disconnect() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
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
|
||||
|
|
|
@ -79,10 +79,35 @@ func (r *ConfigFieldReader) readField(
|
|||
|
||||
k := strings.Join(address, ".")
|
||||
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 {
|
||||
case TypeBool, TypeFloat, TypeInt, TypeString:
|
||||
return r.readPrimitive(k, schema)
|
||||
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)
|
||||
case TypeMap:
|
||||
return r.readMap(k, schema)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -118,9 +118,16 @@ type Schema struct {
|
|||
// 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
|
||||
// cause instability.
|
||||
//
|
||||
// PromoteSingle, if true, will allow single elements to be standalone
|
||||
// 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.
|
||||
//
|
||||
|
@ -1163,6 +1170,14 @@ func (m schemaMap) validateList(
|
|||
// We use reflection to verify the slice because you can't
|
||||
// case to []interface{} unless the slice is exactly that type.
|
||||
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 {
|
||||
return nil, []error{fmt.Errorf(
|
||||
"%s: should be a list", k)}
|
||||
|
|
|
@ -582,6 +582,72 @@ func TestSchemaMap_Diff(t *testing.T) {
|
|||
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{
|
||||
"ports": &Schema{
|
||||
|
@ -3853,6 +3919,40 @@ func TestSchemaMap_Validate(t *testing.T) {
|
|||
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": {
|
||||
Schema: map[string]*Schema{
|
||||
"ingress": &Schema{
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -77,6 +77,19 @@ func (p *ResourceProvisioner) Apply(
|
|||
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 {
|
||||
return p.Client.Close()
|
||||
}
|
||||
|
@ -100,6 +113,10 @@ type ResourceProvisionerApplyResponse struct {
|
|||
Error *plugin.BasicError
|
||||
}
|
||||
|
||||
type ResourceProvisionerStopResponse struct {
|
||||
Error *plugin.BasicError
|
||||
}
|
||||
|
||||
// ResourceProvisionerServer is a net/rpc compatible structure for serving
|
||||
// a ResourceProvisioner. This should not be used directly.
|
||||
type ResourceProvisionerServer struct {
|
||||
|
@ -143,3 +160,14 @@ func (s *ResourceProvisionerServer) Validate(
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ResourceProvisionerServer) Stop(
|
||||
_ interface{},
|
||||
reply *ResourceProvisionerStopResponse) error {
|
||||
err := s.Provisioner.Stop()
|
||||
*reply = ResourceProvisionerStopResponse{
|
||||
Error: plugin.NewBasicError(err),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,6 +14,61 @@ func TestResourceProvisioner_impl(t *testing.T) {
|
|||
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) {
|
||||
// Create a mock provider
|
||||
p := new(terraform.MockResourceProvisioner)
|
||||
|
|
|
@ -19,7 +19,7 @@ var Handshake = plugin.HandshakeConfig{
|
|||
// 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
|
||||
// helper/schema computes diffs, etc.
|
||||
ProtocolVersion: 2,
|
||||
ProtocolVersion: 3,
|
||||
|
||||
// The magic cookie values should NEVER be changed.
|
||||
MagicCookieKey: "TF_PLUGIN_MAGIC_COOKIE",
|
||||
|
|
|
@ -91,7 +91,7 @@ func makeProviderMap(items []plugin) string {
|
|||
func makeProvisionerMap(items []plugin) string {
|
||||
output := ""
|
||||
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
|
||||
}
|
||||
|
@ -254,8 +254,8 @@ func discoverProviders() ([]plugin, error) {
|
|||
|
||||
func discoverProvisioners() ([]plugin, error) {
|
||||
path := "./builtin/provisioners"
|
||||
typeID := "ResourceProvisioner"
|
||||
typeName := ""
|
||||
typeID := "terraform.ResourceProvisioner"
|
||||
typeName := "Provisioner"
|
||||
return discoverTypesInPath(path, typeID, typeName)
|
||||
}
|
||||
|
||||
|
@ -270,6 +270,9 @@ import (
|
|||
IMPORTS
|
||||
"github.com/hashicorp/terraform/plugin"
|
||||
"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{
|
||||
|
@ -280,4 +283,10 @@ var InternalProvisioners = map[string]plugin.ProvisionerFunc{
|
|||
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) }
|
||||
}
|
||||
|
||||
`
|
||||
|
|
|
@ -7,29 +7,29 @@ func TestMakeProvisionerMap(t *testing.T) {
|
|||
{
|
||||
Package: "file",
|
||||
PluginName: "file",
|
||||
TypeName: "ResourceProvisioner",
|
||||
TypeName: "Provisioner",
|
||||
Path: "builtin/provisioners/file",
|
||||
ImportName: "fileresourceprovisioner",
|
||||
ImportName: "fileprovisioner",
|
||||
},
|
||||
{
|
||||
Package: "localexec",
|
||||
PluginName: "local-exec",
|
||||
TypeName: "ResourceProvisioner",
|
||||
TypeName: "Provisioner",
|
||||
Path: "builtin/provisioners/local-exec",
|
||||
ImportName: "localexecresourceprovisioner",
|
||||
ImportName: "localexecprovisioner",
|
||||
},
|
||||
{
|
||||
Package: "remoteexec",
|
||||
PluginName: "remote-exec",
|
||||
TypeName: "ResourceProvisioner",
|
||||
TypeName: "Provisioner",
|
||||
Path: "builtin/provisioners/remote-exec",
|
||||
ImportName: "remoteexecresourceprovisioner",
|
||||
ImportName: "remoteexecprovisioner",
|
||||
},
|
||||
})
|
||||
|
||||
expected := ` "file": func() terraform.ResourceProvisioner { return new(fileresourceprovisioner.ResourceProvisioner) },
|
||||
"local-exec": func() terraform.ResourceProvisioner { return new(localexecresourceprovisioner.ResourceProvisioner) },
|
||||
"remote-exec": func() terraform.ResourceProvisioner { return new(remoteexecresourceprovisioner.ResourceProvisioner) },
|
||||
expected := ` "file": fileprovisioner.Provisioner,
|
||||
"local-exec": localexecprovisioner.Provisioner,
|
||||
"remote-exec": remoteexecprovisioner.Provisioner,
|
||||
`
|
||||
|
||||
if p != expected {
|
||||
|
@ -86,13 +86,10 @@ func TestDiscoverTypesProviders(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 {
|
||||
t.Fatalf(err.Error())
|
||||
}
|
||||
if !contains(plugins, "chef") {
|
||||
t.Errorf("Expected to find chef provisioner")
|
||||
}
|
||||
if !contains(plugins, "remote-exec") {
|
||||
t.Errorf("Expected to find remote-exec provisioner")
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
|
@ -91,8 +92,10 @@ type Context struct {
|
|||
l sync.Mutex // Lock acquired during any task
|
||||
parallelSem Semaphore
|
||||
providerInputConfig map[string]map[string]interface{}
|
||||
runCh <-chan struct{}
|
||||
stopCh chan struct{}
|
||||
runLock sync.Mutex
|
||||
runCond *sync.Cond
|
||||
runContext context.Context
|
||||
runContextCancel context.CancelFunc
|
||||
shadowErr error
|
||||
}
|
||||
|
||||
|
@ -320,8 +323,7 @@ func (c *Context) Interpolater() *Interpolater {
|
|||
// This modifies the configuration in-place, so asking for Input twice
|
||||
// may result in different UI output showing different current values.
|
||||
func (c *Context) Input(mode InputMode) error {
|
||||
v := c.acquireRun("input")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("input")()
|
||||
|
||||
if mode&InputModeVar != 0 {
|
||||
// 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
|
||||
// with the latest state.
|
||||
func (c *Context) Apply() (*State, error) {
|
||||
v := c.acquireRun("apply")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("apply")()
|
||||
|
||||
// Copy our own state
|
||||
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
|
||||
// by the plan, so Apply can be called after.
|
||||
func (c *Context) Plan() (*Plan, error) {
|
||||
v := c.acquireRun("plan")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("plan")()
|
||||
|
||||
p := &Plan{
|
||||
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
|
||||
// will potentially be partially updated.
|
||||
func (c *Context) Refresh() (*State, error) {
|
||||
v := c.acquireRun("refresh")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("refresh")()
|
||||
|
||||
// Copy our own state
|
||||
c.state = c.state.DeepCopy()
|
||||
|
@ -596,30 +595,34 @@ func (c *Context) Refresh() (*State, error) {
|
|||
//
|
||||
// Stop will block until the task completes.
|
||||
func (c *Context) Stop() {
|
||||
c.l.Lock()
|
||||
ch := c.runCh
|
||||
log.Printf("[WARN] terraform: Stop called, initiating interrupt sequence")
|
||||
|
||||
// If we aren't running, then just return
|
||||
if ch == nil {
|
||||
c.l.Unlock()
|
||||
return
|
||||
}
|
||||
c.l.Lock()
|
||||
defer c.l.Unlock()
|
||||
|
||||
// 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()
|
||||
|
||||
// Close the stop channel
|
||||
close(c.stopCh)
|
||||
// Stop the context
|
||||
c.runContextCancel()
|
||||
c.runContextCancel = nil
|
||||
}
|
||||
|
||||
// Wait for us to stop
|
||||
c.l.Unlock()
|
||||
<-ch
|
||||
// Grab the condition var before we exit
|
||||
if cond := c.runCond; cond != nil {
|
||||
cond.Wait()
|
||||
}
|
||||
|
||||
log.Printf("[WARN] terraform: stop complete")
|
||||
}
|
||||
|
||||
// Validate validates the configuration and returns any warnings or errors.
|
||||
func (c *Context) Validate() ([]string, []error) {
|
||||
v := c.acquireRun("validate")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("validate")()
|
||||
|
||||
var errs error
|
||||
|
||||
|
@ -680,26 +683,25 @@ func (c *Context) SetVariable(k string, v interface{}) {
|
|||
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()
|
||||
defer c.l.Unlock()
|
||||
|
||||
dbug.SetPhase(phase)
|
||||
|
||||
// Wait for no channel to exist
|
||||
for c.runCh != nil {
|
||||
c.l.Unlock()
|
||||
ch := c.runCh
|
||||
<-ch
|
||||
c.l.Lock()
|
||||
// Wait until we're no longer running
|
||||
for c.runCond != nil {
|
||||
c.runCond.Wait()
|
||||
}
|
||||
|
||||
// Create the new channel
|
||||
ch := make(chan struct{})
|
||||
c.runCh = ch
|
||||
// Build our lock
|
||||
c.runCond = sync.NewCond(&c.l)
|
||||
|
||||
// Reset the stop channel so we can watch that
|
||||
c.stopCh = make(chan struct{})
|
||||
// Setup debugging
|
||||
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
|
||||
c.sh.Reset()
|
||||
|
@ -707,10 +709,11 @@ func (c *Context) acquireRun(phase string) chan<- struct{} {
|
|||
// Reset the shadow errors
|
||||
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()
|
||||
defer c.l.Unlock()
|
||||
|
||||
|
@ -719,9 +722,19 @@ func (c *Context) releaseRun(ch chan<- struct{}) {
|
|||
// phase
|
||||
dbug.SetPhase("INVALID")
|
||||
|
||||
close(ch)
|
||||
c.runCh = nil
|
||||
c.stopCh = nil
|
||||
// End our run. We check if runContext is non-nil because it can be
|
||||
// set to nil if it was cancelled via Stop()
|
||||
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(
|
||||
|
@ -755,11 +768,13 @@ func (c *Context) walk(
|
|||
walker := &ContextGraphWalker{
|
||||
Context: realCtx,
|
||||
Operation: operation,
|
||||
StopContext: c.runContext,
|
||||
}
|
||||
|
||||
// Watch for a stop so we can call the provider Stop() API.
|
||||
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
|
||||
realErr := graph.Walk(walker)
|
||||
|
@ -854,7 +869,7 @@ func (c *Context) walk(
|
|||
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
|
||||
select {
|
||||
case <-stopCh:
|
||||
|
@ -866,6 +881,7 @@ func (c *Context) watchStop(walker *ContextGraphWalker, stopCh, doneCh <-chan st
|
|||
|
||||
// If we're here, we're stopped, trigger the call.
|
||||
|
||||
{
|
||||
// Copy the providers so that a misbehaved blocking Stop doesn't
|
||||
// completely hang Terraform.
|
||||
walker.providerLock.Lock()
|
||||
|
@ -883,6 +899,24 @@ func (c *Context) watchStop(walker *ContextGraphWalker, stopCh, doneCh <-chan st
|
|||
}
|
||||
}
|
||||
|
||||
{
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseVariableAsHCL parses the value of a single variable as would have been specified
|
||||
// on the command line via -var or in an environment variable named TF_VAR_x, where x is
|
||||
// the name of the variable. In order to get around the restriction of HCL requiring a
|
||||
|
|
|
@ -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) {
|
||||
m := testModule(t, "apply-compute")
|
||||
p := testProvider("aws")
|
||||
|
|
|
@ -40,8 +40,7 @@ type ImportTarget struct {
|
|||
// imported.
|
||||
func (c *Context) Import(opts *ImportOpts) (*State, error) {
|
||||
// Hold a lock since we can modify our own state here
|
||||
v := c.acquireRun("import")
|
||||
defer c.releaseRun(v)
|
||||
defer c.acquireRun("import")()
|
||||
|
||||
// Copy our own state
|
||||
c.state = c.state.DeepCopy()
|
||||
|
|
|
@ -928,6 +928,7 @@ func TestContext2Validate_PlanGraphBuilder(t *testing.T) {
|
|||
Targets: c.targets,
|
||||
}).Build(RootModulePath)
|
||||
|
||||
defer c.acquireRun("validate-test")()
|
||||
walker, err := c.walk(graph, graph, walkValidate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -8,6 +8,10 @@ import (
|
|||
|
||||
// EvalContext is the interface that is given to eval nodes to execute.
|
||||
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() []string
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
@ -12,6 +13,9 @@ import (
|
|||
// BuiltinEvalContext is an EvalContext implementation that is used by
|
||||
// Terraform by default.
|
||||
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 []string
|
||||
|
||||
|
@ -43,6 +47,15 @@ type BuiltinEvalContext struct {
|
|||
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 {
|
||||
for _, h := range ctx.Hooks {
|
||||
action, err := fn(h)
|
||||
|
|
|
@ -9,6 +9,9 @@ import (
|
|||
// MockEvalContext is a mock version of EvalContext that can be used
|
||||
// for tests.
|
||||
type MockEvalContext struct {
|
||||
StoppedCalled bool
|
||||
StoppedValue <-chan struct{}
|
||||
|
||||
HookCalled bool
|
||||
HookHook Hook
|
||||
HookError error
|
||||
|
@ -85,6 +88,11 @@ type MockEvalContext struct {
|
|||
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 {
|
||||
c.HookCalled = true
|
||||
if c.HookHook != nil {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package terraform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
@ -17,6 +18,7 @@ type ContextGraphWalker struct {
|
|||
// Configurable values
|
||||
Context *Context
|
||||
Operation walkOperation
|
||||
StopContext context.Context
|
||||
|
||||
// Outputs, do not set these. Do not read these while the graph
|
||||
// is being walked.
|
||||
|
@ -65,6 +67,7 @@ func (w *ContextGraphWalker) EnterPath(path []string) EvalContext {
|
|||
w.interpolaterVarLock.Unlock()
|
||||
|
||||
ctx := &BuiltinEvalContext{
|
||||
StopContext: w.StopContext,
|
||||
PathValue: path,
|
||||
Hooks: w.Context.hooks,
|
||||
InputValue: w.Context.uiInput,
|
||||
|
|
|
@ -21,6 +21,26 @@ type ResourceProvisioner interface {
|
|||
// is provided since provisioners only run after a resource has been
|
||||
// newly created.
|
||||
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
|
||||
|
|
|
@ -21,6 +21,10 @@ type MockResourceProvisioner struct {
|
|||
ValidateFn func(c *ResourceConfig) ([]string, []error)
|
||||
ValidateReturnWarns []string
|
||||
ValidateReturnErrors []error
|
||||
|
||||
StopCalled bool
|
||||
StopFn func() error
|
||||
StopReturnError error
|
||||
}
|
||||
|
||||
func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) {
|
||||
|
@ -40,14 +44,29 @@ func (p *MockResourceProvisioner) Apply(
|
|||
state *InstanceState,
|
||||
c *ResourceConfig) error {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
p.ApplyCalled = true
|
||||
p.ApplyOutput = output
|
||||
p.ApplyState = state
|
||||
p.ApplyConfig = c
|
||||
if p.ApplyFn != nil {
|
||||
return p.ApplyFn(state, c)
|
||||
fn := p.ApplyFn
|
||||
p.Unlock()
|
||||
return fn(state, c)
|
||||
}
|
||||
|
||||
defer p.Unlock()
|
||||
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
|
||||
}
|
||||
|
|
|
@ -88,7 +88,8 @@ func newShadowContext(c *Context) (*Context, *Context, Shadow) {
|
|||
// l - no copy
|
||||
parallelSem: c.parallelSem,
|
||||
providerInputConfig: c.providerInputConfig,
|
||||
runCh: c.runCh,
|
||||
runContext: c.runContext,
|
||||
runContextCancel: c.runContextCancel,
|
||||
shadowErr: c.shadowErr,
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,10 @@ func (p *shadowResourceProvisionerReal) Apply(
|
|||
return err
|
||||
}
|
||||
|
||||
func (p *shadowResourceProvisionerReal) Stop() error {
|
||||
return p.ResourceProvisioner.Stop()
|
||||
}
|
||||
|
||||
// shadowResourceProvisionerShadow is the shadow resource provisioner. Function
|
||||
// calls never affect real resources. This is paired with the "real" side
|
||||
// which must be called properly to enable recording.
|
||||
|
@ -228,6 +232,13 @@ func (p *shadowResourceProvisionerShadow) Apply(
|
|||
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
|
||||
// are used to carry call information across the real/shadow boundaries.
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
resource "aws_instance" "foo" {
|
||||
num = "2"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
resource "aws_instance" "foo" {
|
||||
num = "2"
|
||||
|
||||
provisioner "shell" {
|
||||
foo = "bar"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue