Skip to content

add replay build command as experimental - #3818

Merged
tonistiigi merged 4 commits into
docker:masterfrom
tonistiigi:replay-commands
Sep 30, 2026
Merged

tonistiigi merged 4 commits into
docker:masterfrom
tonistiigi:replay-commands

Conversation

@tonistiigi

@tonistiigi tonistiigi commented Apr 24, 2026 •

Copy link
Copy Markdown
Member

first milestone for #3803

Comment thread replay/snapshot.go Outdated
if subjectPlat == nil || platforms.Only(*subjectPlat).Match(builderPlat) {
return platforms.Only(builderPlat)
}
return platforms.Any(*subjectPlat, builderPlat)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So with platforms.Any it matches both the subject and builder platforms but doesn't prefer the first one iiuc?

If a multi-platform material contains both, pickPlatformChild can archive whichever manifest appears first in the index, including the builder architecture instead of the subject architecture. Could this use platforms.Ordered(subject, builder) and have a test with both manifests in the index?

Comment thread commands/replay/build.go
Comment on lines +345 to +365
if push {
var used bool
for _, e := range exports {
if e.Type == "image" {
if e.Attrs == nil {
e.Attrs = map[string]string{}
}
e.Attrs["push"] = "true"
if _, ok := e.Attrs["unpack"]; !ok {
e.Attrs["unpack"] = "false"
}
used = true
}
}
if !used {
exports = append(exports, &buildflags.ExportEntry{
Type: "image",
Attrs: map[string]string{"push": "true", "unpack": "false"},
})
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

--output=type=registry is not normalized to image yet, so combining it with --push appends a second image exporter. That moight fail or push the same tag twice. Could the shorthand run after export normalization, as it does for regular build?

Comment thread commands/replay/build.go
Comment on lines +409 to +418
if !explicit {
wantPlatforms = []ocispecs.Platform{platforms.Normalize(platforms.DefaultSpec())}
}

wantNames := make([]string, 0, len(wantPlatforms))
matchers := make([]platforms.MatchComparer, 0, len(wantPlatforms))
for _, platform := range wantPlatforms {
matchers = append(matchers, platforms.OnlyStrict(platform))
wantNames = append(wantNames, platforms.Format(platform))
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This uses the CLI host OS to select a subject. On a Windows client connected to a Linux builder, a Linux-only multi-platform image has no match unless the user specifies --platform. Could the default come from the available subjects or builder instead?

Comment thread replay/snapshot.go
Comment on lines +594 to +615
// Address the root directly by digest so we fetch the exact recorded
// index regardless of tag mutations since the original build.
fetchRef := ref
if !strings.Contains(fetchRef, "@") {
fetchRef = ref + "@" + rootDgst.String()
}
// Resolve the fetchRef first so we learn the root descriptor's size +
// mediaType. Without a size, contentutil.FromFetcher's ReaderAt reports
// size=0 and ReadBlob returns an empty payload (silently succeeding with
// a JSON decode error downstream).
_, rootDesc, err := imgResolver.Resolve(ctx, fetchRef)
if err != nil {
return ocispecs.Descriptor{}, nil, errors.Wrapf(err, "resolve image material %s", m.URI)
}
fetcher, err := imgResolver.Fetcher(ctx, fetchRef)
if err != nil {
return ocispecs.Descriptor{}, nil, errors.Wrapf(err, "fetch image material %s", m.URI)
}
provider := contentutil.FromFetcher(fetcher)
if rootDesc.Digest == "" {
rootDesc.Digest = rootDgst
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If the purl already contains @sha256:X, this fetches X without checking it against rootDgst, the separate digest recorded in provenance. When they disagree, the snapshot can package a different material from the one recorded. Could snapshot reject that mismatch?

Comment thread tests/replay.go
Comment on lines +323 to +328
cmd := buildxCmd(sb, withArgs(
"replay", "build",
"docker-image://"+ref,
"--platform=all",
"--dry-run",
))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thhat parses dry-run output as JSON, but replay build --dry-run defaults to the pretty format. Could it pass --format=json?

Comment thread tests/replay.go Outdated
Comment on lines +334 to +340
var plan struct {
Subjects []struct {
Platform string `json:"platform"`
} `json:"subjects"`
}
require.NoError(t, json.Unmarshal(stdout.Bytes(), &plan), "dry-run must emit JSON plan")
require.Len(t, plan.Subjects, 2)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The plan puts the platform under subjects[].descriptor.platform iiuc, while this test decodes subjects[].platform. The unmarshalling ignores that missing field, so the test only checks the subject count. Could it check both descriptors?

Comment thread replay/verify.go Outdated
Comment on lines +247 to +252
printer, err := progress.NewPrinter(printerCtx, dockerCli.Err(), "auto",
progress.WithDesc(
fmt.Sprintf("verifying %d subject(s) with %q instance using %s driver", len(req.Targets), b.Name, b.Driver),
fmt.Sprintf("%s:%s", b.Driver, b.Name),
),
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It accepts --progress, but this printer always uses "auto" and the selected value never reaches VerifyRequest. Could verify pass through the requested mode?

@tonistiigi
tonistiigi force-pushed the replay-commands branch 3 times, most recently from c84885a to a799871 Compare September 30, 2026 16:42
@tonistiigi tonistiigi changed the title wip: add replay suite of commands add replay build command as experimental Sep 30, 2026
Allow build.Options to select the frontend and pass frontend attributes
directly instead of always solving with dockerfile.v0.

Allow a PolicyConfig to carry a programmatic policy callback without a
policy file. Callback-only entries run after file-based policies, require
BuildKit's session source policy capability, and are not serialized.

Both are needed by buildx replay, which replays the recorded frontend and
pins sources with a policy callback.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Export ParseSLSAMaterial and AddPinToImage from the policy package and add
imagetools.ReadProvenancePredicate to read the provenance predicate of an
attestation manifest.

Fetching referrers from an OCI layout that has none now returns an empty
result instead of ErrNotFound, matching the registry behavior. Callers that
treated ErrNotFound as "no referrers" behave the same.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
Add an experimental `buildx replay build` command that rebuilds an image
from its SLSA provenance. By default, every source is pinned to the digest
recorded in the provenance through a session source policy callback.
--replay-mode=frontend replays the recorded frontend and options without
pinning sources.

Replay requires mode=max provenance, a Git or HTTP(S) build context and
BuildKit v0.27 or later, and rebuilds one platform at a time. Subjects can
be registry images, OCI layouts or attestation files. Sigstore signatures
on the provenance are verified when present. --dry-run prints the replay
plan and runs the same checks as a real replay.

`replay snapshot` and `replay verify` are included as hidden commands until
snapshots can be consumed by replay build and verification results can be
trusted.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
- verify digests when reading the image index, attestation manifest and
  provenance, so replay uses the content covered by the signature
- deny sources whose URI is not recorded in the provenance
- validate every line of .intoto.jsonl inputs and require in-toto
  statements
- reject stdin, OCI layout, local and Bake target contexts, local
  mode=delete outputs, --platform all, and recorded --network=host up
  front
- keep a recorded --network=none unless --network is set
- build the dry-run plan from the actual build options and validate
  outputs in dry-run
- error on a policy config with both a callback and policy files
- add Git context and multi-platform replay integration tests

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
@tonistiigi
tonistiigi marked this pull request as ready for review September 30, 2026 16:45
@tonistiigi tonistiigi added this to the v0.38.0 milestone Sep 30, 2026
@tonistiigi

Copy link
Copy Markdown
Member Author

I cleaned up the current work so we should be able to get it to v0.38 . Only replay build is visible, other commands hidden for now. Marked experimental. Also updated some error messages for several input errors etc.

@crazy-max crazy-max left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@tonistiigi
tonistiigi merged commit e158258 into docker:master Sep 30, 2026
230 of 231 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants