Skip to content

Commit

Permalink
Support multiarch docker image builds (#970)
Browse files Browse the repository at this point in the history
* Support multiarch docker image builds

* Make sure to install docker buildx plugin in circleci

* Use newer ubuntu and docker for buildx

* Remove test

* Fix build

* Make sure to check against the right name

* Actually fix ecr repo tests
  • Loading branch information
yorinasub17 authored Aug 7, 2021
1 parent c66467d commit 2c215cf
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 19 deletions.
24 changes: 22 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ env: &env
BIN_BUILD_PARALLELISM: 3

defaults: &defaults
machine:
enabled: true
image: "ubuntu-2004:202107-02"
<<: *env

# minikube setup requires ubuntu16.04
minikube_defaults: &minikube_defaults
machine:
enabled: true
image: "ubuntu-1604:201903-01"
Expand Down Expand Up @@ -53,6 +60,17 @@ install_gruntwork_utils: &install_gruntwork_utils
sudo ln -s /usr/local/go/bin/go /usr/bin/go
echo "The installed version of Go is now $(go version)"
install_docker_buildx: &install_docker_buildx
name: install docker buildx
command: |
curl -sLO https://github.com/docker/buildx/releases/download/v0.6.1/buildx-v0.6.1.linux-amd64
mkdir -p ~/.docker/cli-plugins
mv buildx-v0.6.1.linux-amd64 ~/.docker/cli-plugins/docker-buildx
chmod a+x ~/.docker/cli-plugins/docker-buildx
# Verify buildx is available
docker buildx create --use
configure_environment_for_gcp: &configure_environment_for_gcp
name: configure environment for gcp
command: |
Expand Down Expand Up @@ -143,6 +161,8 @@ jobs:

- run:
<<: *install_gruntwork_utils
- run:
<<: *install_docker_buildx

# The weird way you have to set PATH in Circle 2.0
- run: echo 'export PATH=$HOME/.local/bin:$HOME/terraform:$HOME/packer:$PATH' >> $BASH_ENV
Expand Down Expand Up @@ -209,7 +229,7 @@ jobs:
path: /tmp/logs

kubernetes_test:
<<: *defaults
<<: *minikube_defaults
steps:
- attach_workspace:
at: /home/circleci
Expand Down Expand Up @@ -245,7 +265,7 @@ jobs:


helm_test:
<<: *defaults
<<: *minikube_defaults
steps:
- attach_workspace:
at: /home/circleci
Expand Down
13 changes: 8 additions & 5 deletions modules/aws/ecr_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package aws

import (
"fmt"
"strings"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand All @@ -12,14 +15,14 @@ func TestEcrRepo(t *testing.T) {
t.Parallel()

region := GetRandomStableRegion(t, nil, nil)
repo1, err := CreateECRRepoE(t, region, "terratest")
ecrRepoName := fmt.Sprintf("terratest%s", strings.ToLower(random.UniqueId()))
repo1, err := CreateECRRepoE(t, region, ecrRepoName)
defer DeleteECRRepo(t, region, repo1)

require.NoError(t, err)
assert.Equal(t, "terratest", aws.StringValue(repo1.RepositoryName))

repo2, err := GetECRRepoE(t, region, "terratest")
assert.Equal(t, ecrRepoName, aws.StringValue(repo1.RepositoryName))

repo2, err := GetECRRepoE(t, region, ecrRepoName)
require.NoError(t, err)
assert.Equal(t, "terratest", aws.StringValue(repo2.RepositoryName))
assert.Equal(t, ecrRepoName, aws.StringValue(repo2.RepositoryName))
}
99 changes: 87 additions & 12 deletions modules/docker/build.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package docker

import (
"strings"

"github.com/gruntwork-io/terratest/modules/logger"
"github.com/gruntwork-io/terratest/modules/shell"
"github.com/gruntwork-io/terratest/modules/testing"
"github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/require"
)

Expand All @@ -18,6 +21,27 @@ type BuildOptions struct {
// Target build arg to pass to the 'docker build' command
Target string

// All architectures to target in a multiarch build. Configuring this variable will cause terratest to use docker
// buildx to construct multiarch images.
// You can read more about multiarch docker builds in the official documentation for buildx:
// https://docs.docker.com/buildx/working-with-buildx/
// NOTE: This list does not automatically include the current platform. For example, if you are building images on
// an Apple Silicon based MacBook, and you configure this variable to []string{"linux/amd64"} to build an amd64
// image, the buildx command will not automatically include linux/arm64 - you must include that explicitly.
Architectures []string

// Whether or not to push images directly to the registry on build. Note that for multiarch images (Architectures is
// not empty), this must be true to ensure availability of all architectures - only the image for the current
// platform will be loaded into the daemon (due to a limitation of the docker daemon), so you won't be able to run a
// `docker push` command later to push the multiarch image.
// See https://github.com/moby/moby/pull/38738 for more info on the limitation of multiarch images in docker daemon.
Push bool

// Whether or not to load the image into the docker daemon at the end of a multiarch build so that it can be used
// locally. Note that this is only used when Architectures is set, and assumes the current architecture is already
// included in the Architectures list.
Load bool

// Custom CLI options that will be passed as-is to the 'docker build' command. This is an "escape hatch" that allows
// Terratest to not have to support every single command-line option offered by the 'docker build' command, and
// solely focus on the most important ones.
Expand All @@ -37,25 +61,77 @@ func Build(t testing.TestingT, path string, options *BuildOptions) {
func BuildE(t testing.TestingT, path string, options *BuildOptions) error {
options.Logger.Logf(t, "Running 'docker build' in %s", path)

args, err := formatDockerBuildArgs(path, options)
if err != nil {
return err
}

cmd := shell.Command{
Command: "docker",
Args: args,
Args: formatDockerBuildArgs(path, options),
Logger: options.Logger,
}

_, buildErr := shell.RunCommandAndGetOutputE(t, cmd)
return buildErr
if err := shell.RunCommandE(t, cmd); err != nil {
return err
}

// For non multiarch images, we need to call docker push for each tag since build does not have a push option like
// buildx.
if len(options.Architectures) == 0 && options.Push {
var errorsOccurred = new(multierror.Error)
for _, tag := range options.Tags {
if err := PushE(t, options.Logger, tag); err != nil {
options.Logger.Logf(t, "ERROR: error pushing tag %s", tag)
errorsOccurred = multierror.Append(err)
}
}
return errorsOccurred.ErrorOrNil()
}

// For multiarch images, if a load is requested call the load command to export the built image into the daemon.
if len(options.Architectures) > 0 && options.Load {
loadCmd := shell.Command{
Command: "docker",
Args: formatDockerBuildxLoadArgs(path, options),
Logger: options.Logger,
}
return shell.RunCommandE(t, loadCmd)
}

return nil
}

// formatDockerBuildArgs formats the arguments for the 'docker build' command.
func formatDockerBuildArgs(path string, options *BuildOptions) ([]string, error) {
args := []string{"build"}
func formatDockerBuildArgs(path string, options *BuildOptions) []string {
args := []string{}

if len(options.Architectures) > 0 {
args = append(
args,
"buildx",
"build",
"--platform",
strings.Join(options.Architectures, ","),
)
if options.Push {
args = append(args, "--push")
}
} else {
args = append(args, "build")
}

return append(args, formatDockerBuildBaseArgs(path, options)...)
}

// formatDockerBuildxLoadArgs formats the arguments for calling load on the 'docker buildx' command.
func formatDockerBuildxLoadArgs(path string, options *BuildOptions) []string {
args := []string{
"buildx",
"build",
"--load",
}
return append(args, formatDockerBuildBaseArgs(path, options)...)
}

// formatDockerBuildBaseArgs formats the common args for the build command, both for `build` and `buildx`.
func formatDockerBuildBaseArgs(path string, options *BuildOptions) []string {
args := []string{}
for _, tag := range options.Tags {
args = append(args, "--tag", tag)
}
Expand All @@ -71,6 +147,5 @@ func formatDockerBuildArgs(path string, options *BuildOptions) ([]string, error)
args = append(args, options.OtherOptions...)

args = append(args, path)

return args, nil
return args
}
18 changes: 18 additions & 0 deletions modules/docker/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ func TestBuild(t *testing.T) {
require.Contains(t, out, text)
}

func TestBuildMultiArch(t *testing.T) {
t.Parallel()

tag := "gruntwork-io/test-image:v1"
text := "Hello, World!"

options := &BuildOptions{
Tags: []string{tag},
BuildArgs: []string{fmt.Sprintf("text=%s", text)},
Architectures: []string{"linux/arm64", "linux/amd64"},
Load: true,
}

Build(t, "../../test/fixtures/docker", options)
out := Run(t, tag, &RunOptions{Remove: true})
require.Contains(t, out, text)
}

func TestBuildWithTarget(t *testing.T) {
t.Parallel()

Expand Down
25 changes: 25 additions & 0 deletions modules/docker/push.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package docker

import (
"github.com/gruntwork-io/terratest/modules/logger"
"github.com/gruntwork-io/terratest/modules/shell"
"github.com/gruntwork-io/terratest/modules/testing"
"github.com/stretchr/testify/require"
)

// Push runs the 'docker push' command to push the given tag. This will fail the test if there are any errors.
func Push(t testing.TestingT, logger *logger.Logger, tag string) {
require.NoError(t, PushE(t, logger, tag))
}

// PushE runs the 'docker push' command to push the given tag.
func PushE(t testing.TestingT, logger *logger.Logger, tag string) error {
logger.Logf(t, "Running 'docker push' for tag %s", tag)

cmd := shell.Command{
Command: "docker",
Args: []string{"push", tag},
Logger: logger,
}
return shell.RunCommandE(t, cmd)
}

0 comments on commit 2c215cf

Please sign in to comment.