Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[24.0 backport] c8d: Fix building Dockerfiles that have FROM scratch #46302

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
89 changes: 57 additions & 32 deletions daemon/containerd/image_commit.go
Expand Up @@ -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"
Expand All @@ -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 (
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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() {
Expand Down
57 changes: 30 additions & 27 deletions daemon/containerd/image_snapshot.go
Expand Up @@ -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))
Expand All @@ -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
}

Expand Down
7 changes: 5 additions & 2 deletions daemon/delete.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down
19 changes: 19 additions & 0 deletions pkg/archive/diff.go
Expand Up @@ -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)
Expand Down