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/exec"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/armon/circbuf"
|
||||
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
|
||||
"github.com/hashicorp/terraform/internal/legacy/terraform"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/provisioners"
|
||||
"github.com/mitchellh/go-linereader"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -21,59 +23,79 @@ const (
|
|||
maxBufSize = 8 * 1024
|
||||
)
|
||||
|
||||
func Provisioner() terraform.ResourceProvisioner {
|
||||
return &schema.Provisioner{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"command": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
func New() provisioners.Interface {
|
||||
return &provisioner{}
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
"interpreter": &schema.Schema{
|
||||
Type: schema.TypeList,
|
||||
Elem: &schema.Schema{Type: schema.TypeString},
|
||||
"interpreter": {
|
||||
Type: cty.List(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
"working_dir": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
"working_dir": {
|
||||
Type: cty.String,
|
||||
Optional: true,
|
||||
},
|
||||
"environment": &schema.Schema{
|
||||
Type: schema.TypeMap,
|
||||
"environment": {
|
||||
Type: cty.Map(cty.String),
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
|
||||
ApplyFunc: applyFn,
|
||||
}
|
||||
|
||||
resp.Provisioner = schema
|
||||
return resp
|
||||
}
|
||||
|
||||
func applyFn(ctx context.Context) error {
|
||||
data := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
|
||||
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
|
||||
func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
|
||||
if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
|
||||
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 == "" {
|
||||
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
|
||||
environment := data.Get("environment").(map[string]interface{})
|
||||
|
||||
envVal := req.Config.GetAttr("environment")
|
||||
var env []string
|
||||
for k := range environment {
|
||||
entry := fmt.Sprintf("%s=%s", k, environment[k].(string))
|
||||
env = append(env, entry)
|
||||
|
||||
if !envVal.IsNull() {
|
||||
for k, v := range envVal.AsValueMap() {
|
||||
entry := fmt.Sprintf("%s=%s", k, v.AsString())
|
||||
env = append(env, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the command using a shell
|
||||
interpreter := data.Get("interpreter").([]interface{})
|
||||
intrVal := req.Config.GetAttr("interpreter")
|
||||
|
||||
var cmdargs []string
|
||||
if len(interpreter) > 0 {
|
||||
for _, i := range interpreter {
|
||||
if arg, ok := i.(string); ok {
|
||||
cmdargs = append(cmdargs, arg)
|
||||
}
|
||||
if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
|
||||
for _, v := range intrVal.AsValueSlice() {
|
||||
cmdargs = append(cmdargs, v.AsString())
|
||||
}
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
|
@ -82,9 +104,13 @@ func applyFn(ctx context.Context) error {
|
|||
cmdargs = []string{"/bin/sh", "-c"}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
// 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
|
||||
pr, pw, err := os.Pipe()
|
||||
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
|
||||
|
@ -118,10 +145,10 @@ func applyFn(ctx context.Context) error {
|
|||
|
||||
// copy the teed output to the UI output
|
||||
copyDoneCh := make(chan struct{})
|
||||
go copyOutput(o, tee, copyDoneCh)
|
||||
go copyUIOutput(req.UIOutput, tee, copyDoneCh)
|
||||
|
||||
// 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
|
||||
err = cmd.Start()
|
||||
|
@ -142,14 +169,26 @@ func applyFn(ctx context.Context) error {
|
|||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error running command '%s': %v. Output: %s",
|
||||
command, err, output.Bytes())
|
||||
resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Error running command '%s': %v. Output: %s",
|
||||
command, err, output.Bytes()))
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p *provisioner) Stop() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.cancel()
|
||||
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)
|
||||
lr := linereader.New(r)
|
||||
for line := range lr.Ch {
|
||||
|
|
|
@ -7,31 +7,30 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform/internal/legacy/helper/schema"
|
||||
"github.com/hashicorp/terraform/internal/legacy/terraform"
|
||||
"github.com/hashicorp/terraform/provisioners"
|
||||
"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) {
|
||||
defer os.Remove("test_out")
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"command": "echo foo > test_out",
|
||||
output := cli.NewMockUi()
|
||||
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)
|
||||
p := Provisioner()
|
||||
|
||||
if err := p.Apply(output, nil, c); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
if resp.Diagnostics.HasErrors() {
|
||||
t.Fatalf("err: %v", resp.Diagnostics.Err())
|
||||
}
|
||||
|
||||
// Check the file
|
||||
|
@ -48,14 +47,18 @@ func TestResourceProvider_Apply(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
|
||||
// makes certain there's a subprocess in the shell.
|
||||
"command": "sleep 30; sleep 30",
|
||||
})
|
||||
|
||||
output := new(terraform.MockUIOutput)
|
||||
p := Provisioner()
|
||||
"command": cty.StringVal("sleep 30; sleep 30"),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
doneCh := make(chan struct{})
|
||||
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
|
||||
// result would be ignored or would cause a panic if the parent goroutine
|
||||
// has already completed.
|
||||
_ = p.Apply(output, nil, c)
|
||||
_ = p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||
Config: c,
|
||||
UIOutput: output,
|
||||
})
|
||||
}()
|
||||
|
||||
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) {
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"interpreter": []interface{}{"echo", "is"},
|
||||
"command": "not really an interpreter",
|
||||
})
|
||||
output := cli.NewMockUi()
|
||||
p := New()
|
||||
|
||||
output := new(terraform.MockUIOutput)
|
||||
p := Provisioner()
|
||||
schema := p.GetSchema().Provisioner
|
||||
|
||||
if err := p.Apply(output, nil, c); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||
"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)
|
||||
want := "is not really an interpreter"
|
||||
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||
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 {
|
||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
@ -145,16 +132,25 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
|
|||
os.Mkdir(testdir, 0755)
|
||||
defer os.Remove(testdir)
|
||||
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"working_dir": testdir,
|
||||
"command": "echo `pwd`",
|
||||
output := cli.NewMockUi()
|
||||
p := New()
|
||||
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)
|
||||
p := Provisioner()
|
||||
|
||||
if err := p.Apply(output, nil, c); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
if resp.Diagnostics.HasErrors() {
|
||||
t.Fatal(resp.Diagnostics.Err())
|
||||
}
|
||||
|
||||
dir, err := os.Getwd()
|
||||
|
@ -162,32 +158,41 @@ func TestResourceProvider_ApplyCustomWorkingDirectory(t *testing.T) {
|
|||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(output.OutputMessage)
|
||||
want := dir + "/" + testdir
|
||||
got := strings.TrimSpace(output.OutputWriter.String())
|
||||
want := "Executing: [\"/bin/sh\" \"-c\" \"echo `pwd`\"]\n" + dir + "/" + testdir
|
||||
if got != want {
|
||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceProvider_ApplyCustomEnv(t *testing.T) {
|
||||
c := testConfig(t, map[string]interface{}{
|
||||
"command": "echo $FOO $BAR $BAZ",
|
||||
"environment": map[string]interface{}{
|
||||
"FOO": "BAR",
|
||||
"BAR": 1,
|
||||
"BAZ": "true",
|
||||
},
|
||||
})
|
||||
output := cli.NewMockUi()
|
||||
p := New()
|
||||
schema := p.GetSchema().Provisioner
|
||||
|
||||
output := new(terraform.MockUIOutput)
|
||||
p := Provisioner()
|
||||
|
||||
if err := p.Apply(output, nil, c); err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
c, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{
|
||||
"command": cty.StringVal("echo $FOO $BAR $BAZ"),
|
||||
"environment": cty.MapVal(map[string]cty.Value{
|
||||
"FOO": cty.StringVal("BAR"),
|
||||
"BAR": cty.StringVal("1"),
|
||||
"BAZ": cty.StringVal("true"),
|
||||
}),
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(output.OutputMessage)
|
||||
want := "BAR 1 true"
|
||||
resp := p.ProvisionResource(provisioners.ProvisionResourceRequest{
|
||||
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 {
|
||||
t.Errorf("wrong output\ngot: %s\nwant: %s", got, want)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue