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 }