github.com/mponton/terratest@v0.44.0/modules/packer/packer.go (about)

     1  // Package packer allows to interact with Packer.
     2  package packer
     3  
     4  import (
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/hashicorp/go-multierror"
    15  	"github.com/mponton/terratest/modules/retry"
    16  	"github.com/stretchr/testify/require"
    17  
    18  	"github.com/hashicorp/go-version"
    19  	"github.com/mponton/terratest/modules/logger"
    20  	"github.com/mponton/terratest/modules/shell"
    21  	"github.com/mponton/terratest/modules/testing"
    22  )
    23  
    24  // Options are the options for Packer.
    25  type Options struct {
    26  	Template                   string            // The path to the Packer template
    27  	Vars                       map[string]string // The custom vars to pass when running the build command
    28  	VarFiles                   []string          // Var file paths to pass Packer using -var-file option
    29  	Only                       string            // If specified, only run the build of this name
    30  	Except                     string            // Runs the build excluding the specified builds and post-processors
    31  	Env                        map[string]string // Custom environment variables to set when running Packer
    32  	RetryableErrors            map[string]string // If packer build fails with one of these (transient) errors, retry. The keys are a regexp to match against the error and the message is what to display to a user if that error is matched.
    33  	MaxRetries                 int               // Maximum number of times to retry errors matching RetryableErrors
    34  	TimeBetweenRetries         time.Duration     // The amount of time to wait between retries
    35  	WorkingDir                 string            // The directory to run packer in
    36  	Logger                     *logger.Logger    // If set, use a non-default logger
    37  	DisableTemporaryPluginPath bool              // If set, do not use a temporary directory for Packer plugins.
    38  }
    39  
    40  // BuildArtifacts can take a map of identifierName <-> Options and then parallelize
    41  // the packer builds. Once all the packer builds have completed a map of identifierName <-> generated identifier
    42  // is returned. The identifierName can be anything you want, it is only used so that you can
    43  // know which generated artifact is which.
    44  func BuildArtifacts(t testing.TestingT, artifactNameToOptions map[string]*Options) map[string]string {
    45  	result, err := BuildArtifactsE(t, artifactNameToOptions)
    46  
    47  	if err != nil {
    48  		t.Fatalf("Error building artifacts: %s", err.Error())
    49  	}
    50  
    51  	return result
    52  }
    53  
    54  // BuildArtifactsE can take a map of identifierName <-> Options and then parallelize
    55  // the packer builds. Once all the packer builds have completed a map of identifierName <-> generated identifier
    56  // is returned. If any artifact fails to build, the errors are accumulated and returned
    57  // as a MultiError. The identifierName can be anything you want, it is only used so that you can
    58  // know which generated artifact is which.
    59  func BuildArtifactsE(t testing.TestingT, artifactNameToOptions map[string]*Options) (map[string]string, error) {
    60  	var waitForArtifacts sync.WaitGroup
    61  	waitForArtifacts.Add(len(artifactNameToOptions))
    62  
    63  	var artifactNameToArtifactId = map[string]string{}
    64  	var errorsOccurred = new(multierror.Error)
    65  
    66  	for artifactName, curOptions := range artifactNameToOptions {
    67  		// The following is necessary to make sure artifactName and curOptions don't
    68  		// get updated due to concurrency within the scope of t.Run(..) below
    69  		artifactName := artifactName
    70  		curOptions := curOptions
    71  		go func() {
    72  			defer waitForArtifacts.Done()
    73  			artifactId, err := BuildArtifactE(t, curOptions)
    74  
    75  			if err != nil {
    76  				errorsOccurred = multierror.Append(errorsOccurred, err)
    77  			} else {
    78  				artifactNameToArtifactId[artifactName] = artifactId
    79  			}
    80  		}()
    81  	}
    82  
    83  	waitForArtifacts.Wait()
    84  
    85  	return artifactNameToArtifactId, errorsOccurred.ErrorOrNil()
    86  }
    87  
    88  // BuildArtifact builds the given Packer template and return the generated Artifact ID.
    89  func BuildArtifact(t testing.TestingT, options *Options) string {
    90  	artifactID, err := BuildArtifactE(t, options)
    91  	if err != nil {
    92  		t.Fatal(err)
    93  	}
    94  	return artifactID
    95  }
    96  
    97  // BuildArtifactE builds the given Packer template and return the generated Artifact ID.
    98  func BuildArtifactE(t testing.TestingT, options *Options) (string, error) {
    99  	options.Logger.Logf(t, "Running Packer to generate a custom artifact for template %s", options.Template)
   100  
   101  	// By default, we download packer plugins to a temporary directory rather than use the global plugin path.
   102  	// This prevents race conditions when multiple tests are running in parallel and each of them attempt
   103  	// to download the same plugin at the same time to the global path.
   104  	// Set DisableTemporaryPluginPath to disable this behavior.
   105  	if !options.DisableTemporaryPluginPath {
   106  		// The built-in env variable defining where plugins are downloaded
   107  		const packerPluginPathEnvVar = "PACKER_PLUGIN_PATH"
   108  		options.Logger.Logf(t, "Creating a temporary directory for Packer plugins")
   109  		pluginDir, err := ioutil.TempDir("", "terratest-packer-")
   110  		require.NoError(t, err)
   111  		if len(options.Env) == 0 {
   112  			options.Env = make(map[string]string)
   113  		}
   114  		options.Env[packerPluginPathEnvVar] = pluginDir
   115  		defer os.RemoveAll(pluginDir)
   116  	}
   117  
   118  	err := packerInit(t, options)
   119  	if err != nil {
   120  		return "", err
   121  	}
   122  
   123  	cmd := shell.Command{
   124  		Command:    "packer",
   125  		Args:       formatPackerArgs(options),
   126  		Env:        options.Env,
   127  		WorkingDir: options.WorkingDir,
   128  	}
   129  
   130  	description := fmt.Sprintf("%s %v", cmd.Command, cmd.Args)
   131  	output, err := retry.DoWithRetryableErrorsE(t, description, options.RetryableErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {
   132  		return shell.RunCommandAndGetOutputE(t, cmd)
   133  	})
   134  
   135  	if err != nil {
   136  		return "", err
   137  	}
   138  
   139  	return extractArtifactID(output)
   140  }
   141  
   142  // BuildAmi builds the given Packer template and return the generated AMI ID.
   143  //
   144  // Deprecated: Use BuildArtifact instead.
   145  func BuildAmi(t testing.TestingT, options *Options) string {
   146  	return BuildArtifact(t, options)
   147  }
   148  
   149  // BuildAmiE builds the given Packer template and return the generated AMI ID.
   150  //
   151  // Deprecated: Use BuildArtifactE instead.
   152  func BuildAmiE(t testing.TestingT, options *Options) (string, error) {
   153  	return BuildArtifactE(t, options)
   154  }
   155  
   156  // The Packer machine-readable log output should contain an entry of this format:
   157  //
   158  // AWS: <timestamp>,<builder>,artifact,<index>,id,<region>:<image_id>
   159  // GCP: <timestamp>,<builder>,artifact,<index>,id,<image_id>
   160  //
   161  // For example:
   162  //
   163  // 1456332887,amazon-ebs,artifact,0,id,us-east-1:ami-b481b3de
   164  // 1533742764,googlecompute,artifact,0,id,terratest-packer-example-2018-08-08t15-35-19z
   165  func extractArtifactID(packerLogOutput string) (string, error) {
   166  	re := regexp.MustCompile(`.+artifact,\d+?,id,(?:.+?:|)(.+)`)
   167  	matches := re.FindStringSubmatch(packerLogOutput)
   168  
   169  	if len(matches) == 2 {
   170  		return matches[1], nil
   171  	}
   172  	return "", errors.New("Could not find Artifact ID pattern in Packer output")
   173  }
   174  
   175  // Check if the local version of Packer has init
   176  func hasPackerInit(t testing.TestingT, options *Options) (bool, error) {
   177  	// The init command was introduced in Packer 1.7.0
   178  	const packerInitVersion = "1.7.0"
   179  	minInitVersion, err := version.NewVersion(packerInitVersion)
   180  	if err != nil {
   181  		return false, err
   182  	}
   183  
   184  	cmd := shell.Command{
   185  		Command:    "packer",
   186  		Args:       []string{"-version"},
   187  		Env:        options.Env,
   188  		WorkingDir: options.WorkingDir,
   189  	}
   190  	localVersion, err := shell.RunCommandAndGetOutputE(t, cmd)
   191  	if err != nil {
   192  		return false, err
   193  	}
   194  	thisVersion, err := version.NewVersion(localVersion)
   195  	if err != nil {
   196  		return false, err
   197  	}
   198  
   199  	if thisVersion.LessThan(minInitVersion) {
   200  		return false, nil
   201  	}
   202  
   203  	return true, nil
   204  }
   205  
   206  // packerInit runs 'packer init' if it is supported by the local packer
   207  func packerInit(t testing.TestingT, options *Options) error {
   208  	hasInit, err := hasPackerInit(t, options)
   209  	if err != nil {
   210  		return err
   211  	}
   212  	if !hasInit {
   213  		options.Logger.Logf(t, "Skipping 'packer init' because it is not present in this version")
   214  		return nil
   215  	}
   216  
   217  	extension := filepath.Ext(options.Template)
   218  	if extension != ".hcl" {
   219  		options.Logger.Logf(t, "Skipping 'packer init' because it is only supported for HCL2 templates")
   220  		return nil
   221  	}
   222  
   223  	cmd := shell.Command{
   224  		Command:    "packer",
   225  		Args:       []string{"init", options.Template},
   226  		Env:        options.Env,
   227  		WorkingDir: options.WorkingDir,
   228  	}
   229  
   230  	description := "Running Packer init"
   231  	_, err = retry.DoWithRetryableErrorsE(t, description, options.RetryableErrors, options.MaxRetries, options.TimeBetweenRetries, func() (string, error) {
   232  		return shell.RunCommandAndGetOutputE(t, cmd)
   233  	})
   234  
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  // Convert the inputs to a format palatable to packer. The build command should have the format:
   243  //
   244  // packer build [OPTIONS] template
   245  func formatPackerArgs(options *Options) []string {
   246  	args := []string{"build", "-machine-readable"}
   247  
   248  	for key, value := range options.Vars {
   249  		args = append(args, "-var", fmt.Sprintf("%s=%s", key, value))
   250  	}
   251  
   252  	for _, filePath := range options.VarFiles {
   253  		args = append(args, "-var-file", filePath)
   254  	}
   255  
   256  	if options.Only != "" {
   257  		args = append(args, fmt.Sprintf("-only=%s", options.Only))
   258  	}
   259  
   260  	if options.Except != "" {
   261  		args = append(args, fmt.Sprintf("-except=%s", options.Except))
   262  	}
   263  
   264  	return append(args, options.Template)
   265  }