github.com/kevinklinger/open_terraform@v1.3.6/noninternal/builtin/provisioners/local-exec/resource_provisioner.go (about)

     1  package localexec
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"os/exec"
     9  	"runtime"
    10  
    11  	"github.com/armon/circbuf"
    12  	"github.com/kevinklinger/open_terraform/noninternal/configs/configschema"
    13  	"github.com/kevinklinger/open_terraform/noninternal/provisioners"
    14  	"github.com/kevinklinger/open_terraform/noninternal/tfdiags"
    15  	"github.com/mitchellh/go-linereader"
    16  	"github.com/zclconf/go-cty/cty"
    17  )
    18  
    19  const (
    20  	// maxBufSize limits how much output we collect from a local
    21  	// invocation. This is to prevent TF memory usage from growing
    22  	// to an enormous amount due to a faulty process.
    23  	maxBufSize = 8 * 1024
    24  )
    25  
    26  func New() provisioners.Interface {
    27  	ctx, cancel := context.WithCancel(context.Background())
    28  	return &provisioner{
    29  		ctx:    ctx,
    30  		cancel: cancel,
    31  	}
    32  }
    33  
    34  type provisioner struct {
    35  	// We store a context here tied to the lifetime of the provisioner.
    36  	// This allows the Stop method to cancel any in-flight requests.
    37  	ctx    context.Context
    38  	cancel context.CancelFunc
    39  }
    40  
    41  func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
    42  	schema := &configschema.Block{
    43  		Attributes: map[string]*configschema.Attribute{
    44  			"command": {
    45  				Type:     cty.String,
    46  				Required: true,
    47  			},
    48  			"interpreter": {
    49  				Type:     cty.List(cty.String),
    50  				Optional: true,
    51  			},
    52  			"working_dir": {
    53  				Type:     cty.String,
    54  				Optional: true,
    55  			},
    56  			"environment": {
    57  				Type:     cty.Map(cty.String),
    58  				Optional: true,
    59  			},
    60  		},
    61  	}
    62  
    63  	resp.Provisioner = schema
    64  	return resp
    65  }
    66  
    67  func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
    68  	if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
    69  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    70  			tfdiags.Error,
    71  			"Invalid local-exec provisioner configuration",
    72  			err.Error(),
    73  		))
    74  	}
    75  	return resp
    76  }
    77  
    78  func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
    79  	command := req.Config.GetAttr("command").AsString()
    80  	if command == "" {
    81  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    82  			tfdiags.Error,
    83  			"Invalid local-exec provisioner command",
    84  			"The command must be a non-empty string.",
    85  		))
    86  		return resp
    87  	}
    88  
    89  	envVal := req.Config.GetAttr("environment")
    90  	var env []string
    91  
    92  	if !envVal.IsNull() {
    93  		for k, v := range envVal.AsValueMap() {
    94  			if !v.IsNull() {
    95  				entry := fmt.Sprintf("%s=%s", k, v.AsString())
    96  				env = append(env, entry)
    97  			}
    98  		}
    99  	}
   100  
   101  	// Execute the command using a shell
   102  	intrVal := req.Config.GetAttr("interpreter")
   103  
   104  	var cmdargs []string
   105  	if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
   106  		for _, v := range intrVal.AsValueSlice() {
   107  			if !v.IsNull() {
   108  				cmdargs = append(cmdargs, v.AsString())
   109  			}
   110  		}
   111  	} else {
   112  		if runtime.GOOS == "windows" {
   113  			cmdargs = []string{"cmd", "/C"}
   114  		} else {
   115  			cmdargs = []string{"/bin/sh", "-c"}
   116  		}
   117  	}
   118  
   119  	cmdargs = append(cmdargs, command)
   120  
   121  	workingdir := ""
   122  	if wdVal := req.Config.GetAttr("working_dir"); !wdVal.IsNull() {
   123  		workingdir = wdVal.AsString()
   124  	}
   125  
   126  	// Set up the reader that will read the output from the command.
   127  	// We use an os.Pipe so that the *os.File can be passed directly to the
   128  	// process, and not rely on goroutines copying the data which may block.
   129  	// See golang.org/issue/18874
   130  	pr, pw, err := os.Pipe()
   131  	if err != nil {
   132  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   133  			tfdiags.Error,
   134  			"local-exec provisioner error",
   135  			fmt.Sprintf("Failed to initialize pipe for output: %s", err),
   136  		))
   137  		return resp
   138  	}
   139  
   140  	var cmdEnv []string
   141  	cmdEnv = os.Environ()
   142  	cmdEnv = append(cmdEnv, env...)
   143  
   144  	// Set up the command
   145  	cmd := exec.CommandContext(p.ctx, cmdargs[0], cmdargs[1:]...)
   146  	cmd.Stderr = pw
   147  	cmd.Stdout = pw
   148  	// Dir specifies the working directory of the command.
   149  	// If Dir is the empty string (this is default), runs the command
   150  	// in the calling process's current directory.
   151  	cmd.Dir = workingdir
   152  	// Env specifies the environment of the command.
   153  	// By default will use the calling process's environment
   154  	cmd.Env = cmdEnv
   155  
   156  	output, _ := circbuf.NewBuffer(maxBufSize)
   157  
   158  	// Write everything we read from the pipe to the output buffer too
   159  	tee := io.TeeReader(pr, output)
   160  
   161  	// copy the teed output to the UI output
   162  	copyDoneCh := make(chan struct{})
   163  	go copyUIOutput(req.UIOutput, tee, copyDoneCh)
   164  
   165  	// Output what we're about to run
   166  	req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs))
   167  
   168  	// Start the command
   169  	err = cmd.Start()
   170  	if err == nil {
   171  		err = cmd.Wait()
   172  	}
   173  
   174  	// Close the write-end of the pipe so that the goroutine mirroring output
   175  	// ends properly.
   176  	pw.Close()
   177  
   178  	// Cancelling the command may block the pipe reader if the file descriptor
   179  	// was passed to a child process which hasn't closed it. In this case the
   180  	// copyOutput goroutine will just hang out until exit.
   181  	select {
   182  	case <-copyDoneCh:
   183  	case <-p.ctx.Done():
   184  	}
   185  
   186  	if err != nil {
   187  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   188  			tfdiags.Error,
   189  			"local-exec provisioner error",
   190  			fmt.Sprintf("Error running command '%s': %v. Output: %s", command, err, output.Bytes()),
   191  		))
   192  		return resp
   193  	}
   194  
   195  	return resp
   196  }
   197  
   198  func (p *provisioner) Stop() error {
   199  	p.cancel()
   200  	return nil
   201  }
   202  
   203  func (p *provisioner) Close() error {
   204  	return nil
   205  }
   206  
   207  func copyUIOutput(o provisioners.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   208  	defer close(doneCh)
   209  	lr := linereader.New(r)
   210  	for line := range lr.Ch {
   211  		o.Output(line)
   212  	}
   213  }