github.com/opentofu/opentofu@v1.7.1/internal/builtin/provisioners/file/resource_provisioner.go (about)

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