github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/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/hashicorp/terraform/internal/configs/configschema"
    13  	"github.com/hashicorp/terraform/internal/provisioners"
    14  	"github.com/hashicorp/terraform/internal/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  			"quiet": {
    61  				Type:     cty.Bool,
    62  				Optional: true,
    63  			},
    64  		},
    65  	}
    66  
    67  	resp.Provisioner = schema
    68  	return resp
    69  }
    70  
    71  func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
    72  	if _, err := p.GetSchema().Provisioner.CoerceValue(req.Config); err != nil {
    73  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    74  			tfdiags.Error,
    75  			"Invalid local-exec provisioner configuration",
    76  			err.Error(),
    77  		))
    78  	}
    79  	return resp
    80  }
    81  
    82  func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
    83  	command := req.Config.GetAttr("command").AsString()
    84  	if command == "" {
    85  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    86  			tfdiags.Error,
    87  			"Invalid local-exec provisioner command",
    88  			"The command must be a non-empty string.",
    89  		))
    90  		return resp
    91  	}
    92  
    93  	envVal := req.Config.GetAttr("environment")
    94  	var env []string
    95  
    96  	if !envVal.IsNull() {
    97  		for k, v := range envVal.AsValueMap() {
    98  			if !v.IsNull() {
    99  				entry := fmt.Sprintf("%s=%s", k, v.AsString())
   100  				env = append(env, entry)
   101  			}
   102  		}
   103  	}
   104  
   105  	// Execute the command using a shell
   106  	intrVal := req.Config.GetAttr("interpreter")
   107  
   108  	var cmdargs []string
   109  	if !intrVal.IsNull() && intrVal.LengthInt() > 0 {
   110  		for _, v := range intrVal.AsValueSlice() {
   111  			if !v.IsNull() {
   112  				cmdargs = append(cmdargs, v.AsString())
   113  			}
   114  		}
   115  	} else {
   116  		if runtime.GOOS == "windows" {
   117  			cmdargs = []string{"cmd", "/C"}
   118  		} else {
   119  			cmdargs = []string{"/bin/sh", "-c"}
   120  		}
   121  	}
   122  
   123  	cmdargs = append(cmdargs, command)
   124  
   125  	workingdir := ""
   126  	if wdVal := req.Config.GetAttr("working_dir"); !wdVal.IsNull() {
   127  		workingdir = wdVal.AsString()
   128  	}
   129  
   130  	// Set up the reader that will read the output from the command.
   131  	// We use an os.Pipe so that the *os.File can be passed directly to the
   132  	// process, and not rely on goroutines copying the data which may block.
   133  	// See golang.org/issue/18874
   134  	pr, pw, err := os.Pipe()
   135  	if err != nil {
   136  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   137  			tfdiags.Error,
   138  			"local-exec provisioner error",
   139  			fmt.Sprintf("Failed to initialize pipe for output: %s", err),
   140  		))
   141  		return resp
   142  	}
   143  
   144  	var cmdEnv []string
   145  	cmdEnv = os.Environ()
   146  	cmdEnv = append(cmdEnv, env...)
   147  
   148  	// Set up the command
   149  	cmd := exec.CommandContext(p.ctx, cmdargs[0], cmdargs[1:]...)
   150  	cmd.Stderr = pw
   151  	cmd.Stdout = pw
   152  	// Dir specifies the working directory of the command.
   153  	// If Dir is the empty string (this is default), runs the command
   154  	// in the calling process's current directory.
   155  	cmd.Dir = workingdir
   156  	// Env specifies the environment of the command.
   157  	// By default will use the calling process's environment
   158  	cmd.Env = cmdEnv
   159  
   160  	output, _ := circbuf.NewBuffer(maxBufSize)
   161  
   162  	// Write everything we read from the pipe to the output buffer too
   163  	tee := io.TeeReader(pr, output)
   164  
   165  	// copy the teed output to the UI output
   166  	copyDoneCh := make(chan struct{})
   167  	go copyUIOutput(req.UIOutput, tee, copyDoneCh)
   168  
   169  	// Output what we're about to run
   170  	if quietVal := req.Config.GetAttr("quiet"); !quietVal.IsNull() && quietVal.True() {
   171  		req.UIOutput.Output("local-exec: Executing: Suppressed by quiet=true")
   172  	} else {
   173  		req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs))
   174  	}
   175  
   176  	// Start the command
   177  	err = cmd.Start()
   178  	if err == nil {
   179  		err = cmd.Wait()
   180  	}
   181  
   182  	// Close the write-end of the pipe so that the goroutine mirroring output
   183  	// ends properly.
   184  	pw.Close()
   185  
   186  	// Cancelling the command may block the pipe reader if the file descriptor
   187  	// was passed to a child process which hasn't closed it. In this case the
   188  	// copyOutput goroutine will just hang out until exit.
   189  	select {
   190  	case <-copyDoneCh:
   191  	case <-p.ctx.Done():
   192  	}
   193  
   194  	if err != nil {
   195  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   196  			tfdiags.Error,
   197  			"local-exec provisioner error",
   198  			fmt.Sprintf("Error running command '%s': %v. Output: %s", command, err, output.Bytes()),
   199  		))
   200  		return resp
   201  	}
   202  
   203  	return resp
   204  }
   205  
   206  func (p *provisioner) Stop() error {
   207  	p.cancel()
   208  	return nil
   209  }
   210  
   211  func (p *provisioner) Close() error {
   212  	return nil
   213  }
   214  
   215  func copyUIOutput(o provisioners.UIOutput, r io.Reader, doneCh chan<- struct{}) {
   216  	defer close(doneCh)
   217  	lr := linereader.New(r)
   218  	for line := range lr.Ch {
   219  		o.Output(line)
   220  	}
   221  }