github.com/rawahars/moby@v24.0.4+incompatible/daemon/containerd/image_builder.go (about)

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"runtime"
     9  	"time"
    10  
    11  	"github.com/containerd/containerd"
    12  	cerrdefs "github.com/containerd/containerd/errdefs"
    13  	"github.com/containerd/containerd/leases"
    14  	"github.com/containerd/containerd/mount"
    15  	"github.com/containerd/containerd/platforms"
    16  	"github.com/containerd/containerd/rootfs"
    17  	"github.com/docker/distribution/reference"
    18  	"github.com/docker/docker/api/types/backend"
    19  	imagetypes "github.com/docker/docker/api/types/image"
    20  	"github.com/docker/docker/api/types/registry"
    21  	registrypkg "github.com/docker/docker/registry"
    22  
    23  	// "github.com/docker/docker/api/types/container"
    24  	containerdimages "github.com/containerd/containerd/images"
    25  	"github.com/docker/docker/api/types/image"
    26  	"github.com/docker/docker/builder"
    27  	"github.com/docker/docker/errdefs"
    28  	dimage "github.com/docker/docker/image"
    29  	"github.com/docker/docker/layer"
    30  	"github.com/docker/docker/pkg/progress"
    31  	"github.com/docker/docker/pkg/streamformatter"
    32  	"github.com/docker/docker/pkg/stringid"
    33  	"github.com/docker/docker/pkg/system"
    34  	"github.com/opencontainers/go-digest"
    35  	"github.com/opencontainers/image-spec/identity"
    36  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    37  	"github.com/sirupsen/logrus"
    38  )
    39  
    40  // GetImageAndReleasableLayer returns an image and releaseable layer for a
    41  // reference or ID. Every call to GetImageAndReleasableLayer MUST call
    42  // releasableLayer.Release() to prevent leaking of layers.
    43  func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) {
    44  	if refOrID == "" { // from SCRATCH
    45  		os := runtime.GOOS
    46  		if runtime.GOOS == "windows" {
    47  			os = "linux"
    48  		}
    49  		if opts.Platform != nil {
    50  			os = opts.Platform.OS
    51  		}
    52  		if !system.IsOSSupported(os) {
    53  			return nil, nil, system.ErrNotSupportedOperatingSystem
    54  		}
    55  		return nil, &rolayer{
    56  			key:         "",
    57  			c:           i.client,
    58  			snapshotter: i.snapshotter,
    59  			diffID:      "",
    60  			root:        "",
    61  		}, nil
    62  	}
    63  
    64  	if opts.PullOption != backend.PullOptionForcePull {
    65  		// TODO(laurazard): same as below
    66  		img, err := i.GetImage(ctx, refOrID, image.GetImageOpts{Platform: opts.Platform})
    67  		if err != nil && opts.PullOption == backend.PullOptionNoPull {
    68  			return nil, nil, err
    69  		}
    70  		imgDesc, err := i.resolveDescriptor(ctx, refOrID)
    71  		if err != nil && !errdefs.IsNotFound(err) {
    72  			return nil, nil, err
    73  		}
    74  		if img != nil {
    75  			if !system.IsOSSupported(img.OperatingSystem()) {
    76  				return nil, nil, system.ErrNotSupportedOperatingSystem
    77  			}
    78  
    79  			layer, err := newROLayerForImage(ctx, &imgDesc, i, opts, refOrID, opts.Platform)
    80  			if err != nil {
    81  				return nil, nil, err
    82  			}
    83  
    84  			return img, layer, nil
    85  		}
    86  	}
    87  
    88  	ctx, _, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
    89  	if err != nil {
    90  		return nil, nil, fmt.Errorf("failed to create lease for commit: %w", err)
    91  	}
    92  
    93  	// TODO(laurazard): do we really need a new method here to pull the image?
    94  	imgDesc, err := i.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output, opts.Platform)
    95  	if err != nil {
    96  		return nil, nil, err
    97  	}
    98  
    99  	// TODO(laurazard): pullForBuilder should return whatever we
   100  	// need here instead of having to go and get it again
   101  	img, err := i.GetImage(ctx, refOrID, imagetypes.GetImageOpts{
   102  		Platform: opts.Platform,
   103  	})
   104  	if err != nil {
   105  		return nil, nil, err
   106  	}
   107  
   108  	layer, err := newROLayerForImage(ctx, imgDesc, i, opts, refOrID, opts.Platform)
   109  	if err != nil {
   110  		return nil, nil, err
   111  	}
   112  
   113  	return img, layer, nil
   114  }
   115  
   116  func (i *ImageService) pullForBuilder(ctx context.Context, name string, authConfigs map[string]registry.AuthConfig, output io.Writer, platform *ocispec.Platform) (*ocispec.Descriptor, error) {
   117  	ref, err := reference.ParseNormalizedNamed(name)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	taggedRef := reference.TagNameOnly(ref)
   122  
   123  	pullRegistryAuth := &registry.AuthConfig{}
   124  	if len(authConfigs) > 0 {
   125  		// The request came with a full auth config, use it
   126  		repoInfo, err := i.registryService.ResolveRepository(ref)
   127  		if err != nil {
   128  			return nil, err
   129  		}
   130  
   131  		resolvedConfig := registrypkg.ResolveAuthConfig(authConfigs, repoInfo.Index)
   132  		pullRegistryAuth = &resolvedConfig
   133  	}
   134  
   135  	if err := i.PullImage(ctx, ref.Name(), taggedRef.(reference.NamedTagged).Tag(), platform, nil, pullRegistryAuth, output); err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	img, err := i.GetImage(ctx, name, imagetypes.GetImageOpts{Platform: platform})
   140  	if err != nil {
   141  		if errdefs.IsNotFound(err) && img != nil && platform != nil {
   142  			imgPlat := ocispec.Platform{
   143  				OS:           img.OS,
   144  				Architecture: img.BaseImgArch(),
   145  				Variant:      img.BaseImgVariant(),
   146  			}
   147  
   148  			p := *platform
   149  			if !platforms.Only(p).Match(imgPlat) {
   150  				po := streamformatter.NewJSONProgressOutput(output, false)
   151  				progress.Messagef(po, "", `
   152  WARNING: Pulled image with specified platform (%s), but the resulting image's configured platform (%s) does not match.
   153  This is most likely caused by a bug in the build system that created the fetched image (%s).
   154  Please notify the image author to correct the configuration.`,
   155  					platforms.Format(p), platforms.Format(imgPlat), name,
   156  				)
   157  				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.")
   158  			}
   159  		} else {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	if !system.IsOSSupported(img.OperatingSystem()) {
   165  		return nil, system.ErrNotSupportedOperatingSystem
   166  	}
   167  
   168  	imgDesc, err := i.resolveDescriptor(ctx, name)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	return &imgDesc, err
   174  }
   175  
   176  func newROLayerForImage(ctx context.Context, imgDesc *ocispec.Descriptor, i *ImageService, opts backend.GetImageAndLayerOptions, refOrID string, platform *ocispec.Platform) (builder.ROLayer, error) {
   177  	if imgDesc == nil {
   178  		return nil, fmt.Errorf("can't make an RO layer for a nil image :'(")
   179  	}
   180  
   181  	platMatcher := platforms.Default()
   182  	if platform != nil {
   183  		platMatcher = platforms.Only(*platform)
   184  	}
   185  
   186  	// this needs it's own context + lease so that it doesn't get cleaned before we're ready
   187  	confDesc, err := containerdimages.Config(ctx, i.client.ContentStore(), *imgDesc, platMatcher)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	diffIDs, err := containerdimages.RootFS(ctx, i.client.ContentStore(), confDesc)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	parent := identity.ChainID(diffIDs).String()
   197  
   198  	s := i.client.SnapshotService(i.snapshotter)
   199  	key := stringid.GenerateRandomID()
   200  	ctx, _, err = i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   201  	if err != nil {
   202  		return nil, fmt.Errorf("failed to create lease for commit: %w", err)
   203  	}
   204  	mounts, err := s.View(ctx, key, parent)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	tempMountLocation := os.TempDir()
   210  	root, err := os.MkdirTemp(tempMountLocation, "rootfs-mount")
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	if err := mount.All(mounts, root); err != nil {
   216  		return nil, err
   217  	}
   218  
   219  	return &rolayer{
   220  		key:                key,
   221  		c:                  i.client,
   222  		snapshotter:        i.snapshotter,
   223  		diffID:             digest.Digest(parent),
   224  		root:               root,
   225  		contentStoreDigest: "",
   226  	}, nil
   227  }
   228  
   229  type rolayer struct {
   230  	key                string
   231  	c                  *containerd.Client
   232  	snapshotter        string
   233  	diffID             digest.Digest
   234  	root               string
   235  	contentStoreDigest digest.Digest
   236  }
   237  
   238  func (rl *rolayer) ContentStoreDigest() digest.Digest {
   239  	return rl.contentStoreDigest
   240  }
   241  
   242  func (rl *rolayer) DiffID() layer.DiffID {
   243  	if rl.diffID == "" {
   244  		return layer.DigestSHA256EmptyTar
   245  	}
   246  	return layer.DiffID(rl.diffID)
   247  }
   248  
   249  func (rl *rolayer) Release() error {
   250  	snapshotter := rl.c.SnapshotService(rl.snapshotter)
   251  	err := snapshotter.Remove(context.TODO(), rl.key)
   252  	if err != nil && !cerrdefs.IsNotFound(err) {
   253  		return err
   254  	}
   255  
   256  	if rl.root == "" { // nothing to release
   257  		return nil
   258  	}
   259  	if err := mount.UnmountAll(rl.root, 0); err != nil {
   260  		logrus.WithError(err).WithField("root", rl.root).Error("failed to unmount ROLayer")
   261  		return err
   262  	}
   263  	if err := os.Remove(rl.root); err != nil {
   264  		logrus.WithError(err).WithField("dir", rl.root).Error("failed to remove mount temp dir")
   265  		return err
   266  	}
   267  	rl.root = ""
   268  	return nil
   269  }
   270  
   271  // NewRWLayer creates a new read-write layer for the builder
   272  func (rl *rolayer) NewRWLayer() (builder.RWLayer, error) {
   273  	snapshotter := rl.c.SnapshotService(rl.snapshotter)
   274  
   275  	// we need this here for the prepared snapshots or
   276  	// we'll have racy behaviour where sometimes they
   277  	// will get GC'd before we commit/use them
   278  	ctx, _, err := rl.c.WithLease(context.TODO(), leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   279  	if err != nil {
   280  		return nil, fmt.Errorf("failed to create lease for commit: %w", err)
   281  	}
   282  
   283  	key := stringid.GenerateRandomID()
   284  	mounts, err := snapshotter.Prepare(ctx, key, rl.diffID.String())
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	root, err := os.MkdirTemp(os.TempDir(), "rootfs-mount")
   290  	if err != nil {
   291  		return nil, err
   292  	}
   293  	if err := mount.All(mounts, root); err != nil {
   294  		return nil, err
   295  	}
   296  
   297  	return &rwlayer{
   298  		key:         key,
   299  		parent:      rl.key,
   300  		c:           rl.c,
   301  		snapshotter: rl.snapshotter,
   302  		root:        root,
   303  	}, nil
   304  }
   305  
   306  type rwlayer struct {
   307  	key         string
   308  	parent      string
   309  	c           *containerd.Client
   310  	snapshotter string
   311  	root        string
   312  }
   313  
   314  func (rw *rwlayer) Root() string {
   315  	return rw.root
   316  }
   317  
   318  func (rw *rwlayer) Commit() (builder.ROLayer, error) {
   319  	// we need this here for the prepared snapshots or
   320  	// we'll have racy behaviour where sometimes they
   321  	// will get GC'd before we commit/use them
   322  	ctx, _, err := rw.c.WithLease(context.TODO(), leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   323  	if err != nil {
   324  		return nil, fmt.Errorf("failed to create lease for commit: %w", err)
   325  	}
   326  	snapshotter := rw.c.SnapshotService(rw.snapshotter)
   327  
   328  	key := stringid.GenerateRandomID()
   329  	err = snapshotter.Commit(ctx, key, rw.key)
   330  	if err != nil && !cerrdefs.IsAlreadyExists(err) {
   331  		return nil, err
   332  	}
   333  
   334  	differ := rw.c.DiffService()
   335  	desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ)
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	info, err := rw.c.ContentStore().Info(ctx, desc.Digest)
   340  	if err != nil {
   341  		return nil, err
   342  	}
   343  	diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
   344  	if !ok {
   345  		return nil, fmt.Errorf("invalid differ response with no diffID")
   346  	}
   347  	diffID, err := digest.Parse(diffIDStr)
   348  	if err != nil {
   349  		return nil, err
   350  	}
   351  
   352  	return &rolayer{
   353  		key:                key,
   354  		c:                  rw.c,
   355  		snapshotter:        rw.snapshotter,
   356  		diffID:             diffID,
   357  		root:               "",
   358  		contentStoreDigest: desc.Digest,
   359  	}, nil
   360  }
   361  
   362  func (rw *rwlayer) Release() error {
   363  	snapshotter := rw.c.SnapshotService(rw.snapshotter)
   364  	err := snapshotter.Remove(context.TODO(), rw.key)
   365  	if err != nil && !cerrdefs.IsNotFound(err) {
   366  		return err
   367  	}
   368  
   369  	if rw.root == "" { // nothing to release
   370  		return nil
   371  	}
   372  	if err := mount.UnmountAll(rw.root, 0); err != nil {
   373  		logrus.WithError(err).WithField("root", rw.root).Error("failed to unmount ROLayer")
   374  		return err
   375  	}
   376  	if err := os.Remove(rw.root); err != nil {
   377  		logrus.WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir")
   378  		return err
   379  	}
   380  	rw.root = ""
   381  	return nil
   382  }
   383  
   384  // CreateImage creates a new image by adding a config and ID to the image store.
   385  // This is similar to LoadImage() except that it receives JSON encoded bytes of
   386  // an image instead of a tar archive.
   387  func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) {
   388  	imgToCreate, err := dimage.NewFromJSON(config)
   389  	if err != nil {
   390  		return nil, err
   391  	}
   392  
   393  	rootfs := ocispec.RootFS{
   394  		Type:    imgToCreate.RootFS.Type,
   395  		DiffIDs: []digest.Digest{},
   396  	}
   397  	for _, diffId := range imgToCreate.RootFS.DiffIDs {
   398  		rootfs.DiffIDs = append(rootfs.DiffIDs, digest.Digest(diffId))
   399  	}
   400  	exposedPorts := make(map[string]struct{}, len(imgToCreate.Config.ExposedPorts))
   401  	for k, v := range imgToCreate.Config.ExposedPorts {
   402  		exposedPorts[string(k)] = v
   403  	}
   404  
   405  	var ociHistory []ocispec.History
   406  	for _, history := range imgToCreate.History {
   407  		created := history.Created
   408  		ociHistory = append(ociHistory, ocispec.History{
   409  			Created:    &created,
   410  			CreatedBy:  history.CreatedBy,
   411  			Author:     history.Author,
   412  			Comment:    history.Comment,
   413  			EmptyLayer: history.EmptyLayer,
   414  		})
   415  	}
   416  
   417  	// make an ocispec.Image from the docker/image.Image
   418  	ociImgToCreate := ocispec.Image{
   419  		Created: &imgToCreate.Created,
   420  		Author:  imgToCreate.Author,
   421  		Platform: ocispec.Platform{
   422  			Architecture: imgToCreate.Architecture,
   423  			Variant:      imgToCreate.Variant,
   424  			OS:           imgToCreate.OS,
   425  			OSVersion:    imgToCreate.OSVersion,
   426  			OSFeatures:   imgToCreate.OSFeatures,
   427  		},
   428  		Config: ocispec.ImageConfig{
   429  			User:         imgToCreate.Config.User,
   430  			ExposedPorts: exposedPorts,
   431  			Env:          imgToCreate.Config.Env,
   432  			Entrypoint:   imgToCreate.Config.Entrypoint,
   433  			Cmd:          imgToCreate.Config.Cmd,
   434  			Volumes:      imgToCreate.Config.Volumes,
   435  			WorkingDir:   imgToCreate.Config.WorkingDir,
   436  			Labels:       imgToCreate.Config.Labels,
   437  			StopSignal:   imgToCreate.Config.StopSignal,
   438  		},
   439  		RootFS:  rootfs,
   440  		History: ociHistory,
   441  	}
   442  
   443  	var layers []ocispec.Descriptor
   444  	// if the image has a parent, we need to start with the parents layers descriptors
   445  	if parent != "" {
   446  		parentDesc, err := i.resolveDescriptor(ctx, parent)
   447  		if err != nil {
   448  			return nil, err
   449  		}
   450  		parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default())
   451  		if err != nil {
   452  			return nil, err
   453  		}
   454  
   455  		layers = parentImageManifest.Layers
   456  	}
   457  
   458  	// get the info for the new layers
   459  	info, err := i.client.ContentStore().Info(ctx, layerDigest)
   460  	if err != nil {
   461  		return nil, err
   462  	}
   463  
   464  	// append the new layer descriptor
   465  	layers = append(layers,
   466  		ocispec.Descriptor{
   467  			MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip,
   468  			Digest:    layerDigest,
   469  			Size:      info.Size,
   470  		},
   471  	)
   472  
   473  	// necessary to prevent the contents from being GC'd
   474  	// between writing them here and creating an image
   475  	ctx, done, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  	defer done(ctx)
   480  
   481  	commitManifestDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), ociImgToCreate, layers)
   482  	if err != nil {
   483  		return nil, err
   484  	}
   485  
   486  	// image create
   487  	img := containerdimages.Image{
   488  		Name:      danglingImageName(commitManifestDesc.Digest),
   489  		Target:    commitManifestDesc,
   490  		CreatedAt: time.Now(),
   491  	}
   492  
   493  	createdImage, err := i.client.ImageService().Update(ctx, img)
   494  	if err != nil {
   495  		if !cerrdefs.IsNotFound(err) {
   496  			return nil, err
   497  		}
   498  
   499  		if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil {
   500  			return nil, fmt.Errorf("failed to create new image: %w", err)
   501  		}
   502  	}
   503  
   504  	if err := i.unpackImage(ctx, createdImage, platforms.DefaultSpec()); err != nil {
   505  		return nil, err
   506  	}
   507  
   508  	newImage := dimage.NewImage(dimage.ID(createdImage.Target.Digest))
   509  	newImage.V1Image = imgToCreate.V1Image
   510  	newImage.V1Image.ID = string(createdImage.Target.Digest)
   511  	newImage.History = imgToCreate.History
   512  	return newImage, nil
   513  }