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

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package packer
     5  
     6  import (
     7  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"log"
    11  	"os"
    12  
    13  	hcpSbomProvisioner "github.com/hashicorp/packer/provisioner/hcp-sbom"
    14  
    15  	hcpPackerModels "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/models"
    16  	"github.com/klauspost/compress/zstd"
    17  
    18  	"time"
    19  
    20  	"github.com/hashicorp/hcl/v2/hcldec"
    21  	packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
    22  	"github.com/hashicorp/packer-plugin-sdk/packerbuilderdata"
    23  )
    24  
    25  // A HookedProvisioner represents a provisioner and information describing it
    26  type HookedProvisioner struct {
    27  	Provisioner packersdk.Provisioner
    28  	Config      interface{}
    29  	TypeName    string
    30  }
    31  
    32  // A Hook implementation that runs the given provisioners.
    33  type ProvisionHook struct {
    34  	// The provisioners to run as part of the hook. These should already
    35  	// be prepared (by calling Prepare) at some earlier stage.
    36  	Provisioners []*HookedProvisioner
    37  }
    38  
    39  // BuilderDataCommonKeys is the list of common keys that all builder will
    40  // return
    41  var BuilderDataCommonKeys = []string{
    42  	"ID",
    43  	// The following correspond to communicator-agnostic functions that are		}
    44  	// part of the SSH and WinRM communicator implementations. These functions
    45  	// are not part of the communicator interface, but are stored on the
    46  	// Communicator Config and return the appropriate values rather than
    47  	// depending on the actual communicator config values. E.g "Password"
    48  	// reprosents either WinRMPassword or SSHPassword, which makes this more
    49  	// useful if a template contains multiple builds.
    50  	"Host",
    51  	"Port",
    52  	"User",
    53  	"Password",
    54  	"ConnType",
    55  	"PackerRunUUID",
    56  	"PackerHTTPPort",
    57  	"PackerHTTPIP",
    58  	"PackerHTTPAddr",
    59  	"SSHPublicKey",
    60  	"SSHPrivateKey",
    61  	"WinRMPassword",
    62  }
    63  
    64  // Provisioners interpolate most of their fields in the prepare stage; this
    65  // placeholder map helps keep fields that are only generated at build time from
    66  // accidentally being interpolated into empty strings at prepare time.
    67  // This helper function generates the most basic placeholder data which should
    68  // be accessible to the provisioners. It is used to initialize provisioners, to
    69  // force validation using the `generated` template function. In the future,
    70  // custom generated data could be passed into provisioners from builders to
    71  // enable specialized builder-specific (but still validated!!) access to builder
    72  // data.
    73  func BasicPlaceholderData() map[string]string {
    74  	placeholderData := map[string]string{}
    75  	for _, key := range BuilderDataCommonKeys {
    76  		placeholderData[key] = fmt.Sprintf("Build_%s. "+packerbuilderdata.PlaceholderMsg, key)
    77  	}
    78  
    79  	// Backwards-compatability: WinRM Password can get through without forcing
    80  	// the generated func validation.
    81  	placeholderData["WinRMPassword"] = "{{.WinRMPassword}}"
    82  
    83  	return placeholderData
    84  }
    85  
    86  func CastDataToMap(data interface{}) map[string]interface{} {
    87  
    88  	if interMap, ok := data.(map[string]interface{}); ok {
    89  		// null and file builder sometimes don't use a communicator and
    90  		// therefore don't go through RPC
    91  		return interMap
    92  	}
    93  
    94  	// Provisioners expect a map[string]interface{} in their data field, but
    95  	// it gets converted into a map[interface]interface on the way over the
    96  	// RPC. Check that data can be cast into such a form, and cast it.
    97  	cast := make(map[string]interface{})
    98  	interMap, ok := data.(map[interface{}]interface{})
    99  	if !ok {
   100  		log.Printf("Unable to read map[string]interface out of data."+
   101  			"Using empty interface: %#v", data)
   102  	} else {
   103  		for key, val := range interMap {
   104  			keyString, ok := key.(string)
   105  			if ok {
   106  				cast[keyString] = val
   107  			} else {
   108  				log.Printf("Error casting generated data key to a string.")
   109  			}
   110  		}
   111  	}
   112  	return cast
   113  }
   114  
   115  // Runs the provisioners in order.
   116  func (h *ProvisionHook) Run(ctx context.Context, name string, ui packersdk.Ui, comm packersdk.Communicator, data interface{}) error {
   117  	// Shortcut
   118  	if len(h.Provisioners) == 0 {
   119  		return nil
   120  	}
   121  
   122  	if comm == nil {
   123  		return fmt.Errorf(
   124  			"No communicator found for provisioners! This is usually because the\n" +
   125  				"`communicator` config was set to \"none\". If you have any provisioners\n" +
   126  				"then a communicator is required. Please fix this to continue.")
   127  	}
   128  	for _, p := range h.Provisioners {
   129  		ts := CheckpointReporter.AddSpan(p.TypeName, "provisioner", p.Config)
   130  
   131  		cast := CastDataToMap(data)
   132  		err := p.Provisioner.Provision(ctx, ui, comm, cast)
   133  
   134  		ts.End(err)
   135  		if err != nil {
   136  			return err
   137  		}
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  // PausedProvisioner is a Provisioner implementation that pauses before
   144  // the provisioner is actually run.
   145  type PausedProvisioner struct {
   146  	PauseBefore time.Duration
   147  	Provisioner packersdk.Provisioner
   148  }
   149  
   150  func (p *PausedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
   151  func (p *PausedProvisioner) FlatConfig() interface{}       { return p.FlatConfig() }
   152  func (p *PausedProvisioner) Prepare(raws ...interface{}) error {
   153  	return p.Provisioner.Prepare(raws...)
   154  }
   155  
   156  func (p *PausedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
   157  
   158  	// Use a select to determine if we get cancelled during the wait
   159  	ui.Say(fmt.Sprintf("Pausing %s before the next provisioner...", p.PauseBefore))
   160  	select {
   161  	case <-time.After(p.PauseBefore):
   162  	case <-ctx.Done():
   163  		return ctx.Err()
   164  	}
   165  
   166  	return p.Provisioner.Provision(ctx, ui, comm, generatedData)
   167  }
   168  
   169  // RetriedProvisioner is a Provisioner implementation that retries
   170  // the provisioner whenever there's an error.
   171  type RetriedProvisioner struct {
   172  	MaxRetries  int
   173  	Provisioner packersdk.Provisioner
   174  }
   175  
   176  func (r *RetriedProvisioner) ConfigSpec() hcldec.ObjectSpec { return r.ConfigSpec() }
   177  func (r *RetriedProvisioner) FlatConfig() interface{}       { return r.FlatConfig() }
   178  func (r *RetriedProvisioner) Prepare(raws ...interface{}) error {
   179  	return r.Provisioner.Prepare(raws...)
   180  }
   181  
   182  func (r *RetriedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
   183  	if ctx.Err() != nil { // context was cancelled
   184  		return ctx.Err()
   185  	}
   186  
   187  	err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
   188  	if err == nil {
   189  		return nil
   190  	}
   191  
   192  	leftTries := r.MaxRetries
   193  	for ; leftTries > 0; leftTries-- {
   194  		if ctx.Err() != nil { // context was cancelled
   195  			return ctx.Err()
   196  		}
   197  
   198  		ui.Say(fmt.Sprintf("Provisioner failed with %q, retrying with %d trie(s) left", err, leftTries))
   199  
   200  		err := r.Provisioner.Provision(ctx, ui, comm, generatedData)
   201  		if err == nil {
   202  			return nil
   203  		}
   204  
   205  	}
   206  	ui.Say("retry limit reached.")
   207  
   208  	return err
   209  }
   210  
   211  // DebuggedProvisioner is a Provisioner implementation that waits until a key
   212  // press before the provisioner is actually run.
   213  type DebuggedProvisioner struct {
   214  	Provisioner packersdk.Provisioner
   215  }
   216  
   217  func (p *DebuggedProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
   218  func (p *DebuggedProvisioner) FlatConfig() interface{}       { return p.FlatConfig() }
   219  func (p *DebuggedProvisioner) Prepare(raws ...interface{}) error {
   220  	return p.Provisioner.Prepare(raws...)
   221  }
   222  
   223  func (p *DebuggedProvisioner) Provision(ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator, generatedData map[string]interface{}) error {
   224  	// Use a select to determine if we get cancelled during the wait
   225  	message := "Pausing before the next provisioner . Press enter to continue."
   226  
   227  	result := make(chan string, 1)
   228  	go func() {
   229  		line, err := ui.Ask(message)
   230  		if err != nil {
   231  			log.Printf("Error asking for input: %s", err)
   232  		}
   233  
   234  		result <- line
   235  	}()
   236  
   237  	select {
   238  	case <-result:
   239  	case <-ctx.Done():
   240  		return ctx.Err()
   241  	}
   242  
   243  	return p.Provisioner.Provision(ctx, ui, comm, generatedData)
   244  }
   245  
   246  // SBOMInternalProvisioner is a wrapper provisioner for the `hcp-sbom` provisioner
   247  // that sets the path for SBOM file download and, after the successful execution of
   248  // the `hcp-sbom` provisioner, compresses the SBOM and prepares the data for API
   249  // integration.
   250  type SBOMInternalProvisioner struct {
   251  	Provisioner    packersdk.Provisioner
   252  	CompressedData []byte
   253  	SBOMFormat     hcpPackerModels.HashicorpCloudPacker20230101SbomFormat
   254  	SBOMName       string
   255  }
   256  
   257  func (p *SBOMInternalProvisioner) ConfigSpec() hcldec.ObjectSpec { return p.ConfigSpec() }
   258  func (p *SBOMInternalProvisioner) FlatConfig() interface{}       { return p.FlatConfig() }
   259  func (p *SBOMInternalProvisioner) Prepare(raws ...interface{}) error {
   260  	return p.Provisioner.Prepare(raws...)
   261  }
   262  
   263  func (p *SBOMInternalProvisioner) Provision(
   264  	ctx context.Context, ui packersdk.Ui, comm packersdk.Communicator,
   265  	generatedData map[string]interface{},
   266  ) error {
   267  	cwd, err := os.Getwd()
   268  	if err != nil {
   269  		return fmt.Errorf("failed to get current working directory for Packer SBOM: %s", err)
   270  	}
   271  
   272  	tmpFile, err := os.CreateTemp(cwd, "packer-sbom-*.json")
   273  	if err != nil {
   274  		return fmt.Errorf("failed to create internal temporary file for Packer SBOM: %s", err)
   275  	}
   276  
   277  	tmpFileName := tmpFile.Name()
   278  	if err = tmpFile.Close(); err != nil {
   279  		return fmt.Errorf("failed to close temporary file for Packer SBOM %s: %s", tmpFileName, err)
   280  	}
   281  
   282  	defer func(name string) {
   283  		fileRemoveErr := os.Remove(name)
   284  		if fileRemoveErr != nil {
   285  			log.Printf("Error removing SBOM temporary file %s: %s", name, fileRemoveErr)
   286  		}
   287  	}(tmpFile.Name())
   288  
   289  	generatedData["dst"] = tmpFile.Name()
   290  
   291  	err = p.Provisioner.Provision(ctx, ui, comm, generatedData)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	packerSbom, err := os.Open(tmpFileName)
   297  	if err != nil {
   298  		return fmt.Errorf("failed to open Packer SBOM file %q: %s", tmpFileName, err)
   299  	}
   300  
   301  	provisionerOut := &hcpSbomProvisioner.PackerSBOM{}
   302  	err = json.NewDecoder(packerSbom).Decode(provisionerOut)
   303  	if err != nil {
   304  		return fmt.Errorf("malformed packer SBOM output from file %q: %s", tmpFileName, err)
   305  	}
   306  
   307  	encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
   308  	if err != nil {
   309  		return fmt.Errorf("failed to create zstd encoder: %s", err)
   310  	}
   311  	p.CompressedData = encoder.EncodeAll(provisionerOut.RawSBOM, nil)
   312  	p.SBOMFormat = provisionerOut.Format
   313  	p.SBOMName = provisionerOut.Name
   314  
   315  	return nil
   316  }