Skip to content

Skip cache lookup for "FROM scratch" in containerd#45818

Merged
neersighted merged 1 commit intomoby:masterfrom
tianon:containerd-from-scratch
Jun 27, 2023
Merged

Skip cache lookup for "FROM scratch" in containerd#45818
neersighted merged 1 commit intomoby:masterfrom
tianon:containerd-from-scratch

Conversation

@tianon
Copy link
Copy Markdown
Member

@tianon tianon commented Jun 26, 2023

Ideally, this should actually do a lookup across images that have no parent, but I wasn't 100% sure how to accomplish that so I opted for the smaller change of having FROM scratch builds not be cached for now.

Before:

$ docker build 'https://github.com/docker-library/busybox.git#dist-amd64:latest/glibc'
Sending build context to Docker daemon  1.781MB
Step 1/3 : FROM scratch
 --->
Step 2/3 : ADD busybox.tar.xz /
invalid reference format

After:

$ docker build 'https://github.com/docker-library/busybox.git#dist-amd64:latest/glibc'
Sending build context to Docker daemon  1.711MB
Step 1/3 : FROM scratch
 ---> 
Step 2/3 : ADD busybox.tar.xz /
 ---> 75399b0f600c
Step 3/3 : CMD ["sh"]
 ---> Running in fbacee298b99
Removing intermediate container fbacee298b99
 ---> d170630036d7
Successfully built d170630036d7

Ideally, this should actually do a lookup across images that have no parent, but I wasn't 100% sure how to accomplish that so I opted for the smaller change of having `FROM scratch` builds not be cached for now.

Signed-off-by: Tianon Gravi <admwiggin@gmail.com>
@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 26, 2023

the well-placed `panic()` I used to narrow down where this was breaking:
2023/06/26 22:34:54 http: panic serving @: wtf tianon
goroutine 273 [running]:
net/http.(*conn).serve.func1()
	net/http/server.go:1854 +0xbf
panic({0x1fb07c0, 0x27b49e0})
	runtime/panic.go:890 +0x263
github.com/docker/distribution/reference.Parse({0xc000fcfe30, 0x12})
	github.com/docker/distribution@v2.8.2+incompatible/reference/reference.go:198 +0x2c5
github.com/docker/distribution/reference.ParseNormalizedNamed({0x0, 0x0})
	github.com/docker/distribution@v2.8.2+incompatible/reference/normalize.go:48 +0x16b
github.com/docker/distribution/reference.ParseAnyReference({0x0, 0x0})
	github.com/docker/distribution@v2.8.2+incompatible/reference/normalize.go:181 +0x9f
github.com/docker/docker/daemon/containerd.(*ImageService).resolveImage(0xc0010d3080, {0x27df0b8, 0xc0001a6008}, {0x0, 0x0})
	github.com/docker/docker/daemon/containerd/image.go:232 +0x7e
github.com/docker/docker/daemon/containerd.(*ImageService).resolveDescriptor(...)
	github.com/docker/docker/daemon/containerd/image.go:223
github.com/docker/docker/daemon/containerd.(*ImageService).GetImage(0xc0010d3080, {0x27df0b8, 0xc0001a6008}, {0x0?, 0xc0010122a8?}, {0x0?, 0xc0?})
	github.com/docker/docker/daemon/containerd/image.go:36 +0xbb
github.com/docker/docker/daemon/containerd.(*imageCache).GetCache(0xc0010aa160, {0x0?, 0x100c0005a8780?}, 0x48cdfe?)
	github.com/docker/docker/daemon/containerd/cache.go:34 +0x72
github.com/docker/docker/builder/dockerfile.(*imageProber).Probe(0xc0010aa060, {0x0?, 0x4f?}, 0xc001093400)
	github.com/docker/docker/builder/dockerfile/imageprobe.go:58 +0x4a
github.com/docker/docker/builder/dockerfile.(*Builder).probeCache(0xc0010f6300, 0xc00109f9d0, 0x1?)
	github.com/docker/docker/builder/dockerfile/internals.go:331 +0x4a
github.com/docker/docker/builder/dockerfile.(*Builder).performCopy(0xc0010f6300, {0x27df080, 0xc000ff5900}, {0xc00109f9d0, 0xc0010a80c8, 0xc0010f6300, {0x27d3e00, 0xc000ec43f0}, 0xc0010aa0c0}, {{0x2420cd9, ...}, ...})
	github.com/docker/docker/builder/dockerfile/internals.go:133 +0x2f7
github.com/docker/docker/builder/dockerfile.dispatchAdd({0x27df080, 0xc000ff5900}, {0xc00109f9d0, 0xc0010a80c8, 0xc0010f6300, {0x27d3e00, 0xc000ec43f0}, 0xc0010aa0c0}, 0xc000cd0370)
	github.com/docker/docker/builder/dockerfile/dispatchers.go:105 +0x4f9
github.com/docker/docker/builder/dockerfile.dispatch({0x27df080, 0xc000ff5900}, {0xc00109f9d0, 0xc0010a80c8, 0xc0010f6300, {0x27d3e00, 0xc000ec43f0}, 0xc0010aa0c0}, {0x27cb670, 0xc000cd0370})
	github.com/docker/docker/builder/dockerfile/evaluator.go:76 +0x5bb
github.com/docker/docker/builder/dockerfile.(*Builder).dispatchDockerfileWithCancellation(0xc0010f6300, {0x27df080, 0xc000ff5900}, {0xc00109a580, 0x1, 0xc000ec43c0?}, {0x0, 0x0, 0x0?}, 0x5c, ...)
	github.com/docker/docker/builder/dockerfile/builder.go:296 +0x8f2
github.com/docker/docker/builder/dockerfile.(*Builder).build(0xc0010f6300, {0x27df080, 0xc000ff5900}, {0x27d3e00, 0xc000ec43f0}, 0xc001094a50)
	github.com/docker/docker/builder/dockerfile/builder.go:211 +0x2e5
github.com/docker/docker/builder/dockerfile.(*BuildManager).Build(0xc00091e500, {0x27df080, 0xc000ff5900}, {{0x27ca9c8, 0xc000ec4240}, {{0x27bc700, 0xc000198120}, {0x27bd380, 0xc000198138}, {0x27bd380, ...}, ...}, ...})
	github.com/docker/docker/builder/dockerfile/builder.go:98 +0x385
github.com/docker/docker/api/server/backend/build.(*Backend).Build(0xc0008e06f0, {0x27df128, 0xc000ec41b0}, {{0x27ca9c8, 0xc000ec4240}, {{0x27bc700, 0xc000198120}, {0x27bd380, 0xc000198138}, {0x27bd380, ...}, ...}, ...})
	github.com/docker/docker/api/server/backend/build/backend.go:69 +0x186
github.com/docker/docker/api/server/router/build.(*buildRouter).postBuild(0xc00056c980, {0x27df128, 0xc000ec41b0}, {0x27ddac0, 0xc0000fe540}, 0xc0009a0800, 0xc000c02160?)
	github.com/docker/docker/api/server/router/build/build_routes.go:280 +0x7c6
github.com/docker/docker/api/server/middleware.ExperimentalMiddleware.WrapHandler.func1({0x27df128, 0xc000ec41b0}, {0x27ddac0?, 0xc0000fe540?}, 0x1fb07c0?, 0xc000e1a0d0?)
	github.com/docker/docker/api/server/middleware/experimental.go:26 +0x15b
github.com/docker/docker/api/server/middleware.VersionMiddleware.WrapHandler.func1({0x27df128, 0xc000ec4180}, {0x27ddac0, 0xc0000fe540}, 0xc000efa080?, 0x0?)
	github.com/docker/docker/api/server/middleware/version.go:62 +0x4d7
github.com/docker/docker/pkg/authorization.(*Middleware).WrapHandler.func1({0x27df128, 0xc000ec4180}, {0x27ddac0?, 0xc0000fe540?}, 0xc0009a0800, 0xc000e1a080?)
	github.com/docker/docker/pkg/authorization/middleware.go:59 +0x6b4
github.com/docker/docker/api/server.(*Server).makeHTTPHandler.func1({0x27ddac0, 0xc0000fe540}, 0xc0009a0700)
	github.com/docker/docker/api/server/server.go:53 +0x1ce
net/http.HandlerFunc.ServeHTTP(0xc0009a0600?, {0x27ddac0?, 0xc0000fe540?}, 0x800?)
	net/http/server.go:2122 +0x2f
github.com/gorilla/mux.(*Router).ServeHTTP(0xc0010f6240, {0x27ddac0, 0xc0000fe540}, 0xc0009a0300)
	github.com/gorilla/mux@v1.8.0/mux.go:210 +0x1cf
net/http.serverHandler.ServeHTTP({0x27ce5a0?}, {0x27ddac0, 0xc0000fe540}, 0xc0009a0300)
	net/http/server.go:2936 +0x316
net/http.(*conn).serve(0xc000628000, {0x27df128, 0xc001094810})
	net/http/server.go:1995 +0x612
created by net/http.(*Server).Serve
	net/http/server.go:3089 +0x5ed

@thaJeztah thaJeztah added status/2-code-review process/cherry-pick containerd-integration Issues and PRs related to containerd integration labels Jun 26, 2023
@thaJeztah thaJeztah added this to the 25.0.0 milestone Jun 26, 2023
@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 26, 2023

Ok, here's an implementation of FROM scratch build cache that seems to work, but I feel like I'm doing something wrong or missing some obvious interface/method that should help make this easier to merge together: 🙈

diff --git a/daemon/containerd/cache.go b/daemon/containerd/cache.go
index 5e696c5967..5b4ec557c6 100644
--- a/daemon/containerd/cache.go
+++ b/daemon/containerd/cache.go
@@ -32,14 +32,13 @@ type imageCache struct {
 func (ic *imageCache) GetCache(parentID string, cfg *container.Config) (imageID string, err error) {
 	ctx := context.TODO()
 
-	if parentID == "" {
-		// TODO handle "parentless" image cache lookups ("FROM scratch")
-		return "", nil
-	}
-
-	parent, err := ic.c.GetImage(ctx, parentID, imagetype.GetImageOpts{})
-	if err != nil {
-		return "", err
+	var parent *image.Image
+	if parentID != "" { // not "FROM scratch"
+		var err error
+		parent, err = ic.c.GetImage(ctx, parentID, imagetype.GetImageOpts{})
+		if err != nil {
+			return "", err
+		}
 	}
 
 	for _, localCachedImage := range ic.images {
@@ -48,6 +47,24 @@ func (ic *imageCache) GetCache(parentID string, cfg *container.Config) (imageID
 		}
 	}
 
+	if parent == nil { // "FROM scratch"
+		imgs, err := ic.c.client.ImageService().List(ctx) // TODO is this right??
+		if err != nil {
+			return "", err
+		}
+		for _, img := range imgs {
+			id := img.Target.Digest.String()
+			image, err := ic.c.GetImage(ctx, id, imagetype.GetImageOpts{})
+			if err != nil {
+				return "", err
+			}
+			if isMatch(image, parent, cfg) {
+				return id, nil
+			}
+		}
+		return "", nil
+	}
+
 	children, err := ic.c.Children(ctx, parent.ID())
 	if err != nil {
 		return "", err
@@ -73,10 +90,19 @@ func (ic *imageCache) GetCache(parentID string, cfg *container.Config) (imageID
 // a parent image with `n` history entries, a valid target must have `n+1`
 // entries and the extra entry must match the provided config
 func isMatch(target, parent *image.Image, cfg *container.Config) bool {
-	if target == nil || parent == nil || cfg == nil {
+	if target == nil || cfg == nil {
 		return false
 	}
 
+	childCreatedBy := target.History[len(target.History)-1].CreatedBy
+	if childCreatedBy != strings.Join(cfg.Cmd, " ") {
+		return false
+	}
+
+	if parent == nil { // "FROM scratch"
+		return len(target.History) == 1 && len(target.RootFS.DiffIDs) == 1
+	}
+
 	if len(target.History) != len(parent.History)+1 ||
 		len(target.RootFS.DiffIDs) != len(parent.RootFS.DiffIDs)+1 {
 		return false
@@ -88,6 +114,5 @@ func isMatch(target, parent *image.Image, cfg *container.Config) bool {
 		}
 	}
 
-	childCreatedBy := target.History[len(target.History)-1].CreatedBy
-	return childCreatedBy == strings.Join(cfg.Cmd, " ")
+	return true
 }

@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 26, 2023

(I guess my proposal for the v25 milestone @thaJeztah put this in would be that I propose that diff as a draft in a follow-up, since just not ever considering cache for FROM scratch is the most "correct" short term fix that's also low risk. 😄)

@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 26, 2023

Oh, a simpler reproducer is this:

FROM scratch
CMD []

I guess we should probably put that in an integration test somewhere? 👀

If we add ENTRYPOINT [] then it can do two layers and test better too:

FROM scratch
ENTRYPOINT []
CMD []

Edit: oh, looking through #45232, there are quite a few tests in "integration-cli (DockerCLIBuildSuite)" which caught this very error 😂 🙈 ❤️

@thaJeztah
Copy link
Copy Markdown
Member

I did a quick rebase of #45232, and cherry-picked this PR into that branch 👍

Copy link
Copy Markdown
Member

@laurazard laurazard left a comment

Choose a reason for hiding this comment

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

LGTM! thanks @tianon (for finding 😅) and fixing this ❤️

@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 27, 2023

Welp, I think this only fixes the FROM scratch followed by ADD or COPY case -- things like the example I shared above still break elsewhere. 🙈

@tianon
Copy link
Copy Markdown
Member Author

tianon commented Jun 27, 2023

(however, instances of "invalid reference format" in the logs for that integration-cli test went down from 78 to 68, so that's something!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

containerd-integration Issues and PRs related to containerd integration process/cherry-picked status/2-code-review

Projects

Development

Successfully merging this pull request may close these issues.

5 participants