github.com/Prakhar-Agarwal-byte/moby@v0.0.0-20231027092010-a14e3e8ab87e/daemon/containerd/image_commit.go (about)

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