github.com/hashicorp/packer@v1.14.3/provisioner/file/provisioner.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  //go:generate packer-sdc mapstructure-to-hcl2 -type Config
     5  //go:generate packer-sdc struct-markdown
     6  
     7  package file
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  
    18  	"github.com/hashicorp/hcl/v2/hcldec"
    19  	"github.com/hashicorp/packer-plugin-sdk/common"
    20  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    21  	"github.com/hashicorp/packer-plugin-sdk/template/config"
    22  	"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
    23  	"github.com/hashicorp/packer-plugin-sdk/tmp"
    24  )
    25  
    26  type Config struct {
    27  	common.PackerConfig `mapstructure:",squash"`
    28  	// This is the content to copy to `destination`. If destination is a file,
    29  	// content will be written to that file, in case of a directory a file named
    30  	// `pkr-file-content` is created. It's recommended to use a file as the
    31  	// destination. The `templatefile` function might be used here, or any
    32  	// interpolation syntax. This attribute cannot be specified with source or
    33  	// sources.
    34  	Content string `mapstructure:"content" required:"true"`
    35  	// The path to a local file or directory to upload to the
    36  	// machine. The path can be absolute or relative. If it is relative, it is
    37  	// relative to the working directory when Packer is executed. If this is a
    38  	// directory, the existence of a trailing slash is important. Read below on
    39  	// uploading directories. Mandatory unless `sources` is set.
    40  	Source string `mapstructure:"source" required:"true"`
    41  	// A list of sources to upload. This can be used in place of the `source`
    42  	// option if you have several files that you want to upload to the same
    43  	// place. Note that the destination must be a directory with a trailing
    44  	// slash, and that all files listed in `sources` will be uploaded to the
    45  	// same directory with their file names preserved.
    46  	Sources []string `mapstructure:"sources" required:"false"`
    47  	// The path where the file will be uploaded to in the machine. This value
    48  	// must be a writable location and any parent directories
    49  	// must already exist. If the provisioning user (generally not root) cannot
    50  	// write to this directory, you will receive a "Permission Denied" error.
    51  	// If the source is a file, it's a good idea to make the destination a file
    52  	// as well, but if you set your destination as a directory, at least make
    53  	// sure that the destination ends in a trailing slash so that Packer knows
    54  	// to use the source's basename in the final upload path. Failure to do so
    55  	// may cause Packer to fail on file uploads. If the destination file
    56  	// already exists, it will be overwritten.
    57  	Destination string `mapstructure:"destination" required:"true"`
    58  	// The direction of the file transfer. This defaults to "upload". If it is
    59  	// set to "download" then the file "source" in the machine will be
    60  	// downloaded locally to "destination"
    61  	Direction string `mapstructure:"direction" required:"false"`
    62  	// For advanced users only. If true, check the file existence only before
    63  	// uploading, rather than upon pre-build validation. This allows users to
    64  	// upload files created on-the-fly. This defaults to false. We
    65  	// don't recommend using this feature, since it can cause Packer to become
    66  	// dependent on system state. We would prefer you generate your files before
    67  	// the Packer run, but realize that there are situations where this may be
    68  	// unavoidable.
    69  	Generated bool `mapstructure:"generated" required:"false"`
    70  
    71  	ctx interpolate.Context
    72  }
    73  
    74  type Provisioner struct {
    75  	config Config
    76  }
    77  
    78  func (p *Provisioner) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
    79  
    80  func (p *Provisioner) Prepare(raws ...interface{}) error {
    81  	err := config.Decode(&p.config, &config.DecodeOpts{
    82  		PluginType:         "file",
    83  		Interpolate:        true,
    84  		InterpolateContext: &p.config.ctx,
    85  		InterpolateFilter: &interpolate.RenderFilter{
    86  			Exclude: []string{},
    87  		},
    88  	}, raws...)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	if p.config.Direction == "" {
    94  		p.config.Direction = "upload"
    95  	}
    96  
    97  	var errs *packersdk.MultiError
    98  
    99  	if p.config.Direction != "download" && p.config.Direction != "upload" {
   100  		errs = packersdk.MultiErrorAppend(errs,
   101  			errors.New("Direction must be one of: download, upload."))
   102  	}
   103  	if p.config.Source != "" {
   104  		p.config.Sources = append(p.config.Sources, p.config.Source)
   105  	}
   106  
   107  	if p.config.Direction == "upload" {
   108  		for _, src := range p.config.Sources {
   109  			if _, err := os.Stat(src); p.config.Generated == false && err != nil {
   110  				errs = packersdk.MultiErrorAppend(errs,
   111  					fmt.Errorf("Bad source '%s': %s", src, err))
   112  			}
   113  		}
   114  	}
   115  
   116  	if len(p.config.Sources) > 0 && p.config.Content != "" {
   117  		errs = packersdk.MultiErrorAppend(errs,
   118  			errors.New("source(s) conflicts with content."))
   119  	}
   120  
   121  	if p.config.Destination == "" {
   122  		errs = packersdk.MultiErrorAppend(errs,
   123  			errors.New("Destination must be specified."))
   124  	}
   125  
   126  	if errs != nil && len(errs.Errors) > 0 {
   127  		return errs
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  func (p *Provisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
   134  	if generatedData == nil {
   135  		generatedData = make(map[string]interface{})
   136  	}
   137  	p.config.ctx.Data = generatedData
   138  
   139  	if p.config.Content != "" {
   140  		file, err := tmp.File("pkr-file-content")
   141  		if err != nil {
   142  			return err
   143  		}
   144  		defer file.Close()
   145  		if _, err := file.WriteString(p.config.Content); err != nil {
   146  			return err
   147  		}
   148  		p.config.Content = ""
   149  		p.config.Sources = append(p.config.Sources, file.Name())
   150  	}
   151  
   152  	if p.config.Direction == "download" {
   153  		return p.ProvisionDownload(ui, comm)
   154  	} else {
   155  		return p.ProvisionUpload(ui, comm)
   156  	}
   157  }
   158  
   159  func (p *Provisioner) ProvisionDownload(ui packersdk.Ui, comm packersdk.Communicator) error {
   160  	dst, err := interpolate.Render(p.config.Destination, &p.config.ctx)
   161  	if err != nil {
   162  		return fmt.Errorf("Error interpolating destination: %s", err)
   163  	}
   164  	for _, src := range p.config.Sources {
   165  		dst := dst
   166  		src, err := interpolate.Render(src, &p.config.ctx)
   167  		if err != nil {
   168  			return fmt.Errorf("Error interpolating source: %s", err)
   169  		}
   170  
   171  		// ensure destination dir exists.  p.config.Destination may either be a file or a dir.
   172  		dir := dst
   173  		// if it doesn't end with a /, set dir as the parent dir
   174  		if !strings.HasSuffix(dst, "/") {
   175  			dir = filepath.Dir(dir)
   176  		} else if !strings.HasSuffix(src, "/") && !strings.HasSuffix(src, "*") {
   177  			dst = filepath.Join(dst, filepath.Base(src))
   178  		}
   179  		ui.Say(fmt.Sprintf("Downloading %s => %s", src, dst))
   180  
   181  		if dir != "" {
   182  			err := os.MkdirAll(dir, os.FileMode(0755))
   183  			if err != nil {
   184  				return err
   185  			}
   186  		}
   187  		// if the src was a dir, download the dir
   188  		if strings.HasSuffix(src, "/") || strings.ContainsAny(src, "*?[") {
   189  			return comm.DownloadDir(src, dst, nil)
   190  		}
   191  
   192  		f, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
   193  		if err != nil {
   194  			return err
   195  		}
   196  		defer f.Close()
   197  
   198  		// Create MultiWriter for the current progress
   199  		pf := io.MultiWriter(f)
   200  
   201  		// Download the file
   202  		if err = comm.Download(src, pf); err != nil {
   203  			ui.Error(fmt.Sprintf("Download failed: %s", err))
   204  			return err
   205  		}
   206  	}
   207  	return nil
   208  }
   209  
   210  func (p *Provisioner) ProvisionUpload(ui packersdk.Ui, comm packersdk.Communicator) error {
   211  	dst, err := interpolate.Render(p.config.Destination, &p.config.ctx)
   212  	if err != nil {
   213  		return fmt.Errorf("Error interpolating destination: %s", err)
   214  	}
   215  	for _, src := range p.config.Sources {
   216  		src, err := interpolate.Render(src, &p.config.ctx)
   217  		if err != nil {
   218  			return fmt.Errorf("Error interpolating source: %s", err)
   219  		}
   220  
   221  		ui.Say(fmt.Sprintf("Uploading %s => %s", src, dst))
   222  
   223  		info, err := os.Stat(src)
   224  		if err != nil {
   225  			return err
   226  		}
   227  
   228  		// If we're uploading a directory, short circuit and do that
   229  		if info.IsDir() {
   230  			if err = comm.UploadDir(dst, src, nil); err != nil {
   231  				ui.Error(fmt.Sprintf("Upload failed: %s", err))
   232  				return err
   233  			}
   234  			continue
   235  		}
   236  
   237  		// We're uploading a file...
   238  		f, err := os.Open(src)
   239  		if err != nil {
   240  			return err
   241  		}
   242  		defer f.Close()
   243  
   244  		fi, err := f.Stat()
   245  		if err != nil {
   246  			return err
   247  		}
   248  
   249  		filedst := dst
   250  		if strings.HasSuffix(dst, "/") {
   251  			filedst = dst + filepath.Base(src)
   252  		}
   253  
   254  		pf := ui.TrackProgress(filepath.Base(src), 0, info.Size(), f)
   255  		defer pf.Close()
   256  
   257  		// Upload the file
   258  		if err = comm.Upload(filedst, pf, &fi); err != nil {
   259  			if strings.Contains(err.Error(), "Error restoring file") {
   260  				ui.Error(fmt.Sprintf("Upload failed: %s; this can occur when "+
   261  					"your file destination is a folder without a trailing "+
   262  					"slash.", err))
   263  			}
   264  			ui.Error(fmt.Sprintf("Upload failed: %s", err))
   265  			return err
   266  		}
   267  	}
   268  	return nil
   269  }