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 (
|
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)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
// 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)}
|
||||||
|
|
|
@ -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{
|
||||||
|
|
|
@ -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
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) }
|
||||||
|
}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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