Jelajahi Sumber

Merge pull request #46302 from thaJeztah/24.0_backport_c8d-legacybuilder-fix-from-scratch

[24.0 backport] c8d: Fix building Dockerfiles that have `FROM scratch`
Djordje Lukic 1 tahun lalu
induk
melakukan
aade22d31e
4 mengubah file dengan 111 tambahan dan 61 penghapusan
  1. 57 32
      daemon/containerd/image_commit.go
  2. 30 27
      daemon/containerd/image_snapshot.go
  3. 5 2
      daemon/delete.go
  4. 19 0
      pkg/archive/diff.go

+ 57 - 32
daemon/containerd/image_commit.go

@@ -20,6 +20,7 @@ import (
 	"github.com/containerd/containerd/snapshots"
 	"github.com/docker/docker/api/types/backend"
 	"github.com/docker/docker/image"
+	"github.com/docker/docker/pkg/archive"
 	"github.com/opencontainers/go-digest"
 	"github.com/opencontainers/image-spec/identity"
 	"github.com/opencontainers/image-spec/specs-go"
@@ -38,28 +39,27 @@ func (i *ImageService) CommitImage(ctx context.Context, cc backend.CommitConfig)
 	container := i.containers.Get(cc.ContainerID)
 	cs := i.client.ContentStore()
 
-	imageManifest, err := getContainerImageManifest(container)
-	if err != nil {
-		return "", err
-	}
+	var parentManifest ocispec.Manifest
+	var parentImage ocispec.Image
 
-	imageManifestBytes, err := content.ReadBlob(ctx, cs, imageManifest)
-	if err != nil {
-		return "", err
-	}
+	// ImageManifest can be nil when committing an image with base FROM scratch
+	if container.ImageManifest != nil {
+		imageManifestBytes, err := content.ReadBlob(ctx, cs, *container.ImageManifest)
+		if err != nil {
+			return "", err
+		}
 
-	var manifest ocispec.Manifest
-	if err := json.Unmarshal(imageManifestBytes, &manifest); err != nil {
-		return "", err
-	}
+		if err := json.Unmarshal(imageManifestBytes, &parentManifest); err != nil {
+			return "", err
+		}
 
-	imageConfigBytes, err := content.ReadBlob(ctx, cs, manifest.Config)
-	if err != nil {
-		return "", err
-	}
-	var ociimage ocispec.Image
-	if err := json.Unmarshal(imageConfigBytes, &ociimage); err != nil {
-		return "", err
+		imageConfigBytes, err := content.ReadBlob(ctx, cs, parentManifest.Config)
+		if err != nil {
+			return "", err
+		}
+		if err := json.Unmarshal(imageConfigBytes, &parentImage); err != nil {
+			return "", err
+		}
 	}
 
 	var (
@@ -78,15 +78,19 @@ func (i *ImageService) CommitImage(ctx context.Context, cc backend.CommitConfig)
 	if err != nil {
 		return "", fmt.Errorf("failed to export layer: %w", err)
 	}
+	imageConfig := generateCommitImageConfig(parentImage, diffID, cc)
+
+	layers := parentManifest.Layers
+	if diffLayerDesc != nil {
+		rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String()
 
-	imageConfig := generateCommitImageConfig(ociimage, diffID, cc)
+		if err := applyDiffLayer(ctx, rootfsID, parentImage, sn, differ, *diffLayerDesc); err != nil {
+			return "", fmt.Errorf("failed to apply diff: %w", err)
+		}
 
-	rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String()
-	if err := applyDiffLayer(ctx, rootfsID, ociimage, sn, differ, diffLayerDesc); err != nil {
-		return "", fmt.Errorf("failed to apply diff: %w", err)
+		layers = append(layers, *diffLayerDesc)
 	}
 
-	layers := append(manifest.Layers, diffLayerDesc)
 	commitManifestDesc, err := writeContentsForImage(ctx, container.Driver, cs, imageConfig, layers)
 	if err != nil {
 		return "", err
@@ -130,6 +134,12 @@ func generateCommitImageConfig(baseConfig ocispec.Image, diffID digest.Digest, o
 		logrus.Warnf("assuming os=%q", os)
 	}
 	logrus.Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os)
+
+	diffIds := baseConfig.RootFS.DiffIDs
+	if diffID != "" {
+		diffIds = append(diffIds, diffID)
+	}
+
 	return ocispec.Image{
 		Platform: ocispec.Platform{
 			Architecture: arch,
@@ -140,7 +150,7 @@ func generateCommitImageConfig(baseConfig ocispec.Image, diffID digest.Digest, o
 		Config:  containerConfigToOciImageConfig(opts.Config),
 		RootFS: ocispec.RootFS{
 			Type:    "layers",
-			DiffIDs: append(baseConfig.RootFS.DiffIDs, diffID),
+			DiffIDs: diffIds,
 		},
 		History: append(baseConfig.History, ocispec.History{
 			Created:   &createdTime,
@@ -217,28 +227,43 @@ func writeContentsForImage(ctx context.Context, snName string, cs content.Store,
 }
 
 // createDiff creates a layer diff into containerd's content store.
-func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (ocispec.Descriptor, digest.Digest, error) {
+// If the diff is empty it returns nil empty digest and no error.
+func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (*ocispec.Descriptor, digest.Digest, error) {
 	newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer)
 	if err != nil {
-		return ocispec.Descriptor{}, "", err
+		return nil, "", err
+	}
+
+	ra, err := cs.ReaderAt(ctx, newDesc)
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to read diff archive: %w", err)
+	}
+	defer ra.Close()
+
+	empty, err := archive.IsEmpty(content.NewReader(ra))
+	if err != nil {
+		return nil, "", fmt.Errorf("failed to check if archive is empty: %w", err)
+	}
+	if empty {
+		return nil, "", nil
 	}
 
 	info, err := cs.Info(ctx, newDesc.Digest)
 	if err != nil {
-		return ocispec.Descriptor{}, "", err
+		return nil, "", fmt.Errorf("failed to get content info: %w", err)
 	}
 
 	diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
 	if !ok {
-		return ocispec.Descriptor{}, "", fmt.Errorf("invalid differ response with no diffID")
+		return nil, "", fmt.Errorf("invalid differ response with no diffID")
 	}
 
 	diffID, err := digest.Parse(diffIDStr)
 	if err != nil {
-		return ocispec.Descriptor{}, "", err
+		return nil, "", err
 	}
 
-	return ocispec.Descriptor{
+	return &ocispec.Descriptor{
 		MediaType: ocispec.MediaTypeImageLayerGzip,
 		Digest:    newDesc.Digest,
 		Size:      info.Size,
@@ -254,7 +279,7 @@ func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn
 
 	mount, err := sn.Prepare(ctx, key, parent)
 	if err != nil {
-		return err
+		return fmt.Errorf("failed to prepare snapshot: %w", err)
 	}
 
 	defer func() {

+ 30 - 27
daemon/containerd/image_snapshot.go

@@ -18,42 +18,45 @@ import (
 
 // PrepareSnapshot prepares a snapshot from a parent image for a container
 func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentImage string, platform *ocispec.Platform) error {
-	img, err := i.resolveImage(ctx, parentImage)
-	if err != nil {
-		return err
-	}
+	var parentSnapshot string
+	if parentImage != "" {
+		img, err := i.resolveImage(ctx, parentImage)
+		if err != nil {
+			return err
+		}
 
-	cs := i.client.ContentStore()
+		cs := i.client.ContentStore()
 
-	matcher := platforms.Default()
-	if platform != nil {
-		matcher = platforms.Only(*platform)
-	}
+		matcher := platforms.Default()
+		if platform != nil {
+			matcher = platforms.Only(*platform)
+		}
 
-	platformImg := containerd.NewImageWithPlatform(i.client, img, matcher)
-	unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
-	if err != nil {
-		return err
-	}
+		platformImg := containerd.NewImageWithPlatform(i.client, img, matcher)
+		unpacked, err := platformImg.IsUnpacked(ctx, i.snapshotter)
+		if err != nil {
+			return err
+		}
 
-	if !unpacked {
-		if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
+		if !unpacked {
+			if err := platformImg.Unpack(ctx, i.snapshotter); err != nil {
+				return err
+			}
+		}
+
+		desc, err := containerdimages.Config(ctx, cs, img.Target, matcher)
+		if err != nil {
 			return err
 		}
-	}
 
-	desc, err := containerdimages.Config(ctx, cs, img.Target, matcher)
-	if err != nil {
-		return err
-	}
+		diffIDs, err := containerdimages.RootFS(ctx, cs, desc)
+		if err != nil {
+			return err
+		}
 
-	diffIDs, err := containerdimages.RootFS(ctx, cs, desc)
-	if err != nil {
-		return err
+		parentSnapshot = identity.ChainID(diffIDs).String()
 	}
 
-	parent := identity.ChainID(diffIDs).String()
-
 	// Add a lease so that containerd doesn't garbage collect our snapshot
 	ls := i.client.LeasesService()
 	lease, err := ls.Create(ctx, leases.WithID(id))
@@ -69,7 +72,7 @@ func (i *ImageService) PrepareSnapshot(ctx context.Context, id string, parentIma
 	}
 
 	s := i.client.SnapshotService(i.StorageDriver())
-	_, err = s.Prepare(ctx, id, parent)
+	_, err = s.Prepare(ctx, id, parentSnapshot)
 	return err
 }
 

+ 5 - 2
daemon/delete.go

@@ -8,6 +8,7 @@ import (
 	"strings"
 	"time"
 
+	cerrdefs "github.com/containerd/containerd/errdefs"
 	"github.com/containerd/containerd/leases"
 	"github.com/docker/docker/api/types"
 	containertypes "github.com/docker/docker/api/types/container"
@@ -144,8 +145,10 @@ func (daemon *Daemon) cleanupContainer(container *container.Container, config ty
 				ID: container.ID,
 			}
 			if err := ls.Delete(context.Background(), lease, leases.SynchronousDelete); err != nil {
-				container.SetRemovalError(err)
-				return err
+				if !cerrdefs.IsNotFound(err) {
+					container.SetRemovalError(err)
+					return err
+				}
 			}
 		}
 	}

+ 19 - 0
pkg/archive/diff.go

@@ -223,6 +223,25 @@ func ApplyUncompressedLayer(dest string, layer io.Reader, options *TarOptions) (
 	return applyLayerHandler(dest, layer, options, false)
 }
 
+// IsEmpty checks if the tar archive is empty (doesn't contain any entries).
+func IsEmpty(rd io.Reader) (bool, error) {
+	decompRd, err := DecompressStream(rd)
+	if err != nil {
+		return true, fmt.Errorf("failed to decompress archive: %v", err)
+	}
+	defer decompRd.Close()
+
+	tarReader := tar.NewReader(decompRd)
+	if _, err := tarReader.Next(); err != nil {
+		if err == io.EOF {
+			return true, nil
+		}
+		return false, fmt.Errorf("failed to read next archive header: %v", err)
+	}
+
+	return false, nil
+}
+
 // do the bulk load of ApplyLayer, but allow for not calling DecompressStream
 func applyLayerHandler(dest string, layer io.Reader, options *TarOptions, decompress bool) (int64, error) {
 	dest = filepath.Clean(dest)