Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support multiarch docker image builds #970

Merged
merged 7 commits into from
Aug 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bonus test stability fix I rolled in so I can get a green build.


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)
}