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/distribution/reference" 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/docker/docker/registry" "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 ResolveRepository(name reference.Named) (*registry.RepositoryInfo, error) } 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 }