package containerd import ( "context" "fmt" "io" "os" "runtime" "time" "github.com/containerd/containerd" cerrdefs "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/leases" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/platforms" "github.com/containerd/containerd/rootfs" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types/backend" imagetypes "github.com/docker/docker/api/types/image" "github.com/docker/docker/api/types/registry" "github.com/docker/docker/internal/compatcontext" registrypkg "github.com/docker/docker/registry" // "github.com/docker/docker/api/types/container" containerdimages "github.com/containerd/containerd/images" "github.com/docker/docker/api/types/image" "github.com/docker/docker/builder" "github.com/docker/docker/errdefs" dimage "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" "github.com/docker/docker/pkg/system" "github.com/opencontainers/go-digest" "github.com/opencontainers/image-spec/identity" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/sirupsen/logrus" ) // GetImageAndReleasableLayer returns an image and releaseable layer for a // reference or ID. Every call to GetImageAndReleasableLayer MUST call // releasableLayer.Release() to prevent leaking of layers. func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) { if refOrID == "" { // from SCRATCH imgOS := runtime.GOOS if runtime.GOOS == "windows" { imgOS = "linux" } if opts.Platform != nil { imgOS = opts.Platform.OS } if !system.IsOSSupported(imgOS) { return nil, nil, system.ErrNotSupportedOperatingSystem } return nil, &rolayer{ key: "", c: i.client, snapshotter: i.snapshotter, diffID: "", root: "", }, nil } if opts.PullOption != backend.PullOptionForcePull { // TODO(laurazard): same as below img, err := i.GetImage(ctx, refOrID, image.GetImageOpts{Platform: opts.Platform}) if err != nil && opts.PullOption == backend.PullOptionNoPull { return nil, nil, err } imgDesc, err := i.resolveDescriptor(ctx, refOrID) if err != nil && !errdefs.IsNotFound(err) { return nil, nil, err } if img != nil { if !system.IsOSSupported(img.OperatingSystem()) { return nil, nil, system.ErrNotSupportedOperatingSystem } roLayer, err := newROLayerForImage(ctx, &imgDesc, i, opts.Platform) if err != nil { return nil, nil, err } return img, roLayer, nil } } ctx, _, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, nil, fmt.Errorf("failed to create lease for commit: %w", err) } // TODO(laurazard): do we really need a new method here to pull the image? imgDesc, err := i.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output, opts.Platform) if err != nil { return nil, nil, err } // TODO(laurazard): pullForBuilder should return whatever we // need here instead of having to go and get it again img, err := i.GetImage(ctx, refOrID, imagetypes.GetImageOpts{ Platform: opts.Platform, }) if err != nil { return nil, nil, err } roLayer, err := newROLayerForImage(ctx, imgDesc, i, opts.Platform) if err != nil { return nil, nil, err } return img, roLayer, nil } func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConfigs map[string]registry.AuthConfig, output io.Writer, platform *ocispec.Platform) (*ocispec.Descriptor, error) { ref, err := reference.ParseNormalizedNamed(name) if err != nil { return nil, err } taggedRef := reference.TagNameOnly(ref) pullRegistryAuth := ®istry.AuthConfig{} if len(authConfigs) > 0 { // The request came with a full auth config, use it repoInfo, err := i.registryService.ResolveRepository(ref) if err != nil { return nil, err } resolvedConfig := registrypkg.ResolveAuthConfig(authConfigs, repoInfo.Index) pullRegistryAuth = &resolvedConfig } if err := i.PullImage(ctx, ref.Name(), taggedRef.(reference.NamedTagged).Tag(), platform, nil, pullRegistryAuth, output); err != nil { return nil, err } img, err := i.GetImage(ctx, name, imagetypes.GetImageOpts{Platform: platform}) if err != nil { if errdefs.IsNotFound(err) && img != nil && platform != nil { imgPlat := ocispec.Platform{ OS: img.OS, Architecture: img.BaseImgArch(), Variant: img.BaseImgVariant(), } p := *platform if !platforms.Only(p).Match(imgPlat) { po := streamformatter.NewJSONProgressOutput(output, false) progress.Messagef(po, "", ` WARNING: Pulled image with specified platform (%s), but the resulting image's configured platform (%s) does not match. This is most likely caused by a bug in the build system that created the fetched image (%s). Please notify the image author to correct the configuration.`, platforms.Format(p), platforms.Format(imgPlat), name, ) logrus.WithError(err).WithField("image", name).Warn("Ignoring error about platform mismatch where the manifest list points to an image whose configuration does not match the platform in the manifest.") } } else { return nil, err } } if !system.IsOSSupported(img.OperatingSystem()) { return nil, system.ErrNotSupportedOperatingSystem } imgDesc, err := i.resolveDescriptor(ctx, name) if err != nil { return nil, err } return &imgDesc, err } func newROLayerForImage(ctx context.Context, imgDesc *ocispec.Descriptor, i *ImageService, platform *ocispec.Platform) (builder.ROLayer, error) { if imgDesc == nil { return nil, fmt.Errorf("can't make an RO layer for a nil image :'(") } platMatcher := platforms.Default() if platform != nil { platMatcher = platforms.Only(*platform) } // this needs it's own context + lease so that it doesn't get cleaned before we're ready confDesc, err := containerdimages.Config(ctx, i.client.ContentStore(), *imgDesc, platMatcher) if err != nil { return nil, err } diffIDs, err := containerdimages.RootFS(ctx, i.client.ContentStore(), confDesc) if err != nil { return nil, err } parent := identity.ChainID(diffIDs).String() s := i.client.SnapshotService(i.snapshotter) key := stringid.GenerateRandomID() ctx, _, err = i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, fmt.Errorf("failed to create lease for commit: %w", err) } mounts, err := s.View(ctx, key, parent) if err != nil { return nil, err } tempMountLocation := os.TempDir() root, err := os.MkdirTemp(tempMountLocation, "rootfs-mount") if err != nil { return nil, err } if err := mount.All(mounts, root); err != nil { return nil, err } return &rolayer{ key: key, c: i.client, snapshotter: i.snapshotter, diffID: digest.Digest(parent), root: root, contentStoreDigest: "", }, nil } type rolayer struct { key string c *containerd.Client snapshotter string diffID digest.Digest root string contentStoreDigest digest.Digest } func (rl *rolayer) ContentStoreDigest() digest.Digest { return rl.contentStoreDigest } func (rl *rolayer) DiffID() layer.DiffID { if rl.diffID == "" { return layer.DigestSHA256EmptyTar } return layer.DiffID(rl.diffID) } func (rl *rolayer) Release() error { snapshotter := rl.c.SnapshotService(rl.snapshotter) err := snapshotter.Remove(context.TODO(), rl.key) if err != nil && !cerrdefs.IsNotFound(err) { return err } if rl.root == "" { // nothing to release return nil } if err := mount.UnmountAll(rl.root, 0); err != nil { logrus.WithError(err).WithField("root", rl.root).Error("failed to unmount ROLayer") return err } if err := os.Remove(rl.root); err != nil { logrus.WithError(err).WithField("dir", rl.root).Error("failed to remove mount temp dir") return err } rl.root = "" return nil } // NewRWLayer creates a new read-write layer for the builder func (rl *rolayer) NewRWLayer() (builder.RWLayer, error) { snapshotter := rl.c.SnapshotService(rl.snapshotter) // we need this here for the prepared snapshots or // we'll have racy behaviour where sometimes they // will get GC'd before we commit/use them ctx, _, err := rl.c.WithLease(context.TODO(), leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, fmt.Errorf("failed to create lease for commit: %w", err) } key := stringid.GenerateRandomID() mounts, err := snapshotter.Prepare(ctx, key, rl.diffID.String()) if err != nil { return nil, err } root, err := os.MkdirTemp(os.TempDir(), "rootfs-mount") if err != nil { return nil, err } if err := mount.All(mounts, root); err != nil { return nil, err } return &rwlayer{ key: key, parent: rl.key, c: rl.c, snapshotter: rl.snapshotter, root: root, }, nil } type rwlayer struct { key string parent string c *containerd.Client snapshotter string root string } func (rw *rwlayer) Root() string { return rw.root } func (rw *rwlayer) Commit() (builder.ROLayer, error) { // we need this here for the prepared snapshots or // we'll have racy behaviour where sometimes they // will get GC'd before we commit/use them ctx, _, err := rw.c.WithLease(context.TODO(), leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, fmt.Errorf("failed to create lease for commit: %w", err) } snapshotter := rw.c.SnapshotService(rw.snapshotter) key := stringid.GenerateRandomID() err = snapshotter.Commit(ctx, key, rw.key) if err != nil && !cerrdefs.IsAlreadyExists(err) { return nil, err } differ := rw.c.DiffService() desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ) if err != nil { return nil, err } info, err := rw.c.ContentStore().Info(ctx, desc.Digest) if err != nil { return nil, err } diffIDStr, ok := info.Labels["containerd.io/uncompressed"] if !ok { return nil, fmt.Errorf("invalid differ response with no diffID") } diffID, err := digest.Parse(diffIDStr) if err != nil { return nil, err } return &rolayer{ key: key, c: rw.c, snapshotter: rw.snapshotter, diffID: diffID, root: "", contentStoreDigest: desc.Digest, }, nil } func (rw *rwlayer) Release() error { snapshotter := rw.c.SnapshotService(rw.snapshotter) err := snapshotter.Remove(context.TODO(), rw.key) if err != nil && !cerrdefs.IsNotFound(err) { return err } if rw.root == "" { // nothing to release return nil } if err := mount.UnmountAll(rw.root, 0); err != nil { logrus.WithError(err).WithField("root", rw.root).Error("failed to unmount ROLayer") return err } if err := os.Remove(rw.root); err != nil { logrus.WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir") return err } rw.root = "" return nil } // CreateImage creates a new image by adding a config and ID to the image store. // This is similar to LoadImage() except that it receives JSON encoded bytes of // an image instead of a tar archive. func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) { imgToCreate, err := dimage.NewFromJSON(config) if err != nil { return nil, err } rootFS := ocispec.RootFS{ Type: imgToCreate.RootFS.Type, DiffIDs: []digest.Digest{}, } for _, diffId := range imgToCreate.RootFS.DiffIDs { rootFS.DiffIDs = append(rootFS.DiffIDs, digest.Digest(diffId)) } exposedPorts := make(map[string]struct{}, len(imgToCreate.Config.ExposedPorts)) for k, v := range imgToCreate.Config.ExposedPorts { exposedPorts[string(k)] = v } var ociHistory []ocispec.History for _, history := range imgToCreate.History { created := history.Created ociHistory = append(ociHistory, ocispec.History{ Created: &created, CreatedBy: history.CreatedBy, Author: history.Author, Comment: history.Comment, EmptyLayer: history.EmptyLayer, }) } // make an ocispec.Image from the docker/image.Image ociImgToCreate := ocispec.Image{ Created: &imgToCreate.Created, Author: imgToCreate.Author, Platform: ocispec.Platform{ Architecture: imgToCreate.Architecture, Variant: imgToCreate.Variant, OS: imgToCreate.OS, OSVersion: imgToCreate.OSVersion, OSFeatures: imgToCreate.OSFeatures, }, Config: ocispec.ImageConfig{ User: imgToCreate.Config.User, ExposedPorts: exposedPorts, Env: imgToCreate.Config.Env, Entrypoint: imgToCreate.Config.Entrypoint, Cmd: imgToCreate.Config.Cmd, Volumes: imgToCreate.Config.Volumes, WorkingDir: imgToCreate.Config.WorkingDir, Labels: imgToCreate.Config.Labels, StopSignal: imgToCreate.Config.StopSignal, }, RootFS: rootFS, History: ociHistory, } var layers []ocispec.Descriptor // if the image has a parent, we need to start with the parents layers descriptors if parent != "" { parentDesc, err := i.resolveDescriptor(ctx, parent) if err != nil { return nil, err } parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default()) if err != nil { return nil, err } layers = parentImageManifest.Layers } // get the info for the new layers info, err := i.client.ContentStore().Info(ctx, layerDigest) if err != nil { return nil, err } // append the new layer descriptor layers = append(layers, ocispec.Descriptor{ MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip, Digest: layerDigest, Size: info.Size, }, ) // necessary to prevent the contents from being GC'd // between writing them here and creating an image ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour)) if err != nil { return nil, err } defer func() { if err := release(compatcontext.WithoutCancel(ctx)); err != nil { logrus.WithError(err).Warn("failed to release lease created for create") } }() commitManifestDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), ociImgToCreate, layers) if err != nil { return nil, err } // image create img := containerdimages.Image{ Name: danglingImageName(commitManifestDesc.Digest), Target: commitManifestDesc, CreatedAt: time.Now(), } createdImage, err := i.client.ImageService().Update(ctx, img) if err != nil { if !cerrdefs.IsNotFound(err) { return nil, err } if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil { return nil, fmt.Errorf("failed to create new image: %w", err) } } if err := i.unpackImage(ctx, createdImage, platforms.DefaultSpec()); err != nil { return nil, err } newImage := dimage.Clone(imgToCreate, dimage.ID(createdImage.Target.Digest)) return newImage, nil }