Docker Content Trust is currently only implemented for the classic
builder, but is known to not work with multi-stage builds, and
requires rewriting the Dockerfile, which is brittle because the
Dockerfile syntax evolved with the introduction of BuildKit as
default builder.
Given that the classic builder is deprecated, and only used for
Windows images, which are not verified by content trust;
# docker pull --disable-content-trust=false mcr.microsoft.com/windows/servercore:ltsc2025
Error: remote trust data does not exist for mcr.microsoft.com/windows/servercore: mcr.microsoft.com does not have trust data for mcr.microsoft.com/windows/servercore
With content trust not implemented in BuildKit, and not implemented
in docker compose, this resulted in an inconsistent behavior.
This patch removes content-trust support for "docker build". As this
is a client-side feature, users who require this feature can still
use an older CLI to to start the build.
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
216 lines
6.0 KiB
Go
216 lines
6.0 KiB
Go
package image
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/docker/cli/cli/streams"
|
|
"github.com/docker/cli/internal/test"
|
|
"github.com/docker/docker/api/types/build"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/moby/go-archive/compression"
|
|
"gotest.tools/v3/assert"
|
|
"gotest.tools/v3/fs"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) {
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
buffer := new(bytes.Buffer)
|
|
fakeBuild := newFakeBuild()
|
|
fakeImageBuild := func(ctx context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
|
tee := io.TeeReader(buildContext, buffer)
|
|
gzipReader, err := gzip.NewReader(tee)
|
|
assert.NilError(t, err)
|
|
return fakeBuild.build(ctx, gzipReader, options)
|
|
}
|
|
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeImageBuild})
|
|
dockerfile := bytes.NewBufferString(`
|
|
FROM alpine:frozen
|
|
COPY foo /
|
|
`)
|
|
cli.SetIn(streams.NewIn(io.NopCloser(dockerfile)))
|
|
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithFile("foo", "some content"))
|
|
defer dir.Remove()
|
|
|
|
options := newBuildOptions()
|
|
options.compress = true
|
|
options.dockerfileName = "-"
|
|
options.context = dir.Path()
|
|
assert.NilError(t, runBuild(context.TODO(), cli, options))
|
|
|
|
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"}
|
|
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
|
|
|
|
header := buffer.Bytes()[:10]
|
|
assert.Equal(t, compression.Gzip, compression.Detect(header))
|
|
}
|
|
|
|
func TestRunBuildResetsUidAndGidInContext(t *testing.T) {
|
|
skip.If(t, os.Getuid() != 0, "root is required to chown files")
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
fakeBuild := newFakeBuild()
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
|
|
|
|
dir := fs.NewDir(t, "test-build-context",
|
|
fs.WithFile("foo", "some content", fs.AsUser(65534, 65534)),
|
|
fs.WithFile("Dockerfile", `
|
|
FROM alpine:frozen
|
|
COPY foo bar /
|
|
`),
|
|
)
|
|
defer dir.Remove()
|
|
|
|
options := newBuildOptions()
|
|
options.context = dir.Path()
|
|
assert.NilError(t, runBuild(context.TODO(), cli, options))
|
|
|
|
headers := fakeBuild.headers(t)
|
|
expected := []*tar.Header{
|
|
{Name: "Dockerfile"},
|
|
{Name: "foo"},
|
|
}
|
|
cmpTarHeaderNameAndOwner := cmp.Comparer(func(x, y tar.Header) bool {
|
|
return x.Name == y.Name && x.Uid == y.Uid && x.Gid == y.Gid
|
|
})
|
|
assert.DeepEqual(t, expected, headers, cmpTarHeaderNameAndOwner)
|
|
}
|
|
|
|
func TestRunBuildDockerfileOutsideContext(t *testing.T) {
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
dir := fs.NewDir(t, t.Name(),
|
|
fs.WithFile("data", "data file"))
|
|
defer dir.Remove()
|
|
|
|
// Dockerfile outside of build-context
|
|
df := fs.NewFile(t, t.Name(),
|
|
fs.WithContent(`
|
|
FROM FOOBAR
|
|
COPY data /data
|
|
`),
|
|
)
|
|
defer df.Remove()
|
|
|
|
fakeBuild := newFakeBuild()
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
|
|
|
|
options := newBuildOptions()
|
|
options.context = dir.Path()
|
|
options.dockerfileName = df.Path()
|
|
assert.NilError(t, runBuild(context.TODO(), cli, options))
|
|
|
|
expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"}
|
|
assert.DeepEqual(t, expected, fakeBuild.filenames(t))
|
|
}
|
|
|
|
// TestRunBuildFromGitHubSpecialCase tests that build contexts
|
|
// starting with `github.com/` are special-cased, and the build command attempts
|
|
// to clone the remote repo.
|
|
// TODO: test "context selection" logic directly when runBuild is refactored
|
|
// to support testing (ex: docker/cli#294)
|
|
func TestRunBuildFromGitHubSpecialCase(t *testing.T) {
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
cmd := NewBuildCommand(test.NewFakeCli(&fakeClient{}))
|
|
// Clone a small repo that exists so git doesn't prompt for credentials
|
|
cmd.SetArgs([]string{"github.com/docker/for-win"})
|
|
cmd.SetOut(io.Discard)
|
|
cmd.SetErr(io.Discard)
|
|
err := cmd.Execute()
|
|
assert.ErrorContains(t, err, "unable to prepare context")
|
|
assert.ErrorContains(t, err, "docker-build-git")
|
|
}
|
|
|
|
// TestRunBuildFromLocalGitHubDir tests that a local directory
|
|
// starting with `github.com` takes precedence over the `github.com` special
|
|
// case.
|
|
func TestRunBuildFromLocalGitHubDir(t *testing.T) {
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
|
|
buildDir := filepath.Join(t.TempDir(), "github.com", "docker", "no-such-repository")
|
|
err := os.MkdirAll(buildDir, 0o777)
|
|
assert.NilError(t, err)
|
|
err = os.WriteFile(filepath.Join(buildDir, "Dockerfile"), []byte("FROM busybox\n"), 0o644)
|
|
assert.NilError(t, err)
|
|
|
|
client := test.NewFakeCli(&fakeClient{})
|
|
cmd := NewBuildCommand(client)
|
|
cmd.SetArgs([]string{buildDir})
|
|
cmd.SetOut(io.Discard)
|
|
err = cmd.Execute()
|
|
assert.NilError(t, err)
|
|
}
|
|
|
|
func TestRunBuildWithSymlinkedContext(t *testing.T) {
|
|
t.Setenv("DOCKER_BUILDKIT", "0")
|
|
dockerfile := `
|
|
FROM alpine:frozen
|
|
RUN echo hello world
|
|
`
|
|
|
|
tmpDir := fs.NewDir(t, t.Name(),
|
|
fs.WithDir("context",
|
|
fs.WithFile("Dockerfile", dockerfile)),
|
|
fs.WithSymlink("context-link", "context"))
|
|
defer tmpDir.Remove()
|
|
|
|
fakeBuild := newFakeBuild()
|
|
cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build})
|
|
options := newBuildOptions()
|
|
options.context = tmpDir.Join("context-link")
|
|
assert.NilError(t, runBuild(context.TODO(), cli, options))
|
|
|
|
assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"})
|
|
}
|
|
|
|
type fakeBuild struct {
|
|
context *tar.Reader
|
|
options build.ImageBuildOptions
|
|
}
|
|
|
|
func newFakeBuild() *fakeBuild {
|
|
return &fakeBuild{}
|
|
}
|
|
|
|
func (f *fakeBuild) build(_ context.Context, buildContext io.Reader, options build.ImageBuildOptions) (build.ImageBuildResponse, error) {
|
|
f.context = tar.NewReader(buildContext)
|
|
f.options = options
|
|
body := new(bytes.Buffer)
|
|
return build.ImageBuildResponse{Body: io.NopCloser(body)}, nil
|
|
}
|
|
|
|
func (f *fakeBuild) headers(t *testing.T) []*tar.Header {
|
|
t.Helper()
|
|
headers := []*tar.Header{}
|
|
for {
|
|
hdr, err := f.context.Next()
|
|
switch err {
|
|
case io.EOF:
|
|
return headers
|
|
case nil:
|
|
headers = append(headers, hdr)
|
|
default:
|
|
assert.NilError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *fakeBuild) filenames(t *testing.T) []string {
|
|
t.Helper()
|
|
names := []string{}
|
|
for _, header := range f.headers(t) {
|
|
names = append(names, header.Name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|