c8d/commit: Don't produce an empty layer

If the diff is empty and don't produce an empty layer.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2023-08-21 16:50:07 +02:00
parent a01bcf9767
commit eb56493f4e
No known key found for this signature in database
GPG key ID: B85EFCFE26DEF92A
2 changed files with 76 additions and 32 deletions
daemon/containerd
pkg/archive

View file

@ -21,6 +21,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)
imageConfig := generateCommitImageConfig(ociimage, diffID, cc)
layers := parentManifest.Layers
if diffLayerDesc != nil {
rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String()
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)
if err := applyDiffLayer(ctx, rootfsID, parentImage, 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
log.G(context.TODO()).Warnf("assuming os=%q", os)
}
log.G(context.TODO()).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() {

View file

@ -224,6 +224,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)