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

     1  package docker
     2  
     3  import (
     4  	"io/ioutil"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/hashicorp/go-multierror"
    10  	"github.com/mponton/terratest/modules/logger"
    11  	"github.com/mponton/terratest/modules/shell"
    12  	"github.com/mponton/terratest/modules/testing"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  // BuildOptions defines options that can be passed to the 'docker build' command.
    17  type BuildOptions struct {
    18  	// Tags for the Docker image
    19  	Tags []string
    20  
    21  	// Build args to pass the 'docker build' command
    22  	BuildArgs []string
    23  
    24  	// Target build arg to pass to the 'docker build' command
    25  	Target string
    26  
    27  	// All architectures to target in a multiarch build. Configuring this variable will cause terratest to use docker
    28  	// buildx to construct multiarch images.
    29  	// You can read more about multiarch docker builds in the official documentation for buildx:
    30  	// https://docs.docker.com/buildx/working-with-buildx/
    31  	// NOTE: This list does not automatically include the current platform. For example, if you are building images on
    32  	// an Apple Silicon based MacBook, and you configure this variable to []string{"linux/amd64"} to build an amd64
    33  	// image, the buildx command will not automatically include linux/arm64 - you must include that explicitly.
    34  	Architectures []string
    35  
    36  	// Whether or not to push images directly to the registry on build. Note that for multiarch images (Architectures is
    37  	// not empty), this must be true to ensure availability of all architectures - only the image for the current
    38  	// platform will be loaded into the daemon (due to a limitation of the docker daemon), so you won't be able to run a
    39  	// `docker push` command later to push the multiarch image.
    40  	// See https://github.com/moby/moby/pull/38738 for more info on the limitation of multiarch images in docker daemon.
    41  	Push bool
    42  
    43  	// Whether or not to load the image into the docker daemon at the end of a multiarch build so that it can be used
    44  	// locally. Note that this is only used when Architectures is set, and assumes the current architecture is already
    45  	// included in the Architectures list.
    46  	Load bool
    47  
    48  	// Custom CLI options that will be passed as-is to the 'docker build' command. This is an "escape hatch" that allows
    49  	// Terratest to not have to support every single command-line option offered by the 'docker build' command, and
    50  	// solely focus on the most important ones.
    51  	OtherOptions []string
    52  
    53  	// Whether ot not to enable buildkit. You can find more information about buildkit here https://docs.docker.com/build/buildkit/#getting-started.
    54  	EnableBuildKit bool
    55  
    56  	// Additional environment variables to pass in when running docker build command.
    57  	Env map[string]string
    58  
    59  	// Set a logger that should be used. See the logger package for more info.
    60  	Logger *logger.Logger
    61  }
    62  
    63  // Build runs the 'docker build' command at the given path with the given options and fails the test if there are any
    64  // errors.
    65  func Build(t testing.TestingT, path string, options *BuildOptions) {
    66  	require.NoError(t, BuildE(t, path, options))
    67  }
    68  
    69  // BuildE runs the 'docker build' command at the given path with the given options and returns any errors.
    70  func BuildE(t testing.TestingT, path string, options *BuildOptions) error {
    71  	options.Logger.Logf(t, "Running 'docker build' in %s", path)
    72  
    73  	env := make(map[string]string)
    74  	if options.Env != nil {
    75  		env = options.Env
    76  	}
    77  
    78  	if options.EnableBuildKit {
    79  		env["DOCKER_BUILDKIT"] = "1"
    80  	}
    81  
    82  	cmd := shell.Command{
    83  		Command: "docker",
    84  		Args:    formatDockerBuildArgs(path, options),
    85  		Logger:  options.Logger,
    86  		Env:     env,
    87  	}
    88  
    89  	if err := shell.RunCommandE(t, cmd); err != nil {
    90  		return err
    91  	}
    92  
    93  	// For non multiarch images, we need to call docker push for each tag since build does not have a push option like
    94  	// buildx.
    95  	if len(options.Architectures) == 0 && options.Push {
    96  		var errorsOccurred = new(multierror.Error)
    97  		for _, tag := range options.Tags {
    98  			if err := PushE(t, options.Logger, tag); err != nil {
    99  				options.Logger.Logf(t, "ERROR: error pushing tag %s", tag)
   100  				errorsOccurred = multierror.Append(err)
   101  			}
   102  		}
   103  		return errorsOccurred.ErrorOrNil()
   104  	}
   105  
   106  	// For multiarch images, if a load is requested call the load command to export the built image into the daemon.
   107  	if len(options.Architectures) > 0 && options.Load {
   108  		loadCmd := shell.Command{
   109  			Command: "docker",
   110  			Args:    formatDockerBuildxLoadArgs(path, options),
   111  			Logger:  options.Logger,
   112  		}
   113  		return shell.RunCommandE(t, loadCmd)
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  // GitCloneAndBuild builds a new Docker image from a given Git repo. This function will clone the given repo at the
   120  // specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo
   121  // root). This will fail the test if there are any errors.
   122  func GitCloneAndBuild(
   123  	t testing.TestingT,
   124  	repo string,
   125  	ref string,
   126  	path string,
   127  	dockerBuildOpts *BuildOptions,
   128  ) {
   129  	require.NoError(t, GitCloneAndBuildE(t, repo, ref, path, dockerBuildOpts))
   130  }
   131  
   132  // GitCloneAndBuildE builds a new Docker image from a given Git repo. This function will clone the given repo at the
   133  // specified ref, and call the docker build command on the cloned repo from the given relative path (relative to repo
   134  // root).
   135  func GitCloneAndBuildE(
   136  	t testing.TestingT,
   137  	repo string,
   138  	ref string,
   139  	path string,
   140  	dockerBuildOpts *BuildOptions,
   141  ) error {
   142  	workingDir, err := ioutil.TempDir("", "")
   143  	if err != nil {
   144  		return err
   145  	}
   146  	defer os.RemoveAll(workingDir)
   147  
   148  	cloneCmd := shell.Command{
   149  		Command: "git",
   150  		Args:    []string{"clone", repo, workingDir},
   151  	}
   152  	if err := shell.RunCommandE(t, cloneCmd); err != nil {
   153  		return err
   154  	}
   155  
   156  	checkoutCmd := shell.Command{
   157  		Command:    "git",
   158  		Args:       []string{"checkout", ref},
   159  		WorkingDir: workingDir,
   160  	}
   161  	if err := shell.RunCommandE(t, checkoutCmd); err != nil {
   162  		return err
   163  	}
   164  
   165  	contextPath := filepath.Join(workingDir, path)
   166  	if err := BuildE(t, contextPath, dockerBuildOpts); err != nil {
   167  		return err
   168  	}
   169  	return nil
   170  }
   171  
   172  // formatDockerBuildArgs formats the arguments for the 'docker build' command.
   173  func formatDockerBuildArgs(path string, options *BuildOptions) []string {
   174  	args := []string{}
   175  
   176  	if len(options.Architectures) > 0 {
   177  		args = append(
   178  			args,
   179  			"buildx",
   180  			"build",
   181  			"--platform",
   182  			strings.Join(options.Architectures, ","),
   183  		)
   184  		if options.Push {
   185  			args = append(args, "--push")
   186  		}
   187  	} else {
   188  		args = append(args, "build")
   189  	}
   190  
   191  	return append(args, formatDockerBuildBaseArgs(path, options)...)
   192  }
   193  
   194  // formatDockerBuildxLoadArgs formats the arguments for calling load on the 'docker buildx' command.
   195  func formatDockerBuildxLoadArgs(path string, options *BuildOptions) []string {
   196  	args := []string{
   197  		"buildx",
   198  		"build",
   199  		"--load",
   200  	}
   201  	return append(args, formatDockerBuildBaseArgs(path, options)...)
   202  }
   203  
   204  // formatDockerBuildBaseArgs formats the common args for the build command, both for `build` and `buildx`.
   205  func formatDockerBuildBaseArgs(path string, options *BuildOptions) []string {
   206  	args := []string{}
   207  	for _, tag := range options.Tags {
   208  		args = append(args, "--tag", tag)
   209  	}
   210  
   211  	for _, arg := range options.BuildArgs {
   212  		args = append(args, "--build-arg", arg)
   213  	}
   214  
   215  	if len(options.Target) > 0 {
   216  		args = append(args, "--target", options.Target)
   217  	}
   218  
   219  	args = append(args, options.OtherOptions...)
   220  
   221  	args = append(args, path)
   222  	return args
   223  }