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

     1  package containerd
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"runtime"
    10  	"time"
    11  
    12  	"github.com/containerd/containerd"
    13  	"github.com/containerd/containerd/content"
    14  	cerrdefs "github.com/containerd/containerd/errdefs"
    15  	"github.com/containerd/containerd/leases"
    16  	"github.com/containerd/containerd/mount"
    17  	"github.com/containerd/containerd/platforms"
    18  	"github.com/containerd/containerd/rootfs"
    19  	"github.com/distribution/reference"
    20  	"github.com/Prakhar-Agarwal-byte/moby/api/types/backend"
    21  	imagetypes "github.com/Prakhar-Agarwal-byte/moby/api/types/image"
    22  	"github.com/Prakhar-Agarwal-byte/moby/api/types/registry"
    23  	"github.com/Prakhar-Agarwal-byte/moby/internal/compatcontext"
    24  	registrypkg "github.com/Prakhar-Agarwal-byte/moby/registry"
    25  
    26  	// "github.com/Prakhar-Agarwal-byte/moby/api/types/container"
    27  	containerdimages "github.com/containerd/containerd/images"
    28  	"github.com/containerd/log"
    29  	"github.com/Prakhar-Agarwal-byte/moby/api/types/image"
    30  	"github.com/Prakhar-Agarwal-byte/moby/builder"
    31  	"github.com/Prakhar-Agarwal-byte/moby/errdefs"
    32  	dimage "github.com/Prakhar-Agarwal-byte/moby/image"
    33  	"github.com/Prakhar-Agarwal-byte/moby/layer"
    34  	"github.com/Prakhar-Agarwal-byte/moby/pkg/archive"
    35  	"github.com/Prakhar-Agarwal-byte/moby/pkg/progress"
    36  	"github.com/Prakhar-Agarwal-byte/moby/pkg/streamformatter"
    37  	"github.com/Prakhar-Agarwal-byte/moby/pkg/stringid"
    38  	"github.com/opencontainers/go-digest"
    39  	"github.com/opencontainers/image-spec/identity"
    40  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    41  )
    42  
    43  const imageLabelClassicBuilderParent = "org.mobyproject.image.parent"
    44  
    45  // GetImageAndReleasableLayer returns an image and releaseable layer for a
    46  // reference or ID. Every call to GetImageAndReleasableLayer MUST call
    47  // releasableLayer.Release() to prevent leaking of layers.
    48  func (i *ImageService) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ROLayer, error) {
    49  	if refOrID == "" { // FROM scratch
    50  		if runtime.GOOS == "windows" {
    51  			return nil, nil, fmt.Errorf(`"FROM scratch" is not supported on Windows`)
    52  		}
    53  		if opts.Platform != nil {
    54  			if err := dimage.CheckOS(opts.Platform.OS); err != nil {
    55  				return nil, nil, err
    56  			}
    57  		}
    58  		return nil, &rolayer{
    59  			c:           i.client,
    60  			snapshotter: i.snapshotter,
    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 err := dimage.CheckOS(img.OperatingSystem()); err != nil {
    76  				return nil, nil, err
    77  			}
    78  
    79  			roLayer, err := newROLayerForImage(ctx, &imgDesc, i, opts.Platform)
    80  			if err != nil {
    81  				return nil, nil, err
    82  			}
    83  
    84  			return img, roLayer, 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  	roLayer, err := newROLayerForImage(ctx, imgDesc, i, opts.Platform)
   109  	if err != nil {
   110  		return nil, nil, err
   111  	}
   112  
   113  	return img, roLayer, 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  
   122  	pullRegistryAuth := &registry.AuthConfig{}
   123  	if len(authConfigs) > 0 {
   124  		// The request came with a full auth config, use it
   125  		repoInfo, err := i.registryService.ResolveRepository(ref)
   126  		if err != nil {
   127  			return nil, err
   128  		}
   129  
   130  		resolvedConfig := registrypkg.ResolveAuthConfig(authConfigs, repoInfo.Index)
   131  		pullRegistryAuth = &resolvedConfig
   132  	}
   133  
   134  	if err := i.PullImage(ctx, reference.TagNameOnly(ref), platform, nil, pullRegistryAuth, output); err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	img, err := i.GetImage(ctx, name, imagetypes.GetImageOpts{Platform: platform})
   139  	if err != nil {
   140  		if errdefs.IsNotFound(err) && img != nil && platform != nil {
   141  			imgPlat := ocispec.Platform{
   142  				OS:           img.OS,
   143  				Architecture: img.BaseImgArch(),
   144  				Variant:      img.BaseImgVariant(),
   145  			}
   146  
   147  			p := *platform
   148  			if !platforms.Only(p).Match(imgPlat) {
   149  				po := streamformatter.NewJSONProgressOutput(output, false)
   150  				progress.Messagef(po, "", `
   151  WARNING: Pulled image with specified platform (%s), but the resulting image's configured platform (%s) does not match.
   152  This is most likely caused by a bug in the build system that created the fetched image (%s).
   153  Please notify the image author to correct the configuration.`,
   154  					platforms.Format(p), platforms.Format(imgPlat), name,
   155  				)
   156  				log.G(ctx).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.")
   157  			}
   158  		} else {
   159  			return nil, err
   160  		}
   161  	}
   162  
   163  	if err := dimage.CheckOS(img.OperatingSystem()); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	imgDesc, err := i.resolveDescriptor(ctx, name)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	return &imgDesc, err
   173  }
   174  
   175  func newROLayerForImage(ctx context.Context, imgDesc *ocispec.Descriptor, i *ImageService, platform *ocispec.Platform) (builder.ROLayer, error) {
   176  	if imgDesc == nil {
   177  		return nil, fmt.Errorf("can't make an RO layer for a nil image :'(")
   178  	}
   179  
   180  	platMatcher := platforms.Default()
   181  	if platform != nil {
   182  		platMatcher = platforms.Only(*platform)
   183  	}
   184  
   185  	confDesc, err := containerdimages.Config(ctx, i.client.ContentStore(), *imgDesc, platMatcher)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  
   190  	diffIDs, err := containerdimages.RootFS(ctx, i.client.ContentStore(), confDesc)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	// TODO(vvoland): Check if image is unpacked, and unpack it if it's not.
   196  	imageSnapshotID := identity.ChainID(diffIDs).String()
   197  
   198  	snapshotter := i.StorageDriver()
   199  	_, lease, err := createLease(ctx, i.client.LeasesService())
   200  	if err != nil {
   201  		return nil, errdefs.System(fmt.Errorf("failed to lease image snapshot %s: %w", imageSnapshotID, err))
   202  	}
   203  
   204  	return &rolayer{
   205  		key:                imageSnapshotID,
   206  		c:                  i.client,
   207  		snapshotter:        snapshotter,
   208  		diffID:             "", // Image RO layer doesn't have a diff.
   209  		contentStoreDigest: "",
   210  		lease:              &lease,
   211  	}, nil
   212  }
   213  
   214  func createLease(ctx context.Context, lm leases.Manager) (context.Context, leases.Lease, error) {
   215  	lease, err := lm.Create(ctx,
   216  		leases.WithExpiration(time.Hour*24),
   217  		leases.WithLabels(map[string]string{
   218  			"org.mobyproject.lease.classicbuilder": "true",
   219  		}),
   220  	)
   221  	if err != nil {
   222  		return nil, leases.Lease{}, fmt.Errorf("failed to create a lease for snapshot: %w", err)
   223  	}
   224  
   225  	return leases.WithLease(ctx, lease.ID), lease, nil
   226  }
   227  
   228  type rolayer struct {
   229  	key                string
   230  	c                  *containerd.Client
   231  	snapshotter        string
   232  	diffID             layer.DiffID
   233  	contentStoreDigest digest.Digest
   234  	lease              *leases.Lease
   235  }
   236  
   237  func (rl *rolayer) ContentStoreDigest() digest.Digest {
   238  	return rl.contentStoreDigest
   239  }
   240  
   241  func (rl *rolayer) DiffID() layer.DiffID {
   242  	if rl.diffID == "" {
   243  		return layer.DigestSHA256EmptyTar
   244  	}
   245  	return rl.diffID
   246  }
   247  
   248  func (rl *rolayer) Release() error {
   249  	if rl.lease != nil {
   250  		lm := rl.c.LeasesService()
   251  		err := lm.Delete(context.TODO(), *rl.lease)
   252  		if err != nil {
   253  			return err
   254  		}
   255  		rl.lease = nil
   256  	}
   257  	return nil
   258  }
   259  
   260  // NewRWLayer creates a new read-write layer for the builder
   261  func (rl *rolayer) NewRWLayer() (_ builder.RWLayer, outErr error) {
   262  	snapshotter := rl.c.SnapshotService(rl.snapshotter)
   263  
   264  	key := stringid.GenerateRandomID()
   265  
   266  	ctx, lease, err := createLease(context.TODO(), rl.c.LeasesService())
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	defer func() {
   271  		if outErr != nil {
   272  			if err := rl.c.LeasesService().Delete(ctx, lease); err != nil {
   273  				log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
   274  			}
   275  		}
   276  	}()
   277  
   278  	mounts, err := snapshotter.Prepare(ctx, key, rl.key)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	root, err := os.MkdirTemp(os.TempDir(), "rootfs-mount")
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  	if err := mount.All(mounts, root); err != nil {
   288  		return nil, err
   289  	}
   290  
   291  	return &rwlayer{
   292  		key:         key,
   293  		parent:      rl.key,
   294  		c:           rl.c,
   295  		snapshotter: rl.snapshotter,
   296  		root:        root,
   297  		lease:       &lease,
   298  	}, nil
   299  }
   300  
   301  type rwlayer struct {
   302  	key         string
   303  	parent      string
   304  	c           *containerd.Client
   305  	snapshotter string
   306  	root        string
   307  	lease       *leases.Lease
   308  }
   309  
   310  func (rw *rwlayer) Root() string {
   311  	return rw.root
   312  }
   313  
   314  func (rw *rwlayer) Commit() (_ builder.ROLayer, outErr error) {
   315  	snapshotter := rw.c.SnapshotService(rw.snapshotter)
   316  
   317  	key := stringid.GenerateRandomID()
   318  
   319  	lm := rw.c.LeasesService()
   320  	ctx, lease, err := createLease(context.TODO(), lm)
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  	defer func() {
   325  		if outErr != nil {
   326  			if err := lm.Delete(ctx, lease); err != nil {
   327  				log.G(ctx).WithError(err).Warn("failed to remove lease after NewRWLayer error")
   328  			}
   329  		}
   330  	}()
   331  
   332  	err = snapshotter.Commit(ctx, key, rw.key)
   333  	if err != nil && !cerrdefs.IsAlreadyExists(err) {
   334  		return nil, err
   335  	}
   336  
   337  	differ := rw.c.DiffService()
   338  	desc, err := rootfs.CreateDiff(ctx, key, snapshotter, differ)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	info, err := rw.c.ContentStore().Info(ctx, desc.Digest)
   343  	if err != nil {
   344  		return nil, err
   345  	}
   346  	diffIDStr, ok := info.Labels["containerd.io/uncompressed"]
   347  	if !ok {
   348  		return nil, fmt.Errorf("invalid differ response with no diffID")
   349  	}
   350  	diffID, err := digest.Parse(diffIDStr)
   351  	if err != nil {
   352  		return nil, err
   353  	}
   354  
   355  	return &rolayer{
   356  		key:                key,
   357  		c:                  rw.c,
   358  		snapshotter:        rw.snapshotter,
   359  		diffID:             layer.DiffID(diffID),
   360  		contentStoreDigest: desc.Digest,
   361  		lease:              &lease,
   362  	}, nil
   363  }
   364  
   365  func (rw *rwlayer) Release() (outErr error) {
   366  	if rw.root == "" { // nothing to release
   367  		return nil
   368  	}
   369  
   370  	if err := mount.UnmountAll(rw.root, 0); err != nil && !errors.Is(err, os.ErrNotExist) {
   371  		log.G(context.TODO()).WithError(err).WithField("root", rw.root).Error("failed to unmount ROLayer")
   372  		return err
   373  	}
   374  	if err := os.Remove(rw.root); err != nil && !errors.Is(err, os.ErrNotExist) {
   375  		log.G(context.TODO()).WithError(err).WithField("dir", rw.root).Error("failed to remove mount temp dir")
   376  		return err
   377  	}
   378  	rw.root = ""
   379  
   380  	if rw.lease != nil {
   381  		lm := rw.c.LeasesService()
   382  		err := lm.Delete(context.TODO(), *rw.lease)
   383  		if err != nil {
   384  			log.G(context.TODO()).WithError(err).Warn("failed to delete lease when releasing RWLayer")
   385  		} else {
   386  			rw.lease = nil
   387  		}
   388  	}
   389  
   390  	return nil
   391  }
   392  
   393  // CreateImage creates a new image by adding a config and ID to the image store.
   394  // This is similar to LoadImage() except that it receives JSON encoded bytes of
   395  // an image instead of a tar archive.
   396  func (i *ImageService) CreateImage(ctx context.Context, config []byte, parent string, layerDigest digest.Digest) (builder.Image, error) {
   397  	imgToCreate, err := dimage.NewFromJSON(config)
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  
   402  	ociImgToCreate := dockerImageToDockerOCIImage(*imgToCreate)
   403  
   404  	var layers []ocispec.Descriptor
   405  
   406  	var parentDigest digest.Digest
   407  	// if the image has a parent, we need to start with the parents layers descriptors
   408  	if parent != "" {
   409  		parentDesc, err := i.resolveDescriptor(ctx, parent)
   410  		if err != nil {
   411  			return nil, err
   412  		}
   413  		parentImageManifest, err := containerdimages.Manifest(ctx, i.client.ContentStore(), parentDesc, platforms.Default())
   414  		if err != nil {
   415  			return nil, err
   416  		}
   417  
   418  		layers = parentImageManifest.Layers
   419  		parentDigest = parentDesc.Digest
   420  	}
   421  
   422  	cs := i.client.ContentStore()
   423  
   424  	ra, err := cs.ReaderAt(ctx, ocispec.Descriptor{Digest: layerDigest})
   425  	if err != nil {
   426  		return nil, fmt.Errorf("failed to read diff archive: %w", err)
   427  	}
   428  	defer ra.Close()
   429  
   430  	empty, err := archive.IsEmpty(content.NewReader(ra))
   431  	if err != nil {
   432  		return nil, fmt.Errorf("failed to check if archive is empty: %w", err)
   433  	}
   434  	if !empty {
   435  		info, err := cs.Info(ctx, layerDigest)
   436  		if err != nil {
   437  			return nil, err
   438  		}
   439  
   440  		layers = append(layers, ocispec.Descriptor{
   441  			MediaType: containerdimages.MediaTypeDockerSchema2LayerGzip,
   442  			Digest:    layerDigest,
   443  			Size:      info.Size,
   444  		})
   445  	}
   446  
   447  	// necessary to prevent the contents from being GC'd
   448  	// between writing them here and creating an image
   449  	ctx, release, err := i.client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	defer func() {
   454  		if err := release(compatcontext.WithoutCancel(ctx)); err != nil {
   455  			log.G(ctx).WithError(err).Warn("failed to release lease created for create")
   456  		}
   457  	}()
   458  
   459  	commitManifestDesc, err := writeContentsForImage(ctx, i.snapshotter, i.client.ContentStore(), ociImgToCreate, layers)
   460  	if err != nil {
   461  		return nil, err
   462  	}
   463  
   464  	// image create
   465  	img := containerdimages.Image{
   466  		Name:      danglingImageName(commitManifestDesc.Digest),
   467  		Target:    commitManifestDesc,
   468  		CreatedAt: time.Now(),
   469  		Labels: map[string]string{
   470  			imageLabelClassicBuilderParent: parentDigest.String(),
   471  		},
   472  	}
   473  
   474  	createdImage, err := i.client.ImageService().Update(ctx, img)
   475  	if err != nil {
   476  		if !cerrdefs.IsNotFound(err) {
   477  			return nil, err
   478  		}
   479  
   480  		if createdImage, err = i.client.ImageService().Create(ctx, img); err != nil {
   481  			return nil, fmt.Errorf("failed to create new image: %w", err)
   482  		}
   483  	}
   484  
   485  	if err := i.unpackImage(ctx, i.StorageDriver(), img, commitManifestDesc); err != nil {
   486  		return nil, err
   487  	}
   488  
   489  	newImage := dimage.Clone(imgToCreate, dimage.ID(createdImage.Target.Digest))
   490  	return newImage, nil
   491  }