github.com/docker/cnab-to-oci@v0.3.0-beta4/remotes/fixup.go (about)

     1  package remotes
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  
     9  	"github.com/cnabio/cnab-go/bundle"
    10  	"github.com/containerd/containerd/images"
    11  	"github.com/containerd/containerd/log"
    12  	"github.com/containerd/containerd/platforms"
    13  	"github.com/containerd/containerd/remotes"
    14  	"github.com/docker/cnab-to-oci/relocation"
    15  	"github.com/docker/distribution/reference"
    16  	ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
    17  )
    18  
    19  // FixupBundle checks that all the references are present in the referenced repository, otherwise it will mount all
    20  // the manifests to that repository. The bundle is then patched with the new digested references.
    21  func FixupBundle(ctx context.Context, b *bundle.Bundle, ref reference.Named, resolver remotes.Resolver, opts ...FixupOption) (relocation.ImageRelocationMap, error) {
    22  	logger := log.G(ctx)
    23  	logger.Debugf("Fixing up bundle %s", ref)
    24  
    25  	// Configure the fixup and the event loop
    26  	cfg, err := newFixupConfig(b, ref, resolver, opts...)
    27  	if err != nil {
    28  		return nil, err
    29  	}
    30  
    31  	events := make(chan FixupEvent)
    32  	eventLoopDone := make(chan struct{})
    33  	defer func() {
    34  		close(events)
    35  		// wait for all queued events to be treated
    36  		<-eventLoopDone
    37  	}()
    38  	go func() {
    39  		defer close(eventLoopDone)
    40  		for ev := range events {
    41  			cfg.eventCallback(ev)
    42  		}
    43  	}()
    44  
    45  	// Fixup invocation images
    46  	if len(b.InvocationImages) != 1 {
    47  		return nil, fmt.Errorf("only one invocation image supported for bundle %q", ref)
    48  	}
    49  
    50  	relocationMap := relocation.ImageRelocationMap{}
    51  	if err := fixupImage(ctx, "InvocationImage", &b.InvocationImages[0].BaseImage, relocationMap, cfg, events, cfg.invocationImagePlatformFilter); err != nil {
    52  		return nil, err
    53  	}
    54  	// Fixup images
    55  	for name, original := range b.Images {
    56  		if err := fixupImage(ctx, name, &original.BaseImage, relocationMap, cfg, events, cfg.componentImagePlatformFilter); err != nil {
    57  			return nil, err
    58  		}
    59  		b.Images[name] = original
    60  	}
    61  
    62  	logger.Debug("Bundle fixed")
    63  	return relocationMap, nil
    64  }
    65  
    66  func fixupImage(
    67  	ctx context.Context,
    68  	name string,
    69  	baseImage *bundle.BaseImage,
    70  	relocationMap relocation.ImageRelocationMap,
    71  	cfg fixupConfig,
    72  	events chan<- FixupEvent,
    73  	platformFilter platforms.Matcher) error {
    74  
    75  	log.G(ctx).Debugf("Updating entry in relocation map for %q", baseImage.Image)
    76  	ctx = withMutedContext(ctx)
    77  	notifyEvent, progress := makeEventNotifier(events, baseImage.Image, cfg.targetRef)
    78  
    79  	notifyEvent(FixupEventTypeCopyImageStart, "", nil)
    80  	// Fixup Base image
    81  	fixupInfo, pushed, err := fixupBaseImage(ctx, name, baseImage, cfg)
    82  	if err != nil {
    83  		return notifyError(notifyEvent, err)
    84  	}
    85  	// Update the relocation map with the original image name and the digested reference of the image pushed inside the bundle repository
    86  	newRef, err := reference.WithDigest(fixupInfo.targetRepo, fixupInfo.resolvedDescriptor.Digest)
    87  	if err != nil {
    88  		return err
    89  	}
    90  
    91  	relocationMap[baseImage.Image] = newRef.String()
    92  
    93  	// if the autoUpdateBundle flag is passed, mutate the bundle with the resolved digest, mediaType, and size
    94  	if cfg.autoBundleUpdate {
    95  		baseImage.Digest = fixupInfo.resolvedDescriptor.Digest.String()
    96  		baseImage.Size = uint64(fixupInfo.resolvedDescriptor.Size)
    97  		baseImage.MediaType = fixupInfo.resolvedDescriptor.MediaType
    98  	} else {
    99  		if baseImage.Digest != fixupInfo.resolvedDescriptor.Digest.String() {
   100  			return fmt.Errorf("image %q digest differs %q after fixup: %q", baseImage.Image, baseImage.Digest, fixupInfo.resolvedDescriptor.Digest.String())
   101  		}
   102  		if baseImage.Size != uint64(fixupInfo.resolvedDescriptor.Size) {
   103  			return fmt.Errorf("image %q size differs %d after fixup: %d", baseImage.Image, baseImage.Size, fixupInfo.resolvedDescriptor.Size)
   104  		}
   105  		if baseImage.MediaType != fixupInfo.resolvedDescriptor.MediaType {
   106  			return fmt.Errorf("image %q media type differs %q after fixup: %q", baseImage.Image, baseImage.MediaType, fixupInfo.resolvedDescriptor.MediaType)
   107  		}
   108  	}
   109  
   110  	if pushed {
   111  		notifyEvent(FixupEventTypeCopyImageEnd, "Image has been pushed for service "+name, nil)
   112  		return nil
   113  	}
   114  
   115  	if fixupInfo.sourceRef.Name() == fixupInfo.targetRepo.Name() {
   116  		notifyEvent(FixupEventTypeCopyImageEnd, "Nothing to do: image reference is already present in repository"+fixupInfo.targetRepo.String(), nil)
   117  		return nil
   118  	}
   119  
   120  	sourceFetcher, err := makeSourceFetcher(ctx, cfg.resolver, fixupInfo.sourceRef.Name())
   121  	if err != nil {
   122  		return notifyError(notifyEvent, err)
   123  	}
   124  
   125  	// Fixup platforms
   126  	if err := fixupPlatforms(ctx, baseImage, relocationMap, &fixupInfo, sourceFetcher, platformFilter); err != nil {
   127  		return notifyError(notifyEvent, err)
   128  	}
   129  
   130  	// Prepare and run the copier
   131  	walkerDep, cleaner, err := makeManifestWalker(ctx, sourceFetcher, notifyEvent, cfg, fixupInfo, progress)
   132  	if err != nil {
   133  		return notifyError(notifyEvent, err)
   134  	}
   135  	defer cleaner()
   136  	if err = walkerDep.wait(); err != nil {
   137  		return notifyError(notifyEvent, err)
   138  	}
   139  
   140  	notifyEvent(FixupEventTypeCopyImageEnd, "", nil)
   141  	return nil
   142  }
   143  
   144  func fixupPlatforms(ctx context.Context,
   145  	baseImage *bundle.BaseImage,
   146  	relocationMap relocation.ImageRelocationMap,
   147  	fixupInfo *imageFixupInfo,
   148  	sourceFetcher sourceFetcherAdder,
   149  	filter platforms.Matcher) error {
   150  
   151  	logger := log.G(ctx)
   152  	logger.Debugf("Fixup platforms for image %v, with relocation map %v", baseImage, relocationMap)
   153  	if filter == nil ||
   154  		(fixupInfo.resolvedDescriptor.MediaType != ocischemav1.MediaTypeImageIndex &&
   155  			fixupInfo.resolvedDescriptor.MediaType != images.MediaTypeDockerSchema2ManifestList) {
   156  		// no platform filter if platform is empty, or if the descriptor is not an OCI Index / Docker Manifest list
   157  		return nil
   158  	}
   159  
   160  	reader, err := sourceFetcher.Fetch(ctx, fixupInfo.resolvedDescriptor)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	defer reader.Close()
   165  
   166  	manifestBytes, err := ioutil.ReadAll(reader)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	var manifestList typelessManifestList
   171  	if err := json.Unmarshal(manifestBytes, &manifestList); err != nil {
   172  		return err
   173  	}
   174  	var validManifests []typelessDescriptor
   175  	for _, d := range manifestList.Manifests {
   176  		if d.Platform != nil && filter.Match(*d.Platform) {
   177  			validManifests = append(validManifests, d)
   178  		}
   179  	}
   180  	if len(validManifests) == 0 {
   181  		return fmt.Errorf("no descriptor matching the platform filter found in %q", fixupInfo.sourceRef)
   182  	}
   183  	manifestList.Manifests = validManifests
   184  	manifestBytes, err = json.Marshal(&manifestList)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	d := sourceFetcher.Add(manifestBytes)
   189  	descriptor := fixupInfo.resolvedDescriptor
   190  	descriptor.Digest = d
   191  	descriptor.Size = int64(len(manifestBytes))
   192  	fixupInfo.resolvedDescriptor = descriptor
   193  
   194  	return nil
   195  }
   196  
   197  func fixupBaseImage(ctx context.Context, name string, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, error) {
   198  	// Check image references
   199  	if err := checkBaseImage(baseImage); err != nil {
   200  		return imageFixupInfo{}, false, fmt.Errorf("invalid image %q for service %q: %s", baseImage.Image, name, err)
   201  	}
   202  	targetRepoOnly, err := reference.ParseNormalizedNamed(cfg.targetRef.Name())
   203  	if err != nil {
   204  		return imageFixupInfo{}, false, err
   205  	}
   206  
   207  	fixups := []func(context.Context, reference.Named, *bundle.BaseImage, fixupConfig) (imageFixupInfo, bool, bool, error){
   208  		pushByDigest,
   209  		resolveImageInRelocationMap,
   210  		resolveImage,
   211  		pushLocalImage,
   212  	}
   213  
   214  	for _, f := range fixups {
   215  		info, pushed, ok, err := f(ctx, targetRepoOnly, baseImage, cfg)
   216  		if err != nil {
   217  			log.G(ctx).Debug(err)
   218  		}
   219  		if ok {
   220  			return info, pushed, nil
   221  		}
   222  	}
   223  
   224  	return imageFixupInfo{}, false, fmt.Errorf("failed to resolve or push image for service %q", name)
   225  }
   226  
   227  func pushByDigest(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
   228  	if baseImage.Image != "" || !cfg.pushImages {
   229  		return imageFixupInfo{}, false, false, nil
   230  	}
   231  	descriptor, err := pushImageToTarget(ctx, baseImage.Digest, cfg)
   232  	return imageFixupInfo{
   233  		targetRepo:         target,
   234  		sourceRef:          nil,
   235  		resolvedDescriptor: descriptor,
   236  	}, true, err == nil, err
   237  }
   238  
   239  func resolveImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
   240  	sourceImageRef, err := ref(baseImage.Image)
   241  	if err != nil {
   242  		return imageFixupInfo{}, false, false, err
   243  	}
   244  	_, descriptor, err := cfg.resolver.Resolve(ctx, sourceImageRef.String())
   245  	return imageFixupInfo{
   246  		targetRepo:         target,
   247  		sourceRef:          sourceImageRef,
   248  		resolvedDescriptor: descriptor,
   249  	}, false, err == nil, err
   250  }
   251  
   252  func resolveImageInRelocationMap(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
   253  	sourceImageRef, err := ref(baseImage.Image)
   254  	if err != nil {
   255  		return imageFixupInfo{}, false, false, err
   256  	}
   257  	relocatedRef, ok := cfg.relocationMap[baseImage.Image]
   258  	if !ok {
   259  		return imageFixupInfo{}, false, false, nil
   260  	}
   261  	relocatedImageRef, err := ref(relocatedRef)
   262  	if err != nil {
   263  		return imageFixupInfo{}, false, false, err
   264  	}
   265  	_, descriptor, err := cfg.resolver.Resolve(ctx, relocatedImageRef.String())
   266  	return imageFixupInfo{
   267  		targetRepo:         target,
   268  		sourceRef:          sourceImageRef,
   269  		resolvedDescriptor: descriptor,
   270  	}, false, err == nil, err
   271  }
   272  
   273  func pushLocalImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) {
   274  	if !cfg.pushImages {
   275  		return imageFixupInfo{}, false, false, nil
   276  	}
   277  	sourceImageRef, err := ref(baseImage.Image)
   278  	if err != nil {
   279  		return imageFixupInfo{}, false, false, err
   280  	}
   281  	descriptor, err := pushImageToTarget(ctx, baseImage.Image, cfg)
   282  	return imageFixupInfo{
   283  		targetRepo:         target,
   284  		sourceRef:          sourceImageRef,
   285  		resolvedDescriptor: descriptor,
   286  	}, true, err == nil, err
   287  }
   288  
   289  func ref(str string) (reference.Named, error) {
   290  	r, err := reference.ParseNormalizedNamed(str)
   291  	if err != nil {
   292  		return nil, fmt.Errorf("%q is not a valid reference: %v", str, err)
   293  	}
   294  	return reference.TagNameOnly(r), nil
   295  }
   296  
   297  // pushImageToTarget pushes the image from the local docker daemon store to the target defined in the configuration.
   298  // Docker image cannot be pushed by digest to a registry. So to be able to push the image inside the targeted repository
   299  // the same behaviour than for multi architecture images is used: all the images are tagged for the targeted repository
   300  // and then pushed.
   301  // Every time a new image is pushed under a tag, the previous tagged image will be untagged. But this untagged image
   302  // remains accessible using its digest. So right after pushing it, the image is resolved to grab its digest from the
   303  // registry and can be added to the index.
   304  // The final workflow is then:
   305  //  - tag the image to push with targeted reference
   306  //  - push the image using a docker `ImageAPIClient`
   307  //  - resolve the pushed image to grab its digest
   308  func pushImageToTarget(ctx context.Context, src string, cfg fixupConfig) (ocischemav1.Descriptor, error) {
   309  	taggedRef := reference.TagNameOnly(cfg.targetRef)
   310  
   311  	if err := cfg.imageClient.ImageTag(ctx, src, cfg.targetRef.String()); err != nil {
   312  		return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q, make sure the image exists locally: %s", src, err)
   313  	}
   314  
   315  	if err := pushTaggedImage(ctx, cfg.imageClient, cfg.targetRef, cfg.pushOut); err != nil {
   316  		return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q: %s", src, err)
   317  	}
   318  
   319  	_, descriptor, err := cfg.resolver.Resolve(ctx, taggedRef.String())
   320  	if err != nil {
   321  		return ocischemav1.Descriptor{}, fmt.Errorf("failed to resolve %q after pushing it: %s", taggedRef, err)
   322  	}
   323  
   324  	return descriptor, nil
   325  }