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 <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2025-07-21 18:36:50 +02:00
parent 71bc8ab3ea
commit 7609dde8d0
9 changed files with 2 additions and 222 deletions

View File

@ -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<image>[^ \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 <image>" 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{

View File

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

View File

@ -2803,7 +2803,6 @@ _docker_image_build() {
"
local boolean_options="
--disable-content-trust=false
--force-rm
--help
--no-cache

View File

@ -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'

View File

@ -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)" \

View File

@ -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 |

View File

@ -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 |

View File

@ -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 |

View File

@ -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")