containerd integration: Handle nil container.ImageManifest
**- What I did**
Fix panic when processing containers created under fork containerd integration (this field was added in the upstream and didn't exist in fork).

This fixes a panic when upgrading from DD 4.18 to 4.19 if the user had created a containers with the containerd image store integration enabled. This is because the fork didn't have the `ImageManifest` which was introduced only in the upstream, so it wasn't set and is now nil.


**- How I did it**
See commit.

**- How to verify it**
fork 4.18 -> upstream 4.19 can be simulated by changing https://github.com/moby/moby/blob/a343ed13e556ec97a26bd5fc34b069a64f86f16b/daemon/create.go#L183 to set `ImageManifest` to nil and creating some containers with nil `ImageManifest`.
Alternatively setting `ImageManifest` to nil directly in the container config directly in `/var/lib/docker/containers/.../config.v2.json`  and restarting the daemon.

Run: `system df`, `commit` and `diff`. 


**- Description for the changelog**
<!--
Write a short (one line) summary that describes the changes in this
pull request for inclusion in the changelog:
-->


**- A picture of a cute animal (not mandatory but encouraged)**


daemon/containerd/service.go
package containerd

import (
	"context"
	"encoding/json"
	"sync/atomic"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/remotes/docker"
	"github.com/containerd/containerd/snapshots"
	"github.com/docker/docker/container"
	daemonevents "github.com/docker/docker/daemon/events"
	"github.com/docker/docker/daemon/images"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/image"
	"github.com/docker/docker/layer"
	"github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/identity"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
)

// ImageService implements daemon.ImageService
type ImageService struct {
	client          *containerd.Client
	containers      container.Store
	snapshotter     string
	registryHosts   RegistryHostsProvider
	registryService RegistryConfigProvider
	eventsService   *daemonevents.Events
	pruneRunning    atomic.Bool
}

type RegistryHostsProvider interface {
	RegistryHosts() docker.RegistryHosts
}

type RegistryConfigProvider interface {
	IsInsecureRegistry(host string) bool
}

type ImageServiceConfig struct {
	Client        *containerd.Client
	Containers    container.Store
	Snapshotter   string
	HostsProvider RegistryHostsProvider
	Registry      RegistryConfigProvider
	EventsService *daemonevents.Events
}

// NewService creates a new ImageService.
func NewService(config ImageServiceConfig) *ImageService {
	return &ImageService{
		client:          config.Client,
		containers:      config.Containers,
		snapshotter:     config.Snapshotter,
		registryHosts:   config.HostsProvider,
		registryService: config.Registry,
		eventsService:   config.EventsService,
	}
}

// DistributionServices return services controlling daemon image storage.
func (i *ImageService) DistributionServices() images.DistributionServices {
	return images.DistributionServices{}
}

// CountImages returns the number of images stored by ImageService
// called from info.go
func (i *ImageService) CountImages() int {
	imgs, err := i.client.ListImages(context.TODO())
	if err != nil {
		return 0
	}

	return len(imgs)
}

// CreateLayer creates a filesystem layer for a container.
// called from create.go
// TODO: accept an opt struct instead of container?
func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
	return nil, errdefs.NotImplemented(errdefs.NotImplemented(errors.New("not implemented")))
}

// LayerStoreStatus returns the status for each layer store
// called from info.go
func (i *ImageService) LayerStoreStatus() [][2]string {
	// TODO(thaJeztah) do we want to add more details about the driver here?
	return [][2]string{
		{"driver-type", string(plugin.SnapshotPlugin)},
	}
}

// GetLayerMountID returns the mount ID for a layer
// called from daemon.go Daemon.Shutdown(), and Daemon.Cleanup() (cleanup is actually continerCleanup)
// TODO: needs to be refactored to Unmount (see callers), or removed and replaced with GetLayerByID
func (i *ImageService) GetLayerMountID(cid string) (string, error) {
	return "", errdefs.NotImplemented(errors.New("not implemented"))
}

// Cleanup resources before the process is shutdown.
// called from daemon.go Daemon.Shutdown()
func (i *ImageService) Cleanup() error {
	return nil
}

// StorageDriver returns the name of the default storage-driver (snapshotter)
// used by the ImageService.
func (i *ImageService) StorageDriver() string {
	return i.snapshotter
}

// ReleaseLayer releases a layer allowing it to be removed
// called from delete.go Daemon.cleanupContainer(), and Daemon.containerExport()
func (i *ImageService) ReleaseLayer(rwlayer layer.RWLayer) error {
	return errdefs.NotImplemented(errors.New("not implemented"))
}

// LayerDiskUsage returns the number of bytes used by layer stores
// called from disk_usage.go
func (i *ImageService) LayerDiskUsage(ctx context.Context) (int64, error) {
	var allLayersSize int64
	// TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273
	snapshotter := i.client.SnapshotService(i.snapshotter)
	snapshotter.Walk(ctx, func(ctx context.Context, info snapshots.Info) error {
		usage, err := snapshotter.Usage(ctx, info.Name)
		if err != nil {
			return err
		}
		allLayersSize += usage.Size
		return nil
	})
	return allLayersSize, nil
}

// UpdateConfig values
//
// called from reload.go
func (i *ImageService) UpdateConfig(maxDownloads, maxUploads int) {
	panic("not implemented")
}

// GetLayerFolders returns the layer folders from an image RootFS.
func (i *ImageService) GetLayerFolders(img *image.Image, rwLayer layer.RWLayer) ([]string, error) {
	return nil, errdefs.NotImplemented(errors.New("not implemented"))
}

// GetContainerLayerSize returns the real size & virtual size of the container.
func (i *ImageService) GetContainerLayerSize(ctx context.Context, containerID string) (int64, int64, error) {
	ctr := i.containers.Get(containerID)
	if ctr == nil {
		return 0, 0, nil
	}
	cs := i.client.ContentStore()

	imageManifestBytes, err := content.ReadBlob(ctx, cs, *ctr.ImageManifest)
	if err != nil {
		return 0, 0, err
	}

	var manifest ocispec.Manifest
	if err := json.Unmarshal(imageManifestBytes, &manifest); err != nil {
		return 0, 0, err
	}

	imageConfigBytes, err := content.ReadBlob(ctx, cs, manifest.Config)
	if err != nil {
		return 0, 0, err
	}
	var img ocispec.Image
	if err := json.Unmarshal(imageConfigBytes, &img); err != nil {
		return 0, 0, err
	}

	snapshotter := i.client.SnapshotService(ctr.Driver)
	usage, err := snapshotter.Usage(ctx, containerID)
	if err != nil {
		return 0, 0, err
	}

	sizeCache := make(map[digest.Digest]int64)
	snapshotSizeFn := func(d digest.Digest) (int64, error) {
		if s, ok := sizeCache[d]; ok {
			return s, nil
		}
		u, err := snapshotter.Usage(ctx, d.String())
		if err != nil {
			return 0, err
		}
		sizeCache[d] = u.Size
		return u.Size, nil
	}

	chainIDs := identity.ChainIDs(img.RootFS.DiffIDs)
	snapShotSize, err := computeSnapshotSize(chainIDs, snapshotSizeFn)
	if err != nil {
		return 0, 0, err
	}

	// TODO(thaJeztah): include content-store size for the image (similar to "GET /images/json")
	return usage.Size, usage.Size + snapShotSize, nil
}

package containerd

import (
	"context"
	"encoding/json"
	"sync/atomic"

	"github.com/containerd/containerd"
	"github.com/containerd/containerd/content"
	"github.com/containerd/containerd/plugin"
	"github.com/containerd/containerd/remotes/docker"
	"github.com/containerd/containerd/snapshots"
	imagetypes "github.com/docker/docker/api/types/image"
	"github.com/docker/docker/container"
	daemonevents "github.com/docker/docker/daemon/events"
	"github.com/docker/docker/daemon/images"
	"github.com/docker/docker/errdefs"
	"github.com/docker/docker/image"
	"github.com/docker/docker/layer"
	"github.com/opencontainers/go-digest"
	"github.com/opencontainers/image-spec/identity"
	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// ImageService implements daemon.ImageService
type ImageService struct {
	client          *containerd.Client
	containers      container.Store
	snapshotter     string
	registryHosts   RegistryHostsProvider
	registryService RegistryConfigProvider
	eventsService   *daemonevents.Events
	pruneRunning    atomic.Bool
}

type RegistryHostsProvider interface {
	RegistryHosts() docker.RegistryHosts
}

type RegistryConfigProvider interface {
	IsInsecureRegistry(host string) bool
}

type ImageServiceConfig struct {
	Client        *containerd.Client
	Containers    container.Store
	Snapshotter   string
	HostsProvider RegistryHostsProvider
	Registry      RegistryConfigProvider
	EventsService *daemonevents.Events
}

// NewService creates a new ImageService.
func NewService(config ImageServiceConfig) *ImageService {
	return &ImageService{
		client:          config.Client,
		containers:      config.Containers,
		snapshotter:     config.Snapshotter,
		registryHosts:   config.HostsProvider,
		registryService: config.Registry,
		eventsService:   config.EventsService,
	}
}

// DistributionServices return services controlling daemon image storage.
func (i *ImageService) DistributionServices() images.DistributionServices {
	return images.DistributionServices{}
}

// CountImages returns the number of images stored by ImageService
// called from info.go
func (i *ImageService) CountImages() int {
	imgs, err := i.client.ListImages(context.TODO())
	if err != nil {
		return 0
	}

	return len(imgs)
}

// CreateLayer creates a filesystem layer for a container.
// called from create.go
// TODO: accept an opt struct instead of container?
func (i *ImageService) CreateLayer(container *container.Container, initFunc layer.MountInit) (layer.RWLayer, error) {
	return nil, errdefs.NotImplemented(errdefs.NotImplemented(errors.New("not implemented")))
}

// LayerStoreStatus returns the status for each layer store
// called from info.go
func (i *ImageService) LayerStoreStatus() [][2]string {
	// TODO(thaJeztah) do we want to add more details about the driver here?
	return [][2]string{
		{"driver-type", string(plugin.SnapshotPlugin)},
	}
}

// GetLayerMountID returns the mount ID for a layer
// called from daemon.go Daemon.Shutdown(), and Daemon.Cleanup() (cleanup is actually continerCleanup)
// TODO: needs to be refactored to Unmount (see callers), or removed and replaced with GetLayerByID
func (i *ImageService) GetLayerMountID(cid string) (string, error) {
	return "", errdefs.NotImplemented(errors.New("not implemented"))
}

// Cleanup resources before the process is shutdown.
// called from daemon.go Daemon.Shutdown()
func (i *ImageService) Cleanup() error {
	return nil
}

// StorageDriver returns the name of the default storage-driver (snapshotter)
// used by the ImageService.
func (i *ImageService) StorageDriver() string {
	return i.snapshotter
}

// ReleaseLayer releases a layer allowing it to be removed
// called from delete.go Daemon.cleanupContainer(), and Daemon.containerExport()
func (i *ImageService) ReleaseLayer(rwlayer layer.RWLayer) error {
	return errdefs.NotImplemented(errors.New("not implemented"))
}

// LayerDiskUsage returns the number of bytes used by layer stores
// called from disk_usage.go
func (i *ImageService) LayerDiskUsage(ctx context.Context) (int64, error) {
	var allLayersSize int64
	// TODO(thaJeztah): do we need to take multiple snapshotters into account? See https://github.com/moby/moby/issues/45273
	snapshotter := i.client.SnapshotService(i.snapshotter)
	snapshotter.Walk(ctx, func(ctx context.Context, info snapshots.Info) error {
		usage, err := snapshotter.Usage(ctx, info.Name)
		if err != nil {
			return err
		}
		allLayersSize += usage.Size
		return nil
	})
	return allLayersSize, nil
}

// UpdateConfig values
//
// called from reload.go
func (i *ImageService) UpdateConfig(maxDownloads, maxUploads int) {
	panic("not implemented")
}

// GetLayerFolders returns the layer folders from an image RootFS.
func (i *ImageService) GetLayerFolders(img *image.Image, rwLayer layer.RWLayer) ([]string, error) {
	return nil, errdefs.NotImplemented(errors.New("not implemented"))
}

// GetContainerLayerSize returns the real size & virtual size of the container.
func (i *ImageService) GetContainerLayerSize(ctx context.Context, containerID string) (int64, int64, error) {
	ctr := i.containers.Get(containerID)
	if ctr == nil {
		return 0, 0, nil
	}

	snapshotter := i.client.SnapshotService(ctr.Driver)

	usage, err := snapshotter.Usage(ctx, containerID)
	if err != nil {
		return 0, 0, err
	}

	imageManifest, err := getContainerImageManifest(ctr)
	if err != nil {
		// Best efforts attempt to pick an image.
		// We don't have platform information at this point, so we can only
		// assume that the platform matches host.
		// Otherwise this will give a wrong base image size (different
		// platform), but should be close enough.
		mfst, err := i.GetImageManifest(ctx, ctr.Config.Image, imagetypes.GetImageOpts{})
		if err != nil {
			// Log error, don't error out whole operation.
			logrus.WithFields(logrus.Fields{
				logrus.ErrorKey: err,
				"container":     containerID,
			}).Warn("empty ImageManifest, can't calculate base image size")
			return usage.Size, 0, nil
		}
		imageManifest = *mfst
	}
	cs := i.client.ContentStore()

	imageManifestBytes, err := content.ReadBlob(ctx, cs, imageManifest)
	if err != nil {
		return 0, 0, err
	}

	var manifest ocispec.Manifest
	if err := json.Unmarshal(imageManifestBytes, &manifest); err != nil {
		return 0, 0, err
	}

	imageConfigBytes, err := content.ReadBlob(ctx, cs, manifest.Config)
	if err != nil {
		return 0, 0, err
	}
	var img ocispec.Image
	if err := json.Unmarshal(imageConfigBytes, &img); err != nil {
		return 0, 0, err
	}

	sizeCache := make(map[digest.Digest]int64)
	snapshotSizeFn := func(d digest.Digest) (int64, error) {
		if s, ok := sizeCache[d]; ok {
			return s, nil
		}
		u, err := snapshotter.Usage(ctx, d.String())
		if err != nil {
			return 0, err
		}
		sizeCache[d] = u.Size
		return u.Size, nil
	}

	chainIDs := identity.ChainIDs(img.RootFS.DiffIDs)
	snapShotSize, err := computeSnapshotSize(chainIDs, snapshotSizeFn)
	if err != nil {
		return 0, 0, err
	}

	// TODO(thaJeztah): include content-store size for the image (similar to "GET /images/json")
	return usage.Size, usage.Size + snapShotSize, nil
}

// getContainerImageManifest safely dereferences ImageManifest.
// ImageManifest can be nil for containers created with Docker Desktop with old
// containerd image store integration enabled which didn't set this field.
func getContainerImageManifest(ctr *container.Container) (ocispec.Descriptor, error) {
	if ctr.ImageManifest == nil {
		return ocispec.Descriptor{}, errdefs.InvalidParameter(errors.New("container is missing ImageManifest (probably created on old version), please recreate it"))
	}

	return *ctr.ImageManifest, nil
}

- opened https://github.com/moby/moby/issues/45408