github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/builtin/provisioners/file/resource_provisioner.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package file
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"os"
    12  
    13  	"github.com/mitchellh/go-homedir"
    14  	"github.com/terramate-io/tf/communicator"
    15  	"github.com/terramate-io/tf/configs/configschema"
    16  	"github.com/terramate-io/tf/provisioners"
    17  	"github.com/terramate-io/tf/tfdiags"
    18  	"github.com/zclconf/go-cty/cty"
    19  )
    20  
    21  func New() provisioners.Interface {
    22  	ctx, cancel := context.WithCancel(context.Background())
    23  	return &provisioner{
    24  		ctx:    ctx,
    25  		cancel: cancel,
    26  	}
    27  }
    28  
    29  type provisioner struct {
    30  	// We store a context here tied to the lifetime of the provisioner.
    31  	// This allows the Stop method to cancel any in-flight requests.
    32  	ctx    context.Context
    33  	cancel context.CancelFunc
    34  }
    35  
    36  func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) {
    37  	schema := &configschema.Block{
    38  		Attributes: map[string]*configschema.Attribute{
    39  			"source": {
    40  				Type:     cty.String,
    41  				Optional: true,
    42  			},
    43  
    44  			"content": {
    45  				Type:     cty.String,
    46  				Optional: true,
    47  			},
    48  
    49  			"destination": {
    50  				Type:     cty.String,
    51  				Required: true,
    52  			},
    53  		},
    54  	}
    55  	resp.Provisioner = schema
    56  	return resp
    57  }
    58  
    59  func (p *provisioner) ValidateProvisionerConfig(req provisioners.ValidateProvisionerConfigRequest) (resp provisioners.ValidateProvisionerConfigResponse) {
    60  	cfg, err := p.GetSchema().Provisioner.CoerceValue(req.Config)
    61  	if err != nil {
    62  		resp.Diagnostics = resp.Diagnostics.Append(err)
    63  	}
    64  
    65  	source := cfg.GetAttr("source")
    66  	content := cfg.GetAttr("content")
    67  
    68  	switch {
    69  	case !source.IsNull() && !content.IsNull():
    70  		resp.Diagnostics = resp.Diagnostics.Append(errors.New("Cannot set both 'source' and 'content'"))
    71  		return resp
    72  	case source.IsNull() && content.IsNull():
    73  		resp.Diagnostics = resp.Diagnostics.Append(errors.New("Must provide one of 'source' or 'content'"))
    74  		return resp
    75  	}
    76  
    77  	return resp
    78  }
    79  
    80  func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) {
    81  	if req.Connection.IsNull() {
    82  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    83  			tfdiags.Error,
    84  			"file provisioner error",
    85  			"Missing connection configuration for provisioner.",
    86  		))
    87  		return resp
    88  	}
    89  
    90  	comm, err := communicator.New(req.Connection)
    91  	if err != nil {
    92  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
    93  			tfdiags.Error,
    94  			"file provisioner error",
    95  			err.Error(),
    96  		))
    97  		return resp
    98  	}
    99  
   100  	// Get the source
   101  	src, deleteSource, err := getSrc(req.Config)
   102  	if err != nil {
   103  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   104  			tfdiags.Error,
   105  			"file provisioner error",
   106  			err.Error(),
   107  		))
   108  		return resp
   109  	}
   110  	if deleteSource {
   111  		defer os.Remove(src)
   112  	}
   113  
   114  	// Begin the file copy
   115  	dst := req.Config.GetAttr("destination").AsString()
   116  	if err := copyFiles(p.ctx, comm, src, dst); err != nil {
   117  		resp.Diagnostics = resp.Diagnostics.Append(tfdiags.WholeContainingBody(
   118  			tfdiags.Error,
   119  			"file provisioner error",
   120  			err.Error(),
   121  		))
   122  		return resp
   123  	}
   124  
   125  	return resp
   126  }
   127  
   128  // getSrc returns the file to use as source
   129  func getSrc(v cty.Value) (string, bool, error) {
   130  	content := v.GetAttr("content")
   131  	src := v.GetAttr("source")
   132  
   133  	switch {
   134  	case !content.IsNull():
   135  		file, err := ioutil.TempFile("", "tf-file-content")
   136  		if err != nil {
   137  			return "", true, err
   138  		}
   139  
   140  		if _, err = file.WriteString(content.AsString()); err != nil {
   141  			return "", true, err
   142  		}
   143  
   144  		return file.Name(), true, nil
   145  
   146  	case !src.IsNull():
   147  		expansion, err := homedir.Expand(src.AsString())
   148  		return expansion, false, err
   149  
   150  	default:
   151  		panic("source and content cannot both be null")
   152  	}
   153  }
   154  
   155  // copyFiles is used to copy the files from a source to a destination
   156  func copyFiles(ctx context.Context, comm communicator.Communicator, src, dst string) error {
   157  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   158  	defer cancel()
   159  
   160  	// Wait and retry until we establish the connection
   161  	err := communicator.Retry(retryCtx, func() error {
   162  		return comm.Connect(nil)
   163  	})
   164  	if err != nil {
   165  		return err
   166  	}
   167  
   168  	// disconnect when the context is canceled, which will close this after
   169  	// Apply as well.
   170  	go func() {
   171  		<-ctx.Done()
   172  		comm.Disconnect()
   173  	}()
   174  
   175  	info, err := os.Stat(src)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	// If we're uploading a directory, short circuit and do that
   181  	if info.IsDir() {
   182  		if err := comm.UploadDir(dst, src); err != nil {
   183  			return fmt.Errorf("Upload failed: %v", err)
   184  		}
   185  		return nil
   186  	}
   187  
   188  	// We're uploading a file...
   189  	f, err := os.Open(src)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	defer f.Close()
   194  
   195  	err = comm.Upload(dst, f)
   196  	if err != nil {
   197  		return fmt.Errorf("Upload failed: %v", err)
   198  	}
   199  
   200  	return err
   201  }
   202  
   203  func (p *provisioner) Stop() error {
   204  	p.cancel()
   205  	return nil
   206  }
   207  
   208  func (p *provisioner) Close() error {
   209  	return nil
   210  }