Skip to content
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
16 changes: 16 additions & 0 deletions earthfile2llb/with_docker_run_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,22 @@ func composeParams(opt WithDockerOpt) []string {
}
}

// stripImageDigest drops a trailing `@sha256:…` (or other algo) from an image
// name so it's a valid `docker tag` target — dockerd refuses digest-bearing
// tags. Digest-pinned compose services still get verified at `docker compose
// up` time: compose re-fetches the manifest upstream (the pre-pull's retagged
// image has no RepoDigest) and reuses local layers. So the pre-pull gives
// layer-level dedup but no offline path for digest-pinned services.
// See issue #512.
func stripImageDigest(name string) string {
i := strings.Index(name, "@")
if i >= 0 {
return name[:i]
}

return name
}

func platformIncompatMsg(platr *platutil.Resolver) string {
currentPlatStr := platr.Materialize(platr.Current()).String()
nativePlatStr := platr.Materialize(platutil.NativePlatform).String()
Expand Down
67 changes: 67 additions & 0 deletions earthfile2llb/with_docker_run_base_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package earthfile2llb

import "testing"

// Test_stripImageDigest covers the retag target produced for WITH DOCKER pulls.
// Docker engine refuses `docker tag <src> <dst>` when <dst> carries an
// `@sha256:...` (or any digest-algorithm) suffix, so we strip it before retagging.
// Regression: https://github.com/EarthBuild/earthbuild/issues/512
func Test_stripImageDigest(t *testing.T) {
t.Parallel()

const digest64 = "d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc"

tests := []struct {
name string
in string
want string
}{
{
name: "no digest, tag-only",
in: "alpine:3.20",
want: "alpine:3.20",
},
{
name: "no digest, no tag",
in: "alpine",
want: "alpine",
},
{
name: "tag plus digest",
in: "alpine:3.20@sha256:" + digest64,
want: "alpine:3.20",
},
{
name: "digest only, no tag",
in: "alpine@sha256:" + digest64,
want: "alpine",
},
{
name: "registry path with port, tag, digest",
in: "registry.example.com:5000/team/app:v1.2.3@sha256:" + digest64,
want: "registry.example.com:5000/team/app:v1.2.3",
},
{
name: "non-sha256 digest algorithm",
in: "alpine:3.20@sha512:" + digest64 + digest64,
want: "alpine:3.20",
},
{
name: "empty",
in: "",
want: "",
},
}

for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

got := stripImageDigest(tc.in)
if got != tc.want {
t.Errorf("stripImageDigest(%q) = %q; want %q", tc.in, got, tc.want)
}
})
}
}
8 changes: 6 additions & 2 deletions earthfile2llb/with_docker_run_local_reg.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,16 @@ func (w *withDockerRunLocalReg) Run(ctx context.Context, args []string, opt With
return err
}

// Strip any `@sha256:…` before retagging — `docker tag` rejects
// digest-bearing targets. See stripImageDigest. Issue #512.
retagAs := stripImageDigest(result.FinalImageName)

err = w.c.containerFrontend.ImageTag(ctx, containerutil.ImageTag{
SourceRef: pullImage,
TargetRef: result.FinalImageName,
TargetRef: retagAs,
})
if err != nil {
return errors.Wrapf(err, "tag image %q", result.FinalImageName)
return errors.Wrapf(err, "tag image %q", retagAs)
}
}

Expand Down
6 changes: 5 additions & 1 deletion earthfile2llb/with_docker_run_reg.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,14 @@ func (w *withDockerRunRegistry) Run(ctx context.Context, args []string, opt With

imgsWithDigests := make([]string, 0, len(results))
for _, result := range results {
// Strip any `@sha256:…` before handing the name to the wrapper's retag
// step — `docker tag` rejects digest-bearing targets. The pin is still
// enforced at compose-up time (see stripImageDigest). Issue #512.
retagAs := stripImageDigest(result.FinalImageName)
// This will be decoded in the wrapper.
if result.NewInterImgFormat {
pullImages = append(
pullImages, fmt.Sprintf("%s|%s", result.IntermediateImageName, result.FinalImageName))
pullImages, fmt.Sprintf("%s|%s", result.IntermediateImageName, retagAs))
} else {
pullImages = append(pullImages, result.IntermediateImageName)
}
Expand Down
9 changes: 9 additions & 0 deletions tests/with-docker-compose/Earthfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ all:
--INDEX=3 \
--INDEX=4 \
--INDEX=5
BUILD +test-digest-pinned-image

print-countries:
FROM jbergknoff/postgresql-client:latest
Expand All @@ -29,3 +30,11 @@ test:
RUN while ! pg_isready --host=localhost --port=5432 --dbname=iso3166 --username=postgres; do sleep 1; done ;\
docker-compose up --exit-code-from print-countries print-countries | grep Brazil
END

# Regression: https://github.com/EarthBuild/earthbuild/issues/512 — a compose
# service pinned by `name:tag@sha256:…` must not break the wrapper's retag step.
test-digest-pinned-image:
COPY docker-compose.digest.yml ./docker-compose.yml
WITH DOCKER --compose docker-compose.yml
RUN docker compose up --exit-code-from app | grep 'hello from digest-pinned'
END
4 changes: 4 additions & 0 deletions tests/with-docker-compose/docker-compose.digest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
app:
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
command: ["sh", "-c", "echo hello from digest-pinned alpine"]
Loading