rewrite local-exec to run internally
Use the new interface directly, so that there are no shims or plugins involved with the execution of the provisioner.
This commit is contained in:
parent
dc9ded8618
commit
9adfaa9b5d
|
@ -7,11 +7,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/armon/circbuf"
|
"github.com/armon/circbuf"
|
||||||
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
|
"github.com/hashicorp/terraform/configs/configschema"
|
||||||
"github.com/hashicorp/terraform/internal/legacy/terraform"
|
"github.com/hashicorp/terraform/provisioners"
|
||||||
"github.com/mitchellh/go-linereader"
|
"github.com/mitchellh/go-linereader"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,59 +23,79 @@ const (
|
||||||
maxBufSize = 8 * 1024
|
maxBufSize = 8 * 1024
|
||||||
)
|
)
|
||||||
|
|
||||||
func Provisioner() terraform.ResourceProvisioner {
|
func New() provisioners.Interface {
|
||||||
return &schema.Provisioner{
|
return &provisioner{}
|
||||||
Schema: map[string]*schema.Schema{
|
}
|
||||||
"command": &schema.Schema{
|
|
||||||
Type: schema.TypeString,
|
type provisioner struct {
|
||||||
|
// this stored from the running context, so that Stop() can cancel the
|
||||||
|
// command
|
||||||
|
mu sync.Mutex
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
|
||||||
|
schema := &configschema.Block{
|
||||||
|
Attributes: map[string]*configschema.Attribute{
|
||||||
|
"command": {
|
||||||
|
Type: cty.String,
|
||||||
Required: true,
|
Required: true,
|
||||||
},
|
},
|
||||||
"interpreter": &schema.Schema{
|
"interpreter": {
|
||||||
Type: schema.TypeList,
|
Type: cty.List(cty.String),
|
||||||
Elem: &schema.Schema{Type: schema.TypeString},
|
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"working_dir": &schema.Schema{
|
"working_dir": {
|
||||||
Type: schema.TypeString,
|
Type: cty.String,
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
"environment": &schema.Schema{
|
"environment": {
|
||||||
Type: schema.TypeMap,
|
Type: cty.Map(cty.String),
|
||||||
Optional: true,
|
Optional: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
ApplyFunc: applyFn,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resp.Provisioner = schema
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyFn(ctx context.Context) error {
|
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
|
||||||
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
|
||||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
resp.Diagnostics = resp.Diagnostics.Append(err)
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
command := data.Get("command").(string)
|
func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
|
||||||
|
p.mu.Lock()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
p.cancel = cancel
|
||||||
|
p.mu.Unlock()
|
||||||
|
|
||||||
|
command := req.Config.GetAttr("command").AsString()
|
||||||
if command == "" {
|
if command == "" {
|
||||||
return fmt.Errorf("local-exec provisioner command must be a non-empty string")
|
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("local-exec provisioner command must be a non-empty string"))
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command with env
|
envVal := req.Config.GetAttr("environment")
|
||||||
environment := data.Get("environment").(map[string]interface{})
|
|
||||||
|
|
||||||
var env []string
|
var env []string
|
||||||
for k := range environment {
|
|
||||||
entry := fmt.Sprintf("%s=%s", k, environment[k].(string))
|
if !envVal.IsNull() {
|
||||||
env = append(env, entry)
|
for k, v := range envVal.AsValueMap() {
|
||||||
|
entry := fmt.Sprintf("%s=%s", k, v.AsString())
|
||||||
|
env = append(env, entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command using a shell
|
// Execute the command using a shell
|
||||||
interpreter := data.Get("interpreter").([]interface{})
|
intrVal := req.Config.GetAttr("interpreter")
|
||||||
|
|
||||||
var cmdargs []string
|
var cmdargs []string
|
||||||
if len(interpreter) > 0 {
|
if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
|
||||||
for _, i := range interpreter {
|
for _, v := range intrVal.AsValueSlice() {
|
||||||
if arg, ok := i.(string); ok {
|
cmdargs = append(cmdargs, v.AsString())
|
||||||
cmdargs = append(cmdargs, arg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
|
@ -82,9 +104,13 @@ func applyFn(ctx context.Context) error {
|
||||||
cmdargs = []string{"/bin/sh", "-c"}
|
cmdargs = []string{"/bin/sh", "-c"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdargs = append(cmdargs, command)
|
cmdargs = append(cmdargs, command)
|
||||||
|
|
||||||
workingdir := data.Get("working_dir").(string)
|
workingdir := ""
|
||||||
|
if wdVal := req.Config.GetAttr("working_dir"); !wdVal.IsNull() {
|
||||||
|
workingdir = wdVal.AsString()
|
||||||
|
}
|
||||||
|
|
||||||
// Setup the reader that will read the output from the command.
|
// Setup the reader that will read the output from the command.
|
||||||
// We use an os.Pipe so that the *os.File can be passed directly to the
|
// We use an os.Pipe so that the *os.File can be passed directly to the
|
||||||
|
@ -92,7 +118,8 @@ func applyFn(ctx context.Context) error {
|
||||||
// See golang.org/issue/18874
|
// See golang.org/issue/18874
|
||||||
pr, pw, err := os.Pipe()
|
pr, pw, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to initialize pipe for output: %s", err)
|
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("failed to initialize pipe for output: %s", err))
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdEnv []string
|
var cmdEnv []string
|
||||||
|
@ -118,10 +145,10 @@ func applyFn(ctx context.Context) error {
|
||||||
|
|
||||||
// copy the teed output to the UI output
|
// copy the teed output to the UI output
|
||||||
copyDoneCh := make(chan struct{})
|
copyDoneCh := make(chan struct{})
|
||||||
go copyOutput(o, tee, copyDoneCh)
|
go copyUIOutput(req.UIOutput, tee, copyDoneCh)
|
||||||
|
|
||||||
// Output what we're about to run
|
// Output what we're about to run
|
||||||
o.Output(fmt.Sprintf("Executing: %q", cmdargs))
|
req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs))
|
||||||
|
|
||||||
// Start the command
|
// Start the command
|
||||||
err = cmd.Start()
|
err = cmd.Start()
|
||||||
|
@ -142,14 +169,26 @@ func applyFn(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Error running command '%s': %v. Output: %s",
|
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Error running command '%s': %v. Output: %s",
|
||||||
command, err, output.Bytes())
|
command, err, output.Bytes()))
|
||||||
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) Stop() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.cancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyOutput(o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) {
|
func (p *provisioner) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyUIOutput(o provisioners.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 {
|
||||||
|
|
|
@ -7,31 +7,30 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
|
"github.com/hashicorp/terraform/provisioners"
|
||||||
"github.com/hashicorp/terraform/internal/legacy/terraform"
|
"github.com/mitchellh/cli"
|
||||||
|
"github.com/zclconf/go-cty/cty"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestResourceProvisioner_impl(t *testing.T) {
|
|
||||||
var _ terraform.ResourceProvisioner = Provisioner()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestProvisioner(t *testing.T) {
|
|
||||||
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
|
||||||
t.Fatalf("err: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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{}{
|
output := cli.NewMockUi()
|
||||||
"command": "echo foo > test_out",
|
p := New()
|
||||||
|
schema := p.GetSchema().Provisioner
|
||||||
|
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"command": cty.StringVal("echo foo > test_out"),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||||
|
Config: c,
|
||||||
|
UIOutput: output,
|
||||||
})
|
})
|
||||||
|
|
||||||
output := new(terraform.MockUIOutput)
|
if resp.Diagnostics.HasErrors() {
|
||||||
p := Provisioner()
|
t.Fatalf("err: %v", resp.Diagnostics.Err())
|
||||||
|
|
||||||
if err := p.Apply(output, nil, c); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the file
|
// Check the file
|
||||||
|
@ -48,14 +47,18 @@ func TestResourceProvider_Apply(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceProvider_stop(t *testing.T) {
|
func TestResourceProvider_stop(t *testing.T) {
|
||||||
c := testConfig(t, map[string]interface{}{
|
output := cli.NewMockUi()
|
||||||
|
p := New()
|
||||||
|
schema := p.GetSchema().Provisioner
|
||||||
|
|
||||||
|
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
// bash/zsh/ksh will exec a single command in the same process. This
|
// bash/zsh/ksh will exec a single command in the same process. This
|
||||||
// makes certain there's a subprocess in the shell.
|
// makes certain there's a subprocess in the shell.
|
||||||
"command": "sleep 30; sleep 30",
|
"command": cty.StringVal("sleep 30; sleep 30"),
|
||||||
})
|
}))
|
||||||
|
if err != nil {
|
||||||
output := new(terraform.MockUIOutput)
|
t.Fatal(err)
|
||||||
p := Provisioner()
|
}
|
||||||
|
|
||||||
doneCh := make(chan struct{})
|
doneCh := make(chan struct{})
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
@ -65,7 +68,10 @@ func TestResourceProvider_stop(t *testing.T) {
|
||||||
// Because p.Apply is called in a goroutine, trying to t.Fatal() on its
|
// Because p.Apply is called in a goroutine, trying to t.Fatal() on its
|
||||||
// result would be ignored or would cause a panic if the parent goroutine
|
// result would be ignored or would cause a panic if the parent goroutine
|
||||||
// has already completed.
|
// has already completed.
|
||||||
_ = p.Apply(output, nil, c)
|
_ = p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||||
|
Config: c,
|
||||||
|
UIOutput: output,
|
||||||
|
})
|
||||||
}()
|
}()
|
||||||
|
|
||||||
mustExceed := (50 * time.Millisecond)
|
mustExceed := (50 * time.Millisecond)
|
||||||
|
@ -90,51 +96,32 @@ func TestResourceProvider_stop(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceProvider_Validate_good(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
|
||||||
"command": "echo foo",
|
|
||||||
})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) > 0 {
|
|
||||||
t.Fatalf("Errors: %v", errs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_Validate_missing(t *testing.T) {
|
|
||||||
c := testConfig(t, map[string]interface{}{})
|
|
||||||
|
|
||||||
warn, errs := Provisioner().Validate(c)
|
|
||||||
if len(warn) > 0 {
|
|
||||||
t.Fatalf("Warnings: %v", warn)
|
|
||||||
}
|
|
||||||
if len(errs) == 0 {
|
|
||||||
t.Fatalf("Should have errors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
|
||||||
return terraform.NewResourceConfigRaw(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResourceProvider_ApplyCustomInterpreter(t *testing.T) {
|
func TestResourceProvider_ApplyCustomInterpreter(t *testing.T) {
|
||||||
c := testConfig(t, map[string]interface{}{
|
output := cli.NewMockUi()
|
||||||
"interpreter": []interface{}{"echo", "is"},
|
p := New()
|
||||||
"command": "not really an interpreter",
|
|
||||||
})
|
|
||||||
|
|
||||||
output := new(terraform.MockUIOutput)
|
schema := p.GetSchema().Provisioner
|
||||||
p := Provisioner()
|
|
||||||
|
|
||||||
if err := p.Apply(output, nil, c); err != nil {
|
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
t.Fatalf("err: %v", err)
|
"interpreter": cty.ListVal([]cty.Value{cty.StringVal("echo"), cty.StringVal("is")}),
|
||||||
|
"command": cty.StringVal("not really an interpreter"),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got := strings.TrimSpace(output.OutputMessage)
|
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||||
want := "is not really an interpreter"
|
Config: c,
|
||||||
|
UIOutput: output,
|
||||||
|
})
|
||||||
|
|
||||||
|
if resp.Diagnostics.HasErrors() {
|
||||||
|
t.Fatal(resp.Diagnostics.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := strings.TrimSpace(output.OutputWriter.String())
|
||||||
|
want := `Executing: ["echo" "is" "not really an interpreter"]
|
||||||
|
is not really an interpreter`
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||||
}
|
}
|
||||||
|
@ -145,16 +132,25 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
|
||||||
os.Mkdir(testdir, 0755)
|
os.Mkdir(testdir, 0755)
|
||||||
defer os.Remove(testdir)
|
defer os.Remove(testdir)
|
||||||
|
|
||||||
c := testConfig(t, map[string]interface{}{
|
output := cli.NewMockUi()
|
||||||
"working_dir": testdir,
|
p := New()
|
||||||
"command": "echo `pwd`",
|
schema := p.GetSchema().Provisioner
|
||||||
|
|
||||||
|
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"working_dir": cty.StringVal(testdir),
|
||||||
|
"command": cty.StringVal("echo `pwd`"),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||||
|
Config: c,
|
||||||
|
UIOutput: output,
|
||||||
})
|
})
|
||||||
|
|
||||||
output := new(terraform.MockUIOutput)
|
if resp.Diagnostics.HasErrors() {
|
||||||
p := Provisioner()
|
t.Fatal(resp.Diagnostics.Err())
|
||||||
|
|
||||||
if err := p.Apply(output, nil, c); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dir, err := os.Getwd()
|
dir, err := os.Getwd()
|
||||||
|
@ -162,32 +158,41 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got := strings.TrimSpace(output.OutputMessage)
|
got := strings.TrimSpace(output.OutputWriter.String())
|
||||||
want := dir + "/" + testdir
|
want := "Executing: [\"/bin/sh\" \"-c\" \"echo `pwd`\"]\n" + dir + "/" + testdir
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResourceProvider_ApplyCustomEnv(t *testing.T) {
|
func TestResourceProvider_ApplyCustomEnv(t *testing.T) {
|
||||||
c := testConfig(t, map[string]interface{}{
|
output := cli.NewMockUi()
|
||||||
"command": "echo $FOO $BAR $BAZ",
|
p := New()
|
||||||
"environment": map[string]interface{}{
|
schema := p.GetSchema().Provisioner
|
||||||
"FOO": "BAR",
|
|
||||||
"BAR": 1,
|
|
||||||
"BAZ": "true",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
output := new(terraform.MockUIOutput)
|
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||||
p := Provisioner()
|
"command": cty.StringVal("echo $FOO $BAR $BAZ"),
|
||||||
|
"environment": cty.MapVal(map[string]cty.Value{
|
||||||
if err := p.Apply(output, nil, c); err != nil {
|
"FOO": cty.StringVal("BAR"),
|
||||||
t.Fatalf("err: %v", err)
|
"BAR": cty.StringVal("1"),
|
||||||
|
"BAZ": cty.StringVal("true"),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
got := strings.TrimSpace(output.OutputMessage)
|
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||||
want := "BAR 1 true"
|
Config: c,
|
||||||
|
UIOutput: output,
|
||||||
|
})
|
||||||
|
if resp.Diagnostics.HasErrors() {
|
||||||
|
t.Fatal(resp.Diagnostics.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
got := strings.TrimSpace(output.OutputWriter.String())
|
||||||
|
want := `Executing: ["/bin/sh" "-c" "echo $FOO $BAR $BAZ"]
|
||||||
|
BAR 1 true`
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue