github.com/moby/docker@v26.1.3+incompatible/daemon/containerd/image_commit.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"crypto/rand"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"fmt"
     9  	"runtime"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/containerd/containerd/content"
    14  	"github.com/containerd/containerd/diff"
    15  	cerrdefs "github.com/containerd/containerd/errdefs"
    16  	"github.com/containerd/containerd/leases"
    17  	"github.com/containerd/containerd/mount"
    18  	"github.com/containerd/containerd/pkg/cleanup"
    19  	"github.com/containerd/containerd/snapshots"
    20  	"github.com/containerd/log"
    21  	"github.com/docker/docker/api/types/backend"
    22  	"github.com/docker/docker/image"
    23  	"github.com/docker/docker/internal/compatcontext"
    24  	"github.com/docker/docker/pkg/archive"
    25  	imagespec "github.com/moby/docker-image-spec/specs-go/v1"
    26  	"github.com/opencontainers/go-digest"
    27  	"github.com/opencontainers/image-spec/identity"
    28  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    29  	"github.com/pkg/errors"
    30  )
    31  
    32  /*
    33  This code is based on `commit` support in nerdctl, under Apache License
    34  https://github.com/containerd/nerdctl/blob/master/pkg/imgutil/commit/commit.go
    35  with adaptations to match the Moby data model and services.
    36  */
    37  
    38  // CommitImage creates a new image from a commit config.
    39  func (i *ImageService) CommitImage(ctx context.Context, cc backend.CommitConfig) (image.ID, error) {
    40  	container := i.containers.Get(cc.ContainerID)
    41  	cs := i.content
    42  
    43  	var parentManifest ocispec.Manifest
    44  	var parentImage imagespec.DockerOCIImage
    45  
    46  	// ImageManifest can be nil when committing an image with base FROM scratch
    47  	if container.ImageManifest != nil {
    48  		imageManifestBytes, err := content.ReadBlob(ctx, cs, *container.ImageManifest)
    49  		if err != nil {
    50  			return "", err
    51  		}
    52  
    53  		if err := json.Unmarshal(imageManifestBytes, &parentManifest); err != nil {
    54  			return "", err
    55  		}
    56  
    57  		imageConfigBytes, err := content.ReadBlob(ctx, cs, parentManifest.Config)
    58  		if err != nil {
    59  			return "", err
    60  		}
    61  		if err := json.Unmarshal(imageConfigBytes, &parentImage); err != nil {
    62  			return "", err
    63  		}
    64  	}
    65  
    66  	var (
    67  		differ = i.client.DiffService()
    68  		sn     = i.client.SnapshotService(container.Driver)
    69  	)
    70  
    71  	// Don't gc me and clean the dirty data after 1 hour!
    72  	ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
    73  	if err != nil {
    74  		return "", fmt.Errorf("failed to create lease for commit: %w", err)
    75  	}
    76  	defer func() {
    77  		if err := release(compatcontext.WithoutCancel(ctx)); err != nil {
    78  			log.G(ctx).WithError(err).Warn("failed to release lease created for commit")
    79  		}
    80  	}()
    81  
    82  	diffLayerDesc, diffID, err := i.createDiff(ctx, cc.ContainerID, sn, cs, differ)
    83  	if err != nil {
    84  		return "", fmt.Errorf("failed to export layer: %w", err)
    85  	}
    86  	imageConfig := generateCommitImageConfig(parentImage, diffID, cc)
    87  
    88  	layers := parentManifest.Layers
    89  	if diffLayerDesc != nil {
    90  		rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String()
    91  
    92  		if err := i.applyDiffLayer(ctx, rootfsID, cc.ContainerID, sn, differ, *diffLayerDesc); err != nil {
    93  			return "", fmt.Errorf("failed to apply diff: %w", err)
    94  		}
    95  
    96  		layers = append(layers, *diffLayerDesc)
    97  	}
    98  
    99  	return i.createImageOCI(ctx, imageConfig, digest.Digest(cc.ParentImageID), layers, *cc.ContainerConfig)
   100  }
   101  
   102  // generateCommitImageConfig generates an OCI Image config based on the
   103  // container's image and the CommitConfig options.
   104  func generateCommitImageConfig(baseConfig imagespec.DockerOCIImage, diffID digest.Digest, opts backend.CommitConfig) imagespec.DockerOCIImage {
   105  	if opts.Author == "" {
   106  		opts.Author = baseConfig.Author
   107  	}
   108  
   109  	createdTime := time.Now()
   110  	arch := baseConfig.Architecture
   111  	if arch == "" {
   112  		arch = runtime.GOARCH
   113  		log.G(context.TODO()).Warnf("assuming arch=%q", arch)
   114  	}
   115  	os := baseConfig.OS
   116  	if os == "" {
   117  		os = runtime.GOOS
   118  		log.G(context.TODO()).Warnf("assuming os=%q", os)
   119  	}
   120  	log.G(context.TODO()).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os)
   121  
   122  	diffIds := baseConfig.RootFS.DiffIDs
   123  	if diffID != "" {
   124  		diffIds = append(diffIds, diffID)
   125  	}
   126  
   127  	return imagespec.DockerOCIImage{
   128  		Image: ocispec.Image{
   129  			Platform: ocispec.Platform{
   130  				Architecture: arch,
   131  				OS:           os,
   132  			},
   133  			Created: &createdTime,
   134  			Author:  opts.Author,
   135  			RootFS: ocispec.RootFS{
   136  				Type:    "layers",
   137  				DiffIDs: diffIds,
   138  			},
   139  			History: append(baseConfig.History, ocispec.History{
   140  				Created:    &createdTime,
   141  				CreatedBy:  strings.Join(opts.ContainerConfig.Cmd, " "),
   142  				Author:     opts.Author,
   143  				Comment:    opts.Comment,
   144  				EmptyLayer: diffID == "",
   145  			}),
   146  		},
   147  		Config: containerConfigToDockerOCIImageConfig(opts.Config),
   148  	}
   149  }
   150  
   151  // createDiff creates a layer diff into containerd's content store.
   152  // If the diff is empty it returns nil empty digest and no error.
   153  func (i *ImageService) createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (*ocispec.Descriptor, digest.Digest, error) {
   154  	info, err := sn.Stat(ctx, name)
   155  	if err != nil {
   156  		return nil, "", err
   157  	}
   158  
   159  	var upper []mount.Mount
   160  	if !i.idMapping.Empty() {
   161  		// The rootfs of the container is remapped if an id mapping exists, we
   162  		// need to "unremap" it before committing the snapshot
   163  		rootPair := i.idMapping.RootPair()
   164  		usernsID := fmt.Sprintf("%s-%d-%d-%s", name, rootPair.UID, rootPair.GID, uniquePart())
   165  		remappedID := usernsID + remapSuffix
   166  		baseName := name
   167  
   168  		if info.Kind == snapshots.KindActive {
   169  			source, err := sn.Mounts(ctx, name)
   170  			if err != nil {
   171  				return nil, "", err
   172  			}
   173  
   174  			// No need to use parent since the whole snapshot is copied.
   175  			// Using parent would require doing diff/apply while starting
   176  			// from empty can just copy the whole snapshot.
   177  			// TODO: Optimize this for overlay mounts, can use parent
   178  			// and just copy upper directories without mounting
   179  			upper, err = sn.Prepare(ctx, remappedID, "")
   180  			if err != nil {
   181  				return nil, "", err
   182  			}
   183  
   184  			if err := i.copyAndUnremapRootFS(ctx, upper, source); err != nil {
   185  				return nil, "", err
   186  			}
   187  		} else {
   188  			upper, err = sn.Prepare(ctx, remappedID, baseName)
   189  			if err != nil {
   190  				return nil, "", err
   191  			}
   192  
   193  			if err := i.unremapRootFS(ctx, upper); err != nil {
   194  				return nil, "", err
   195  			}
   196  		}
   197  	} else {
   198  		if info.Kind == snapshots.KindActive {
   199  			upper, err = sn.Mounts(ctx, name)
   200  			if err != nil {
   201  				return nil, "", err
   202  			}
   203  		} else {
   204  			upperKey := fmt.Sprintf("%s-view-%s", name, uniquePart())
   205  			upper, err = sn.View(ctx, upperKey, name)
   206  			if err != nil {
   207  				return nil, "", err
   208  			}
   209  			defer cleanup.Do(ctx, func(ctx context.Context) {
   210  				sn.Remove(ctx, upperKey)
   211  			})
   212  		}
   213  	}
   214  
   215  	lowerKey := fmt.Sprintf("%s-parent-view-%s", info.Parent, uniquePart())
   216  	lower, err := sn.View(ctx, lowerKey, info.Parent)
   217  	if err != nil {
   218  		return nil, "", err
   219  	}
   220  	defer cleanup.Do(ctx, func(ctx context.Context) {
   221  		sn.Remove(ctx, lowerKey)
   222  	})
   223  
   224  	newDesc, err := comparer.Compare(ctx, lower, upper)
   225  	if err != nil {
   226  		return nil, "", errors.Wrap(err, "CreateDiff")
   227  	}
   228  
   229  	ra, err := cs.ReaderAt(ctx, newDesc)
   230  	if err != nil {
   231  		return nil, "", fmt.Errorf("failed to read diff archive: %w", err)
   232  	}
   233  	defer ra.Close()
   234  
   235  	empty, err := archive.IsEmpty(content.NewReader(ra))
   236  	if err != nil {
   237  		return nil, "", fmt.Errorf("failed to check if archive is empty: %w", err)
   238  	}
   239  	if empty {
   240  		return nil, "", nil
   241  	}
   242  
   243  	cinfo, err := cs.Info(ctx, newDesc.Digest)
   244  	if err != nil {
   245  		return nil, "", fmt.Errorf("failed to get content info: %w", err)
   246  	}
   247  
   248  	diffIDStr, ok := cinfo.Labels["containerd.io/uncompressed"]
   249  	if !ok {
   250  		return nil, "", fmt.Errorf("invalid differ response with no diffID")
   251  	}
   252  
   253  	diffID, err := digest.Parse(diffIDStr)
   254  	if err != nil {
   255  		return nil, "", err
   256  	}
   257  
   258  	return &ocispec.Descriptor{
   259  		MediaType: ocispec.MediaTypeImageLayerGzip,
   260  		Digest:    newDesc.Digest,
   261  		Size:      cinfo.Size,
   262  	}, diffID, nil
   263  }
   264  
   265  // applyDiffLayer will apply diff layer content created by createDiff into the snapshotter.
   266  func (i *ImageService) applyDiffLayer(ctx context.Context, name string, containerID string, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor) (retErr error) {
   267  	// Let containerd know that this snapshot is only for diff-applying.
   268  	key := snapshots.UnpackKeyPrefix + "-" + uniquePart() + "-" + name
   269  
   270  	info, err := sn.Stat(ctx, containerID)
   271  	if err != nil {
   272  		return err
   273  	}
   274  
   275  	mounts, err := sn.Prepare(ctx, key, info.Parent)
   276  	if err != nil {
   277  		return fmt.Errorf("failed to prepare snapshot: %w", err)
   278  	}
   279  
   280  	defer func() {
   281  		if retErr != nil {
   282  			// NOTE: the snapshotter should be held by lease. Even
   283  			// if the cleanup fails, the containerd gc can delete it.
   284  			if err := sn.Remove(ctx, key); err != nil {
   285  				log.G(ctx).Warnf("failed to cleanup aborted apply %s: %s", key, err)
   286  			}
   287  		}
   288  	}()
   289  
   290  	if _, err = differ.Apply(ctx, diffDesc, mounts); err != nil {
   291  		return err
   292  	}
   293  
   294  	if err = sn.Commit(ctx, name, key); err != nil {
   295  		if cerrdefs.IsAlreadyExists(err) {
   296  			return nil
   297  		}
   298  		return err
   299  	}
   300  
   301  	return nil
   302  }
   303  
   304  // copied from github.com/containerd/containerd/rootfs/apply.go
   305  func uniquePart() string {
   306  	t := time.Now()
   307  	var b [3]byte
   308  	// Ignore read failures, just decreases uniqueness
   309  	rand.Read(b[:])
   310  	return fmt.Sprintf("%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:]))
   311  }
   312  
   313  // CommitBuildStep is used by the builder to create an image for each step in
   314  // the build.
   315  //
   316  // This method is different from CreateImageFromContainer:
   317  //   - it doesn't attempt to validate container state
   318  //   - it doesn't send a commit action to metrics
   319  //   - it doesn't log a container commit event
   320  //
   321  // This is a temporary shim. Should be removed when builder stops using commit.
   322  func (i *ImageService) CommitBuildStep(ctx context.Context, c backend.CommitConfig) (image.ID, error) {
   323  	ctr := i.containers.Get(c.ContainerID)
   324  	if ctr == nil {
   325  		// TODO: use typed error
   326  		return "", fmt.Errorf("container not found: %s", c.ContainerID)
   327  	}
   328  	c.ContainerMountLabel = ctr.MountLabel
   329  	c.ContainerOS = ctr.OS
   330  	c.ParentImageID = string(ctr.ImageID)
   331  	return i.CommitImage(ctx, c)
   332  }