Skip to content

fix: Always pass --load during docker build #2335

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
50 changes: 32 additions & 18 deletions pkg/docker/docker_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"

"github.com/creack/pty"
Expand Down Expand Up @@ -261,21 +261,24 @@ func (c *DockerCommand) ContainerStop(ctx context.Context, containerID string) e
func (c *DockerCommand) ImageBuild(ctx context.Context, options command.ImageBuildOptions) error {
console.Debugf("=== DockerCommand.ImageBuild %s", options.ImageName)

args := c.imageBuildArgs(options)

in := strings.NewReader(options.DockerfileContents)

return c.exec(ctx, in, nil, nil, options.WorkingDir, args)
}

// imageBuildArgs builds a string slice of arguments that will be provided to the `docker` command.
func (c *DockerCommand) imageBuildArgs(options command.ImageBuildOptions) []string {
args := []string{
"buildx", "build",
// disable provenance attestations since we don't want them cluttering the registry
"--provenance", "false",
// Fixes "WARNING: The requested image's platform (linux/amd64) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested"
// We do this regardless of the host platform so windows/*. linux/arm64, etc work as well
"--platform", "linux/amd64",
}

if util.IsAppleSiliconMac(runtime.GOOS, runtime.GOARCH) {
args = append(args,
// buildx doesn't load images by default, so we tell it to load here. _however_, the
// --output type=docker,rewrite-timestamp=true flag also loads the image, this may not be necessary
"--load",
)
// Ensure that the resultant image is loaded into docker
"--load",
}

for _, secret := range options.Secrets {
Expand All @@ -286,7 +289,14 @@ func (c *DockerCommand) ImageBuild(ctx context.Context, options command.ImageBui
args = append(args, "--no-cache")
}

for k, v := range options.Labels {
// Sort label keys for deterministic order
labelKeys := make([]string, 0, len(options.Labels))
for k := range options.Labels {
labelKeys = append(labelKeys, k)
}
sort.Strings(labelKeys)
for _, k := range labelKeys {
v := options.Labels[k]
// Unlike in Dockerfiles, the value here does not need quoting -- Docker merely
// splits on the first '=' in the argument and the rest is the label value.
args = append(args, "--label", fmt.Sprintf(`%s=%s`, k, v))
Expand All @@ -297,9 +307,9 @@ func (c *DockerCommand) ImageBuild(ctx context.Context, options command.ImageBui
// equivalent to `--output type=docker`
if options.Epoch != nil && *options.Epoch >= 0 {
args = append(args,
"--build-arg", fmt.Sprintf("SOURCE_DATE_EPOCH=%d", options.Epoch),
"--build-arg", fmt.Sprintf("SOURCE_DATE_EPOCH=%d", *options.Epoch),
"--output", "type=docker,rewrite-timestamp=true")
console.Infof("Forcing timestamp rewriting to epoch %d", options.Epoch)
console.Infof("Forcing timestamp rewriting to epoch %d", *options.Epoch)
}

if cogconfig.BuildXCachePath != "" {
Expand All @@ -312,8 +322,15 @@ func (c *DockerCommand) ImageBuild(ctx context.Context, options command.ImageBui
args = append(args, "--cache-to", "type=inline")
}

for name, dir := range options.BuildContexts {
args = append(args, "--build-context", name+"="+dir)
// Sort build context keys for deterministic order
contextKeys := make([]string, 0, len(options.BuildContexts))
for k := range options.BuildContexts {
contextKeys = append(contextKeys, k)
}
sort.Strings(contextKeys)
for _, k := range contextKeys {
dir := options.BuildContexts[k]
args = append(args, "--build-context", k+"="+dir)
}

if options.ProgressOutput != "" {
Expand All @@ -330,10 +347,7 @@ func (c *DockerCommand) ImageBuild(ctx context.Context, options command.ImageBui
"--tag", options.ImageName,
options.ContextDir,
)

in := strings.NewReader(options.DockerfileContents)

return c.exec(ctx, in, nil, nil, options.WorkingDir, args)
return args
}

func (c *DockerCommand) ContainerStart(ctx context.Context, options command.RunOptions) (string, error) {
Expand Down
183 changes: 183 additions & 0 deletions pkg/docker/docker_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"testing"

"github.com/stretchr/testify/require"

"github.com/replicate/cog/pkg/docker/command"
)

func TestDockerPush(t *testing.T) {
Expand All @@ -13,3 +15,184 @@ func TestDockerPush(t *testing.T) {
err := command.Push(t.Context(), "test")
require.NoError(t, err)
}

func TestDockerArgs(t *testing.T) {
tests := []struct {
name string
options command.ImageBuildOptions
expected []string
}{
{
name: "basic build",
options: command.ImageBuildOptions{
ImageName: "some-image",
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with secrets",
options: command.ImageBuildOptions{
ImageName: "some-image",
Secrets: []string{"id=secret1", "id=secret2"},
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--secret", "id=secret1",
"--secret", "id=secret2",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with no cache",
options: command.ImageBuildOptions{
ImageName: "some-image",
NoCache: true,
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--no-cache",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with labels",
options: command.ImageBuildOptions{
ImageName: "some-image",
Labels: map[string]string{
"org.opencontainers.image.version": "1.0.0",
"org.opencontainers.image.source": "https://github.com/org/repo",
},
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--label", "org.opencontainers.image.source=https://github.com/org/repo",
"--label", "org.opencontainers.image.version=1.0.0",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with epoch timestamp",
options: command.ImageBuildOptions{
ImageName: "some-image",
Epoch: ptr(int64(1374391507024)),
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--build-arg", "SOURCE_DATE_EPOCH=1374391507024",
"--output", "type=docker,rewrite-timestamp=true",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with build contexts",
options: command.ImageBuildOptions{
ImageName: "some-image",
BuildContexts: map[string]string{
"context1": "/path/to/context1",
"context2": "/path/to/context2",
},
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--cache-to", "type=inline",
"--build-context", "context1=/path/to/context1",
"--build-context", "context2=/path/to/context2",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with progress output",
options: command.ImageBuildOptions{
ImageName: "some-image",
ProgressOutput: "plain",
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--cache-to", "type=inline",
"--progress", "plain",
"--file", "-",
"--tag", "some-image",
".",
},
},
{
name: "with custom context dir",
options: command.ImageBuildOptions{
ImageName: "some-image",
ContextDir: "/custom/context",
},
expected: []string{
"buildx",
"build",
"--provenance", "false",
"--platform", "linux/amd64",
"--load",
"--cache-to", "type=inline",
"--file", "-",
"--tag", "some-image",
"/custom/context",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewDockerCommand()
actual := c.imageBuildArgs(tt.options)
require.Equal(t, tt.expected, actual)
})
}
}

// ptr returns a pointer to the given value
func ptr[T any](v T) *T {
return &v
}