From 7609dde8d0ab23b25be29adc9eb6069f4edb5e80 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Mon, 21 Jul 2025 18:36:50 +0200 Subject: [PATCH] build: remove DCT support for classic builder 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 --- cli/command/image/build.go | 151 +------------------- cli/command/image/build_test.go | 4 - contrib/completion/bash/docker | 1 - contrib/completion/fish/docker.fish | 1 - contrib/completion/zsh/_docker | 1 - docs/reference/commandline/build.md | 1 - docs/reference/commandline/builder_build.md | 1 - docs/reference/commandline/image_build.md | 1 - e2e/image/build_test.go | 63 -------- 9 files changed, 2 insertions(+), 222 deletions(-) diff --git a/cli/command/image/build.go b/cli/command/image/build.go index d51078496d..99e99fce0f 100644 --- a/cli/command/image/build.go +++ b/cli/command/image/build.go @@ -1,8 +1,6 @@ package image import ( - "archive/tar" - "bufio" "bytes" "context" "encoding/json" @@ -20,9 +18,7 @@ import ( "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/image/build" "github.com/docker/cli/cli/streams" - "github.com/docker/cli/cli/trust" "github.com/docker/cli/internal/jsonstream" - "github.com/docker/cli/internal/lazyregexp" "github.com/docker/cli/opts" buildtypes "github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/container" @@ -65,7 +61,6 @@ type buildOptions struct { target string imageIDFile string platform string - untrusted bool } // dockerfileFromStdin returns true when the user specified that the Dockerfile @@ -144,7 +139,8 @@ func NewBuildCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation("target", annotation.ExternalURL, []string{"https://docs.docker.com/reference/cli/docker/buildx/build/#target"}) flags.StringVar(&options.imageIDFile, "iidfile", "", "Write the image ID to the file") - command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) + flags.Bool("disable-content-trust", dockerCli.ContentTrustEnabled(), "Skip image verification (deprecated)") + _ = flags.MarkHidden("disable-content-trust") flags.StringVar(&options.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"), "Set platform if server is multi-platform capable") flags.SetAnnotation("platform", "version", []string{"1.38"}) @@ -286,26 +282,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) ctx, cancel := context.WithCancel(ctx) defer cancel() - var resolvedTags []*resolvedTag - if !options.untrusted { - translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) { - return TrustedReference(ctx, dockerCli, ref) - } - // if there is a tar wrapper, the dockerfile needs to be replaced inside it - if buildCtx != nil { - // Wrap the tar archive to replace the Dockerfile entry with the rewritten - // Dockerfile which uses trusted pulls. - buildCtx = replaceDockerfileForContentTrust(ctx, buildCtx, relDockerfile, translator, &resolvedTags) - } else if dockerfileCtx != nil { - // if there was not archive context still do the possible replacements in Dockerfile - newDockerfile, _, err := rewriteDockerfileFromForContentTrust(ctx, dockerfileCtx, translator) - if err != nil { - return err - } - dockerfileCtx = io.NopCloser(bytes.NewBuffer(newDockerfile)) - } - } - if options.compress { buildCtx, err = build.Compress(buildCtx) if err != nil { @@ -402,21 +378,10 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) return err } } - if !options.untrusted { - // Since the build was successful, now we must tag any of the resolved - // images from the above Dockerfile rewrite. - for _, resolved := range resolvedTags { - if err := trust.TagTrusted(ctx, dockerCli.Client(), dockerCli.Err(), resolved.digestRef, resolved.tagRef); err != nil { - return err - } - } - } return nil } -type translatorFunc func(context.Context, reference.NamedTagged) (reference.Canonical, error) - // validateTag checks if the given image name can be resolved. func validateTag(rawRepo string) (string, error) { _, err := reference.ParseNormalizedNamed(rawRepo) @@ -427,118 +392,6 @@ func validateTag(rawRepo string) (string, error) { return rawRepo, nil } -var dockerfileFromLinePattern = lazyregexp.New(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P[^ \f\r\t\v\n#]+)`) - -// resolvedTag records the repository, tag, and resolved digest reference -// from a Dockerfile rewrite. -type resolvedTag struct { - digestRef reference.Canonical - tagRef reference.NamedTagged -} - -// noBaseImageSpecifier is the symbol used by the FROM -// command to specify that no base image is to be used. -const noBaseImageSpecifier = "scratch" - -// rewriteDockerfileFromForContentTrust rewrites the given Dockerfile by resolving images in -// "FROM " instructions to a digest reference. `translator` is a -// function that takes a repository name and tag reference and returns a -// trusted digest reference. -// This should be called *only* when content trust is enabled -func rewriteDockerfileFromForContentTrust(ctx context.Context, dockerfile io.Reader, translator translatorFunc) (newDockerfile []byte, resolvedTags []*resolvedTag, err error) { - scanner := bufio.NewScanner(dockerfile) - buf := bytes.NewBuffer(nil) - - // Scan the lines of the Dockerfile, looking for a "FROM" line. - for scanner.Scan() { - line := scanner.Text() - - matches := dockerfileFromLinePattern.FindStringSubmatch(line) - if matches != nil && matches[1] != noBaseImageSpecifier { - // Replace the line with a resolved "FROM repo@digest" - var ref reference.Named - ref, err = reference.ParseNormalizedNamed(matches[1]) - if err != nil { - return nil, nil, err - } - ref = reference.TagNameOnly(ref) - if ref, ok := ref.(reference.NamedTagged); ok { - trustedRef, err := translator(ctx, ref) - if err != nil { - return nil, nil, err - } - - line = dockerfileFromLinePattern.ReplaceAllLiteralString(line, "FROM "+reference.FamiliarString(trustedRef)) - resolvedTags = append(resolvedTags, &resolvedTag{ - digestRef: trustedRef, - tagRef: ref, - }) - } - } - - _, err := fmt.Fprintln(buf, line) - if err != nil { - return nil, nil, err - } - } - - return buf.Bytes(), resolvedTags, scanner.Err() -} - -// replaceDockerfileForContentTrust wraps the given input tar archive stream and -// uses the translator to replace the Dockerfile which uses a trusted reference. -// Returns a new tar archive stream with the replaced Dockerfile. -func replaceDockerfileForContentTrust(ctx context.Context, inputTarStream io.ReadCloser, dockerfileName string, translator translatorFunc, resolvedTags *[]*resolvedTag) io.ReadCloser { - pipeReader, pipeWriter := io.Pipe() - go func() { - tarReader := tar.NewReader(inputTarStream) - tarWriter := tar.NewWriter(pipeWriter) - - defer inputTarStream.Close() - - for { - hdr, err := tarReader.Next() - if err == io.EOF { - // Signals end of archive. - _ = tarWriter.Close() - _ = pipeWriter.Close() - return - } - if err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - - content := io.Reader(tarReader) - if hdr.Name == dockerfileName { - // This entry is the Dockerfile. Since the tar archive was - // generated from a directory on the local filesystem, the - // Dockerfile will only appear once in the archive. - var newDockerfile []byte - newDockerfile, *resolvedTags, err = rewriteDockerfileFromForContentTrust(ctx, content, translator) - if err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - hdr.Size = int64(len(newDockerfile)) - content = bytes.NewBuffer(newDockerfile) - } - - if err := tarWriter.WriteHeader(hdr); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - - if _, err := io.Copy(tarWriter, content); err != nil { - _ = pipeWriter.CloseWithError(err) - return - } - } - }() - - return pipeReader -} - func imageBuildOptions(dockerCli command.Cli, options buildOptions) buildtypes.ImageBuildOptions { configFile := dockerCli.ConfigFile() return buildtypes.ImageBuildOptions{ diff --git a/cli/command/image/build_test.go b/cli/command/image/build_test.go index 22105e47c0..9e12ec03ee 100644 --- a/cli/command/image/build_test.go +++ b/cli/command/image/build_test.go @@ -47,7 +47,6 @@ func TestRunBuildDockerfileFromStdinWithCompress(t *testing.T) { options.compress = true options.dockerfileName = "-" options.context = dir.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "foo"} @@ -74,7 +73,6 @@ func TestRunBuildResetsUidAndGidInContext(t *testing.T) { options := newBuildOptions() options.context = dir.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) headers := fakeBuild.headers(t) @@ -109,7 +107,6 @@ COPY data /data options := newBuildOptions() options.context = dir.Path() options.dockerfileName = df.Path() - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) expected := []string{fakeBuild.options.Dockerfile, ".dockerignore", "data"} @@ -170,7 +167,6 @@ RUN echo hello world cli := test.NewFakeCli(&fakeClient{imageBuildFunc: fakeBuild.build}) options := newBuildOptions() options.context = tmpDir.Join("context-link") - options.untrusted = true assert.NilError(t, runBuild(context.TODO(), cli, options)) assert.DeepEqual(t, fakeBuild.filenames(t), []string{"Dockerfile"}) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index 595dec6ce1..388fd2165b 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -2803,7 +2803,6 @@ _docker_image_build() { " local boolean_options=" - --disable-content-trust=false --force-rm --help --no-cache diff --git a/contrib/completion/fish/docker.fish b/contrib/completion/fish/docker.fish index 10d72f28b5..6cb4df7856 100644 --- a/contrib/completion/fish/docker.fish +++ b/contrib/completion/fish/docker.fish @@ -139,7 +139,6 @@ complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpu-quota -d complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s c -l cpu-shares -d 'CPU shares (relative weight)' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpuset-cpus -d 'CPUs in which to allow execution (0-3, 0,1)' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l cpuset-mems -d 'MEMs in which to allow execution (0-3, 0,1)' -complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l disable-content-trust -d 'Skip image verification' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -s f -l file -d "Name of the Dockerfile (Default is ‘PATH/Dockerfile’)" complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l force-rm -d 'Always remove intermediate containers' complete -c docker -A -f -n '__fish_seen_subcommand_from build' -l help -d 'Print usage' diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 88350aabfb..05ff5d500c 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -1005,7 +1005,6 @@ __docker_image_subcommand() { "($help)--cpu-rt-runtime=[Limit the CPU real-time runtime]:CPU real-time runtime in microseconds: " \ "($help)--cpuset-cpus=[CPUs in which to allow execution]:CPUs: " \ "($help)--cpuset-mems=[MEMs in which to allow execution]:MEMs: " \ - "($help)--disable-content-trust[Skip image verification]" \ "($help -f --file)"{-f=,--file=}"[Name of the Dockerfile]:Dockerfile:_files" \ "($help)--force-rm[Always remove intermediate containers]" \ "($help)--isolation=[Container isolation technology]:isolation:(default hyperv process)" \ diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index dca5ee76ab..491b0477db 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | diff --git a/docs/reference/commandline/builder_build.md b/docs/reference/commandline/builder_build.md index ad9c095321..71589da94b 100644 --- a/docs/reference/commandline/builder_build.md +++ b/docs/reference/commandline/builder_build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | diff --git a/docs/reference/commandline/image_build.md b/docs/reference/commandline/image_build.md index bd9ae8ee3d..62575c4e08 100644 --- a/docs/reference/commandline/image_build.md +++ b/docs/reference/commandline/image_build.md @@ -21,7 +21,6 @@ Build an image from a Dockerfile | `-c`, `--cpu-shares` | `int64` | `0` | CPU shares (relative weight) | | `--cpuset-cpus` | `string` | | CPUs in which to allow execution (0-3, 0,1) | | `--cpuset-mems` | `string` | | MEMs in which to allow execution (0-3, 0,1) | -| `--disable-content-trust` | `bool` | `true` | Skip image verification | | [`-f`](https://docs.docker.com/reference/cli/docker/buildx/build/#file), [`--file`](https://docs.docker.com/reference/cli/docker/buildx/build/#file) | `string` | | Name of the Dockerfile (Default is `PATH/Dockerfile`) | | `--force-rm` | `bool` | | Always remove intermediate containers | | `--iidfile` | `string` | | Write the image ID to the file | diff --git a/e2e/image/build_test.go b/e2e/image/build_test.go index fadd139224..08f4a5a7e9 100644 --- a/e2e/image/build_test.go +++ b/e2e/image/build_test.go @@ -14,7 +14,6 @@ import ( is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" "gotest.tools/v3/icmd" - "gotest.tools/v3/skip" ) func TestBuildFromContextDirectoryWithTag(t *testing.T) { @@ -62,68 +61,6 @@ func TestBuildFromContextDirectoryWithTag(t *testing.T) { }) } -func TestTrustedBuild(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - t.Setenv("DOCKER_BUILDKIT", "0") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - image1 := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-build1", "latest") - image2 := fixtures.CreateMaskedTrustedRemoteImage(t, registryPrefix, "trust-build2", "latest") - - buildDir := fs.NewDir(t, "test-trusted-build-context-dir", - fs.WithFile("Dockerfile", fmt.Sprintf(` - FROM %s as build-base - RUN echo ok > /foo - FROM %s - COPY --from=build-base foo bar - `, image1, image2))) - defer buildDir.Remove() - - result := icmd.RunCmd( - icmd.Command("docker", "build", "-t", "myimage", "."), - withWorkingDir(buildDir), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - - result.Assert(t, icmd.Expected{ - Out: fmt.Sprintf("FROM %s@sha", image1[:len(image1)-7]), - Err: fmt.Sprintf("Tagging %s@sha", image1[:len(image1)-7]), - }) - result.Assert(t, icmd.Expected{ - Out: fmt.Sprintf("FROM %s@sha", image2[:len(image2)-7]), - }) -} - -func TestTrustedBuildUntrustedImage(t *testing.T) { - skip.If(t, environment.RemoteDaemon()) - t.Setenv("DOCKER_BUILDKIT", "0") - - dir := fixtures.SetupConfigFile(t) - defer dir.Remove() - buildDir := fs.NewDir(t, "test-trusted-build-context-dir", - fs.WithFile("Dockerfile", fmt.Sprintf(` - FROM %s - RUN [] - `, fixtures.AlpineImage))) - defer buildDir.Remove() - - result := icmd.RunCmd( - icmd.Command("docker", "build", "-t", "myimage", "."), - withWorkingDir(buildDir), - fixtures.WithConfig(dir.Path()), - fixtures.WithTrust, - fixtures.WithNotary, - ) - - result.Assert(t, icmd.Expected{ - ExitCode: 1, - Err: "does not have trust data for", - }) -} - func TestBuildIidFileSquash(t *testing.T) { environment.SkipIfNotExperimentalDaemon(t) t.Setenv("DOCKER_BUILDKIT", "0")