Skip to content

Commit b1e7909

Browse files
committed
OCPBUGS-65845: fix stale image digest race condition
Change the default pull policy from PullIfMissing to PullIfNewer in buildDaemonlessImage and extractSourceFromImage. With PullIfMissing, if an image was already pulled (e.g. during a pre-pull step), a newer version pushed to the registry would not be detected. PullIfNewer adds a lightweight registry manifest check that catches cases where a tag was updated between Build object creation and the actual pull. For digest-pinned references this is a no-op. Also fix DaemonlessClient.PullImage to use '@' instead of ':' when the tag field contains a digest (e.g. "sha256:..."), preventing incorrect image name reconstruction.
1 parent 711f2e6 commit b1e7909

4 files changed

Lines changed: 139 additions & 3 deletions

File tree

pkg/build/builder/daemonless.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ func buildDaemonlessImage(sc types.SystemContext, store storage.Store, isolation
255255
args[ev.Name] = ev.Value
256256
}
257257

258-
pullPolicy := buildah.PullIfMissing
258+
pullPolicy := buildah.PullIfNewer
259259
if opts.Pull {
260260
log.V(2).Infof("Forcing fresh pull of base image.")
261261
pullPolicy = buildah.PullAlways
@@ -838,7 +838,11 @@ func (d *DaemonlessClient) RemoveContainer(opts docker.RemoveContainerOptions) e
838838
func (d *DaemonlessClient) PullImage(opts docker.PullImageOptions, searchPaths []string) error {
839839
imageName := opts.Repository
840840
if opts.Tag != "" {
841-
imageName = imageName + ":" + opts.Tag
841+
if strings.Contains(opts.Tag, ":") {
842+
imageName = imageName + "@" + opts.Tag
843+
} else {
844+
imageName = imageName + ":" + opts.Tag
845+
}
842846
}
843847
return pullDaemonlessImage(d.SystemContext, d.Store, imageName, searchPaths, d.BlobCacheDirectory)
844848
}

pkg/build/builder/daemonless_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,65 @@ import (
1515
builderutil "github.com/openshift/builder/pkg/build/builder/util"
1616
)
1717

18+
func TestPullImageNameReconstruction(t *testing.T) {
19+
tests := []struct {
20+
name string
21+
repo string
22+
tag string
23+
want string
24+
}{
25+
{
26+
name: "tag reference",
27+
repo: "registry:5000/ns/image",
28+
tag: "latest",
29+
want: "registry:5000/ns/image:latest",
30+
},
31+
{
32+
name: "digest reference passed as tag",
33+
repo: "registry:5000/ns/image",
34+
tag: "sha256:abc123def456",
35+
want: "registry:5000/ns/image@sha256:abc123def456",
36+
},
37+
{
38+
name: "digest reference in repository (normal flow)",
39+
repo: "registry:5000/ns/image@sha256:abc123def456",
40+
tag: "",
41+
want: "registry:5000/ns/image@sha256:abc123def456",
42+
},
43+
{
44+
name: "simple image with tag",
45+
repo: "busybox",
46+
tag: "latest",
47+
want: "busybox:latest",
48+
},
49+
{
50+
name: "image without tag",
51+
repo: "busybox",
52+
tag: "",
53+
want: "busybox",
54+
},
55+
}
56+
for _, tt := range tests {
57+
t.Run(tt.name, func(t *testing.T) {
58+
opts := docker.PullImageOptions{
59+
Repository: tt.repo,
60+
Tag: tt.tag,
61+
}
62+
imageName := opts.Repository
63+
if opts.Tag != "" {
64+
if strings.Contains(opts.Tag, ":") {
65+
imageName = imageName + "@" + opts.Tag
66+
} else {
67+
imageName = imageName + ":" + opts.Tag
68+
}
69+
}
70+
if imageName != tt.want {
71+
t.Errorf("reconstructed image name = %q, want %q", imageName, tt.want)
72+
}
73+
})
74+
}
75+
}
76+
1877
func TestMergeNodeCredentials(t *testing.T) {
1978
for _, tt := range []struct {
2079
name string

pkg/build/builder/docker_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,79 @@ USER 1001`
596596
}
597597
}
598598

599+
func TestPullImageDigestReference(t *testing.T) {
600+
tests := []struct {
601+
name string
602+
input string
603+
wantRepo string
604+
wantTag string
605+
}{
606+
{
607+
name: "tag reference",
608+
input: "registry:5000/ns/image:latest",
609+
wantRepo: "registry:5000/ns/image",
610+
wantTag: "latest",
611+
},
612+
{
613+
name: "digest reference preserves full name in repository",
614+
input: "registry:5000/ns/image@sha256:abc123def456",
615+
wantRepo: "registry:5000/ns/image@sha256:abc123def456",
616+
wantTag: "",
617+
},
618+
{
619+
name: "internal registry digest reference",
620+
input: "image-registry.openshift-image-registry.svc:5000/ci-op-xxx/pipeline@sha256:aabbccdd",
621+
wantRepo: "image-registry.openshift-image-registry.svc:5000/ci-op-xxx/pipeline@sha256:aabbccdd",
622+
wantTag: "",
623+
},
624+
{
625+
name: "simple tag",
626+
input: "myimage:v1",
627+
wantRepo: "myimage",
628+
wantTag: "v1",
629+
},
630+
{
631+
name: "no tag",
632+
input: "myimage",
633+
wantRepo: "myimage",
634+
wantTag: "",
635+
},
636+
}
637+
for _, tt := range tests {
638+
t.Run(tt.name, func(t *testing.T) {
639+
var gotOpts docker.PullImageOptions
640+
dockerClient := &FakeDocker{
641+
pullImageFunc: func(opts docker.PullImageOptions, searchPaths []string) error {
642+
gotOpts = opts
643+
return nil
644+
},
645+
}
646+
d := &DockerBuilder{
647+
dockerClient: dockerClient,
648+
build: &buildapiv1.Build{
649+
Spec: buildapiv1.BuildSpec{
650+
CommonSpec: buildapiv1.CommonSpec{
651+
Strategy: buildapiv1.BuildStrategy{
652+
DockerStrategy: &buildapiv1.DockerBuildStrategy{},
653+
},
654+
},
655+
},
656+
},
657+
}
658+
err := d.pullImage(tt.input, nil)
659+
if err != nil {
660+
t.Fatalf("unexpected error: %v", err)
661+
}
662+
if gotOpts.Repository != tt.wantRepo {
663+
t.Errorf("Repository = %q, want %q", gotOpts.Repository, tt.wantRepo)
664+
}
665+
if gotOpts.Tag != tt.wantTag {
666+
t.Errorf("Tag = %q, want %q", gotOpts.Tag, tt.wantTag)
667+
}
668+
})
669+
}
670+
}
671+
599672
// TestCopyLocalObject verifies that we are able to copy mounted Kubernetes Secret or ConfigMap
600673
// data to the build directory. The build directory is typically where git source code is cloned,
601674
// though other sources of code may be used as well.

pkg/build/builder/source.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ func copyImageSourceFromFilesytem(sourceDir, destDir string) error {
381381
func extractSourceFromImage(ctx context.Context, dockerClient DockerClient, store storage.Store, image, buildDir string, imageSecretIndex int, paths []buildapiv1.ImageSourcePath, forcePull bool, blobCacheDirectory string) error {
382382
log.V(4).Infof("Extracting image source from image %s", image)
383383

384-
pullPolicy := buildah.PullIfMissing
384+
pullPolicy := buildah.PullIfNewer
385385
if forcePull {
386386
pullPolicy = buildah.PullAlways
387387
}

0 commit comments

Comments
 (0)