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 }