github.com/containerd/nerdctl/v2@v2.0.0-beta.5.0.20240520001846-b5758f54fa28/pkg/imgutil/commit/commit.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package commit
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/rand"
    23  	"encoding/base64"
    24  	"encoding/json"
    25  	"fmt"
    26  	"runtime"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/containerd/containerd"
    31  	"github.com/containerd/containerd/cio"
    32  	"github.com/containerd/containerd/content"
    33  	"github.com/containerd/containerd/diff"
    34  	"github.com/containerd/containerd/errdefs"
    35  	"github.com/containerd/containerd/images"
    36  	"github.com/containerd/containerd/leases"
    37  	"github.com/containerd/containerd/rootfs"
    38  	"github.com/containerd/containerd/snapshots"
    39  	"github.com/containerd/log"
    40  	imgutil "github.com/containerd/nerdctl/v2/pkg/imgutil"
    41  	"github.com/containerd/nerdctl/v2/pkg/labels"
    42  	"github.com/containerd/platforms"
    43  	"github.com/opencontainers/go-digest"
    44  	"github.com/opencontainers/image-spec/identity"
    45  	"github.com/opencontainers/image-spec/specs-go"
    46  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    47  )
    48  
    49  type Changes struct {
    50  	CMD, Entrypoint []string
    51  }
    52  
    53  type Opts struct {
    54  	Author  string
    55  	Message string
    56  	Ref     string
    57  	Pause   bool
    58  	Changes Changes
    59  }
    60  
    61  var (
    62  	emptyGZLayer = digest.Digest("sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1")
    63  	emptyDigest  = digest.Digest("")
    64  )
    65  
    66  func Commit(ctx context.Context, client *containerd.Client, container containerd.Container, opts *Opts) (digest.Digest, error) {
    67  	id := container.ID()
    68  	info, err := container.Info(ctx)
    69  	if err != nil {
    70  		return emptyDigest, err
    71  	}
    72  
    73  	// NOTE: Moby uses provided rootfs to run container. It doesn't support
    74  	// to commit container created by moby.
    75  	baseImgWithoutPlatform, err := client.ImageService().Get(ctx, info.Image)
    76  	if err != nil {
    77  		return emptyDigest, fmt.Errorf("container %q lacks image (wasn't created by nerdctl?): %w", id, err)
    78  	}
    79  	platformLabel := info.Labels[labels.Platform]
    80  	if platformLabel == "" {
    81  		platformLabel = platforms.DefaultString()
    82  		log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel)
    83  	}
    84  	ocispecPlatform, err := platforms.Parse(platformLabel)
    85  	if err != nil {
    86  		return emptyDigest, err
    87  	}
    88  	log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform))
    89  	platformMC := platforms.Only(ocispecPlatform)
    90  	baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC)
    91  
    92  	baseImgConfig, _, err := imgutil.ReadImageConfig(ctx, baseImg)
    93  	if err != nil {
    94  		return emptyDigest, err
    95  	}
    96  
    97  	task, err := container.Task(ctx, cio.Load)
    98  	if err != nil {
    99  		return emptyDigest, err
   100  	}
   101  
   102  	if opts.Pause {
   103  		status, err := task.Status(ctx)
   104  		if err != nil {
   105  			return emptyDigest, err
   106  		}
   107  
   108  		switch status.Status {
   109  		case containerd.Paused, containerd.Created, containerd.Stopped:
   110  		default:
   111  			if err := task.Pause(ctx); err != nil {
   112  				return emptyDigest, fmt.Errorf("failed to pause container: %w", err)
   113  			}
   114  
   115  			defer func() {
   116  				if err := task.Resume(ctx); err != nil {
   117  					log.G(ctx).Warnf("failed to unpause container %v: %v", id, err)
   118  				}
   119  			}()
   120  		}
   121  	}
   122  
   123  	var (
   124  		differ = client.DiffService()
   125  		snName = info.Snapshotter
   126  		sn     = client.SnapshotService(snName)
   127  	)
   128  
   129  	// Don't gc me and clean the dirty data after 1 hour!
   130  	ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   131  	if err != nil {
   132  		return emptyDigest, fmt.Errorf("failed to create lease for commit: %w", err)
   133  	}
   134  	defer done(ctx)
   135  
   136  	diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ)
   137  	if err != nil {
   138  		return emptyDigest, fmt.Errorf("failed to export layer: %w", err)
   139  	}
   140  
   141  	imageConfig, err := generateCommitImageConfig(ctx, container, baseImg, diffID, opts)
   142  	if err != nil {
   143  		return emptyDigest, fmt.Errorf("failed to generate commit image config: %w", err)
   144  	}
   145  
   146  	rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String()
   147  	if err := applyDiffLayer(ctx, rootfsID, baseImgConfig, sn, differ, diffLayerDesc); err != nil {
   148  		return emptyDigest, fmt.Errorf("failed to apply diff: %w", err)
   149  	}
   150  
   151  	commitManifestDesc, configDigest, err := writeContentsForImage(ctx, snName, baseImg, imageConfig, diffLayerDesc)
   152  	if err != nil {
   153  		return emptyDigest, err
   154  	}
   155  
   156  	// image create
   157  	img := images.Image{
   158  		Name:      opts.Ref,
   159  		Target:    commitManifestDesc,
   160  		CreatedAt: time.Now(),
   161  	}
   162  
   163  	if _, err := client.ImageService().Update(ctx, img); err != nil {
   164  		if !errdefs.IsNotFound(err) {
   165  			return emptyDigest, err
   166  		}
   167  
   168  		if _, err := client.ImageService().Create(ctx, img); err != nil {
   169  			return emptyDigest, fmt.Errorf("failed to create new image %s: %w", opts.Ref, err)
   170  		}
   171  	}
   172  	return configDigest, nil
   173  }
   174  
   175  // generateCommitImageConfig returns commit oci image config based on the container's image.
   176  func generateCommitImageConfig(ctx context.Context, container containerd.Container, img containerd.Image, diffID digest.Digest, opts *Opts) (ocispec.Image, error) {
   177  	spec, err := container.Spec(ctx)
   178  	if err != nil {
   179  		return ocispec.Image{}, err
   180  	}
   181  
   182  	baseConfig, _, err := imgutil.ReadImageConfig(ctx, img) // aware of img.platform
   183  	if err != nil {
   184  		return ocispec.Image{}, err
   185  	}
   186  
   187  	// TODO(fuweid): support updating the USER/ENV/... fields?
   188  	if opts.Changes.CMD != nil {
   189  		baseConfig.Config.Cmd = opts.Changes.CMD
   190  	}
   191  	if opts.Changes.Entrypoint != nil {
   192  		baseConfig.Config.Entrypoint = opts.Changes.Entrypoint
   193  	}
   194  	if opts.Author == "" {
   195  		opts.Author = baseConfig.Author
   196  	}
   197  
   198  	createdBy := ""
   199  	if spec.Process != nil {
   200  		createdBy = strings.Join(spec.Process.Args, " ")
   201  	}
   202  
   203  	createdTime := time.Now()
   204  	arch := baseConfig.Architecture
   205  	if arch == "" {
   206  		arch = runtime.GOARCH
   207  		log.G(ctx).Warnf("assuming arch=%q", arch)
   208  	}
   209  	os := baseConfig.OS
   210  	if os == "" {
   211  		os = runtime.GOOS
   212  		log.G(ctx).Warnf("assuming os=%q", os)
   213  	}
   214  	log.G(ctx).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os)
   215  	return ocispec.Image{
   216  		Platform: ocispec.Platform{
   217  			Architecture: arch,
   218  			OS:           os,
   219  		},
   220  
   221  		Created: &createdTime,
   222  		Author:  opts.Author,
   223  		Config:  baseConfig.Config,
   224  		RootFS: ocispec.RootFS{
   225  			Type:    "layers",
   226  			DiffIDs: append(baseConfig.RootFS.DiffIDs, diffID),
   227  		},
   228  		History: append(baseConfig.History, ocispec.History{
   229  			Created:    &createdTime,
   230  			CreatedBy:  createdBy,
   231  			Author:     opts.Author,
   232  			Comment:    opts.Message,
   233  			EmptyLayer: (diffID == emptyGZLayer),
   234  		}),
   235  	}, nil
   236  }
   237  
   238  // writeContentsForImage will commit oci image config and manifest into containerd's content store.
   239  func writeContentsForImage(ctx context.Context, snName string, baseImg containerd.Image, newConfig ocispec.Image, diffLayerDesc ocispec.Descriptor) (ocispec.Descriptor, digest.Digest, error) {
   240  	newConfigJSON, err := json.Marshal(newConfig)
   241  	if err != nil {
   242  		return ocispec.Descriptor{}, emptyDigest, err
   243  	}
   244  
   245  	configDesc := ocispec.Descriptor{
   246  		MediaType: images.MediaTypeDockerSchema2Config,
   247  		Digest:    digest.FromBytes(newConfigJSON),
   248  		Size:      int64(len(newConfigJSON)),
   249  	}
   250  
   251  	cs := baseImg.ContentStore()
   252  	baseMfst, _, err := imgutil.ReadManifest(ctx, baseImg)
   253  	if err != nil {
   254  		return ocispec.Descriptor{}, emptyDigest, err
   255  	}
   256  	layers := append(baseMfst.Layers, diffLayerDesc)
   257  
   258  	newMfst := struct {
   259  		MediaType string `json:"mediaType,omitempty"`
   260  		ocispec.Manifest
   261  	}{
   262  		MediaType: images.MediaTypeDockerSchema2Manifest,
   263  		Manifest: ocispec.Manifest{
   264  			Versioned: specs.Versioned{
   265  				SchemaVersion: 2,
   266  			},
   267  			Config: configDesc,
   268  			Layers: layers,
   269  		},
   270  	}
   271  
   272  	newMfstJSON, err := json.MarshalIndent(newMfst, "", "    ")
   273  	if err != nil {
   274  		return ocispec.Descriptor{}, emptyDigest, err
   275  	}
   276  
   277  	newMfstDesc := ocispec.Descriptor{
   278  		MediaType: images.MediaTypeDockerSchema2Manifest,
   279  		Digest:    digest.FromBytes(newMfstJSON),
   280  		Size:      int64(len(newMfstJSON)),
   281  	}
   282  
   283  	// new manifest should reference the layers and config content
   284  	labels := map[string]string{
   285  		"containerd.io/gc.ref.content.0": configDesc.Digest.String(),
   286  	}
   287  	for i, l := range layers {
   288  		labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i+1)] = l.Digest.String()
   289  	}
   290  
   291  	err = content.WriteBlob(ctx, cs, newMfstDesc.Digest.String(), bytes.NewReader(newMfstJSON), newMfstDesc, content.WithLabels(labels))
   292  	if err != nil {
   293  		return ocispec.Descriptor{}, emptyDigest, err
   294  	}
   295  
   296  	// config should reference to snapshotter
   297  	labelOpt := content.WithLabels(map[string]string{
   298  		fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snName): identity.ChainID(newConfig.RootFS.DiffIDs).String(),
   299  	})
   300  	err = content.WriteBlob(ctx, cs, configDesc.Digest.String(), bytes.NewReader(newConfigJSON), configDesc, labelOpt)
   301  	if err != nil {
   302  		return ocispec.Descriptor{}, emptyDigest, err
   303  	}
   304  
   305  	return newMfstDesc, configDesc.Digest, nil
   306  }
   307  
   308  // createDiff creates a layer diff into containerd's content store.
   309  func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (ocispec.Descriptor, digest.Digest, error) {
   310  	newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer)
   311  	if err != nil {
   312  		return ocispec.Descriptor{}, digest.Digest(""), err
   313  	}
   314  
   315  	info, err := cs.Info(ctx, newDesc.Digest)
   316  	if err != nil {
   317  		return ocispec.Descriptor{}, digest.Digest(""), err
   318  	}
   319  
   320  	diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
   321  	if !ok {
   322  		return ocispec.Descriptor{}, digest.Digest(""), fmt.Errorf("invalid differ response with no diffID")
   323  	}
   324  
   325  	diffID, err := digest.Parse(diffIDStr)
   326  	if err != nil {
   327  		return ocispec.Descriptor{}, digest.Digest(""), err
   328  	}
   329  
   330  	return ocispec.Descriptor{
   331  		MediaType: images.MediaTypeDockerSchema2LayerGzip,
   332  		Digest:    newDesc.Digest,
   333  		Size:      info.Size,
   334  	}, diffID, nil
   335  }
   336  
   337  // applyDiffLayer will apply diff layer content created by createDiff into the snapshotter.
   338  func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor) (retErr error) {
   339  	var (
   340  		key    = uniquePart() + "-" + name
   341  		parent = identity.ChainID(baseImg.RootFS.DiffIDs).String()
   342  	)
   343  
   344  	mount, err := sn.Prepare(ctx, key, parent)
   345  	if err != nil {
   346  		return err
   347  	}
   348  
   349  	defer func() {
   350  		if retErr != nil {
   351  			// NOTE: the snapshotter should be hold by lease. Even
   352  			// if the cleanup fails, the containerd gc can delete it.
   353  			if err := sn.Remove(ctx, key); err != nil {
   354  				log.G(ctx).Warnf("failed to cleanup aborted apply %s: %s", key, err)
   355  			}
   356  		}
   357  	}()
   358  
   359  	if _, err = differ.Apply(ctx, diffDesc, mount); err != nil {
   360  		return err
   361  	}
   362  
   363  	if err = sn.Commit(ctx, name, key); err != nil {
   364  		if errdefs.IsAlreadyExists(err) {
   365  			return nil
   366  		}
   367  		return err
   368  	}
   369  	return nil
   370  }
   371  
   372  // copied from github.com/containerd/containerd/rootfs/apply.go
   373  func uniquePart() string {
   374  	t := time.Now()
   375  	var b [3]byte
   376  	// Ignore read failures, just decreases uniqueness
   377  	rand.Read(b[:])
   378  	return fmt.Sprintf("%d-%s", t.Nanosecond(), base64.URLEncoding.EncodeToString(b[:]))
   379  }