github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/builtin/provisioners/remote-exec/resource_provisioner.go (about)

     1  package remoteexec
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"log"
    11  	"os"
    12  	"strings"
    13  
    14  	"github.com/hashicorp/terraform/internal/communicator"
    15  	"github.com/hashicorp/terraform/internal/communicator/remote"
    16  	"github.com/hashicorp/terraform/internal/configs/configschema"
    17  	"github.com/hashicorp/terraform/internal/provisioners"
    18  	"github.com/hashicorp/terraform/internal/tfdiags"
    19  	"github.com/mitchellh/go-linereader"
    20  	"github.com/zclconf/go-cty/cty"
    21  )
    22  
    23  func New() provisioners.Interface {
    24  	ctx, cancel := context.WithCancel(context.Background())
    25  	return &provisioner{
    26  		ctx:    ctx,
    27  		cancel: cancel,
    28  	}
    29  }
    30  
    31  type provisioner struct {
    32  	// We store a context here tied to the lifetime of the provisioner.
    33  	// This allows the Stop method to cancel any in-flight requests.
    34  	ctx    context.Context
    35  	cancel context.CancelFunc
    36  }
    37  
    38  func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
    39  	schema := &configschema.Block{
    40  		Attributes: map[string]*configschema.Attribute{
    41  			"inline": {
    42  				Type:     cty.List(cty.String),
    43  				Optional: true,
    44  			},
    45  			"script": {
    46  				Type:     cty.String,
    47  				Optional: true,
    48  			},
    49  			"scripts": {
    50  				Type:     cty.List(cty.String),
    51  				Optional: true,
    52  			},
    53  		},
    54  	}
    55  
    56  	resp.Provisioner = schema
    57  	return resp
    58  }
    59  
    60  func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
    61  	cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
    62  	if err != nil {
    63  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    64  			tfdiags.Error,
    65  			"Invalid remote-exec provisioner configuration",
    66  			err.Error(),
    67  		))
    68  		return resp
    69  	}
    70  
    71  	inline := cfg.GetAttr("inline")
    72  	script := cfg.GetAttr("script")
    73  	scripts := cfg.GetAttr("scripts")
    74  
    75  	set := 0
    76  	if !inline.IsNull() {
    77  		set++
    78  	}
    79  	if !script.IsNull() {
    80  		set++
    81  	}
    82  	if !scripts.IsNull() {
    83  		set++
    84  	}
    85  	if set != 1 {
    86  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    87  			tfdiags.Error,
    88  			"Invalid remote-exec provisioner configuration",
    89  			`Only one of "inline", "script", or "scripts" must be set`,
    90  		))
    91  	}
    92  	return resp
    93  }
    94  
    95  func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
    96  	if req.Connection.IsNull() {
    97  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    98  			tfdiags.Error,
    99  			"remote-exec provisioner error",
   100  			"Missing connection configuration for provisioner.",
   101  		))
   102  		return resp
   103  	}
   104  
   105  	comm, err := communicator.New(req.Connection)
   106  	if err != nil {
   107  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   108  			tfdiags.Error,
   109  			"remote-exec provisioner error",
   110  			err.Error(),
   111  		))
   112  		return resp
   113  	}
   114  
   115  	// Collect the scripts
   116  	scripts, err := collectScripts(req.Config)
   117  	if err != nil {
   118  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   119  			tfdiags.Error,
   120  			"remote-exec provisioner error",
   121  			err.Error(),
   122  		))
   123  		return resp
   124  	}
   125  	for _, s := range scripts {
   126  		defer s.Close()
   127  	}
   128  
   129  	// Copy and execute each script
   130  	if err := runScripts(p.ctx, req.UIOutput, comm, scripts); err != nil {
   131  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   132  			tfdiags.Error,
   133  			"remote-exec provisioner error",
   134  			err.Error(),
   135  		))
   136  		return resp
   137  	}
   138  
   139  	return resp
   140  }
   141  
   142  func (p *provisioner) Stop() error {
   143  	p.cancel()
   144  	return nil
   145  }
   146  
   147  func (p *provisioner) Close() error {
   148  	return nil
   149  }
   150  
   151  // generateScripts takes the configuration and creates a script from each inline config
   152  func generateScripts(inline cty.Value) ([]string, error) {
   153  	var lines []string
   154  	for _, l := range inline.AsValueSlice() {
   155  		if l.IsNull() {
   156  			return nil, errors.New("invalid null string in 'scripts'")
   157  		}
   158  
   159  		s := l.AsString()
   160  		if s == "" {
   161  			return nil, errors.New("invalid empty string in 'scripts'")
   162  		}
   163  		lines = append(lines, s)
   164  	}
   165  	lines = append(lines, "")
   166  
   167  	return []string{strings.Join(lines, "\n")}, nil
   168  }
   169  
   170  // collectScripts is used to collect all the scripts we need
   171  // to execute in preparation for copying them.
   172  func collectScripts(v cty.Value) ([]io.ReadCloser, error) {
   173  	// Check if inline
   174  	if inline := v.GetAttr("inline"); !inline.IsNull() {
   175  		scripts, err := generateScripts(inline)
   176  		if err != nil {
   177  			return nil, err
   178  		}
   179  
   180  		var r []io.ReadCloser
   181  		for _, script := range scripts {
   182  			r = append(r, ioutil.NopCloser(bytes.NewReader([]byte(script))))
   183  		}
   184  
   185  		return r, nil
   186  	}
   187  
   188  	// Collect scripts
   189  	var scripts []string
   190  	if script := v.GetAttr("script"); !script.IsNull() {
   191  		s := script.AsString()
   192  		if s == "" {
   193  			return nil, errors.New("invalid empty string in 'script'")
   194  		}
   195  		scripts = append(scripts, s)
   196  	}
   197  
   198  	if scriptList := v.GetAttr("scripts"); !scriptList.IsNull() {
   199  		for _, script := range scriptList.AsValueSlice() {
   200  			if script.IsNull() {
   201  				return nil, errors.New("invalid null string in 'script'")
   202  			}
   203  			s := script.AsString()
   204  			if s == "" {
   205  				return nil, errors.New("invalid empty string in 'script'")
   206  			}
   207  			scripts = append(scripts, s)
   208  		}
   209  	}
   210  
   211  	// Open all the scripts
   212  	var fhs []io.ReadCloser
   213  	for _, s := range scripts {
   214  		fh, err := os.Open(s)
   215  		if err != nil {
   216  			for _, fh := range fhs {
   217  				fh.Close()
   218  			}
   219  			return nil, fmt.Errorf("Failed to open script '%s': %v", s, err)
   220  		}
   221  		fhs = append(fhs, fh)
   222  	}
   223  
   224  	// Done, return the file handles
   225  	return fhs, nil
   226  }
   227  
   228  // runScripts is used to copy and execute a set of scripts
   229  func runScripts(ctx context.Context, o provisioners.UIOutput, comm communicator.Communicator, scripts []io.ReadCloser) error {
   230  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   231  	defer cancel()
   232  
   233  	// Wait and retry until we establish the connection
   234  	err := communicator.Retry(retryCtx, func() error {
   235  		return comm.Connect(o)
   236  	})
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	// Wait for the context to end and then disconnect
   242  	go func() {
   243  		<-ctx.Done()
   244  		comm.Disconnect()
   245  	}()
   246  
   247  	for _, script := range scripts {
   248  		var cmd *remote.Cmd
   249  
   250  		outR, outW := io.Pipe()
   251  		errR, errW := io.Pipe()
   252  		defer outW.Close()
   253  		defer errW.Close()
   254  
   255  		go copyUIOutput(o, outR)
   256  		go copyUIOutput(o, errR)
   257  
   258  		remotePath := comm.ScriptPath()
   259  
   260  		if err := comm.UploadScript(remotePath, script); err != nil {
   261  			return fmt.Errorf("Failed to upload script: %v", err)
   262  		}
   263  
   264  		cmd = &remote.Cmd{
   265  			Command: remotePath,
   266  			Stdout:  outW,
   267  			Stderr:  errW,
   268  		}
   269  		if err := comm.Start(cmd); err != nil {
   270  			return fmt.Errorf("Error starting script: %v", err)
   271  		}
   272  
   273  		if err := cmd.Wait(); err != nil {
   274  			return err
   275  		}
   276  
   277  		// Upload a blank follow up file in the same path to prevent residual
   278  		// script contents from remaining on remote machine
   279  		empty := bytes.NewReader([]byte(""))
   280  		if err := comm.Upload(remotePath, empty); err != nil {
   281  			// This feature is best-effort.
   282  			log.Printf("[WARN] Failed to upload empty follow up script: %v", err)
   283  		}
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  func copyUIOutput(o provisioners.UIOutput, r io.Reader) {
   290  	lr := linereader.New(r)
   291  	for line := range lr.Ch {
   292  		o.Output(line)
   293  	}
   294  }