Add manifest list support
Add the manifest.List interface, and implementations for OCIv1 Index and
Docker Schema2List documents.
Add an instanceDigest parameter to PutManifest(), PutSignatures(), and
LayerInfosForCopy, for symmetry with GetManifest() and GetSignatures().
Return an error if the instanceDigest is supplied to destinations which
don't support them, and add stubs that do so even to the transports
which would support it, so that we don't break compilation here.
Add a MultipleImages flag to copy.Options, and if the source for a copy
operation contains multiple images, copy all of the images if we can.
If we can't copy them all, but we were told to, return an error.
Use the generic manifest list API to select a single image to copy from
a list, so that we aren't just limited to the Docker manifest list
format for those cases.
When guessing at the type of a manifest, if the manifest contains a list
of manifests, use its declared MIME type if it included one, else assume
it's an OCI index, because an OCI index doesn't include its MIME type.
When copying, switch from using an encode-then-compare of the original
and updated versions of the list to checking if the instance list was
changed (one of the things we might have changed) or if its type has
changed due to conversion (the other change we might have made). If
neither has changed, then we don't need to change the encoded value of
the manifest.
When copying, when checking for a digest mismatch in a target image
reference, ignore a mismatch between the digest in the reference and the
digest of the main manifest if we're copying one element from a list,
and the digest in the reference matches the digest of the manifest list.
When copying, if conversion of manifests for single images is being
forced, convert manifest lists to the corresponding list types.
When copying, supply the unparsed top level to Commit() by attaching the
value to the context.Context.
Support manifest lists in the directory transport by using the instance
digest as a prefix of the filename used to store a manifest or a piece
of signature data.
Support manifest lists in the oci-layout transport by accepting indexes
as we do images, and stop guessing about Platform values to add to the
top-level index.
Support storing manifest lists to registries in the docker: transport by
using the manifest digest when we're writing one image as part of
pushing a list of them, and by using the instance digest when reading or
writing signature data, when one is specified, or the cached digest of
the non-instanced digest when one is not specified.
Add partial support for manifest lists to the storage transport: when
committing one image from a list into storage, also add a copy of the
manifest list by extracting it from the context.Context. The logic is
already in place to enable locating an image using any of multiple
manifest digests.
When writing an image that has an instanceDigest value (meaning it's a
secondary image), don't try to generate a canonical reference to add to
the image's list of names if the reference for the primary image doesn't
contain a name. That should only happen if we're writing using just an
image ID, which is unlikely, but we still need to handle it.
Avoid computing the digest of the manifest, or retrieving the
either-a-tag-or-a-digest value from the target reference, if we're given
an instanceDigest, which would override them anyway.
Move the check for non-nil instanceDigest values up into the main
PutSignatures() method instead of duplicating it in the per-strategy
helpers.
Add mention of the instanceDigest parameter and its use to various
PutManifest, PutSignatures, and LayerInfosForCopy implementations and
their declarations in interfaces.
Signed-off-by: Nalin Dahyabhai <nalin@redhat.com>
Nalin Dahyabhai
4 years ago
21 | 21 | "github.com/containers/image/v4/transports" |
22 | 22 | "github.com/containers/image/v4/types" |
23 | 23 | digest "github.com/opencontainers/go-digest" |
24 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
24 | 25 | "github.com/pkg/errors" |
25 | 26 | "github.com/sirupsen/logrus" |
26 | 27 | "github.com/vbauerster/mpb" |
109 | 110 | canSubstituteBlobs bool |
110 | 111 | } |
111 | 112 | |
113 | const ( | |
114 | // CopySystemImage is the default value which, when set in | |
115 | // Options.ImageListSelection, indicates that the caller expects only one | |
116 | // image to be copied, so if the source reference refers to a list of | |
117 | // images, one that matches the current system will be selected. | |
118 | CopySystemImage ImageListSelection = iota | |
119 | // CopyAllImages is a value which, when set in Options.ImageListSelection, | |
120 | // indicates that the caller expects to copy multiple images, and if | |
121 | // the source reference refers to a list, that the list and every image | |
122 | // to which it refers will be copied. If the source reference refers | |
123 | // to a list, the target reference can not accept lists, an error | |
124 | // should be returned. | |
125 | CopyAllImages | |
126 | // CopySpecificImages is a value which, when set in | |
127 | // Options.ImageListSelection, indicates that the caller expects the | |
128 | // source reference to be either a single image or a list of images, | |
129 | // and if the source reference is a list, wants only specific instances | |
130 | // from it copied (or none of them, if the list of instances to copy is | |
131 | // empty), along with the list itself. If the target reference can | |
132 | // only accept one image (i.e., it cannot accept lists), an error | |
133 | // should be returned. | |
134 | CopySpecificImages | |
135 | ) | |
136 | ||
137 | // ImageListSelection is one of CopySystemImage, CopyAllImages, or | |
138 | // CopySpecificImages, to control whether, when the source reference is a list, | |
139 | // copy.Image() copies only an image which matches the current runtime | |
140 | // environment, or all images which match the supplied reference, or only | |
141 | // specific images from the source reference. | |
142 | type ImageListSelection int | |
143 | ||
112 | 144 | // Options allows supplying non-default configuration modifying the behavior of CopyImage. |
113 | 145 | type Options struct { |
114 | 146 | RemoveSignatures bool // Remove any pre-existing signatures. SignBy will still add a new signature. |
120 | 152 | Progress chan types.ProgressProperties // Reported to when ProgressInterval has arrived for a single artifact+offset. |
121 | 153 | // manifest MIME type of image set by user. "" is default and means use the autodetection to the the manifest MIME type |
122 | 154 | ForceManifestMIMEType string |
155 | ImageListSelection ImageListSelection // set to either CopySystemImage (the default), CopyAllImages, or CopySpecificImages to control which instances we copy when the source reference is a list; ignored if the source reference is not a list | |
156 | Instances []digest.Digest // if ImageListSelection is CopySpecificImages, copy only these instances and the list itself | |
157 | } | |
158 | ||
159 | // validateImageListSelection returns an error if the passed-in value is not one that we recognize as a valid ImageListSelection value | |
160 | func validateImageListSelection(selection ImageListSelection) error { | |
161 | switch selection { | |
162 | case CopySystemImage, CopyAllImages, CopySpecificImages: | |
163 | return nil | |
164 | default: | |
165 | return errors.Errorf("Invalid value for options.ImageListSelection: %d", selection) | |
166 | } | |
123 | 167 | } |
124 | 168 | |
125 | 169 | // Image copies image from srcRef to destRef, using policyContext to validate |
126 | 170 | // source image admissibility. It returns the manifest which was written to |
127 | 171 | // the new copy of the image. |
128 | func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) (manifest []byte, retErr error) { | |
172 | func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, srcRef types.ImageReference, options *Options) (copiedManifest []byte, retErr error) { | |
129 | 173 | // NOTE this function uses an output parameter for the error return value. |
130 | 174 | // Setting this and returning is the ideal way to return an error. |
131 | 175 | // |
133 | 177 | // which can be valuable context in the middle of a multi-streamed copy. |
134 | 178 | if options == nil { |
135 | 179 | options = &Options{} |
180 | } | |
181 | ||
182 | if err := validateImageListSelection(options.ImageListSelection); err != nil { | |
183 | return nil, err | |
136 | 184 | } |
137 | 185 | |
138 | 186 | reportWriter := ioutil.Discard |
205 | 253 | } |
206 | 254 | |
207 | 255 | if !multiImage { |
208 | // The simple case: Just copy a single image. | |
209 | if manifest, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel); err != nil { | |
256 | // The simple case: just copy a single image. | |
257 | if copiedManifest, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedToplevel, nil); err != nil { | |
210 | 258 | return nil, err |
211 | 259 | } |
260 | } else if options.ImageListSelection == CopySystemImage { | |
261 | // This is a manifest list, and we weren't asked to copy multiple images. Choose a single image that | |
262 | // matches the current system to copy, and copy it. | |
263 | mfest, manifestType, err := unparsedToplevel.Manifest(ctx) | |
264 | if err != nil { | |
265 | return nil, errors.Wrapf(err, "Error reading manifest for %s", transports.ImageName(srcRef)) | |
266 | } | |
267 | manifestList, err := manifest.ListFromBlob(mfest, manifestType) | |
268 | if err != nil { | |
269 | return nil, errors.Wrapf(err, "Error parsing primary manifest as list for %s", transports.ImageName(srcRef)) | |
270 | } | |
271 | instanceDigest, err := manifestList.ChooseInstance(options.DestinationCtx) // try to pick one that matches options.DestinationCtx | |
272 | if err != nil { | |
273 | return nil, errors.Wrapf(err, "Error choosing an image from manifest list %s", transports.ImageName(srcRef)) | |
274 | } | |
275 | logrus.Debugf("Source is a manifest list; copying (only) instance %s for current system", instanceDigest) | |
276 | unparsedInstance := image.UnparsedInstance(rawSource, &instanceDigest) | |
277 | ||
278 | if copiedManifest, _, _, err = c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, nil); err != nil { | |
279 | return nil, err | |
280 | } | |
281 | } else { /* options.ImageListSelection == CopyAllImages or options.ImageListSelection == CopySpecificImages, */ | |
282 | // If we were asked to copy multiple images and can't, that's an error. | |
283 | if !supportsMultipleImages(c.dest) { | |
284 | return nil, errors.Errorf("Error copying multiple images: destination transport %q does not support copying multiple images as a group", destRef.Transport().Name()) | |
285 | } | |
286 | // Copy some or all of the images. | |
287 | switch options.ImageListSelection { | |
288 | case CopyAllImages: | |
289 | logrus.Debugf("Source is a manifest list; copying all instances") | |
290 | case CopySpecificImages: | |
291 | logrus.Debugf("Source is a manifest list; copying some instances") | |
292 | } | |
293 | if copiedManifest, _, err = c.copyMultipleImages(ctx, policyContext, options, unparsedToplevel); err != nil { | |
294 | return nil, err | |
295 | } | |
296 | } | |
297 | ||
298 | if err := c.dest.Commit(ctx, unparsedToplevel); err != nil { | |
299 | return nil, errors.Wrap(err, "Error committing the finished image") | |
300 | } | |
301 | ||
302 | return copiedManifest, nil | |
303 | } | |
304 | ||
305 | // Checks if the destination supports accepting multiple images by checking if it can support | |
306 | // manifest types that are lists of other manifests. | |
307 | func supportsMultipleImages(dest types.ImageDestination) bool { | |
308 | mtypes := dest.SupportedManifestMIMETypes() | |
309 | if len(mtypes) == 0 { | |
310 | // Anything goes! | |
311 | return true | |
312 | } | |
313 | for _, mtype := range mtypes { | |
314 | if manifest.MIMETypeIsMultiImage(mtype) { | |
315 | return true | |
316 | } | |
317 | } | |
318 | return false | |
319 | } | |
320 | ||
321 | // copyMultipleImages copies some or all of an image list's instances, using | |
322 | // policyContext to validate source image admissibility. | |
323 | func (c *copier) copyMultipleImages(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel *image.UnparsedImage) (copiedManifest []byte, copiedManifestType string, retErr error) { | |
324 | // Parse the list and get a copy of the original value after it's re-encoded. | |
325 | manifestList, manifestType, err := unparsedToplevel.Manifest(ctx) | |
326 | if err != nil { | |
327 | return nil, "", errors.Wrapf(err, "Error reading manifest list") | |
328 | } | |
329 | list, err := manifest.ListFromBlob(manifestList, manifestType) | |
330 | if err != nil { | |
331 | return nil, "", errors.Wrapf(err, "Error parsing manifest list %q", string(manifestList)) | |
332 | } | |
333 | originalList := list.Clone() | |
334 | ||
335 | // Read and/or clear the set of signatures for this list. | |
336 | var sigs [][]byte | |
337 | if options.RemoveSignatures { | |
338 | sigs = [][]byte{} | |
212 | 339 | } else { |
213 | // This is a manifest list. Choose a single image and copy it. | |
214 | // FIXME: Copy to destinations which support manifest lists, one image at a time. | |
215 | instanceDigest, err := image.ChooseManifestInstanceFromManifestList(ctx, options.SourceCtx, unparsedToplevel) | |
216 | if err != nil { | |
217 | return nil, errors.Wrapf(err, "Error choosing an image from manifest list %s", transports.ImageName(srcRef)) | |
218 | } | |
219 | logrus.Debugf("Source is a manifest list; copying (only) instance %s", instanceDigest) | |
220 | unparsedInstance := image.UnparsedInstance(rawSource, &instanceDigest) | |
221 | ||
222 | if manifest, err = c.copyOneImage(ctx, policyContext, options, unparsedInstance); err != nil { | |
223 | return nil, err | |
224 | } | |
225 | } | |
226 | ||
227 | if err := c.dest.Commit(ctx); err != nil { | |
228 | return nil, errors.Wrap(err, "Error committing the finished image") | |
229 | } | |
230 | ||
231 | return manifest, nil | |
232 | } | |
233 | ||
234 | // Image copies a single (on-manifest-list) image unparsedImage, using policyContext to validate | |
340 | c.Printf("Getting image list signatures\n") | |
341 | s, err := c.rawSource.GetSignatures(ctx, nil) | |
342 | if err != nil { | |
343 | return nil, "", errors.Wrap(err, "Error reading signatures") | |
344 | } | |
345 | sigs = s | |
346 | } | |
347 | if len(sigs) != 0 { | |
348 | c.Printf("Checking if image list destination supports signatures\n") | |
349 | if err := c.dest.SupportsSignatures(ctx); err != nil { | |
350 | return nil, "", errors.Wrap(err, "Can not copy signatures") | |
351 | } | |
352 | } | |
353 | ||
354 | // Determine if we'll need to convert the manifest list to a different format. | |
355 | forceListMIMEType := options.ForceManifestMIMEType | |
356 | switch forceListMIMEType { | |
357 | case manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType, manifest.DockerV2Schema2MediaType: | |
358 | forceListMIMEType = manifest.DockerV2ListMediaType | |
359 | case imgspecv1.MediaTypeImageManifest: | |
360 | forceListMIMEType = imgspecv1.MediaTypeImageIndex | |
361 | } | |
362 | selectedListType, err := c.determineListConversion(manifestType, c.dest.SupportedManifestMIMETypes(), forceListMIMEType) | |
363 | if err != nil { | |
364 | return nil, "", errors.Wrapf(err, "Error determining manifest list type to write to destination") | |
365 | } | |
366 | if selectedListType != list.MIMEType() { | |
367 | canModifyManifestList := (len(sigs) == 0) | |
368 | if !canModifyManifestList { | |
369 | return nil, "", errors.Errorf("Error: manifest list must be converted to type %q to be written to destination, but that would invalidate signatures", selectedListType) | |
370 | } | |
371 | } | |
372 | ||
373 | // Copy each image, or just the ones we want to copy, in turn. | |
374 | instanceDigests := list.Instances() | |
375 | imagesToCopy := len(instanceDigests) | |
376 | if options.ImageListSelection == CopySpecificImages { | |
377 | imagesToCopy = len(options.Instances) | |
378 | } | |
379 | c.Printf("Copying %d of %d images in list\n", imagesToCopy, len(instanceDigests)) | |
380 | updates := make([]manifest.ListUpdate, len(instanceDigests)) | |
381 | instancesCopied := 0 | |
382 | for i, instanceDigest := range instanceDigests { | |
383 | if options.ImageListSelection == CopySpecificImages { | |
384 | skip := true | |
385 | for _, instance := range options.Instances { | |
386 | if instance == instanceDigest { | |
387 | skip = false | |
388 | break | |
389 | } | |
390 | } | |
391 | if skip { | |
392 | update, err := list.Instance(instanceDigest) | |
393 | if err != nil { | |
394 | return nil, "", err | |
395 | } | |
396 | logrus.Debugf("Skipping instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) | |
397 | // Record the digest/size/type of the manifest that we didn't copy. | |
398 | updates[i] = update | |
399 | continue | |
400 | } | |
401 | } | |
402 | logrus.Debugf("Copying instance %s (%d/%d)", instanceDigest, i+1, len(instanceDigests)) | |
403 | c.Printf("Copying image %s (%d/%d)\n", instanceDigest, instancesCopied+1, imagesToCopy) | |
404 | unparsedInstance := image.UnparsedInstance(c.rawSource, &instanceDigest) | |
405 | updatedManifest, updatedManifestType, updatedManifestDigest, err := c.copyOneImage(ctx, policyContext, options, unparsedToplevel, unparsedInstance, &instanceDigest) | |
406 | if err != nil { | |
407 | return nil, "", err | |
408 | } | |
409 | instancesCopied++ | |
410 | // Record the result of a possible conversion here. | |
411 | update := manifest.ListUpdate{ | |
412 | Digest: updatedManifestDigest, | |
413 | Size: int64(len(updatedManifest)), | |
414 | MediaType: updatedManifestType, | |
415 | } | |
416 | updates[i] = update | |
417 | } | |
418 | ||
419 | // Now reset the digest/size/types of the manifests in the list to account for any conversions that we made. | |
420 | if err = list.UpdateInstances(updates); err != nil { | |
421 | return nil, "", errors.Wrapf(err, "Error updating manifest list") | |
422 | } | |
423 | ||
424 | // Check if the updates meaningfully changed the list of images. | |
425 | listIsModified := false | |
426 | if !reflect.DeepEqual(list.Instances(), originalList.Instances()) { | |
427 | listIsModified = true | |
428 | } | |
429 | ||
430 | // Perform the list conversion. | |
431 | if selectedListType != list.MIMEType() { | |
432 | list, err = list.ConvertToMIMEType(selectedListType) | |
433 | if err != nil { | |
434 | return nil, "", errors.Wrapf(err, "Error converting manifest list to list with MIME type %q", selectedListType) | |
435 | } | |
436 | } | |
437 | ||
438 | // If we can't use the original value, but we have to change it, flag an error. | |
439 | if listIsModified { | |
440 | manifestList, err = list.Serialize() | |
441 | if err != nil { | |
442 | return nil, "", errors.Wrapf(err, "Error encoding updated manifest list (%q: %#v)", list.MIMEType(), list.Instances()) | |
443 | } | |
444 | logrus.Debugf("Manifest list has been updated") | |
445 | } | |
446 | ||
447 | // Save the manifest list. | |
448 | c.Printf("Writing manifest list to image destination\n") | |
449 | if err = c.dest.PutManifest(ctx, manifestList, nil); err != nil { | |
450 | return nil, "", errors.Wrapf(err, "Error writing manifest list %q", string(manifestList)) | |
451 | } | |
452 | ||
453 | // Sign the manifest list. | |
454 | if options.SignBy != "" { | |
455 | newSig, err := c.createSignature(manifestList, options.SignBy) | |
456 | if err != nil { | |
457 | return nil, "", err | |
458 | } | |
459 | sigs = append(sigs, newSig) | |
460 | } | |
461 | ||
462 | c.Printf("Storing list signatures\n") | |
463 | if err := c.dest.PutSignatures(ctx, sigs, nil); err != nil { | |
464 | return nil, "", errors.Wrap(err, "Error writing signatures") | |
465 | } | |
466 | ||
467 | return manifestList, selectedListType, nil | |
468 | } | |
469 | ||
470 | // copyOneImage copies a single (non-manifest-list) image unparsedImage, using policyContext to validate | |
235 | 471 | // source image admissibility. |
236 | func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedImage *image.UnparsedImage) (manifestBytes []byte, retErr error) { | |
472 | func (c *copier) copyOneImage(ctx context.Context, policyContext *signature.PolicyContext, options *Options, unparsedToplevel, unparsedImage *image.UnparsedImage, targetInstance *digest.Digest) (retManifest []byte, retManifestType string, retManifestDigest digest.Digest, retErr error) { | |
237 | 473 | // The caller is handling manifest lists; this could happen only if a manifest list contains a manifest list. |
238 | 474 | // Make sure we fail cleanly in such cases. |
239 | 475 | multiImage, err := isMultiImage(ctx, unparsedImage) |
240 | 476 | if err != nil { |
241 | 477 | // FIXME FIXME: How to name a reference for the sub-image? |
242 | return nil, errors.Wrapf(err, "Error determining manifest MIME type for %s", transports.ImageName(unparsedImage.Reference())) | |
478 | return nil, "", "", errors.Wrapf(err, "Error determining manifest MIME type for %s", transports.ImageName(unparsedImage.Reference())) | |
243 | 479 | } |
244 | 480 | if multiImage { |
245 | return nil, fmt.Errorf("Unexpectedly received a manifest list instead of a manifest for a single image") | |
481 | return nil, "", "", fmt.Errorf("Unexpectedly received a manifest list instead of a manifest for a single image") | |
246 | 482 | } |
247 | 483 | |
248 | 484 | // Please keep this policy check BEFORE reading any other information about the image. |
249 | // (the multiImage check above only matches the MIME type, which we have received anyway. | |
485 | // (The multiImage check above only matches the MIME type, which we have received anyway. | |
250 | 486 | // Actual parsing of anything should be deferred.) |
251 | 487 | if allowed, err := policyContext.IsRunningImageAllowed(ctx, unparsedImage); !allowed || err != nil { // Be paranoid and fail if either return value indicates so. |
252 | return nil, errors.Wrap(err, "Source image rejected") | |
488 | return nil, "", "", errors.Wrap(err, "Source image rejected") | |
253 | 489 | } |
254 | 490 | src, err := image.FromUnparsedImage(ctx, options.SourceCtx, unparsedImage) |
255 | 491 | if err != nil { |
256 | return nil, errors.Wrapf(err, "Error initializing image from source %s", transports.ImageName(c.rawSource.Reference())) | |
492 | return nil, "", "", errors.Wrapf(err, "Error initializing image from source %s", transports.ImageName(c.rawSource.Reference())) | |
257 | 493 | } |
258 | 494 | |
259 | 495 | // If the destination is a digested reference, make a note of that, determine what digest value we're |
260 | // expecting, and check that the source manifest matches it. | |
496 | // expecting, and check that the source manifest matches it. If the source manifest doesn't, but it's | |
497 | // one item from a manifest list that matches it, accept that as a match. | |
261 | 498 | destIsDigestedReference := false |
262 | 499 | if named := c.dest.Reference().DockerReference(); named != nil { |
263 | 500 | if digested, ok := named.(reference.Digested); ok { |
264 | 501 | destIsDigestedReference = true |
265 | 502 | sourceManifest, _, err := src.Manifest(ctx) |
266 | 503 | if err != nil { |
267 | return nil, errors.Wrapf(err, "Error reading manifest from source image") | |
504 | return nil, "", "", errors.Wrapf(err, "Error reading manifest from source image") | |
268 | 505 | } |
269 | 506 | matches, err := manifest.MatchesDigest(sourceManifest, digested.Digest()) |
270 | 507 | if err != nil { |
271 | return nil, errors.Wrapf(err, "Error computing digest of source image's manifest") | |
508 | return nil, "", "", errors.Wrapf(err, "Error computing digest of source image's manifest") | |
272 | 509 | } |
273 | 510 | if !matches { |
274 | return nil, errors.New("Digest of source image's manifest would not match destination reference") | |
511 | manifestList, _, err := unparsedToplevel.Manifest(ctx) | |
512 | if err != nil { | |
513 | return nil, "", "", errors.Wrapf(err, "Error reading manifest from source image") | |
514 | } | |
515 | matches, err = manifest.MatchesDigest(manifestList, digested.Digest()) | |
516 | if err != nil { | |
517 | return nil, "", "", errors.Wrapf(err, "Error computing digest of source image's manifest") | |
518 | } | |
519 | if !matches { | |
520 | return nil, "", "", errors.New("Digest of source image's manifest would not match destination reference") | |
521 | } | |
275 | 522 | } |
276 | 523 | } |
277 | 524 | } |
278 | 525 | |
279 | 526 | if err := checkImageDestinationForCurrentRuntimeOS(ctx, options.DestinationCtx, src, c.dest); err != nil { |
280 | return nil, err | |
527 | return nil, "", "", err | |
281 | 528 | } |
282 | 529 | |
283 | 530 | var sigs [][]byte |
287 | 534 | c.Printf("Getting image source signatures\n") |
288 | 535 | s, err := src.Signatures(ctx) |
289 | 536 | if err != nil { |
290 | return nil, errors.Wrap(err, "Error reading signatures") | |
537 | return nil, "", "", errors.Wrap(err, "Error reading signatures") | |
291 | 538 | } |
292 | 539 | sigs = s |
293 | 540 | } |
294 | 541 | if len(sigs) != 0 { |
295 | 542 | c.Printf("Checking if image destination supports signatures\n") |
296 | 543 | if err := c.dest.SupportsSignatures(ctx); err != nil { |
297 | return nil, errors.Wrap(err, "Can not copy signatures") | |
544 | return nil, "", "", errors.Wrap(err, "Can not copy signatures") | |
298 | 545 | } |
299 | 546 | } |
300 | 547 | |
314 | 561 | ic.canSubstituteBlobs = ic.canModifyManifest && options.SignBy == "" |
315 | 562 | |
316 | 563 | if err := ic.updateEmbeddedDockerReference(); err != nil { |
317 | return nil, err | |
564 | return nil, "", "", err | |
318 | 565 | } |
319 | 566 | |
320 | 567 | // We compute preferredManifestMIMEType only to show it in error messages. |
321 | 568 | // Without having to add this context in an error message, we would be happy enough to know only that no conversion is needed. |
322 | 569 | preferredManifestMIMEType, otherManifestMIMETypeCandidates, err := ic.determineManifestConversion(ctx, c.dest.SupportedManifestMIMETypes(), options.ForceManifestMIMEType) |
323 | 570 | if err != nil { |
324 | return nil, err | |
571 | return nil, "", "", err | |
325 | 572 | } |
326 | 573 | |
327 | 574 | // If src.UpdatedImageNeedsLayerDiffIDs(ic.manifestUpdates) will be true, it needs to be true by the time we get here. |
328 | 575 | ic.diffIDsAreNeeded = src.UpdatedImageNeedsLayerDiffIDs(*ic.manifestUpdates) |
329 | 576 | |
330 | 577 | if err := ic.copyLayers(ctx); err != nil { |
331 | return nil, err | |
578 | return nil, "", "", err | |
332 | 579 | } |
333 | 580 | |
334 | 581 | // With docker/distribution registries we do not know whether the registry accepts schema2 or schema1 only; |
335 | 582 | // and at least with the OpenShift registry "acceptschema2" option, there is no way to detect the support |
336 | 583 | // without actually trying to upload something and getting a types.ManifestTypeRejectedError. |
337 | 584 | // So, try the preferred manifest MIME type. If the process succeeds, fine… |
338 | manifestBytes, err = ic.copyUpdatedConfigAndManifest(ctx) | |
585 | manifestBytes, retManifestDigest, err := ic.copyUpdatedConfigAndManifest(ctx, targetInstance) | |
586 | retManifestType = preferredManifestMIMEType | |
339 | 587 | if err != nil { |
340 | 588 | logrus.Debugf("Writing manifest using preferred type %s failed: %v", preferredManifestMIMEType, err) |
341 | 589 | // … if it fails, _and_ the failure is because the manifest is rejected, we may have other options. |
343 | 591 | // We don’t have other options. |
344 | 592 | // In principle the code below would handle this as well, but the resulting error message is fairly ugly. |
345 | 593 | // Don’t bother the user with MIME types if we have no choice. |
346 | return nil, err | |
594 | return nil, "", "", err | |
347 | 595 | } |
348 | 596 | // If the original MIME type is acceptable, determineManifestConversion always uses it as preferredManifestMIMEType. |
349 | 597 | // So if we are here, we will definitely be trying to convert the manifest. |
350 | 598 | // With !ic.canModifyManifest, that would just be a string of repeated failures for the same reason, |
351 | 599 | // so let’s bail out early and with a better error message. |
352 | 600 | if !ic.canModifyManifest { |
353 | return nil, errors.Wrap(err, "Writing manifest failed (and converting it is not possible)") | |
601 | return nil, "", "", errors.Wrap(err, "Writing manifest failed (and converting it is not possible)") | |
354 | 602 | } |
355 | 603 | |
356 | 604 | // errs is a list of errors when trying various manifest types. Also serves as an "upload succeeded" flag when set to nil. |
358 | 606 | for _, manifestMIMEType := range otherManifestMIMETypeCandidates { |
359 | 607 | logrus.Debugf("Trying to use manifest type %s…", manifestMIMEType) |
360 | 608 | ic.manifestUpdates.ManifestMIMEType = manifestMIMEType |
361 | attemptedManifest, err := ic.copyUpdatedConfigAndManifest(ctx) | |
609 | attemptedManifest, attemptedManifestDigest, err := ic.copyUpdatedConfigAndManifest(ctx, targetInstance) | |
362 | 610 | if err != nil { |
363 | 611 | logrus.Debugf("Upload of manifest type %s failed: %v", manifestMIMEType, err) |
364 | 612 | errs = append(errs, fmt.Sprintf("%s(%v)", manifestMIMEType, err)) |
367 | 615 | |
368 | 616 | // We have successfully uploaded a manifest. |
369 | 617 | manifestBytes = attemptedManifest |
618 | retManifestDigest = attemptedManifestDigest | |
619 | retManifestType = manifestMIMEType | |
370 | 620 | errs = nil // Mark this as a success so that we don't abort below. |
371 | 621 | break |
372 | 622 | } |
373 | 623 | if errs != nil { |
374 | return nil, fmt.Errorf("Uploading manifest failed, attempted the following formats: %s", strings.Join(errs, ", ")) | |
624 | return nil, "", "", fmt.Errorf("Uploading manifest failed, attempted the following formats: %s", strings.Join(errs, ", ")) | |
375 | 625 | } |
376 | 626 | } |
377 | 627 | |
378 | 628 | if options.SignBy != "" { |
379 | 629 | newSig, err := c.createSignature(manifestBytes, options.SignBy) |
380 | 630 | if err != nil { |
381 | return nil, err | |
631 | return nil, "", "", err | |
382 | 632 | } |
383 | 633 | sigs = append(sigs, newSig) |
384 | 634 | } |
385 | 635 | |
386 | 636 | c.Printf("Storing signatures\n") |
387 | if err := c.dest.PutSignatures(ctx, sigs); err != nil { | |
388 | return nil, errors.Wrap(err, "Error writing signatures") | |
389 | } | |
390 | ||
391 | return manifestBytes, nil | |
637 | if err := c.dest.PutSignatures(ctx, sigs, targetInstance); err != nil { | |
638 | return nil, "", "", errors.Wrap(err, "Error writing signatures") | |
639 | } | |
640 | ||
641 | return manifestBytes, retManifestType, retManifestDigest, nil | |
392 | 642 | } |
393 | 643 | |
394 | 644 | // Printf writes a formatted string to c.reportWriter. |
553 | 803 | } |
554 | 804 | |
555 | 805 | // copyUpdatedConfigAndManifest updates the image per ic.manifestUpdates, if necessary, |
556 | // stores the resulting config and manifest to the destination, and returns the stored manifest. | |
557 | func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context) ([]byte, error) { | |
806 | // stores the resulting config and manifest to the destination, and returns the stored manifest | |
807 | // and its digest. | |
808 | func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, digest.Digest, error) { | |
558 | 809 | pendingImage := ic.src |
559 | 810 | if !reflect.DeepEqual(*ic.manifestUpdates, types.ManifestUpdateOptions{InformationOnly: ic.manifestUpdates.InformationOnly}) { |
560 | 811 | if !ic.canModifyManifest { |
561 | return nil, errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden") | |
812 | return nil, "", errors.Errorf("Internal error: copy needs an updated manifest but that was known to be forbidden") | |
562 | 813 | } |
563 | 814 | if !ic.diffIDsAreNeeded && ic.src.UpdatedImageNeedsLayerDiffIDs(*ic.manifestUpdates) { |
564 | 815 | // We have set ic.diffIDsAreNeeded based on the preferred MIME type returned by determineManifestConversion. |
567 | 818 | // when ic.c.dest.SupportedManifestMIMETypes() includes both s1 and s2, the upload using s1 failed, and we are now trying s2. |
568 | 819 | // Supposedly s2-only registries do not exist or are extremely rare, so failing with this error message is good enough for now. |
569 | 820 | // If handling such registries turns out to be necessary, we could compute ic.diffIDsAreNeeded based on the full list of manifest MIME type candidates. |
570 | return nil, errors.Errorf("Can not convert image to %s, preparing DiffIDs for this case is not supported", ic.manifestUpdates.ManifestMIMEType) | |
821 | return nil, "", errors.Errorf("Can not convert image to %s, preparing DiffIDs for this case is not supported", ic.manifestUpdates.ManifestMIMEType) | |
571 | 822 | } |
572 | 823 | pi, err := ic.src.UpdatedImage(ctx, *ic.manifestUpdates) |
573 | 824 | if err != nil { |
574 | return nil, errors.Wrap(err, "Error creating an updated image manifest") | |
825 | return nil, "", errors.Wrap(err, "Error creating an updated image manifest") | |
575 | 826 | } |
576 | 827 | pendingImage = pi |
577 | 828 | } |
578 | manifest, _, err := pendingImage.Manifest(ctx) | |
579 | if err != nil { | |
580 | return nil, errors.Wrap(err, "Error reading manifest") | |
829 | man, _, err := pendingImage.Manifest(ctx) | |
830 | if err != nil { | |
831 | return nil, "", errors.Wrap(err, "Error reading manifest") | |
581 | 832 | } |
582 | 833 | |
583 | 834 | if err := ic.c.copyConfig(ctx, pendingImage); err != nil { |
584 | return nil, err | |
835 | return nil, "", err | |
585 | 836 | } |
586 | 837 | |
587 | 838 | ic.c.Printf("Writing manifest to image destination\n") |
588 | if err := ic.c.dest.PutManifest(ctx, manifest); err != nil { | |
589 | return nil, errors.Wrap(err, "Error writing manifest") | |
590 | } | |
591 | return manifest, nil | |
839 | manifestDigest, err := manifest.Digest(man) | |
840 | if err != nil { | |
841 | return nil, "", err | |
842 | } | |
843 | if instanceDigest != nil { | |
844 | instanceDigest = &manifestDigest | |
845 | } | |
846 | if err := ic.c.dest.PutManifest(ctx, man, instanceDigest); err != nil { | |
847 | return nil, "", errors.Wrap(err, "Error writing manifest") | |
848 | } | |
849 | return man, manifestDigest, nil | |
592 | 850 | } |
593 | 851 | |
594 | 852 | // newProgressPool creates a *mpb.Progress and a cleanup function. |
118 | 118 | } |
119 | 119 | return manifest.MIMETypeIsMultiImage(mt), nil |
120 | 120 | } |
121 | ||
122 | // determineListConversion takes the current MIME type of a list of manifests, | |
123 | // the list of MIME types supported for a given destination, and a possible | |
124 | // forced value, and returns the MIME type to which we should convert the list | |
125 | // of manifests, whether we are converting to it or using it unmodified. | |
126 | func (c *copier) determineListConversion(currentListMIMEType string, destSupportedMIMETypes []string, forcedListMIMEType string) (string, error) { | |
127 | // If we're forcing it, we prefer the forced value over everything else. | |
128 | if forcedListMIMEType != "" { | |
129 | return forcedListMIMEType, nil | |
130 | } | |
131 | // If there's no list of supported types, then anything we support is expected to be supported. | |
132 | if len(destSupportedMIMETypes) == 0 { | |
133 | destSupportedMIMETypes = manifest.SupportedListMIMETypes | |
134 | } | |
135 | var selectedType string | |
136 | for i := range destSupportedMIMETypes { | |
137 | // The second priority is the first member of the list of acceptable types that is a list, | |
138 | // but keep going in case current type occurs later in the list. | |
139 | if selectedType == "" && manifest.MIMETypeIsMultiImage(destSupportedMIMETypes[i]) { | |
140 | selectedType = destSupportedMIMETypes[i] | |
141 | } | |
142 | // The first priority is the current type, if it's in the list, since that lets us avoid a | |
143 | // conversion that isn't strictly necessary. | |
144 | if destSupportedMIMETypes[i] == currentListMIMEType { | |
145 | selectedType = destSupportedMIMETypes[i] | |
146 | } | |
147 | } | |
148 | if selectedType == "" { | |
149 | return "", errors.Errorf("destination does not support any supported manifest list types (%v)", manifest.SupportedListMIMETypes) | |
150 | } | |
151 | // Done. | |
152 | return selectedType, nil | |
153 | } |
202 | 202 | }{ |
203 | 203 | {manifest.DockerV2ListMediaType, true}, |
204 | 204 | {manifest.DockerV2Schema2MediaType, false}, |
205 | {v1.MediaTypeImageManifest, false}, | |
206 | {v1.MediaTypeImageIndex, true}, | |
205 | 207 | } { |
206 | 208 | src := fakeImageSource(c.mt) |
207 | 209 | res, err := isMultiImage(context.Background(), src) |
198 | 198 | } |
199 | 199 | |
200 | 200 | // PutManifest writes manifest to the destination. |
201 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write the manifest for (when | |
202 | // the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
203 | // It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated | |
204 | // by `manifest.Digest()`. | |
201 | 205 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
202 | 206 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
203 | 207 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
204 | func (d *dirImageDestination) PutManifest(ctx context.Context, manifest []byte) error { | |
205 | return ioutil.WriteFile(d.ref.manifestPath(), manifest, 0644) | |
206 | } | |
207 | ||
208 | func (d *dirImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
208 | func (d *dirImageDestination) PutManifest(ctx context.Context, manifest []byte, instanceDigest *digest.Digest) error { | |
209 | return ioutil.WriteFile(d.ref.manifestPath(instanceDigest), manifest, 0644) | |
210 | } | |
211 | ||
212 | // PutSignatures writes a set of signatures to the destination. | |
213 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for | |
214 | // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
215 | func (d *dirImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
209 | 216 | for i, sig := range signatures { |
210 | if err := ioutil.WriteFile(d.ref.signaturePath(i), sig, 0644); err != nil { | |
217 | if err := ioutil.WriteFile(d.ref.signaturePath(i, instanceDigest), sig, 0644); err != nil { | |
211 | 218 | return err |
212 | 219 | } |
213 | 220 | } |
218 | 225 | // WARNING: This does not have any transactional semantics: |
219 | 226 | // - Uploaded data MAY be visible to others before Commit() is called |
220 | 227 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
221 | func (d *dirImageDestination) Commit(ctx context.Context) error { | |
228 | func (d *dirImageDestination) Commit(context.Context, types.UnparsedImage) error { | |
222 | 229 | return nil |
223 | 230 | } |
224 | 231 |
8 | 8 | "github.com/containers/image/v4/manifest" |
9 | 9 | "github.com/containers/image/v4/types" |
10 | 10 | "github.com/opencontainers/go-digest" |
11 | "github.com/pkg/errors" | |
12 | 11 | ) |
13 | 12 | |
14 | 13 | type dirImageSource struct { |
37 | 36 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list); |
38 | 37 | // this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists). |
39 | 38 | func (s *dirImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { |
40 | if instanceDigest != nil { | |
41 | return nil, "", errors.Errorf(`Getting target manifest not supported by "dir:"`) | |
42 | } | |
43 | m, err := ioutil.ReadFile(s.ref.manifestPath()) | |
39 | m, err := ioutil.ReadFile(s.ref.manifestPath(instanceDigest)) | |
44 | 40 | if err != nil { |
45 | 41 | return nil, "", err |
46 | 42 | } |
72 | 68 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list |
73 | 69 | // (e.g. if the source never returns manifest lists). |
74 | 70 | func (s *dirImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { |
75 | if instanceDigest != nil { | |
76 | return nil, errors.Errorf(`Manifests lists are not supported by "dir:"`) | |
77 | } | |
78 | 71 | signatures := [][]byte{} |
79 | 72 | for i := 0; ; i++ { |
80 | signature, err := ioutil.ReadFile(s.ref.signaturePath(i)) | |
73 | signature, err := ioutil.ReadFile(s.ref.signaturePath(i, instanceDigest)) | |
81 | 74 | if err != nil { |
82 | 75 | if os.IsNotExist(err) { |
83 | 76 | break |
89 | 82 | return signatures, nil |
90 | 83 | } |
91 | 84 | |
92 | // LayerInfosForCopy() returns updated layer info that should be used when copying, in preference to values in the manifest, if specified. | |
93 | func (s *dirImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
85 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
86 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
87 | // to read the image's layers. | |
88 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
89 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
90 | // (e.g. if the source never returns manifest lists). | |
91 | // The Digest field is guaranteed to be provided; Size may be -1. | |
92 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
93 | func (s *dirImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
94 | 94 | return nil, nil |
95 | 95 | } |
31 | 31 | defer os.RemoveAll(tmpDir) |
32 | 32 | |
33 | 33 | man := []byte("test-manifest") |
34 | dest, err := ref.NewImageDestination(context.Background(), nil) | |
35 | require.NoError(t, err) | |
36 | defer dest.Close() | |
37 | err = dest.PutManifest(context.Background(), man) | |
38 | assert.NoError(t, err) | |
39 | err = dest.Commit(context.Background()) | |
34 | list := []byte("test-manifest-list") | |
35 | md, err := manifest.Digest(man) | |
36 | require.NoError(t, err) | |
37 | dest, err := ref.NewImageDestination(context.Background(), nil) | |
38 | require.NoError(t, err) | |
39 | defer dest.Close() | |
40 | err = dest.PutManifest(context.Background(), man, &md) | |
41 | assert.NoError(t, err) | |
42 | err = dest.PutManifest(context.Background(), list, nil) | |
43 | assert.NoError(t, err) | |
44 | err = dest.Commit(context.Background(), nil) | |
40 | 45 | assert.NoError(t, err) |
41 | 46 | |
42 | 47 | src, err := ref.NewImageSource(context.Background(), nil) |
43 | 48 | require.NoError(t, err) |
44 | 49 | defer src.Close() |
45 | 50 | m, mt, err := src.GetManifest(context.Background(), nil) |
51 | assert.NoError(t, err) | |
52 | assert.Equal(t, list, m) | |
53 | assert.Equal(t, "", mt) | |
54 | ||
55 | m, mt, err = src.GetManifest(context.Background(), &md) | |
46 | 56 | assert.NoError(t, err) |
47 | 57 | assert.Equal(t, man, m) |
48 | 58 | assert.Equal(t, "", mt) |
49 | ||
50 | // Non-default instances are not supported | |
51 | md, err := manifest.Digest(man) | |
52 | require.NoError(t, err) | |
53 | _, _, err = src.GetManifest(context.Background(), &md) | |
54 | assert.Error(t, err) | |
55 | 59 | } |
56 | 60 | |
57 | 61 | func TestGetPutBlob(t *testing.T) { |
66 | 70 | assert.Equal(t, types.PreserveOriginal, dest.DesiredLayerCompression()) |
67 | 71 | info, err := dest.PutBlob(context.Background(), bytes.NewReader(blob), types.BlobInfo{Digest: digest.Digest("sha256:digest-test"), Size: int64(9)}, cache, false) |
68 | 72 | assert.NoError(t, err) |
69 | err = dest.Commit(context.Background()) | |
73 | err = dest.Commit(context.Background(), nil) | |
70 | 74 | assert.NoError(t, err) |
71 | 75 | assert.Equal(t, int64(9), info.Size) |
72 | 76 | assert.Equal(t, digest.FromBytes(blob), info.Digest) |
125 | 129 | _, err = dest.PutBlob(context.Background(), reader, types.BlobInfo{Digest: blobDigest, Size: -1}, cache, false) |
126 | 130 | assert.Error(t, err) |
127 | 131 | assert.Contains(t, digestErrorString, err.Error()) |
128 | err = dest.Commit(context.Background()) | |
132 | err = dest.Commit(context.Background(), nil) | |
129 | 133 | assert.NoError(t, err) |
130 | 134 | |
131 | 135 | _, err = os.Lstat(blobPath) |
147 | 151 | } |
148 | 152 | err = dest.SupportsSignatures(context.Background()) |
149 | 153 | assert.NoError(t, err) |
150 | err = dest.PutManifest(context.Background(), man) | |
151 | require.NoError(t, err) | |
152 | ||
153 | err = dest.PutSignatures(context.Background(), signatures) | |
154 | assert.NoError(t, err) | |
155 | err = dest.Commit(context.Background()) | |
154 | err = dest.PutManifest(context.Background(), man, nil) | |
155 | require.NoError(t, err) | |
156 | ||
157 | err = dest.PutSignatures(context.Background(), signatures, nil) | |
158 | listSignatures := [][]byte{ | |
159 | []byte("sig3"), | |
160 | []byte("sig4"), | |
161 | } | |
162 | md, err := manifest.Digest(man) | |
163 | require.NoError(t, err) | |
164 | ||
165 | err = dest.SupportsSignatures(context.Background()) | |
166 | assert.NoError(t, err) | |
167 | err = dest.PutManifest(context.Background(), man, nil) | |
168 | require.NoError(t, err) | |
169 | err = dest.PutManifest(context.Background(), man, &md) | |
170 | require.NoError(t, err) | |
171 | ||
172 | err = dest.PutSignatures(context.Background(), listSignatures, nil) | |
173 | assert.NoError(t, err) | |
174 | err = dest.PutSignatures(context.Background(), signatures, &md) | |
175 | assert.NoError(t, err) | |
176 | err = dest.Commit(context.Background(), nil) | |
156 | 177 | assert.NoError(t, err) |
157 | 178 | |
158 | 179 | src, err := ref.NewImageSource(context.Background(), nil) |
160 | 181 | defer src.Close() |
161 | 182 | sigs, err := src.GetSignatures(context.Background(), nil) |
162 | 183 | assert.NoError(t, err) |
184 | assert.Equal(t, listSignatures, sigs) | |
185 | ||
186 | sigs, err = src.GetSignatures(context.Background(), &md) | |
187 | assert.NoError(t, err) | |
163 | 188 | assert.Equal(t, signatures, sigs) |
164 | ||
165 | // Non-default instances are not supported | |
166 | md, err := manifest.Digest(man) | |
167 | require.NoError(t, err) | |
168 | _, err = src.GetSignatures(context.Background(), &md) | |
169 | assert.Error(t, err) | |
170 | 189 | } |
171 | 190 | |
172 | 191 | func TestSourceReference(t *testing.T) { |
165 | 165 | } |
166 | 166 | |
167 | 167 | // manifestPath returns a path for the manifest within a directory using our conventions. |
168 | func (ref dirReference) manifestPath() string { | |
168 | func (ref dirReference) manifestPath(instanceDigest *digest.Digest) string { | |
169 | if instanceDigest != nil { | |
170 | return filepath.Join(ref.path, instanceDigest.Encoded()+".manifest.json") | |
171 | } | |
169 | 172 | return filepath.Join(ref.path, "manifest.json") |
170 | 173 | } |
171 | 174 | |
172 | 175 | // layerPath returns a path for a layer tarball within a directory using our conventions. |
173 | 176 | func (ref dirReference) layerPath(digest digest.Digest) string { |
174 | 177 | // FIXME: Should we keep the digest identification? |
175 | return filepath.Join(ref.path, digest.Hex()) | |
178 | return filepath.Join(ref.path, digest.Encoded()) | |
176 | 179 | } |
177 | 180 | |
178 | 181 | // signaturePath returns a path for a signature within a directory using our conventions. |
179 | func (ref dirReference) signaturePath(index int) string { | |
182 | func (ref dirReference) signaturePath(index int, instanceDigest *digest.Digest) string { | |
183 | if instanceDigest != nil { | |
184 | return filepath.Join(ref.path, fmt.Sprintf(instanceDigest.Encoded()+".signature-%d", index+1)) | |
185 | } | |
180 | 186 | return filepath.Join(ref.path, fmt.Sprintf("signature-%d", index+1)) |
181 | 187 | } |
182 | 188 |
8 | 8 | |
9 | 9 | _ "github.com/containers/image/v4/internal/testing/explicitfilepath-tmpdir" |
10 | 10 | "github.com/containers/image/v4/types" |
11 | digest "github.com/opencontainers/go-digest" | |
11 | 12 | "github.com/stretchr/testify/assert" |
12 | 13 | "github.com/stretchr/testify/require" |
13 | 14 | ) |
156 | 157 | defer dest.Close() |
157 | 158 | mFixture, err := ioutil.ReadFile("../manifest/fixtures/v2s1.manifest.json") |
158 | 159 | require.NoError(t, err) |
159 | err = dest.PutManifest(context.Background(), mFixture) | |
160 | assert.NoError(t, err) | |
161 | err = dest.Commit(context.Background()) | |
160 | err = dest.PutManifest(context.Background(), mFixture, nil) | |
161 | assert.NoError(t, err) | |
162 | err = dest.Commit(context.Background(), nil) | |
162 | 163 | assert.NoError(t, err) |
163 | 164 | |
164 | 165 | img, err := ref.NewImage(context.Background(), nil) |
173 | 174 | dest, err := ref.NewImageDestination(context.Background(), nil) |
174 | 175 | require.NoError(t, err) |
175 | 176 | defer dest.Close() |
176 | err = dest.PutManifest(context.Background(), []byte(`{"schemaVersion":1}`)) | |
177 | assert.NoError(t, err) | |
178 | err = dest.Commit(context.Background()) | |
177 | err = dest.PutManifest(context.Background(), []byte(`{"schemaVersion":1}`), nil) | |
178 | assert.NoError(t, err) | |
179 | err = dest.Commit(context.Background(), nil) | |
179 | 180 | assert.NoError(t, err) |
180 | 181 | |
181 | 182 | _, err = ref.NewImage(context.Background(), nil) |
206 | 207 | } |
207 | 208 | |
208 | 209 | func TestReferenceManifestPath(t *testing.T) { |
209 | ref, tmpDir := refToTempDir(t) | |
210 | defer os.RemoveAll(tmpDir) | |
211 | dirRef, ok := ref.(dirReference) | |
212 | require.True(t, ok) | |
213 | assert.Equal(t, tmpDir+"/manifest.json", dirRef.manifestPath()) | |
210 | dhex := digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") | |
211 | ||
212 | ref, tmpDir := refToTempDir(t) | |
213 | defer os.RemoveAll(tmpDir) | |
214 | dirRef, ok := ref.(dirReference) | |
215 | require.True(t, ok) | |
216 | assert.Equal(t, tmpDir+"/manifest.json", dirRef.manifestPath(nil)) | |
217 | assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".manifest.json", dirRef.manifestPath(&dhex)) | |
214 | 218 | } |
215 | 219 | |
216 | 220 | func TestReferenceLayerPath(t *testing.T) { |
224 | 228 | } |
225 | 229 | |
226 | 230 | func TestReferenceSignaturePath(t *testing.T) { |
227 | ref, tmpDir := refToTempDir(t) | |
228 | defer os.RemoveAll(tmpDir) | |
229 | dirRef, ok := ref.(dirReference) | |
230 | require.True(t, ok) | |
231 | assert.Equal(t, tmpDir+"/signature-1", dirRef.signaturePath(0)) | |
232 | assert.Equal(t, tmpDir+"/signature-10", dirRef.signaturePath(9)) | |
231 | dhex := digest.Digest("sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") | |
232 | ||
233 | ref, tmpDir := refToTempDir(t) | |
234 | defer os.RemoveAll(tmpDir) | |
235 | dirRef, ok := ref.(dirReference) | |
236 | require.True(t, ok) | |
237 | assert.Equal(t, tmpDir+"/signature-1", dirRef.signaturePath(0, nil)) | |
238 | assert.Equal(t, tmpDir+"/signature-10", dirRef.signaturePath(9, nil)) | |
239 | assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".signature-1", dirRef.signaturePath(0, &dhex)) | |
240 | assert.Equal(t, tmpDir+"/"+dhex.Encoded()+".signature-10", dirRef.signaturePath(9, &dhex)) | |
233 | 241 | } |
234 | 242 | |
235 | 243 | func TestReferenceVersionPath(t *testing.T) { |
66 | 66 | // WARNING: This does not have any transactional semantics: |
67 | 67 | // - Uploaded data MAY be visible to others before Commit() is called |
68 | 68 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
69 | func (d *archiveImageDestination) Commit(ctx context.Context) error { | |
69 | func (d *archiveImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { | |
70 | 70 | return d.Destination.Commit(ctx) |
71 | 71 | } |
32 | 32 | func (s *archiveImageSource) Reference() types.ImageReference { |
33 | 33 | return s.ref |
34 | 34 | } |
35 | ||
36 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
37 | func (s *archiveImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
38 | return nil, nil | |
39 | } |
123 | 123 | // WARNING: This does not have any transactional semantics: |
124 | 124 | // - Uploaded data MAY be visible to others before Commit() is called |
125 | 125 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
126 | func (d *daemonImageDestination) Commit(ctx context.Context) error { | |
126 | func (d *daemonImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { | |
127 | 127 | logrus.Debugf("docker-daemon: Closing tar stream") |
128 | 128 | if err := d.Destination.Commit(ctx); err != nil { |
129 | 129 | return err |
54 | 54 | func (s *daemonImageSource) Reference() types.ImageReference { |
55 | 55 | return s.ref |
56 | 56 | } |
57 | ||
58 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
59 | func (s *daemonImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
60 | return nil, nil | |
61 | } |
60 | 60 | return []string{ |
61 | 61 | imgspecv1.MediaTypeImageManifest, |
62 | 62 | manifest.DockerV2Schema2MediaType, |
63 | imgspecv1.MediaTypeImageIndex, | |
64 | manifest.DockerV2ListMediaType, | |
63 | 65 | manifest.DockerV2Schema1SignedMediaType, |
64 | 66 | manifest.DockerV2Schema1MediaType, |
65 | 67 | } |
342 | 344 | } |
343 | 345 | |
344 | 346 | // PutManifest writes manifest to the destination. |
347 | // When the primary manifest is a manifest list, if instanceDigest is nil, we're saving the list | |
348 | // itself, else instanceDigest contains a digest of the specific manifest instance to overwrite the | |
349 | // manifest for; when the primary manifest is not a manifest list, instanceDigest should always be nil. | |
345 | 350 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
346 | 351 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
347 | 352 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
348 | func (d *dockerImageDestination) PutManifest(ctx context.Context, m []byte) error { | |
349 | digest, err := manifest.Digest(m) | |
350 | if err != nil { | |
351 | return err | |
352 | } | |
353 | d.manifestDigest = digest | |
354 | ||
355 | refTail, err := d.ref.tagOrDigest() | |
356 | if err != nil { | |
357 | return err | |
358 | } | |
353 | func (d *dockerImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { | |
354 | refTail := "" | |
355 | if instanceDigest != nil { | |
356 | // If the instanceDigest is provided, then use it as the refTail, because the reference, | |
357 | // whether it includes a tag or a digest, refers to the list as a whole, and not this | |
358 | // particular instance. | |
359 | refTail = instanceDigest.String() | |
360 | // Double-check that the manifest we've been given matches the digest we've been given. | |
361 | matches, err := manifest.MatchesDigest(m, *instanceDigest) | |
362 | if err != nil { | |
363 | return errors.Wrapf(err, "error digesting manifest in PutManifest") | |
364 | } | |
365 | if !matches { | |
366 | manifestDigest, merr := manifest.Digest(m) | |
367 | if merr != nil { | |
368 | return errors.Wrapf(err, "Attempted to PutManifest using an explicitly specified digest (%q) that didn't match the manifest's digest (%v attempting to compute it)", instanceDigest.String(), merr) | |
369 | } | |
370 | return errors.Errorf("Attempted to PutManifest using an explicitly specified digest (%q) that didn't match the manifest's digest (%q)", instanceDigest.String(), manifestDigest.String()) | |
371 | } | |
372 | } else { | |
373 | // Compute the digest of the main manifest, or the list if it's a list, so that we | |
374 | // have a digest value to use if we're asked to save a signature for the manifest. | |
375 | digest, err := manifest.Digest(m) | |
376 | if err != nil { | |
377 | return err | |
378 | } | |
379 | d.manifestDigest = digest | |
380 | // The refTail should be either a digest (which we expect to match the value we just | |
381 | // computed) or a tag name. | |
382 | refTail, err = d.ref.tagOrDigest() | |
383 | if err != nil { | |
384 | return err | |
385 | } | |
386 | } | |
387 | ||
359 | 388 | path := fmt.Sprintf(manifestPath, reference.Path(d.ref.ref), refTail) |
360 | 389 | |
361 | 390 | headers := map[string][]string{} |
415 | 444 | } |
416 | 445 | } |
417 | 446 | |
418 | func (d *dockerImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
447 | // PutSignatures uploads a set of signatures to the relevant lookaside or API extension point. | |
448 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to upload the signatures for (when | |
449 | // the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
450 | func (d *dockerImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
419 | 451 | // Do not fail if we don’t really need to support signatures. |
420 | 452 | if len(signatures) == 0 { |
421 | 453 | return nil |
422 | 454 | } |
455 | if instanceDigest == nil { | |
456 | if d.manifestDigest.String() == "" { | |
457 | // This shouldn’t happen, ImageDestination users are required to call PutManifest before PutSignatures | |
458 | return errors.Errorf("Unknown manifest digest, can't add signatures") | |
459 | } | |
460 | instanceDigest = &d.manifestDigest | |
461 | } | |
462 | ||
423 | 463 | if err := d.c.detectProperties(ctx); err != nil { |
424 | 464 | return err |
425 | 465 | } |
426 | 466 | switch { |
427 | 467 | case d.c.signatureBase != nil: |
428 | return d.putSignaturesToLookaside(signatures) | |
468 | return d.putSignaturesToLookaside(signatures, instanceDigest) | |
429 | 469 | case d.c.supportsSignatures: |
430 | return d.putSignaturesToAPIExtension(ctx, signatures) | |
470 | return d.putSignaturesToAPIExtension(ctx, signatures, instanceDigest) | |
431 | 471 | default: |
432 | 472 | return errors.Errorf("X-Registry-Supports-Signatures extension not supported, and lookaside is not configured") |
433 | 473 | } |
435 | 475 | |
436 | 476 | // putSignaturesToLookaside implements PutSignatures() from the lookaside location configured in s.c.signatureBase, |
437 | 477 | // which is not nil. |
438 | func (d *dockerImageDestination) putSignaturesToLookaside(signatures [][]byte) error { | |
478 | func (d *dockerImageDestination) putSignaturesToLookaside(signatures [][]byte, instanceDigest *digest.Digest) error { | |
439 | 479 | // FIXME? This overwrites files one at a time, definitely not atomic. |
440 | 480 | // A failure when updating signatures with a reordered copy could lose some of them. |
441 | 481 | |
444 | 484 | return nil |
445 | 485 | } |
446 | 486 | |
447 | if d.manifestDigest.String() == "" { | |
448 | // This shouldn’t happen, ImageDestination users are required to call PutManifest before PutSignatures | |
449 | return errors.Errorf("Unknown manifest digest, can't add signatures") | |
450 | } | |
451 | ||
452 | 487 | // NOTE: Keep this in sync with docs/signature-protocols.md! |
453 | 488 | for i, signature := range signatures { |
454 | url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i) | |
489 | url := signatureStorageURL(d.c.signatureBase, *instanceDigest, i) | |
455 | 490 | if url == nil { |
456 | 491 | return errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") |
457 | 492 | } |
466 | 501 | // is enough for dockerImageSource to stop looking for other signatures, so that |
467 | 502 | // is sufficient. |
468 | 503 | for i := len(signatures); ; i++ { |
469 | url := signatureStorageURL(d.c.signatureBase, d.manifestDigest, i) | |
504 | url := signatureStorageURL(d.c.signatureBase, *instanceDigest, i) | |
470 | 505 | if url == nil { |
471 | 506 | return errors.Errorf("Internal error: signatureStorageURL with non-nil base returned nil") |
472 | 507 | } |
526 | 561 | } |
527 | 562 | |
528 | 563 | // putSignaturesToAPIExtension implements PutSignatures() using the X-Registry-Supports-Signatures API extension. |
529 | func (d *dockerImageDestination) putSignaturesToAPIExtension(ctx context.Context, signatures [][]byte) error { | |
564 | func (d *dockerImageDestination) putSignaturesToAPIExtension(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
530 | 565 | // Skip dealing with the manifest digest, or reading the old state, if not necessary. |
531 | 566 | if len(signatures) == 0 { |
532 | 567 | return nil |
533 | 568 | } |
534 | 569 | |
535 | if d.manifestDigest.String() == "" { | |
536 | // This shouldn’t happen, ImageDestination users are required to call PutManifest before PutSignatures | |
537 | return errors.Errorf("Unknown manifest digest, can't add signatures") | |
538 | } | |
539 | ||
540 | 570 | // Because image signatures are a shared resource in Atomic Registry, the default upload |
541 | 571 | // always adds signatures. Eventually we should also allow removing signatures, |
542 | 572 | // but the X-Registry-Supports-Signatures API extension does not support that yet. |
543 | 573 | |
544 | existingSignatures, err := d.c.getExtensionsSignatures(ctx, d.ref, d.manifestDigest) | |
574 | existingSignatures, err := d.c.getExtensionsSignatures(ctx, d.ref, *instanceDigest) | |
545 | 575 | if err != nil { |
546 | 576 | return err |
547 | 577 | } |
566 | 596 | if err != nil || n != 16 { |
567 | 597 | return errors.Wrapf(err, "Error generating random signature len %d", n) |
568 | 598 | } |
569 | signatureName = fmt.Sprintf("%s@%032x", d.manifestDigest.String(), randBytes) | |
599 | signatureName = fmt.Sprintf("%s@%032x", instanceDigest.String(), randBytes) | |
570 | 600 | if _, ok := existingSigNames[signatureName]; !ok { |
571 | 601 | break |
572 | 602 | } |
605 | 635 | // WARNING: This does not have any transactional semantics: |
606 | 636 | // - Uploaded data MAY be visible to others before Commit() is called |
607 | 637 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
608 | func (d *dockerImageDestination) Commit(ctx context.Context) error { | |
638 | func (d *dockerImageDestination) Commit(context.Context, types.UnparsedImage) error { | |
609 | 639 | return nil |
610 | 640 | } |
102 | 102 | return nil |
103 | 103 | } |
104 | 104 | |
105 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
106 | func (s *dockerImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
105 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
106 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
107 | // to read the image's layers. | |
108 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
109 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
110 | // (e.g. if the source never returns manifest lists). | |
111 | // The Digest field is guaranteed to be provided; Size may be -1. | |
112 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
113 | func (s *dockerImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
107 | 114 | return nil, nil |
108 | 115 | } |
109 | 116 |
194 | 194 | } |
195 | 195 | |
196 | 196 | // PutManifest writes manifest to the destination. |
197 | // The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so | |
198 | // there can be no secondary manifests. | |
197 | 199 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
198 | 200 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
199 | 201 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
200 | func (d *Destination) PutManifest(ctx context.Context, m []byte) error { | |
202 | func (d *Destination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { | |
203 | if instanceDigest != nil { | |
204 | return errors.New(`Manifest lists are not supported for docker tar files`) | |
205 | } | |
201 | 206 | // We do not bother with types.ManifestTypeRejectedError; our .SupportedManifestMIMETypes() above is already providing only one alternative, |
202 | 207 | // so the caller trying a different manifest kind would be pointless. |
203 | 208 | var man manifest.Schema2 |
389 | 394 | return nil |
390 | 395 | } |
391 | 396 | |
392 | // PutSignatures adds the given signatures to the docker tarfile (currently not | |
393 | // supported). MUST be called after PutManifest (signatures reference manifest | |
394 | // contents) | |
395 | func (d *Destination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
397 | // PutSignatures would add the given signatures to the docker tarfile (currently not supported). | |
398 | // The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so | |
399 | // there can be no secondary manifests. MUST be called after PutManifest (signatures reference manifest contents). | |
400 | func (d *Destination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
401 | if instanceDigest != nil { | |
402 | return errors.Errorf(`Manifest lists are not supported for docker tar files`) | |
403 | } | |
396 | 404 | if len(signatures) != 0 { |
397 | 405 | return errors.Errorf("Storing signatures for docker tar files is not supported") |
398 | 406 | } |
348 | 348 | // It may use a remote (= slow) service. |
349 | 349 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list); |
350 | 350 | // this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists). |
351 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
352 | // as the primary manifest can not be a list, so there can be no secondary instances. | |
351 | 353 | func (s *Source) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { |
352 | 354 | if instanceDigest != nil { |
353 | 355 | // How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType. |
354 | return nil, "", errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`) | |
356 | return nil, "", errors.New(`Manifest lists are not supported by "docker-daemon:"`) | |
355 | 357 | } |
356 | 358 | if s.generatedManifest == nil { |
357 | 359 | if err := s.ensureCachedDataIsPresent(); err != nil { |
465 | 467 | } |
466 | 468 | |
467 | 469 | // GetSignatures returns the image's signatures. It may use a remote (= slow) service. |
468 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve signatures for | |
469 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
470 | // (e.g. if the source never returns manifest lists). | |
470 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
471 | // as there can be no secondary manifests. | |
471 | 472 | func (s *Source) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { |
472 | 473 | if instanceDigest != nil { |
473 | 474 | // How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType. |
475 | 476 | } |
476 | 477 | return [][]byte{}, nil |
477 | 478 | } |
479 | ||
480 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
481 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
482 | // to read the image's layers. | |
483 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
484 | // as the primary manifest can not be a list, so there can be no secondary manifests. | |
485 | // The Digest field is guaranteed to be provided; Size may be -1. | |
486 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
487 | func (s *Source) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
488 | return nil, nil | |
489 | } |
1 | 1 | |
2 | 2 | import ( |
3 | 3 | "context" |
4 | "encoding/json" | |
5 | "fmt" | |
6 | "runtime" | |
7 | 4 | |
8 | 5 | "github.com/containers/image/v4/manifest" |
9 | 6 | "github.com/containers/image/v4/types" |
10 | "github.com/opencontainers/go-digest" | |
11 | 7 | "github.com/pkg/errors" |
12 | 8 | ) |
13 | 9 | |
14 | type platformSpec struct { | |
15 | Architecture string `json:"architecture"` | |
16 | OS string `json:"os"` | |
17 | OSVersion string `json:"os.version,omitempty"` | |
18 | OSFeatures []string `json:"os.features,omitempty"` | |
19 | Variant string `json:"variant,omitempty"` | |
20 | Features []string `json:"features,omitempty"` // removed in OCI | |
21 | } | |
22 | ||
23 | // A manifestDescriptor references a platform-specific manifest. | |
24 | type manifestDescriptor struct { | |
25 | manifest.Schema2Descriptor | |
26 | Platform platformSpec `json:"platform"` | |
27 | } | |
28 | ||
29 | type manifestList struct { | |
30 | SchemaVersion int `json:"schemaVersion"` | |
31 | MediaType string `json:"mediaType"` | |
32 | Manifests []manifestDescriptor `json:"manifests"` | |
33 | } | |
34 | ||
35 | // chooseDigestFromManifestList parses blob as a schema2 manifest list, | |
36 | // and returns the digest of the image appropriate for the current environment. | |
37 | func chooseDigestFromManifestList(sys *types.SystemContext, blob []byte) (digest.Digest, error) { | |
38 | wantedArch := runtime.GOARCH | |
39 | if sys != nil && sys.ArchitectureChoice != "" { | |
40 | wantedArch = sys.ArchitectureChoice | |
10 | func manifestSchema2FromManifestList(ctx context.Context, sys *types.SystemContext, src types.ImageSource, manblob []byte) (genericManifest, error) { | |
11 | list, err := manifest.Schema2ListFromManifest(manblob) | |
12 | if err != nil { | |
13 | return nil, errors.Wrapf(err, "Error parsing schema2 manifest list") | |
41 | 14 | } |
42 | wantedOS := runtime.GOOS | |
43 | if sys != nil && sys.OSChoice != "" { | |
44 | wantedOS = sys.OSChoice | |
45 | } | |
46 | ||
47 | list := manifestList{} | |
48 | if err := json.Unmarshal(blob, &list); err != nil { | |
49 | return "", err | |
50 | } | |
51 | for _, d := range list.Manifests { | |
52 | if d.Platform.Architecture == wantedArch && d.Platform.OS == wantedOS { | |
53 | return d.Digest, nil | |
54 | } | |
55 | } | |
56 | return "", fmt.Errorf("no image found in manifest list for architecture %s, OS %s", wantedArch, wantedOS) | |
57 | } | |
58 | ||
59 | func manifestSchema2FromManifestList(ctx context.Context, sys *types.SystemContext, src types.ImageSource, manblob []byte) (genericManifest, error) { | |
60 | targetManifestDigest, err := chooseDigestFromManifestList(sys, manblob) | |
15 | targetManifestDigest, err := list.ChooseInstance(sys) | |
61 | 16 | if err != nil { |
62 | return nil, err | |
17 | return nil, errors.Wrapf(err, "Error choosing image instance") | |
63 | 18 | } |
64 | 19 | manblob, mt, err := src.GetManifest(ctx, &targetManifestDigest) |
65 | 20 | if err != nil { |
66 | return nil, err | |
21 | return nil, errors.Wrapf(err, "Error loading manifest for target platform") | |
67 | 22 | } |
68 | 23 | |
69 | 24 | matches, err := manifest.MatchesDigest(manblob, targetManifestDigest) |
71 | 26 | return nil, errors.Wrap(err, "Error computing manifest digest") |
72 | 27 | } |
73 | 28 | if !matches { |
74 | return nil, errors.Errorf("Manifest image does not match selected manifest digest %s", targetManifestDigest) | |
29 | return nil, errors.Errorf("Image manifest does not match selected manifest digest %s", targetManifestDigest) | |
75 | 30 | } |
76 | 31 | |
77 | 32 | return manifestInstanceFromBlob(ctx, sys, src, manblob, mt) |
78 | 33 | } |
79 | ||
80 | // ChooseManifestInstanceFromManifestList returns a digest of a manifest appropriate | |
81 | // for the current system from the manifest available from src. | |
82 | func ChooseManifestInstanceFromManifestList(ctx context.Context, sys *types.SystemContext, src types.UnparsedImage) (digest.Digest, error) { | |
83 | // For now this only handles manifest.DockerV2ListMediaType; we can generalize it later, | |
84 | // probably along with manifest list editing. | |
85 | blob, mt, err := src.Manifest(ctx) | |
86 | if err != nil { | |
87 | return "", err | |
88 | } | |
89 | if mt != manifest.DockerV2ListMediaType { | |
90 | return "", fmt.Errorf("Internal error: Trying to select an image from a non-manifest-list manifest type %s", mt) | |
91 | } | |
92 | return chooseDigestFromManifestList(sys, blob) | |
93 | } |
0 | package image | |
1 | ||
2 | import ( | |
3 | "bytes" | |
4 | "io/ioutil" | |
5 | "path/filepath" | |
6 | "testing" | |
7 | ||
8 | "github.com/containers/image/v4/types" | |
9 | "github.com/opencontainers/go-digest" | |
10 | "github.com/stretchr/testify/assert" | |
11 | "github.com/stretchr/testify/require" | |
12 | ) | |
13 | ||
14 | func TestChooseDigestFromManifestList(t *testing.T) { | |
15 | manifest, err := ioutil.ReadFile(filepath.Join("fixtures", "schema2list.json")) | |
16 | require.NoError(t, err) | |
17 | ||
18 | // Match found | |
19 | for arch, expected := range map[string]digest.Digest{ | |
20 | "amd64": "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af", | |
21 | "s390x": "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642", | |
22 | // There are several "arm" images with different variants; | |
23 | // the current code returns the first match. NOTE: This is NOT an API promise. | |
24 | "arm": "sha256:9142d97ef280a7953cf1a85716de49a24cc1dd62776352afad67e635331ff77a", | |
25 | } { | |
26 | digest, err := chooseDigestFromManifestList(&types.SystemContext{ | |
27 | ArchitectureChoice: arch, | |
28 | OSChoice: "linux", | |
29 | }, manifest) | |
30 | require.NoError(t, err, arch) | |
31 | assert.Equal(t, expected, digest) | |
32 | } | |
33 | ||
34 | // Invalid manifest list | |
35 | _, err = chooseDigestFromManifestList(&types.SystemContext{ | |
36 | ArchitectureChoice: "amd64", OSChoice: "linux", | |
37 | }, bytes.Join([][]byte{manifest, []byte("!INVALID")}, nil)) | |
38 | assert.Error(t, err) | |
39 | ||
40 | // Not found | |
41 | _, err = chooseDigestFromManifestList(&types.SystemContext{OSChoice: "Unmatched"}, manifest) | |
42 | assert.Error(t, err) | |
43 | } |
40 | 40 | func (f unusedImageSource) GetSignatures(context.Context, *digest.Digest) ([][]byte, error) { |
41 | 41 | panic("Unexpected call to a mock function") |
42 | 42 | } |
43 | func (f unusedImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
43 | func (f unusedImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
44 | 44 | panic("Unexpected call to a mock function") |
45 | 45 | } |
46 | 46 | |
429 | 429 | func (d *memoryImageDest) TryReusingBlob(context.Context, types.BlobInfo, types.BlobInfoCache, bool) (bool, types.BlobInfo, error) { |
430 | 430 | panic("Unexpected call to a mock function") |
431 | 431 | } |
432 | func (d *memoryImageDest) PutManifest(ctx context.Context, m []byte) error { | |
433 | panic("Unexpected call to a mock function") | |
434 | } | |
435 | func (d *memoryImageDest) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
436 | panic("Unexpected call to a mock function") | |
437 | } | |
438 | func (d *memoryImageDest) Commit(ctx context.Context) error { | |
432 | func (d *memoryImageDest) PutManifest(context.Context, []byte, *digest.Digest) error { | |
433 | panic("Unexpected call to a mock function") | |
434 | } | |
435 | func (d *memoryImageDest) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
436 | panic("Unexpected call to a mock function") | |
437 | } | |
438 | func (d *memoryImageDest) Commit(context.Context, types.UnparsedImage) error { | |
439 | 439 | panic("Unexpected call to a mock function") |
440 | 440 | } |
441 | 441 |
0 | { | |
1 | "schemaVersion": 2, | |
2 | "manifests": [ | |
3 | { | |
4 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | |
5 | "size": 7143, | |
6 | "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
7 | "platform": { | |
8 | "architecture": "ppc64le", | |
9 | "os": "linux" | |
10 | } | |
11 | }, | |
12 | { | |
13 | "mediaType": "application/vnd.oci.image.manifest.v1+json", | |
14 | "size": 7682, | |
15 | "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", | |
16 | "platform": { | |
17 | "architecture": "amd64", | |
18 | "os": "linux", | |
19 | "os.features": [ | |
20 | "sse4" | |
21 | ] | |
22 | } | |
23 | } | |
24 | ], | |
25 | "annotations": { | |
26 | "com.example.key1": "value1", | |
27 | "com.example.key2": "value2" | |
28 | } | |
29 | } |
57 | 57 | return manifestSchema2FromManifest(src, manblob) |
58 | 58 | case manifest.DockerV2ListMediaType: |
59 | 59 | return manifestSchema2FromManifestList(ctx, sys, src, manblob) |
60 | case imgspecv1.MediaTypeImageIndex: | |
61 | return manifestOCI1FromImageIndex(ctx, sys, src, manblob) | |
60 | 62 | default: // Note that this may not be reachable, manifest.NormalizedMIMEType has a default for unknown values. |
61 | 63 | return nil, fmt.Errorf("Unimplemented manifest MIME type %s", mt) |
62 | 64 | } |
0 | package image | |
1 | ||
2 | import ( | |
3 | "context" | |
4 | ||
5 | "github.com/containers/image/v4/manifest" | |
6 | "github.com/containers/image/v4/types" | |
7 | "github.com/pkg/errors" | |
8 | ) | |
9 | ||
10 | func manifestOCI1FromImageIndex(ctx context.Context, sys *types.SystemContext, src types.ImageSource, manblob []byte) (genericManifest, error) { | |
11 | index, err := manifest.OCI1IndexFromManifest(manblob) | |
12 | if err != nil { | |
13 | return nil, errors.Wrapf(err, "Error parsing OCI1 index") | |
14 | } | |
15 | targetManifestDigest, err := index.ChooseInstance(sys) | |
16 | if err != nil { | |
17 | return nil, errors.Wrapf(err, "Error choosing image instance") | |
18 | } | |
19 | manblob, mt, err := src.GetManifest(ctx, &targetManifestDigest) | |
20 | if err != nil { | |
21 | return nil, errors.Wrapf(err, "Error loading manifest for target platform") | |
22 | } | |
23 | ||
24 | matches, err := manifest.MatchesDigest(manblob, targetManifestDigest) | |
25 | if err != nil { | |
26 | return nil, errors.Wrap(err, "Error computing manifest digest") | |
27 | } | |
28 | if !matches { | |
29 | return nil, errors.Errorf("Image manifest does not match selected manifest digest %s", targetManifestDigest) | |
30 | } | |
31 | ||
32 | return manifestInstanceFromBlob(ctx, sys, src, manblob, mt) | |
33 | } |
99 | 99 | } |
100 | 100 | |
101 | 101 | func (i *sourcedImage) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { |
102 | return i.UnparsedImage.src.LayerInfosForCopy(ctx) | |
102 | return i.UnparsedImage.src.LayerInfosForCopy(ctx, i.UnparsedImage.instanceDigest) | |
103 | 103 | } |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "fmt" | |
5 | "runtime" | |
6 | ||
7 | "github.com/containers/image/v4/types" | |
8 | "github.com/opencontainers/go-digest" | |
9 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
10 | "github.com/pkg/errors" | |
11 | ) | |
12 | ||
13 | // Schema2PlatformSpec describes the platform which a particular manifest is | |
14 | // specialized for. | |
15 | type Schema2PlatformSpec struct { | |
16 | Architecture string `json:"architecture"` | |
17 | OS string `json:"os"` | |
18 | OSVersion string `json:"os.version,omitempty"` | |
19 | OSFeatures []string `json:"os.features,omitempty"` | |
20 | Variant string `json:"variant,omitempty"` | |
21 | Features []string `json:"features,omitempty"` // removed in OCI | |
22 | } | |
23 | ||
24 | // Schema2ManifestDescriptor references a platform-specific manifest. | |
25 | type Schema2ManifestDescriptor struct { | |
26 | Schema2Descriptor | |
27 | Platform Schema2PlatformSpec `json:"platform"` | |
28 | } | |
29 | ||
30 | // Schema2List is a list of platform-specific manifests. | |
31 | type Schema2List struct { | |
32 | SchemaVersion int `json:"schemaVersion"` | |
33 | MediaType string `json:"mediaType"` | |
34 | Manifests []Schema2ManifestDescriptor `json:"manifests"` | |
35 | } | |
36 | ||
37 | // MIMEType returns the MIME type of this particular manifest list. | |
38 | func (list *Schema2List) MIMEType() string { | |
39 | return list.MediaType | |
40 | } | |
41 | ||
42 | // Instances returns a slice of digests of the manifests that this list knows of. | |
43 | func (list *Schema2List) Instances() []digest.Digest { | |
44 | results := make([]digest.Digest, len(list.Manifests)) | |
45 | for i, m := range list.Manifests { | |
46 | results[i] = m.Digest | |
47 | } | |
48 | return results | |
49 | } | |
50 | ||
51 | // Instance returns the ListUpdate of a particular instance in the list. | |
52 | func (list *Schema2List) Instance(instanceDigest digest.Digest) (ListUpdate, error) { | |
53 | for _, manifest := range list.Manifests { | |
54 | if manifest.Digest == instanceDigest { | |
55 | return ListUpdate{ | |
56 | Digest: manifest.Digest, | |
57 | Size: manifest.Size, | |
58 | MediaType: manifest.MediaType, | |
59 | }, nil | |
60 | } | |
61 | } | |
62 | return ListUpdate{}, errors.Errorf("unable to find instance %s passed to Schema2List.Instances", instanceDigest) | |
63 | } | |
64 | ||
65 | // UpdateInstances updates the sizes, digests, and media types of the manifests | |
66 | // which the list catalogs. | |
67 | func (list *Schema2List) UpdateInstances(updates []ListUpdate) error { | |
68 | if len(updates) != len(list.Manifests) { | |
69 | return errors.Errorf("incorrect number of update entries passed to Schema2List.UpdateInstances: expected %d, got %d", len(list.Manifests), len(updates)) | |
70 | } | |
71 | for i := range updates { | |
72 | if err := updates[i].Digest.Validate(); err != nil { | |
73 | return errors.Wrapf(err, "update %d of %d passed to Schema2List.UpdateInstances contained an invalid digest", i+1, len(updates)) | |
74 | } | |
75 | list.Manifests[i].Digest = updates[i].Digest | |
76 | if updates[i].Size < 0 { | |
77 | return errors.Errorf("update %d of %d passed to Schema2List.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) | |
78 | } | |
79 | list.Manifests[i].Size = updates[i].Size | |
80 | if updates[i].MediaType == "" { | |
81 | return errors.Errorf("update %d of %d passed to Schema2List.UpdateInstances had no media type (was %q)", i+1, len(updates), list.Manifests[i].MediaType) | |
82 | } | |
83 | if err := SupportedSchema2MediaType(updates[i].MediaType); err != nil && SupportedOCI1MediaType(updates[i].MediaType) != nil { | |
84 | return errors.Wrapf(err, "update %d of %d passed to Schema2List.UpdateInstances had an unsupported media type (was %q): %q", i+1, len(updates), list.Manifests[i].MediaType, updates[i].MediaType) | |
85 | } | |
86 | list.Manifests[i].MediaType = updates[i].MediaType | |
87 | } | |
88 | return nil | |
89 | } | |
90 | ||
91 | // ChooseInstance parses blob as a schema2 manifest list, and returns the digest | |
92 | // of the image which is appropriate for the current environment. | |
93 | func (list *Schema2List) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { | |
94 | wantedArch := runtime.GOARCH | |
95 | if ctx != nil && ctx.ArchitectureChoice != "" { | |
96 | wantedArch = ctx.ArchitectureChoice | |
97 | } | |
98 | wantedOS := runtime.GOOS | |
99 | if ctx != nil && ctx.OSChoice != "" { | |
100 | wantedOS = ctx.OSChoice | |
101 | } | |
102 | ||
103 | for _, d := range list.Manifests { | |
104 | if d.Platform.Architecture == wantedArch && d.Platform.OS == wantedOS { | |
105 | return d.Digest, nil | |
106 | } | |
107 | } | |
108 | return "", fmt.Errorf("no image found in manifest list for architecture %s, OS %s", wantedArch, wantedOS) | |
109 | } | |
110 | ||
111 | // Serialize returns the list in a blob format. | |
112 | // NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! | |
113 | func (list *Schema2List) Serialize() ([]byte, error) { | |
114 | buf, err := json.Marshal(list) | |
115 | if err != nil { | |
116 | return nil, errors.Wrapf(err, "error marshaling Schema2List %#v", list) | |
117 | } | |
118 | return buf, nil | |
119 | } | |
120 | ||
121 | // Schema2ListFromComponents creates a Schema2 manifest list instance from the | |
122 | // supplied data. | |
123 | func Schema2ListFromComponents(components []Schema2ManifestDescriptor) *Schema2List { | |
124 | list := Schema2List{ | |
125 | SchemaVersion: 2, | |
126 | MediaType: DockerV2ListMediaType, | |
127 | Manifests: make([]Schema2ManifestDescriptor, len(components)), | |
128 | } | |
129 | for i, component := range components { | |
130 | m := Schema2ManifestDescriptor{ | |
131 | Schema2Descriptor{ | |
132 | MediaType: component.MediaType, | |
133 | Size: component.Size, | |
134 | Digest: component.Digest, | |
135 | URLs: dupStringSlice(component.URLs), | |
136 | }, | |
137 | Schema2PlatformSpec{ | |
138 | Architecture: component.Platform.Architecture, | |
139 | OS: component.Platform.OS, | |
140 | OSVersion: component.Platform.OSVersion, | |
141 | OSFeatures: dupStringSlice(component.Platform.OSFeatures), | |
142 | Variant: component.Platform.Variant, | |
143 | Features: dupStringSlice(component.Platform.Features), | |
144 | }, | |
145 | } | |
146 | list.Manifests[i] = m | |
147 | } | |
148 | return &list | |
149 | } | |
150 | ||
151 | // Schema2ListClone creates a deep copy of the passed-in list. | |
152 | func Schema2ListClone(list *Schema2List) *Schema2List { | |
153 | return Schema2ListFromComponents(list.Manifests) | |
154 | } | |
155 | ||
156 | // ToOCI1Index returns the list encoded as an OCI1 index. | |
157 | func (list *Schema2List) ToOCI1Index() (*OCI1Index, error) { | |
158 | components := make([]imgspecv1.Descriptor, 0, len(list.Manifests)) | |
159 | for _, manifest := range list.Manifests { | |
160 | converted := imgspecv1.Descriptor{ | |
161 | MediaType: manifest.MediaType, | |
162 | Size: manifest.Size, | |
163 | Digest: manifest.Digest, | |
164 | URLs: dupStringSlice(manifest.URLs), | |
165 | Platform: &imgspecv1.Platform{ | |
166 | OS: manifest.Platform.OS, | |
167 | Architecture: manifest.Platform.Architecture, | |
168 | OSFeatures: dupStringSlice(manifest.Platform.OSFeatures), | |
169 | OSVersion: manifest.Platform.OSVersion, | |
170 | Variant: manifest.Platform.Variant, | |
171 | }, | |
172 | } | |
173 | components = append(components, converted) | |
174 | } | |
175 | oci := OCI1IndexFromComponents(components, nil) | |
176 | return oci, nil | |
177 | } | |
178 | ||
179 | // ToSchema2List returns the list encoded as a Schema2 list. | |
180 | func (list *Schema2List) ToSchema2List() (*Schema2List, error) { | |
181 | return Schema2ListClone(list), nil | |
182 | } | |
183 | ||
184 | // Schema2ListFromManifest creates a Schema2 manifest list instance from marshalled | |
185 | // JSON, presumably generated by encoding a Schema2 manifest list. | |
186 | func Schema2ListFromManifest(manifest []byte) (*Schema2List, error) { | |
187 | list := Schema2List{ | |
188 | Manifests: []Schema2ManifestDescriptor{}, | |
189 | } | |
190 | if err := json.Unmarshal(manifest, &list); err != nil { | |
191 | return nil, errors.Wrapf(err, "error unmarshaling Schema2List %q", string(manifest)) | |
192 | } | |
193 | return &list, nil | |
194 | } | |
195 | ||
196 | // Clone returns a deep copy of this list and its contents. | |
197 | func (list *Schema2List) Clone() List { | |
198 | return Schema2ListClone(list) | |
199 | } | |
200 | ||
201 | // ConvertToMIMEType converts the passed-in manifest list to a manifest | |
202 | // list of the specified type. | |
203 | func (list *Schema2List) ConvertToMIMEType(manifestMIMEType string) (List, error) { | |
204 | switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { | |
205 | case DockerV2ListMediaType: | |
206 | return list.Clone(), nil | |
207 | case imgspecv1.MediaTypeImageIndex: | |
208 | return list.ToOCI1Index() | |
209 | case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: | |
210 | return nil, fmt.Errorf("Can not convert manifest list to MIME type %q, which is not a list type", manifestMIMEType) | |
211 | default: | |
212 | // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. | |
213 | return nil, fmt.Errorf("Unimplemented manifest list MIME type %s", manifestMIMEType) | |
214 | } | |
215 | } |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "fmt" | |
4 | ||
5 | "github.com/containers/image/v4/types" | |
6 | digest "github.com/opencontainers/go-digest" | |
7 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
8 | ) | |
9 | ||
10 | var ( | |
11 | // SupportedListMIMETypes is a list of the manifest list types that we know how to | |
12 | // read/manipulate/write. | |
13 | SupportedListMIMETypes = []string{ | |
14 | DockerV2ListMediaType, | |
15 | imgspecv1.MediaTypeImageIndex, | |
16 | } | |
17 | ) | |
18 | ||
19 | // List is an interface for parsing, modifying lists of image manifests. | |
20 | // Callers can either use this abstract interface without understanding the details of the formats, | |
21 | // or instantiate a specific implementation (e.g. manifest.OCI1Index) and access the public members | |
22 | // directly. | |
23 | type List interface { | |
24 | // MIMEType returns the MIME type of this particular manifest list. | |
25 | MIMEType() string | |
26 | ||
27 | // Instances returns a list of the manifests that this list knows of, other than its own. | |
28 | Instances() []digest.Digest | |
29 | ||
30 | // Update information about the list's instances. The length of the passed-in slice must | |
31 | // match the length of the list of instances which the list already contains, and every field | |
32 | // must be specified. | |
33 | UpdateInstances([]ListUpdate) error | |
34 | ||
35 | // Instance returns the size and MIME type of a particular instance in the list. | |
36 | Instance(digest.Digest) (ListUpdate, error) | |
37 | ||
38 | // ChooseInstance selects which manifest is most appropriate for the platform described by the | |
39 | // SystemContext, or for the current platform if the SystemContext doesn't specify any details. | |
40 | ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) | |
41 | ||
42 | // Serialize returns the list in a blob format. | |
43 | // NOTE: Serialize() does not in general reproduce the original blob if this object was loaded | |
44 | // from, even if no modifications were made! | |
45 | Serialize() ([]byte, error) | |
46 | ||
47 | // ConvertToMIMEType returns the list rebuilt to the specified MIME type, or an error. | |
48 | ConvertToMIMEType(mimeType string) (List, error) | |
49 | ||
50 | // Clone returns a deep copy of this list and its contents. | |
51 | Clone() List | |
52 | } | |
53 | ||
54 | // ListUpdate includes the fields which a List's UpdateInstances() method will modify. | |
55 | type ListUpdate struct { | |
56 | Digest digest.Digest | |
57 | Size int64 | |
58 | MediaType string | |
59 | } | |
60 | ||
61 | // dupStringSlice returns a deep copy of a slice of strings, or nil if the | |
62 | // source slice is empty. | |
63 | func dupStringSlice(list []string) []string { | |
64 | if len(list) == 0 { | |
65 | return nil | |
66 | } | |
67 | dup := make([]string, len(list)) | |
68 | for i := range list { | |
69 | dup[i] = list[i] | |
70 | } | |
71 | return dup | |
72 | } | |
73 | ||
74 | // dupStringStringMap returns a deep copy of a map[string]string, or nil if the | |
75 | // passed-in map is nil or has no keys. | |
76 | func dupStringStringMap(m map[string]string) map[string]string { | |
77 | if len(m) == 0 { | |
78 | return nil | |
79 | } | |
80 | result := make(map[string]string) | |
81 | for k, v := range m { | |
82 | result[k] = v | |
83 | } | |
84 | return result | |
85 | } | |
86 | ||
87 | // ListFromBlob parses a list of manifests. | |
88 | func ListFromBlob(manifest []byte, manifestMIMEType string) (List, error) { | |
89 | normalized := NormalizedMIMEType(manifestMIMEType) | |
90 | switch normalized { | |
91 | case DockerV2ListMediaType: | |
92 | return Schema2ListFromManifest(manifest) | |
93 | case imgspecv1.MediaTypeImageIndex: | |
94 | return OCI1IndexFromManifest(manifest) | |
95 | case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: | |
96 | return nil, fmt.Errorf("Treating single images as manifest lists is not implemented") | |
97 | } | |
98 | return nil, fmt.Errorf("Unimplemented manifest list MIME type %s (normalized as %s)", manifestMIMEType, normalized) | |
99 | } | |
100 | ||
101 | // ConvertListToMIMEType converts the passed-in manifest list to a manifest | |
102 | // list of the specified type. | |
103 | func ConvertListToMIMEType(list List, manifestMIMEType string) (List, error) { | |
104 | return list.ConvertToMIMEType(manifestMIMEType) | |
105 | } |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "io/ioutil" | |
4 | "path/filepath" | |
5 | "testing" | |
6 | ||
7 | "github.com/containers/image/v4/types" | |
8 | digest "github.com/opencontainers/go-digest" | |
9 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
10 | "github.com/stretchr/testify/assert" | |
11 | "github.com/stretchr/testify/require" | |
12 | ) | |
13 | ||
14 | func pare(m List) { | |
15 | if impl, ok := m.(*OCI1Index); ok { | |
16 | impl.Annotations = nil | |
17 | } | |
18 | if impl, ok := m.(*Schema2List); ok { | |
19 | for i := range impl.Manifests { | |
20 | impl.Manifests[i].Platform.Features = nil | |
21 | } | |
22 | } | |
23 | return | |
24 | } | |
25 | ||
26 | func TestParseLists(t *testing.T) { | |
27 | cases := []struct { | |
28 | path string | |
29 | mimeType string | |
30 | }{ | |
31 | {"ociv1.image.index.json", imgspecv1.MediaTypeImageIndex}, | |
32 | {"v2list.manifest.json", DockerV2ListMediaType}, | |
33 | } | |
34 | for _, c := range cases { | |
35 | manifest, err := ioutil.ReadFile(filepath.Join("fixtures", c.path)) | |
36 | require.NoError(t, err, "error reading file %q", filepath.Join("fixtures", c.path)) | |
37 | assert.Equal(t, GuessMIMEType(manifest), c.mimeType) | |
38 | ||
39 | _, err = FromBlob(manifest, c.mimeType) | |
40 | require.Error(t, err, "manifest list %q should not parse as single images", c.path) | |
41 | ||
42 | m, err := ListFromBlob(manifest, c.mimeType) | |
43 | require.NoError(t, err, "manifest list %q should parse as list types", c.path) | |
44 | assert.Equal(t, m.MIMEType(), c.mimeType, "manifest %q is not of the expected MIME type", c.path) | |
45 | ||
46 | clone := m.Clone() | |
47 | assert.Equal(t, clone, m, "manifest %q is missing some fields after being cloned", c.path) | |
48 | ||
49 | pare(m) | |
50 | ||
51 | index, err := m.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) | |
52 | require.NoError(t, err, "error converting %q to an OCI1Index", c.path) | |
53 | ||
54 | list, err := m.ConvertToMIMEType(DockerV2ListMediaType) | |
55 | require.NoError(t, err, "error converting %q to an Schema2List", c.path) | |
56 | ||
57 | index2, err := list.ConvertToMIMEType(imgspecv1.MediaTypeImageIndex) | |
58 | assert.Equal(t, index, index2, "index %q lost data in conversion", c.path) | |
59 | ||
60 | list2, err := index.ConvertToMIMEType(DockerV2ListMediaType) | |
61 | assert.Equal(t, list, list2, "list %q lost data in conversion", c.path) | |
62 | } | |
63 | } | |
64 | ||
65 | func TestChooseInstance(t *testing.T) { | |
66 | for _, manifestList := range []struct { | |
67 | listFile string | |
68 | matchedInstances map[string]digest.Digest | |
69 | unmatchedInstances []string | |
70 | }{ | |
71 | { | |
72 | listFile: "schema2list.json", | |
73 | matchedInstances: map[string]digest.Digest{ | |
74 | "amd64": "sha256:030fcb92e1487b18c974784dcc110a93147c9fc402188370fbfd17efabffc6af", | |
75 | "s390x": "sha256:e5aa1b0a24620228b75382997a0977f609b3ca3a95533dafdef84c74cc8df642", | |
76 | // There are several "arm" images with different variants; | |
77 | // the current code returns the first match. NOTE: This is NOT an API promise. | |
78 | "arm": "sha256:9142d97ef280a7953cf1a85716de49a24cc1dd62776352afad67e635331ff77a", | |
79 | }, | |
80 | unmatchedInstances: []string{ | |
81 | "unmatched", | |
82 | }, | |
83 | }, | |
84 | { | |
85 | listFile: "oci1index.json", | |
86 | matchedInstances: map[string]digest.Digest{ | |
87 | "amd64": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", | |
88 | "ppc64le": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", | |
89 | }, | |
90 | unmatchedInstances: []string{ | |
91 | "unmatched", | |
92 | }, | |
93 | }, | |
94 | } { | |
95 | man, err := ioutil.ReadFile(filepath.Join("..", "image", "fixtures", manifestList.listFile)) | |
96 | require.NoError(t, err) | |
97 | rawManifest := man | |
98 | list, err := ListFromBlob(rawManifest, GuessMIMEType(rawManifest)) | |
99 | require.NoError(t, err) | |
100 | // Match found | |
101 | for arch, expected := range manifestList.matchedInstances { | |
102 | digest, err := list.ChooseInstance(&types.SystemContext{ | |
103 | ArchitectureChoice: arch, | |
104 | OSChoice: "linux", | |
105 | }) | |
106 | require.NoError(t, err, arch) | |
107 | assert.Equal(t, expected, digest) | |
108 | } | |
109 | // Not found | |
110 | for _, arch := range manifestList.unmatchedInstances { | |
111 | _, err := list.ChooseInstance(&types.SystemContext{ | |
112 | ArchitectureChoice: arch, | |
113 | OSChoice: "linux", | |
114 | }) | |
115 | assert.Error(t, err) | |
116 | } | |
117 | } | |
118 | } |
51 | 51 | DockerV2Schema1SignedMediaType, |
52 | 52 | DockerV2Schema1MediaType, |
53 | 53 | DockerV2ListMediaType, |
54 | imgspecv1.MediaTypeImageIndex, | |
54 | 55 | } |
55 | 56 | |
56 | 57 | // Manifest is an interface for parsing, modifying image manifests in isolation. |
139 | 140 | if err := json.Unmarshal(manifest, &ociIndex); err != nil { |
140 | 141 | return "" |
141 | 142 | } |
142 | if len(ociIndex.Manifests) != 0 && ociIndex.Manifests[0].MediaType == imgspecv1.MediaTypeImageManifest { | |
143 | return imgspecv1.MediaTypeImageIndex | |
143 | if len(ociIndex.Manifests) != 0 { | |
144 | if ociMan.Config.MediaType == "" { | |
145 | return imgspecv1.MediaTypeImageIndex | |
146 | } | |
147 | return ociMan.Config.MediaType | |
144 | 148 | } |
145 | 149 | return DockerV2Schema2MediaType |
146 | 150 | } |
198 | 202 | |
199 | 203 | // MIMETypeIsMultiImage returns true if mimeType is a list of images |
200 | 204 | func MIMETypeIsMultiImage(mimeType string) bool { |
201 | return mimeType == DockerV2ListMediaType | |
205 | return mimeType == DockerV2ListMediaType || mimeType == imgspecv1.MediaTypeImageIndex | |
202 | 206 | } |
203 | 207 | |
204 | 208 | // NormalizedMIMEType returns the effective MIME type of a manifest MIME type returned by a server, |
212 | 216 | return DockerV2Schema1SignedMediaType |
213 | 217 | case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, |
214 | 218 | imgspecv1.MediaTypeImageManifest, |
219 | imgspecv1.MediaTypeImageIndex, | |
215 | 220 | DockerV2Schema2MediaType, |
216 | 221 | DockerV2ListMediaType: |
217 | 222 | return input |
231 | 236 | |
232 | 237 | // FromBlob returns a Manifest instance for the specified manifest blob and the corresponding MIME type |
233 | 238 | func FromBlob(manblob []byte, mt string) (Manifest, error) { |
234 | switch NormalizedMIMEType(mt) { | |
239 | nmt := NormalizedMIMEType(mt) | |
240 | switch nmt { | |
235 | 241 | case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType: |
236 | 242 | return Schema1FromManifest(manblob) |
237 | 243 | case imgspecv1.MediaTypeImageManifest: |
238 | 244 | return OCI1FromManifest(manblob) |
239 | 245 | case DockerV2Schema2MediaType: |
240 | 246 | return Schema2FromManifest(manblob) |
241 | case DockerV2ListMediaType: | |
247 | case DockerV2ListMediaType, imgspecv1.MediaTypeImageIndex: | |
242 | 248 | return nil, fmt.Errorf("Treating manifest lists as individual manifests is not implemented") |
243 | default: // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. | |
244 | return nil, fmt.Errorf("Unimplemented manifest MIME type %s", mt) | |
245 | } | |
249 | } | |
250 | // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. | |
251 | return nil, fmt.Errorf("Unimplemented manifest MIME type %s (normalized as %s)", mt, nmt) | |
246 | 252 | } |
247 | 253 | |
248 | 254 | // layerInfosToStrings converts a list of layer infos, presumably obtained from a Manifest.LayerInfos() |
133 | 133 | {DockerV2Schema1MediaType, false}, |
134 | 134 | {DockerV2Schema1SignedMediaType, false}, |
135 | 135 | {DockerV2Schema2MediaType, false}, |
136 | {imgspecv1.MediaTypeImageIndex, true}, | |
137 | {imgspecv1.MediaTypeImageManifest, false}, | |
136 | 138 | } { |
137 | 139 | res := MIMETypeIsMultiImage(c.mt) |
138 | 140 | assert.Equal(t, c.expected, res, c.mt) |
0 | package manifest | |
1 | ||
2 | import ( | |
3 | "encoding/json" | |
4 | "fmt" | |
5 | "runtime" | |
6 | ||
7 | "github.com/containers/image/v4/types" | |
8 | "github.com/opencontainers/go-digest" | |
9 | imgspec "github.com/opencontainers/image-spec/specs-go" | |
10 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
11 | "github.com/pkg/errors" | |
12 | ) | |
13 | ||
14 | // OCI1Index is just an alias for the OCI index type, but one which we can | |
15 | // provide methods for. | |
16 | type OCI1Index struct { | |
17 | imgspecv1.Index | |
18 | } | |
19 | ||
20 | // MIMEType returns the MIME type of this particular manifest index. | |
21 | func (index *OCI1Index) MIMEType() string { | |
22 | return imgspecv1.MediaTypeImageIndex | |
23 | } | |
24 | ||
25 | // Instances returns a slice of digests of the manifests that this index knows of. | |
26 | func (index *OCI1Index) Instances() []digest.Digest { | |
27 | results := make([]digest.Digest, len(index.Manifests)) | |
28 | for i, m := range index.Manifests { | |
29 | results[i] = m.Digest | |
30 | } | |
31 | return results | |
32 | } | |
33 | ||
34 | // Instance returns the ListUpdate of a particular instance in the index. | |
35 | func (index *OCI1Index) Instance(instanceDigest digest.Digest) (ListUpdate, error) { | |
36 | for _, manifest := range index.Manifests { | |
37 | if manifest.Digest == instanceDigest { | |
38 | return ListUpdate{ | |
39 | Digest: manifest.Digest, | |
40 | Size: manifest.Size, | |
41 | MediaType: manifest.MediaType, | |
42 | }, nil | |
43 | } | |
44 | } | |
45 | return ListUpdate{}, errors.Errorf("unable to find instance %s in OCI1Index", instanceDigest) | |
46 | } | |
47 | ||
48 | // UpdateInstances updates the sizes, digests, and media types of the manifests | |
49 | // which the list catalogs. | |
50 | func (index *OCI1Index) UpdateInstances(updates []ListUpdate) error { | |
51 | if len(updates) != len(index.Manifests) { | |
52 | return errors.Errorf("incorrect number of update entries passed to OCI1Index.UpdateInstances: expected %d, got %d", len(index.Manifests), len(updates)) | |
53 | } | |
54 | for i := range updates { | |
55 | if err := updates[i].Digest.Validate(); err != nil { | |
56 | return errors.Wrapf(err, "update %d of %d passed to OCI1Index.UpdateInstances contained an invalid digest", i+1, len(updates)) | |
57 | } | |
58 | index.Manifests[i].Digest = updates[i].Digest | |
59 | if updates[i].Size < 0 { | |
60 | return errors.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had an invalid size (%d)", i+1, len(updates), updates[i].Size) | |
61 | } | |
62 | index.Manifests[i].Size = updates[i].Size | |
63 | if updates[i].MediaType == "" { | |
64 | return errors.Errorf("update %d of %d passed to OCI1Index.UpdateInstances had no media type (was %q)", i+1, len(updates), index.Manifests[i].MediaType) | |
65 | } | |
66 | if err := SupportedOCI1MediaType(updates[i].MediaType); err != nil && SupportedSchema2MediaType(updates[i].MediaType) != nil && updates[i].MediaType != imgspecv1.MediaTypeImageIndex { | |
67 | return errors.Wrapf(err, "update %d of %d passed to OCI1Index.UpdateInstances had an unsupported media type (was %q): %q", i+1, len(updates), index.Manifests[i].MediaType, updates[i].MediaType) | |
68 | } | |
69 | index.Manifests[i].MediaType = updates[i].MediaType | |
70 | } | |
71 | return nil | |
72 | } | |
73 | ||
74 | // ChooseInstance parses blob as an oci v1 manifest index, and returns the digest | |
75 | // of the image which is appropriate for the current environment. | |
76 | func (index *OCI1Index) ChooseInstance(ctx *types.SystemContext) (digest.Digest, error) { | |
77 | wantedArch := runtime.GOARCH | |
78 | if ctx != nil && ctx.ArchitectureChoice != "" { | |
79 | wantedArch = ctx.ArchitectureChoice | |
80 | } | |
81 | wantedOS := runtime.GOOS | |
82 | if ctx != nil && ctx.OSChoice != "" { | |
83 | wantedOS = ctx.OSChoice | |
84 | } | |
85 | ||
86 | for _, d := range index.Manifests { | |
87 | if d.Platform != nil && d.Platform.Architecture == wantedArch && d.Platform.OS == wantedOS { | |
88 | return d.Digest, nil | |
89 | } | |
90 | } | |
91 | for _, d := range index.Manifests { | |
92 | if d.Platform == nil { | |
93 | return d.Digest, nil | |
94 | } | |
95 | } | |
96 | return "", fmt.Errorf("no image found in image index for architecture %s, OS %s", wantedArch, wantedOS) | |
97 | } | |
98 | ||
99 | // Serialize returns the index in a blob format. | |
100 | // NOTE: Serialize() does not in general reproduce the original blob if this object was loaded from one, even if no modifications were made! | |
101 | func (index *OCI1Index) Serialize() ([]byte, error) { | |
102 | buf, err := json.Marshal(index) | |
103 | if err != nil { | |
104 | return nil, errors.Wrapf(err, "error marshaling OCI1Index %#v", index) | |
105 | } | |
106 | return buf, nil | |
107 | } | |
108 | ||
109 | // OCI1IndexFromComponents creates an OCI1 image index instance from the | |
110 | // supplied data. | |
111 | func OCI1IndexFromComponents(components []imgspecv1.Descriptor, annotations map[string]string) *OCI1Index { | |
112 | index := OCI1Index{ | |
113 | imgspecv1.Index{ | |
114 | Versioned: imgspec.Versioned{SchemaVersion: 2}, | |
115 | Manifests: make([]imgspecv1.Descriptor, len(components)), | |
116 | Annotations: dupStringStringMap(annotations), | |
117 | }, | |
118 | } | |
119 | for i, component := range components { | |
120 | var platform *imgspecv1.Platform | |
121 | if component.Platform != nil { | |
122 | platform = &imgspecv1.Platform{ | |
123 | Architecture: component.Platform.Architecture, | |
124 | OS: component.Platform.OS, | |
125 | OSVersion: component.Platform.OSVersion, | |
126 | OSFeatures: dupStringSlice(component.Platform.OSFeatures), | |
127 | Variant: component.Platform.Variant, | |
128 | } | |
129 | } | |
130 | m := imgspecv1.Descriptor{ | |
131 | MediaType: component.MediaType, | |
132 | Size: component.Size, | |
133 | Digest: component.Digest, | |
134 | URLs: dupStringSlice(component.URLs), | |
135 | Annotations: dupStringStringMap(component.Annotations), | |
136 | Platform: platform, | |
137 | } | |
138 | index.Manifests[i] = m | |
139 | } | |
140 | return &index | |
141 | } | |
142 | ||
143 | // OCI1IndexClone creates a deep copy of the passed-in index. | |
144 | func OCI1IndexClone(index *OCI1Index) *OCI1Index { | |
145 | return OCI1IndexFromComponents(index.Manifests, index.Annotations) | |
146 | } | |
147 | ||
148 | // ToOCI1Index returns the index encoded as an OCI1 index. | |
149 | func (index *OCI1Index) ToOCI1Index() (*OCI1Index, error) { | |
150 | return OCI1IndexClone(index), nil | |
151 | } | |
152 | ||
153 | // ToSchema2List returns the index encoded as a Schema2 list. | |
154 | func (index *OCI1Index) ToSchema2List() (*Schema2List, error) { | |
155 | components := make([]Schema2ManifestDescriptor, 0, len(index.Manifests)) | |
156 | for _, manifest := range index.Manifests { | |
157 | platform := manifest.Platform | |
158 | if platform == nil { | |
159 | platform = &imgspecv1.Platform{ | |
160 | OS: runtime.GOOS, | |
161 | Architecture: runtime.GOARCH, | |
162 | } | |
163 | } | |
164 | converted := Schema2ManifestDescriptor{ | |
165 | Schema2Descriptor{ | |
166 | MediaType: manifest.MediaType, | |
167 | Size: manifest.Size, | |
168 | Digest: manifest.Digest, | |
169 | URLs: dupStringSlice(manifest.URLs), | |
170 | }, | |
171 | Schema2PlatformSpec{ | |
172 | OS: platform.OS, | |
173 | Architecture: platform.Architecture, | |
174 | OSFeatures: dupStringSlice(platform.OSFeatures), | |
175 | OSVersion: platform.OSVersion, | |
176 | Variant: platform.Variant, | |
177 | }, | |
178 | } | |
179 | components = append(components, converted) | |
180 | } | |
181 | s2 := Schema2ListFromComponents(components) | |
182 | return s2, nil | |
183 | } | |
184 | ||
185 | // OCI1IndexFromManifest creates an OCI1 manifest index instance from marshalled | |
186 | // JSON, presumably generated by encoding a OCI1 manifest index. | |
187 | func OCI1IndexFromManifest(manifest []byte) (*OCI1Index, error) { | |
188 | index := OCI1Index{ | |
189 | Index: imgspecv1.Index{ | |
190 | Versioned: imgspec.Versioned{SchemaVersion: 2}, | |
191 | Manifests: []imgspecv1.Descriptor{}, | |
192 | Annotations: make(map[string]string), | |
193 | }, | |
194 | } | |
195 | if err := json.Unmarshal(manifest, &index); err != nil { | |
196 | return nil, errors.Wrapf(err, "error unmarshaling OCI1Index %q", string(manifest)) | |
197 | } | |
198 | return &index, nil | |
199 | } | |
200 | ||
201 | // Clone returns a deep copy of this list and its contents. | |
202 | func (index *OCI1Index) Clone() List { | |
203 | return OCI1IndexClone(index) | |
204 | } | |
205 | ||
206 | // ConvertToMIMEType converts the passed-in image index to a manifest list of | |
207 | // the specified type. | |
208 | func (index *OCI1Index) ConvertToMIMEType(manifestMIMEType string) (List, error) { | |
209 | switch normalized := NormalizedMIMEType(manifestMIMEType); normalized { | |
210 | case DockerV2ListMediaType: | |
211 | return index.ToSchema2List() | |
212 | case imgspecv1.MediaTypeImageIndex: | |
213 | return index.Clone(), nil | |
214 | case DockerV2Schema1MediaType, DockerV2Schema1SignedMediaType, imgspecv1.MediaTypeImageManifest, DockerV2Schema2MediaType: | |
215 | return nil, fmt.Errorf("Can not convert image index to MIME type %q, which is not a list type", manifestMIMEType) | |
216 | default: | |
217 | // Note that this may not be reachable, NormalizedMIMEType has a default for unknown values. | |
218 | return nil, fmt.Errorf("Unimplemented manifest MIME type %s", manifestMIMEType) | |
219 | } | |
220 | } |
6 | 6 | |
7 | 7 | "github.com/containers/image/v4/types" |
8 | 8 | "github.com/containers/storage/pkg/archive" |
9 | digest "github.com/opencontainers/go-digest" | |
9 | 10 | "github.com/pkg/errors" |
10 | 11 | ) |
11 | 12 | |
104 | 105 | return d.unpackedDest.TryReusingBlob(ctx, info, cache, canSubstitute) |
105 | 106 | } |
106 | 107 | |
107 | // PutManifest writes manifest to the destination | |
108 | func (d *ociArchiveImageDestination) PutManifest(ctx context.Context, m []byte) error { | |
109 | return d.unpackedDest.PutManifest(ctx, m) | |
108 | // PutManifest writes the manifest to the destination. | |
109 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to overwrite the manifest for (when | |
110 | // the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
111 | // It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated | |
112 | // by `manifest.Digest()`. | |
113 | func (d *ociArchiveImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { | |
114 | return d.unpackedDest.PutManifest(ctx, m, instanceDigest) | |
110 | 115 | } |
111 | 116 | |
112 | func (d *ociArchiveImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
113 | return d.unpackedDest.PutSignatures(ctx, signatures) | |
117 | // PutSignatures writes a set of signatures to the destination. | |
118 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for | |
119 | // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
120 | func (d *ociArchiveImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
121 | return d.unpackedDest.PutSignatures(ctx, signatures, instanceDigest) | |
114 | 122 | } |
115 | 123 | |
116 | 124 | // Commit marks the process of storing the image as successful and asks for the image to be persisted |
117 | 125 | // after the directory is made, it is tarred up into a file and the directory is deleted |
118 | func (d *ociArchiveImageDestination) Commit(ctx context.Context) error { | |
119 | if err := d.unpackedDest.Commit(ctx); err != nil { | |
126 | func (d *ociArchiveImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { | |
127 | if err := d.unpackedDest.Commit(ctx, unparsedToplevel); err != nil { | |
120 | 128 | return errors.Wrapf(err, "error storing image %q", d.ref.image) |
121 | 129 | } |
122 | 130 |
95 | 95 | return s.unpackedSrc.GetSignatures(ctx, instanceDigest) |
96 | 96 | } |
97 | 97 | |
98 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
99 | func (s *ociArchiveImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
100 | return nil, nil | |
98 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
99 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
100 | // to read the image's layers. | |
101 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
102 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
103 | // (e.g. if the source never returns manifest lists). | |
104 | // The Digest field is guaranteed to be provided; Size may be -1. | |
105 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
106 | func (s *ociArchiveImageSource) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) { | |
107 | return s.unpackedSrc.LayerInfosForCopy(ctx, instanceDigest) | |
101 | 108 | } |
37 | 37 | Versioned: imgspec.Versioned{ |
38 | 38 | SchemaVersion: 2, |
39 | 39 | }, |
40 | Annotations: make(map[string]string), | |
40 | 41 | } |
41 | 42 | } |
42 | 43 | |
72 | 73 | func (d *ociImageDestination) SupportedManifestMIMETypes() []string { |
73 | 74 | return []string{ |
74 | 75 | imgspecv1.MediaTypeImageManifest, |
76 | imgspecv1.MediaTypeImageIndex, | |
75 | 77 | } |
76 | 78 | } |
77 | 79 | |
204 | 206 | return true, types.BlobInfo{Digest: info.Digest, Size: finfo.Size()}, nil |
205 | 207 | } |
206 | 208 | |
207 | // PutManifest writes manifest to the destination. | |
209 | // PutManifest writes a manifest to the destination. Per our list of supported manifest MIME types, | |
210 | // this should be either an OCI manifest (possibly converted to this format by the caller) or index, | |
211 | // neither of which we'll need to modify further. | |
212 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to overwrite the manifest for (when | |
213 | // the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
214 | // It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated | |
215 | // by `manifest.Digest()`. | |
208 | 216 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
209 | 217 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
210 | 218 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
211 | func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte) error { | |
212 | digest, err := manifest.Digest(m) | |
213 | if err != nil { | |
214 | return err | |
215 | } | |
219 | func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { | |
220 | var digest digest.Digest | |
221 | var err error | |
222 | if instanceDigest != nil { | |
223 | digest = *instanceDigest | |
224 | } else { | |
225 | digest, err = manifest.Digest(m) | |
226 | if err != nil { | |
227 | return err | |
228 | } | |
229 | } | |
230 | ||
231 | blobPath, err := d.ref.blobPath(digest, d.sharedBlobDir) | |
232 | if err != nil { | |
233 | return err | |
234 | } | |
235 | if err := ensureParentDirectoryExists(blobPath); err != nil { | |
236 | return err | |
237 | } | |
238 | if err := ioutil.WriteFile(blobPath, m, 0644); err != nil { | |
239 | return err | |
240 | } | |
241 | ||
242 | if instanceDigest != nil { | |
243 | return nil | |
244 | } | |
245 | ||
246 | // If we had platform information, we'd build an imgspecv1.Platform structure here. | |
247 | ||
248 | // Start filling out the descriptor for this entry | |
216 | 249 | desc := imgspecv1.Descriptor{} |
217 | 250 | desc.Digest = digest |
218 | // TODO(runcom): beaware and add support for OCI manifest list | |
219 | desc.MediaType = imgspecv1.MediaTypeImageManifest | |
220 | 251 | desc.Size = int64(len(m)) |
221 | ||
222 | blobPath, err := d.ref.blobPath(digest, d.sharedBlobDir) | |
223 | if err != nil { | |
224 | return err | |
225 | } | |
226 | if err := ensureParentDirectoryExists(blobPath); err != nil { | |
227 | return err | |
228 | } | |
229 | if err := ioutil.WriteFile(blobPath, m, 0644); err != nil { | |
230 | return err | |
231 | } | |
232 | ||
233 | 252 | if d.ref.image != "" { |
234 | annotations := make(map[string]string) | |
235 | annotations["org.opencontainers.image.ref.name"] = d.ref.image | |
236 | desc.Annotations = annotations | |
237 | } | |
238 | desc.Platform = &imgspecv1.Platform{ | |
239 | Architecture: runtime.GOARCH, | |
240 | OS: runtime.GOOS, | |
241 | } | |
253 | desc.Annotations = make(map[string]string) | |
254 | desc.Annotations[imgspecv1.AnnotationRefName] = d.ref.image | |
255 | } | |
256 | ||
257 | // If we knew the MIME type, we wouldn't have to guess here. | |
258 | desc.MediaType = manifest.GuessMIMEType(m) | |
259 | ||
242 | 260 | d.addManifest(&desc) |
243 | 261 | |
244 | 262 | return nil |
245 | 263 | } |
246 | 264 | |
247 | 265 | func (d *ociImageDestination) addManifest(desc *imgspecv1.Descriptor) { |
266 | // If the new entry has a name, remove any conflicting names which we already have. | |
267 | if desc.Annotations != nil && desc.Annotations[imgspecv1.AnnotationRefName] != "" { | |
268 | // The name is being set on a new entry, so remove any older ones that had the same name. | |
269 | // We might be storing an index and all of its component images, and we'll want to attach | |
270 | // the name to the last one, which is the index. | |
271 | for i, manifest := range d.index.Manifests { | |
272 | if manifest.Annotations[imgspecv1.AnnotationRefName] == desc.Annotations[imgspecv1.AnnotationRefName] { | |
273 | delete(d.index.Manifests[i].Annotations, imgspecv1.AnnotationRefName) | |
274 | break | |
275 | } | |
276 | } | |
277 | } | |
278 | // If it has the same digest as another entry in the index, we already overwrote the file, | |
279 | // so just pick up the other information. | |
248 | 280 | for i, manifest := range d.index.Manifests { |
249 | if manifest.Annotations["org.opencontainers.image.ref.name"] == desc.Annotations["org.opencontainers.image.ref.name"] { | |
250 | // TODO Should there first be a cleanup based on the descriptor we are going to replace? | |
281 | if manifest.Digest == desc.Digest { | |
282 | // Replace it completely. | |
251 | 283 | d.index.Manifests[i] = *desc |
252 | 284 | return |
253 | 285 | } |
254 | 286 | } |
287 | // It's a new entry to be added to the index. | |
255 | 288 | d.index.Manifests = append(d.index.Manifests, *desc) |
256 | 289 | } |
257 | 290 | |
258 | func (d *ociImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
291 | // PutSignatures would add the given signatures to the oci layout (currently not supported). | |
292 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for | |
293 | // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
294 | func (d *ociImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
259 | 295 | if len(signatures) != 0 { |
260 | 296 | return errors.Errorf("Pushing signatures for OCI images is not supported") |
261 | 297 | } |
266 | 302 | // WARNING: This does not have any transactional semantics: |
267 | 303 | // - Uploaded data MAY be visible to others before Commit() is called |
268 | 304 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
269 | func (d *ociImageDestination) Commit(ctx context.Context) error { | |
305 | func (d *ociImageDestination) Commit(context.Context, types.UnparsedImage) error { | |
270 | 306 | if err := ioutil.WriteFile(d.ref.ociLayoutPath(), []byte(`{"imageLayoutVersion": "1.0.0"}`), 0644); err != nil { |
271 | 307 | return err |
272 | 308 | } |
0 | 0 | package layout |
1 | 1 | |
2 | 2 | import ( |
3 | "bytes" | |
3 | 4 | "context" |
5 | "io/ioutil" | |
4 | 6 | "os" |
5 | 7 | "path/filepath" |
6 | 8 | "testing" |
7 | 9 | |
8 | 10 | "github.com/containers/image/v4/pkg/blobinfocache/memory" |
9 | 11 | "github.com/containers/image/v4/types" |
12 | digest "github.com/opencontainers/go-digest" | |
10 | 13 | "github.com/pkg/errors" |
11 | 14 | "github.com/stretchr/testify/assert" |
12 | 15 | "github.com/stretchr/testify/require" |
55 | 58 | _, err = dest.PutBlob(context.Background(), reader, types.BlobInfo{Digest: blobDigest, Size: -1}, cache, false) |
56 | 59 | assert.Error(t, err) |
57 | 60 | assert.Contains(t, digestErrorString, err.Error()) |
58 | err = dest.Commit(context.Background()) | |
61 | err = dest.Commit(context.Background(), nil) | |
59 | 62 | assert.NoError(t, err) |
60 | 63 | |
61 | 64 | _, err = os.Lstat(blobPath) |
95 | 98 | ociRef, ok := ref.(ociReference) |
96 | 99 | require.True(t, ok) |
97 | 100 | |
101 | putTestConfig(t, ociRef, tmpDir) | |
98 | 102 | putTestManifest(t, ociRef, tmpDir) |
99 | 103 | putTestManifest(t, ociRef, tmpDir) |
100 | 104 | |
101 | 105 | index, err := ociRef.getIndex() |
102 | 106 | assert.NoError(t, err) |
103 | assert.Equal(t, 1, len(index.Manifests), "Unexpected number of manifests") | |
107 | assert.Equal(t, 2, len(index.Manifests), "Unexpected number of manifests") | |
104 | 108 | } |
105 | 109 | |
106 | func putTestManifest(t *testing.T, ociRef ociReference, tmpDir string) { | |
110 | func putTestConfig(t *testing.T, ociRef ociReference, tmpDir string) { | |
111 | data, err := ioutil.ReadFile("../../image/fixtures/oci1-config.json") | |
112 | assert.NoError(t, err) | |
107 | 113 | imageDest, err := newImageDestination(nil, ociRef) |
108 | 114 | assert.NoError(t, err) |
109 | 115 | |
110 | data := []byte("abc") | |
111 | err = imageDest.PutManifest(context.Background(), data) | |
116 | cache := memory.New() | |
117 | ||
118 | _, err = imageDest.PutBlob(context.Background(), bytes.NewReader(data), types.BlobInfo{Size: int64(len(data)), Digest: digest.FromBytes(data)}, cache, true) | |
112 | 119 | assert.NoError(t, err) |
113 | 120 | |
114 | err = imageDest.Commit(context.Background()) | |
121 | err = imageDest.Commit(context.Background(), nil) | |
115 | 122 | assert.NoError(t, err) |
116 | 123 | |
117 | 124 | paths := []string{} |
120 | 127 | return nil |
121 | 128 | }) |
122 | 129 | |
123 | digest := "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" | |
130 | digest := digest.FromBytes(data).Encoded() | |
131 | assert.Contains(t, paths, filepath.Join(tmpDir, "blobs", "sha256", digest), "The OCI directory does not contain the new config data") | |
132 | } | |
133 | ||
134 | func putTestManifest(t *testing.T, ociRef ociReference, tmpDir string) { | |
135 | data, err := ioutil.ReadFile("../../image/fixtures/oci1.json") | |
136 | assert.NoError(t, err) | |
137 | imageDest, err := newImageDestination(nil, ociRef) | |
138 | assert.NoError(t, err) | |
139 | ||
140 | err = imageDest.PutManifest(context.Background(), data, nil) | |
141 | assert.NoError(t, err) | |
142 | ||
143 | err = imageDest.Commit(context.Background(), nil) | |
144 | assert.NoError(t, err) | |
145 | ||
146 | paths := []string{} | |
147 | filepath.Walk(tmpDir, func(path string, info os.FileInfo, err error) error { | |
148 | paths = append(paths, path) | |
149 | return nil | |
150 | }) | |
151 | ||
152 | digest := digest.FromBytes(data).Encoded() | |
124 | 153 | assert.Contains(t, paths, filepath.Join(tmpDir, "blobs", "sha256", digest), "The OCI directory does not contain the new manifest data") |
125 | 154 | } |
7 | 7 | "os" |
8 | 8 | "strconv" |
9 | 9 | |
10 | "github.com/containers/image/v4/manifest" | |
10 | 11 | "github.com/containers/image/v4/pkg/tlsclientconfig" |
11 | 12 | "github.com/containers/image/v4/types" |
12 | 13 | "github.com/docker/go-connections/tlsconfig" |
17 | 18 | |
18 | 19 | type ociImageSource struct { |
19 | 20 | ref ociReference |
21 | index *imgspecv1.Index | |
20 | 22 | descriptor imgspecv1.Descriptor |
21 | 23 | client *http.Client |
22 | 24 | sharedBlobDir string |
40 | 42 | if err != nil { |
41 | 43 | return nil, err |
42 | 44 | } |
43 | d := &ociImageSource{ref: ref, descriptor: descriptor, client: client} | |
45 | index, err := ref.getIndex() | |
46 | if err != nil { | |
47 | return nil, err | |
48 | } | |
49 | d := &ociImageSource{ref: ref, index: index, descriptor: descriptor, client: client} | |
44 | 50 | if sys != nil { |
45 | 51 | // TODO(jonboulle): check dir existence? |
46 | 52 | d.sharedBlobDir = sys.OCISharedBlobDirPath |
65 | 71 | func (s *ociImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { |
66 | 72 | var dig digest.Digest |
67 | 73 | var mimeType string |
74 | var err error | |
75 | ||
68 | 76 | if instanceDigest == nil { |
69 | 77 | dig = digest.Digest(s.descriptor.Digest) |
70 | 78 | mimeType = s.descriptor.MediaType |
71 | 79 | } else { |
72 | 80 | dig = *instanceDigest |
73 | // XXX: instanceDigest means that we don't immediately have the context of what | |
74 | // mediaType the manifest has. In OCI this means that we don't know | |
75 | // what reference it came from, so we just *assume* that its | |
76 | // MediaTypeImageManifest. | |
77 | // FIXME: We should actually be able to look up the manifest in the index, | |
78 | // and see the MIME type there. | |
79 | mimeType = imgspecv1.MediaTypeImageManifest | |
81 | for _, md := range s.index.Manifests { | |
82 | if md.Digest == dig { | |
83 | mimeType = md.MediaType | |
84 | break | |
85 | } | |
86 | } | |
80 | 87 | } |
81 | 88 | |
82 | 89 | manifestPath, err := s.ref.blobPath(dig, s.sharedBlobDir) |
83 | 90 | if err != nil { |
84 | 91 | return nil, "", err |
85 | 92 | } |
93 | ||
86 | 94 | m, err := ioutil.ReadFile(manifestPath) |
87 | 95 | if err != nil { |
88 | 96 | return nil, "", err |
97 | } | |
98 | if mimeType == "" { | |
99 | mimeType = manifest.GuessMIMEType(m) | |
89 | 100 | } |
90 | 101 | |
91 | 102 | return m, mimeType, nil |
156 | 167 | return nil, 0, errWrap |
157 | 168 | } |
158 | 169 | |
159 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
160 | func (s *ociImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
170 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
171 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
172 | // to read the image's layers. | |
173 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
174 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
175 | // (e.g. if the source never returns manifest lists). | |
176 | // The Digest field is guaranteed to be provided; Size may be -1. | |
177 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
178 | func (s *ociImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
161 | 179 | return nil, nil |
162 | 180 | } |
163 | 181 |
194 | 194 | } else { |
195 | 195 | // if image specified, look through all manifests for a match |
196 | 196 | for _, md := range index.Manifests { |
197 | if md.MediaType != imgspecv1.MediaTypeImageManifest { | |
197 | if md.MediaType != imgspecv1.MediaTypeImageManifest && md.MediaType != imgspecv1.MediaTypeImageIndex { | |
198 | 198 | continue |
199 | 199 | } |
200 | refName, ok := md.Annotations["org.opencontainers.image.ref.name"] | |
200 | refName, ok := md.Annotations[imgspecv1.AnnotationRefName] | |
201 | 201 | if !ok { |
202 | 202 | continue |
203 | 203 | } |
230 | 230 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list |
231 | 231 | // (e.g. if the source never returns manifest lists). |
232 | 232 | func (s *openshiftImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { |
233 | var imageName string | |
233 | var imageStreamImageName string | |
234 | 234 | if instanceDigest == nil { |
235 | 235 | if err := s.ensureImageIsResolved(ctx); err != nil { |
236 | 236 | return nil, err |
237 | 237 | } |
238 | imageName = s.imageStreamImageName | |
238 | imageStreamImageName = s.imageStreamImageName | |
239 | 239 | } else { |
240 | imageName = instanceDigest.String() | |
241 | } | |
242 | image, err := s.client.getImage(ctx, imageName) | |
240 | imageStreamImageName = instanceDigest.String() | |
241 | } | |
242 | image, err := s.client.getImage(ctx, imageStreamImageName) | |
243 | 243 | if err != nil { |
244 | 244 | return nil, err |
245 | 245 | } |
252 | 252 | return sigs, nil |
253 | 253 | } |
254 | 254 | |
255 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
256 | func (s *openshiftImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
255 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
256 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
257 | // to read the image's layers. | |
258 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
259 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
260 | // (e.g. if the source never returns manifest lists). | |
261 | // The Digest field is guaranteed to be provided; Size may be -1. | |
262 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
263 | func (s *openshiftImageSource) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) { | |
257 | 264 | return nil, nil |
258 | 265 | } |
259 | 266 | |
413 | 420 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
414 | 421 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
415 | 422 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
416 | func (d *openshiftImageDestination) PutManifest(ctx context.Context, m []byte) error { | |
417 | manifestDigest, err := manifest.Digest(m) | |
418 | if err != nil { | |
419 | return err | |
420 | } | |
421 | d.imageStreamImageName = manifestDigest.String() | |
422 | ||
423 | return d.docker.PutManifest(ctx, m) | |
424 | } | |
425 | ||
426 | func (d *openshiftImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
427 | if d.imageStreamImageName == "" { | |
428 | return errors.Errorf("Internal error: Unknown manifest digest, can't add signatures") | |
429 | } | |
423 | func (d *openshiftImageDestination) PutManifest(ctx context.Context, m []byte, instanceDigest *digest.Digest) error { | |
424 | if instanceDigest == nil { | |
425 | manifestDigest, err := manifest.Digest(m) | |
426 | if err != nil { | |
427 | return err | |
428 | } | |
429 | d.imageStreamImageName = manifestDigest.String() | |
430 | } | |
431 | return d.docker.PutManifest(ctx, m, instanceDigest) | |
432 | } | |
433 | ||
434 | func (d *openshiftImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
435 | var imageStreamName string | |
436 | if instanceDigest == nil { | |
437 | if d.imageStreamImageName == "" { | |
438 | return errors.Errorf("Internal error: Unknown manifest digest, can't add signatures") | |
439 | } | |
440 | imageStreamName = d.imageStreamImageName | |
441 | } else { | |
442 | imageStreamName = instanceDigest.String() | |
443 | } | |
444 | ||
430 | 445 | // Because image signatures are a shared resource in Atomic Registry, the default upload |
431 | 446 | // always adds signatures. Eventually we should also allow removing signatures. |
432 | 447 | |
434 | 449 | return nil // No need to even read the old state. |
435 | 450 | } |
436 | 451 | |
437 | image, err := d.client.getImage(ctx, d.imageStreamImageName) | |
452 | image, err := d.client.getImage(ctx, imageStreamName) | |
438 | 453 | if err != nil { |
439 | 454 | return err |
440 | 455 | } |
459 | 474 | if err != nil || n != 16 { |
460 | 475 | return errors.Wrapf(err, "Error generating random signature len %d", n) |
461 | 476 | } |
462 | signatureName = fmt.Sprintf("%s@%032x", d.imageStreamImageName, randBytes) | |
477 | signatureName = fmt.Sprintf("%s@%032x", imageStreamName, randBytes) | |
463 | 478 | if _, ok := existingSigNames[signatureName]; !ok { |
464 | 479 | break |
465 | 480 | } |
488 | 503 | // WARNING: This does not have any transactional semantics: |
489 | 504 | // - Uploaded data MAY be visible to others before Commit() is called |
490 | 505 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
491 | func (d *openshiftImageDestination) Commit(ctx context.Context) error { | |
492 | return d.docker.Commit(ctx) | |
506 | func (d *openshiftImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { | |
507 | return d.docker.Commit(ctx, unparsedToplevel) | |
493 | 508 | } |
494 | 509 | |
495 | 510 | // These structs are subsets of github.com/openshift/origin/pkg/image/api/v1 and its dependencies. |
375 | 375 | } |
376 | 376 | |
377 | 377 | // PutManifest writes manifest to the destination. |
378 | // The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so | |
379 | // there can be no secondary manifests. | |
378 | 380 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
379 | 381 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
380 | 382 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
381 | func (d *ostreeImageDestination) PutManifest(ctx context.Context, manifestBlob []byte) error { | |
383 | func (d *ostreeImageDestination) PutManifest(ctx context.Context, manifestBlob []byte, instanceDigest *digest.Digest) error { | |
384 | if instanceDigest != nil { | |
385 | return errors.New(`Manifest lists are not supported by "ostree:"`) | |
386 | } | |
387 | ||
382 | 388 | d.manifest = string(manifestBlob) |
383 | 389 | |
384 | 390 | if err := json.Unmarshal(manifestBlob, &d.schema); err != nil { |
399 | 405 | return ioutil.WriteFile(manifestPath, manifestBlob, 0644) |
400 | 406 | } |
401 | 407 | |
402 | func (d *ostreeImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
408 | // PutSignatures writes signatures to the destination. | |
409 | // The instanceDigest value is expected to always be nil, because this transport does not support manifest lists, so | |
410 | // there can be no secondary manifests. | |
411 | func (d *ostreeImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
412 | if instanceDigest != nil { | |
413 | return errors.New(`Manifest lists are not supported by "ostree:"`) | |
414 | } | |
415 | ||
403 | 416 | path := filepath.Join(d.tmpDirPath, d.ref.signaturePath(0)) |
404 | 417 | if err := ensureParentDirectoryExists(path); err != nil { |
405 | 418 | return err |
415 | 428 | return nil |
416 | 429 | } |
417 | 430 | |
418 | func (d *ostreeImageDestination) Commit(ctx context.Context) error { | |
431 | func (d *ostreeImageDestination) Commit(context.Context, types.UnparsedImage) error { | |
419 | 432 | runtime.LockOSThread() |
420 | 433 | defer runtime.UnlockOSThread() |
421 | 434 |
97 | 97 | |
98 | 98 | // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). |
99 | 99 | // It may use a remote (= slow) service. |
100 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
101 | // as the primary manifest can not be a list, so there can be non-default instances. | |
100 | 102 | func (s *ostreeImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { |
101 | 103 | if instanceDigest != nil { |
102 | return nil, "", errors.Errorf(`Manifest lists are not supported by "ostree:"`) | |
104 | return nil, "", errors.New(`Manifest lists are not supported by "ostree:"`) | |
103 | 105 | } |
104 | 106 | if s.repo == nil { |
105 | 107 | repo, err := openRepo(s.ref.repo) |
274 | 276 | |
275 | 277 | // Ensure s.compressed is initialized. It is build by LayerInfosForCopy. |
276 | 278 | if s.compressed == nil { |
277 | _, err := s.LayerInfosForCopy(ctx) | |
279 | _, err := s.LayerInfosForCopy(ctx, nil) | |
278 | 280 | if err != nil { |
279 | 281 | return nil, -1, err |
280 | 282 | } |
336 | 338 | return rc, layerSize, nil |
337 | 339 | } |
338 | 340 | |
341 | // GetSignatures returns the image's signatures. It may use a remote (= slow) service. | |
342 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
343 | // as there can be no secondary manifests. | |
339 | 344 | func (s *ostreeImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { |
340 | 345 | if instanceDigest != nil { |
341 | return nil, errors.New("manifest lists are not supported by this transport") | |
346 | return nil, errors.New(`Manifest lists are not supported by "ostree:"`) | |
342 | 347 | } |
343 | 348 | lenSignatures, err := s.getLenSignatures() |
344 | 349 | if err != nil { |
371 | 376 | return signatures, nil |
372 | 377 | } |
373 | 378 | |
374 | // LayerInfosForCopy() returns the list of layer blobs that make up the root filesystem of | |
375 | // the image, after they've been decompressed. | |
376 | func (s *ostreeImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
379 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
380 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
381 | // to read the image's layers. | |
382 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
383 | // as the primary manifest can not be a list, so there can be secondary manifests. | |
384 | // The Digest field is guaranteed to be provided; Size may be -1. | |
385 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
386 | func (s *ostreeImageSource) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) { | |
387 | if instanceDigest != nil { | |
388 | return nil, errors.New(`Manifest lists are not supported by "ostree:"`) | |
389 | } | |
390 | ||
377 | 391 | updatedBlobInfos := []types.BlobInfo{} |
378 | 392 | manifestBlob, manifestType, err := s.GetManifest(ctx, nil) |
379 | 393 | if err != nil { |
5 | 5 | "bytes" |
6 | 6 | "context" |
7 | 7 | "encoding/json" |
8 | stderrors "errors" | |
8 | 9 | "fmt" |
9 | 10 | "io" |
10 | 11 | "io/ioutil" |
31 | 32 | var ( |
32 | 33 | // ErrBlobDigestMismatch is returned when PutBlob() is given a blob |
33 | 34 | // with a digest-based name that doesn't match its contents. |
34 | ErrBlobDigestMismatch = errors.New("blob digest mismatch") | |
35 | ErrBlobDigestMismatch = stderrors.New("blob digest mismatch") | |
35 | 36 | // ErrBlobSizeMismatch is returned when PutBlob() is given a blob |
36 | 37 | // with an expected size that doesn't match the reader. |
37 | ErrBlobSizeMismatch = errors.New("blob size mismatch") | |
38 | // ErrNoManifestLists is returned when GetManifest() is called. | |
39 | // with a non-nil instanceDigest. | |
40 | ErrNoManifestLists = errors.New("manifest lists are not supported by this transport") | |
38 | ErrBlobSizeMismatch = stderrors.New("blob size mismatch") | |
41 | 39 | // ErrNoSuchImage is returned when we attempt to access an image which |
42 | 40 | // doesn't exist in the storage area. |
43 | 41 | ErrNoSuchImage = storage.ErrNotAnImage |
44 | 42 | ) |
45 | 43 | |
46 | 44 | type storageImageSource struct { |
47 | imageRef storageReference | |
48 | image *storage.Image | |
49 | layerPosition map[digest.Digest]int // Where we are in reading a blob's layers | |
50 | cachedManifest []byte // A cached copy of the manifest, if already known, or nil | |
51 | getBlobMutex sync.Mutex // Mutex to sync state for parallel GetBlob executions | |
52 | SignatureSizes []int `json:"signature-sizes,omitempty"` // List of sizes of each signature slice | |
45 | imageRef storageReference | |
46 | image *storage.Image | |
47 | layerPosition map[digest.Digest]int // Where we are in reading a blob's layers | |
48 | cachedManifest []byte // A cached copy of the manifest, if already known, or nil | |
49 | getBlobMutex sync.Mutex // Mutex to sync state for parallel GetBlob executions | |
50 | SignatureSizes []int `json:"signature-sizes,omitempty"` // List of sizes of each signature slice | |
51 | SignaturesSizes map[digest.Digest][]int `json:"signatures-sizes,omitempty"` // List of sizes of each signature slice | |
53 | 52 | } |
54 | 53 | |
55 | 54 | type storageImageDestination struct { |
56 | imageRef storageReference | |
57 | directory string // Temporary directory where we store blobs until Commit() time | |
58 | nextTempFileID int32 // A counter that we use for computing filenames to assign to blobs | |
59 | manifest []byte // Manifest contents, temporary | |
60 | signatures []byte // Signature contents, temporary | |
61 | putBlobMutex sync.Mutex // Mutex to sync state for parallel PutBlob executions | |
62 | blobDiffIDs map[digest.Digest]digest.Digest // Mapping from layer blobsums to their corresponding DiffIDs | |
63 | fileSizes map[digest.Digest]int64 // Mapping from layer blobsums to their sizes | |
64 | filenames map[digest.Digest]string // Mapping from layer blobsums to names of files we used to hold them | |
65 | SignatureSizes []int `json:"signature-sizes,omitempty"` // List of sizes of each signature slice | |
55 | imageRef storageReference | |
56 | directory string // Temporary directory where we store blobs until Commit() time | |
57 | nextTempFileID int32 // A counter that we use for computing filenames to assign to blobs | |
58 | manifest []byte // Manifest contents, temporary | |
59 | signatures []byte // Signature contents, temporary | |
60 | signatureses map[digest.Digest][]byte // Instance signature contents, temporary | |
61 | putBlobMutex sync.Mutex // Mutex to sync state for parallel PutBlob executions | |
62 | blobDiffIDs map[digest.Digest]digest.Digest // Mapping from layer blobsums to their corresponding DiffIDs | |
63 | fileSizes map[digest.Digest]int64 // Mapping from layer blobsums to their sizes | |
64 | filenames map[digest.Digest]string // Mapping from layer blobsums to names of files we used to hold them | |
65 | SignatureSizes []int `json:"signature-sizes,omitempty"` // List of sizes of each signature slice | |
66 | SignaturesSizes map[digest.Digest][]int `json:"signatures-sizes,omitempty"` // Sizes of each manifest's signature slice | |
66 | 67 | } |
67 | 68 | |
68 | 69 | type storageImageCloser struct { |
71 | 72 | } |
72 | 73 | |
73 | 74 | // manifestBigDataKey returns a key suitable for recording a manifest with the specified digest using storage.Store.ImageBigData and related functions. |
74 | // If a specific manifest digest is explicitly requested by the user, the key retruned function should be used preferably; | |
75 | // If a specific manifest digest is explicitly requested by the user, the key returned by this function should be used preferably; | |
75 | 76 | // for compatibility, if a manifest is not available under this key, check also storage.ImageDigestBigDataKey |
76 | 77 | func manifestBigDataKey(digest digest.Digest) string { |
77 | 78 | return storage.ImageDigestManifestBigDataNamePrefix + "-" + digest.String() |
78 | 79 | } |
79 | 80 | |
81 | // signatureBigDataKey returns a key suitable for recording the signatures associated with the manifest with the specified digest using storage.Store.ImageBigData and related functions. | |
82 | // If a specific manifest digest is explicitly requested by the user, the key returned by this function should be used preferably; | |
83 | func signatureBigDataKey(digest digest.Digest) string { | |
84 | return "signature-" + digest.Encoded() | |
85 | } | |
86 | ||
80 | 87 | // newImageSource sets up an image for reading. |
81 | func newImageSource(imageRef storageReference) (*storageImageSource, error) { | |
88 | func newImageSource(ctx context.Context, sys *types.SystemContext, imageRef storageReference) (*storageImageSource, error) { | |
82 | 89 | // First, locate the image. |
83 | img, err := imageRef.resolveImage() | |
90 | img, err := imageRef.resolveImage(sys) | |
84 | 91 | if err != nil { |
85 | 92 | return nil, err |
86 | 93 | } |
87 | 94 | |
88 | 95 | // Build the reader object. |
89 | 96 | image := &storageImageSource{ |
90 | imageRef: imageRef, | |
91 | image: img, | |
92 | layerPosition: make(map[digest.Digest]int), | |
93 | SignatureSizes: []int{}, | |
97 | imageRef: imageRef, | |
98 | image: img, | |
99 | layerPosition: make(map[digest.Digest]int), | |
100 | SignatureSizes: []int{}, | |
101 | SignaturesSizes: make(map[digest.Digest][]int), | |
94 | 102 | } |
95 | 103 | if img.Metadata != "" { |
96 | 104 | if err := json.Unmarshal([]byte(img.Metadata), image); err != nil { |
181 | 189 | // GetManifest() reads the image's manifest. |
182 | 190 | func (s *storageImageSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) (manifestBlob []byte, MIMEType string, err error) { |
183 | 191 | if instanceDigest != nil { |
184 | return nil, "", ErrNoManifestLists | |
192 | key := manifestBigDataKey(*instanceDigest) | |
193 | blob, err := s.imageRef.transport.store.ImageBigData(s.image.ID, key) | |
194 | if err != nil { | |
195 | return nil, "", errors.Wrapf(err, "error reading manifest for image instance %q", *instanceDigest) | |
196 | } | |
197 | return blob, manifest.GuessMIMEType(blob), err | |
185 | 198 | } |
186 | 199 | if len(s.cachedManifest) == 0 { |
187 | 200 | // The manifest is stored as a big data item. |
213 | 226 | |
214 | 227 | // LayerInfosForCopy() returns the list of layer blobs that make up the root filesystem of |
215 | 228 | // the image, after they've been decompressed. |
216 | func (s *storageImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
217 | manifestBlob, manifestType, err := s.GetManifest(ctx, nil) | |
229 | func (s *storageImageSource) LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]types.BlobInfo, error) { | |
230 | manifestBlob, manifestType, err := s.GetManifest(ctx, instanceDigest) | |
218 | 231 | if err != nil { |
219 | 232 | return nil, errors.Wrapf(err, "error reading image manifest for %q", s.image.ID) |
233 | } | |
234 | if manifest.MIMETypeIsMultiImage(manifestType) { | |
235 | return nil, errors.Errorf("can't copy layers for a manifest list (shouldn't be attempted)") | |
220 | 236 | } |
221 | 237 | man, err := manifest.FromBlob(manifestBlob, manifestType) |
222 | 238 | if err != nil { |
291 | 307 | |
292 | 308 | // GetSignatures() parses the image's signatures blob into a slice of byte slices. |
293 | 309 | func (s *storageImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) (signatures [][]byte, err error) { |
294 | if instanceDigest != nil { | |
295 | return nil, ErrNoManifestLists | |
296 | } | |
297 | 310 | var offset int |
298 | 311 | sigslice := [][]byte{} |
299 | 312 | signature := []byte{} |
300 | if len(s.SignatureSizes) > 0 { | |
301 | signatureBlob, err := s.imageRef.transport.store.ImageBigData(s.image.ID, "signatures") | |
313 | signatureSizes := s.SignatureSizes | |
314 | key := "signatures" | |
315 | instance := "default instance" | |
316 | if instanceDigest != nil { | |
317 | signatureSizes = s.SignaturesSizes[*instanceDigest] | |
318 | key = signatureBigDataKey(*instanceDigest) | |
319 | instance = instanceDigest.Encoded() | |
320 | } | |
321 | if len(signatureSizes) > 0 { | |
322 | signatureBlob, err := s.imageRef.transport.store.ImageBigData(s.image.ID, key) | |
302 | 323 | if err != nil { |
303 | return nil, errors.Wrapf(err, "error looking up signatures data for image %q", s.image.ID) | |
324 | return nil, errors.Wrapf(err, "error looking up signatures data for image %q (%s)", s.image.ID, instance) | |
304 | 325 | } |
305 | 326 | signature = signatureBlob |
306 | 327 | } |
307 | for _, length := range s.SignatureSizes { | |
328 | for _, length := range signatureSizes { | |
329 | if offset+length > len(signature) { | |
330 | return nil, errors.Wrapf(err, "error looking up signatures data for image %q (%s): expected at least %d bytes, only found %d", s.image.ID, instance, len(signature), offset+length) | |
331 | } | |
308 | 332 | sigslice = append(sigslice, signature[offset:offset+length]) |
309 | 333 | offset += length |
310 | 334 | } |
311 | 335 | if offset != len(signature) { |
312 | return nil, errors.Errorf("signatures data contained %d extra bytes", len(signatures)-offset) | |
336 | return nil, errors.Errorf("signatures data (%s) contained %d extra bytes", instance, len(signatures)-offset) | |
313 | 337 | } |
314 | 338 | return sigslice, nil |
315 | 339 | } |
322 | 346 | return nil, errors.Wrapf(err, "error creating a temporary directory") |
323 | 347 | } |
324 | 348 | image := &storageImageDestination{ |
325 | imageRef: imageRef, | |
326 | directory: directory, | |
327 | blobDiffIDs: make(map[digest.Digest]digest.Digest), | |
328 | fileSizes: make(map[digest.Digest]int64), | |
329 | filenames: make(map[digest.Digest]string), | |
330 | SignatureSizes: []int{}, | |
349 | imageRef: imageRef, | |
350 | directory: directory, | |
351 | signatureses: make(map[digest.Digest][]byte), | |
352 | blobDiffIDs: make(map[digest.Digest]digest.Digest), | |
353 | fileSizes: make(map[digest.Digest]int64), | |
354 | filenames: make(map[digest.Digest]string), | |
355 | SignatureSizes: []int{}, | |
356 | SignaturesSizes: make(map[digest.Digest][]int), | |
331 | 357 | } |
332 | 358 | return image, nil |
333 | 359 | } |
403 | 429 | } |
404 | 430 | // Ensure that any information that we were given about the blob is correct. |
405 | 431 | if blobinfo.Digest.Validate() == nil && blobinfo.Digest != hasher.Digest() { |
406 | return errorBlobInfo, ErrBlobDigestMismatch | |
432 | return errorBlobInfo, errors.WithStack(ErrBlobDigestMismatch) | |
407 | 433 | } |
408 | 434 | if blobinfo.Size >= 0 && blobinfo.Size != counter.Count { |
409 | return errorBlobInfo, ErrBlobSizeMismatch | |
435 | return errorBlobInfo, errors.WithStack(ErrBlobSizeMismatch) | |
410 | 436 | } |
411 | 437 | // Record information about the blob. |
412 | 438 | s.putBlobMutex.Lock() |
578 | 604 | return nil, errors.New("blob not found") |
579 | 605 | } |
580 | 606 | |
581 | func (s *storageImageDestination) Commit(ctx context.Context) error { | |
607 | func (s *storageImageDestination) Commit(ctx context.Context, unparsedToplevel types.UnparsedImage) error { | |
608 | if len(s.manifest) == 0 { | |
609 | return errors.New("Internal error: storageImageDestination.Commit() called without PutManifest()") | |
610 | } | |
611 | toplevelManifest, _, err := unparsedToplevel.Manifest(ctx) | |
612 | if err != nil { | |
613 | return errors.Wrapf(err, "error retrieving top-level manifest") | |
614 | } | |
615 | // If the name we're saving to includes a digest, then check that the | |
616 | // manifests that we're about to save all either match the one from the | |
617 | // unparsedToplevel, or match the digest in the name that we're using. | |
618 | if s.imageRef.named != nil { | |
619 | if digested, ok := s.imageRef.named.(reference.Digested); ok { | |
620 | matches, err := manifest.MatchesDigest(s.manifest, digested.Digest()) | |
621 | if err != nil { | |
622 | return err | |
623 | } | |
624 | if !matches { | |
625 | matches, err = manifest.MatchesDigest(toplevelManifest, digested.Digest()) | |
626 | if err != nil { | |
627 | return err | |
628 | } | |
629 | } | |
630 | if !matches { | |
631 | return fmt.Errorf("Manifest to be saved does not match expected digest %s", digested.Digest()) | |
632 | } | |
633 | } | |
634 | } | |
582 | 635 | // Find the list of layer blobs. |
583 | 636 | if len(s.manifest) == 0 { |
584 | 637 | return errors.New("Internal error: storageImageDestination.Commit() called without PutManifest()") |
746 | 799 | return errors.Wrapf(err, "error saving big data %q for image %q", blob.String(), img.ID) |
747 | 800 | } |
748 | 801 | } |
749 | // Set the reference's name on the image. | |
802 | // Set the reference's name on the image. We don't need to worry about avoiding duplicate | |
803 | // values because SetNames() will deduplicate the list that we pass to it. | |
750 | 804 | if name := s.imageRef.DockerReference(); len(oldNames) > 0 || name != nil { |
751 | 805 | names := []string{} |
752 | 806 | if name != nil { |
764 | 818 | } |
765 | 819 | logrus.Debugf("set names of image %q to %v", img.ID, names) |
766 | 820 | } |
767 | // Save the manifest. Allow looking it up by digest by using the key convention defined by the Store. | |
821 | // Save the unparsedToplevel's manifest. | |
822 | if len(toplevelManifest) != 0 { | |
823 | manifestDigest, err := manifest.Digest(toplevelManifest) | |
824 | if err != nil { | |
825 | return errors.Wrapf(err, "error digesting top-level manifest") | |
826 | } | |
827 | key := manifestBigDataKey(manifestDigest) | |
828 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, key, toplevelManifest, manifest.Digest); err != nil { | |
829 | if _, err2 := s.imageRef.transport.store.DeleteImage(img.ID, true); err2 != nil { | |
830 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) | |
831 | } | |
832 | logrus.Debugf("error saving top-level manifest for image %q: %v", img.ID, err) | |
833 | return errors.Wrapf(err, "error saving top-level manifest for image %q", img.ID) | |
834 | } | |
835 | } | |
836 | // Save the image's manifest. Allow looking it up by digest by using the key convention defined by the Store. | |
768 | 837 | // Record the manifest twice: using a digest-specific key to allow references to that specific digest instance, |
769 | 838 | // and using storage.ImageDigestBigDataKey for future users that don’t specify any digest and for compatibility with older readers. |
770 | 839 | manifestDigest, err := manifest.Digest(s.manifest) |
771 | 840 | if err != nil { |
772 | 841 | return errors.Wrapf(err, "error computing manifest digest") |
773 | 842 | } |
774 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, manifestBigDataKey(manifestDigest), s.manifest, manifest.Digest); err != nil { | |
843 | key := manifestBigDataKey(manifestDigest) | |
844 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, key, s.manifest, manifest.Digest); err != nil { | |
775 | 845 | if _, err2 := s.imageRef.transport.store.DeleteImage(img.ID, true); err2 != nil { |
776 | 846 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) |
777 | 847 | } |
778 | 848 | logrus.Debugf("error saving manifest for image %q: %v", img.ID, err) |
779 | return err | |
780 | } | |
781 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, storage.ImageDigestBigDataKey, s.manifest, manifest.Digest); err != nil { | |
849 | return errors.Wrapf(err, "error saving manifest for image %q", img.ID) | |
850 | } | |
851 | key = storage.ImageDigestBigDataKey | |
852 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, key, s.manifest, manifest.Digest); err != nil { | |
782 | 853 | if _, err2 := s.imageRef.transport.store.DeleteImage(img.ID, true); err2 != nil { |
783 | 854 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) |
784 | 855 | } |
785 | 856 | logrus.Debugf("error saving manifest for image %q: %v", img.ID, err) |
786 | return err | |
857 | return errors.Wrapf(err, "error saving manifest for image %q", img.ID) | |
787 | 858 | } |
788 | 859 | // Save the signatures, if we have any. |
789 | 860 | if len(s.signatures) > 0 { |
792 | 863 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) |
793 | 864 | } |
794 | 865 | logrus.Debugf("error saving signatures for image %q: %v", img.ID, err) |
795 | return err | |
866 | return errors.Wrapf(err, "error saving signatures for image %q", img.ID) | |
867 | } | |
868 | } | |
869 | for instanceDigest, signatures := range s.signatureses { | |
870 | key := signatureBigDataKey(instanceDigest) | |
871 | if err := s.imageRef.transport.store.SetImageBigData(img.ID, key, signatures, manifest.Digest); err != nil { | |
872 | if _, err2 := s.imageRef.transport.store.DeleteImage(img.ID, true); err2 != nil { | |
873 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) | |
874 | } | |
875 | logrus.Debugf("error saving signatures for image %q: %v", img.ID, err) | |
876 | return errors.Wrapf(err, "error saving signatures for image %q", img.ID) | |
796 | 877 | } |
797 | 878 | } |
798 | 879 | // Save our metadata. |
802 | 883 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) |
803 | 884 | } |
804 | 885 | logrus.Debugf("error encoding metadata for image %q: %v", img.ID, err) |
805 | return err | |
886 | return errors.Wrapf(err, "error encoding metadata for image %q", img.ID) | |
806 | 887 | } |
807 | 888 | if len(metadata) != 0 { |
808 | 889 | if err = s.imageRef.transport.store.SetMetadata(img.ID, string(metadata)); err != nil { |
810 | 891 | logrus.Debugf("error deleting incomplete image %q: %v", img.ID, err2) |
811 | 892 | } |
812 | 893 | logrus.Debugf("error saving metadata for image %q: %v", img.ID, err) |
813 | return err | |
894 | return errors.Wrapf(err, "error saving metadata for image %q", img.ID) | |
814 | 895 | } |
815 | 896 | logrus.Debugf("saved image metadata %q", string(metadata)) |
816 | 897 | } |
829 | 910 | } |
830 | 911 | |
831 | 912 | // PutManifest writes the manifest to the destination. |
832 | func (s *storageImageDestination) PutManifest(ctx context.Context, manifestBlob []byte) error { | |
833 | if s.imageRef.named != nil { | |
834 | if digested, ok := s.imageRef.named.(reference.Digested); ok { | |
835 | matches, err := manifest.MatchesDigest(manifestBlob, digested.Digest()) | |
836 | if err != nil { | |
837 | return err | |
838 | } | |
839 | if !matches { | |
840 | return fmt.Errorf("Manifest does not match expected digest %s", digested.Digest()) | |
841 | } | |
842 | } | |
843 | } | |
844 | ||
845 | s.manifest = make([]byte, len(manifestBlob)) | |
846 | copy(s.manifest, manifestBlob) | |
913 | func (s *storageImageDestination) PutManifest(ctx context.Context, manifestBlob []byte, instanceDigest *digest.Digest) error { | |
914 | newBlob := make([]byte, len(manifestBlob)) | |
915 | copy(newBlob, manifestBlob) | |
916 | s.manifest = newBlob | |
847 | 917 | return nil |
848 | 918 | } |
849 | 919 | |
872 | 942 | } |
873 | 943 | |
874 | 944 | // PutSignatures records the image's signatures for committing as a single data blob. |
875 | func (s *storageImageDestination) PutSignatures(ctx context.Context, signatures [][]byte) error { | |
945 | func (s *storageImageDestination) PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error { | |
876 | 946 | sizes := []int{} |
877 | 947 | sigblob := []byte{} |
878 | 948 | for _, sig := range signatures { |
882 | 952 | copy(newblob[len(sigblob):], sig) |
883 | 953 | sigblob = newblob |
884 | 954 | } |
885 | s.signatures = sigblob | |
886 | s.SignatureSizes = sizes | |
955 | if instanceDigest == nil { | |
956 | s.signatures = sigblob | |
957 | s.SignatureSizes = sizes | |
958 | } | |
959 | if instanceDigest == nil && len(s.manifest) > 0 { | |
960 | manifestDigest, err := manifest.Digest(s.manifest) | |
961 | if err != nil { | |
962 | return err | |
963 | } | |
964 | instanceDigest = &manifestDigest | |
965 | } | |
966 | if instanceDigest != nil { | |
967 | s.signatureses[*instanceDigest] = sigblob | |
968 | s.SignaturesSizes[*instanceDigest] = sizes | |
969 | } | |
887 | 970 | return nil |
888 | 971 | } |
889 | 972 | |
939 | 1022 | |
940 | 1023 | // newImage creates an image that also knows its size |
941 | 1024 | func newImage(ctx context.Context, sys *types.SystemContext, s storageReference) (types.ImageCloser, error) { |
942 | src, err := newImageSource(s) | |
1025 | src, err := newImageSource(ctx, sys, s) | |
943 | 1026 | if err != nil { |
944 | 1027 | return nil, err |
945 | 1028 | } |
6 | 6 | "strings" |
7 | 7 | |
8 | 8 | "github.com/containers/image/v4/docker/reference" |
9 | "github.com/containers/image/v4/manifest" | |
9 | 10 | "github.com/containers/image/v4/types" |
10 | 11 | "github.com/containers/storage" |
12 | digest "github.com/opencontainers/go-digest" | |
13 | imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" | |
11 | 14 | "github.com/pkg/errors" |
12 | 15 | "github.com/sirupsen/logrus" |
13 | 16 | ) |
50 | 53 | return false |
51 | 54 | } |
52 | 55 | |
56 | // imageMatchesSystemContext checks if the passed-in image both contains a | |
57 | // manifest that matches the passed-in digest, and identifies itself as being | |
58 | // appropriate for running on the system that matches sys. | |
59 | // If we somehow ended up sharing the same storage among multiple types of | |
60 | // systems, and managed to download multiple images from the same manifest | |
61 | // list, their image records will all contain copies of the manifest list, and | |
62 | // this check will help us decide which of them we want to return when we've | |
63 | // been asked to resolve an image reference that uses the list's digest to a | |
64 | // specific image ID. | |
65 | func imageMatchesSystemContext(store storage.Store, img *storage.Image, manifestDigest digest.Digest, sys *types.SystemContext) bool { | |
66 | // First, check if the image record has a manifest that matches the | |
67 | // specified digest. | |
68 | key := manifestBigDataKey(manifestDigest) | |
69 | manifestBytes, err := store.ImageBigData(img.ID, key) | |
70 | if err != nil { | |
71 | return false | |
72 | } | |
73 | // The manifest is either a list, or not a list. If it's a list, find | |
74 | // the digest of the instance that matches the current system, and try | |
75 | // to load that manifest from the image record, and use it. | |
76 | manifestType := manifest.GuessMIMEType(manifestBytes) | |
77 | if manifest.MIMETypeIsMultiImage(manifestType) { | |
78 | list, err := manifest.ListFromBlob(manifestBytes, manifestType) | |
79 | if err != nil { | |
80 | return false | |
81 | } | |
82 | manifestDigest, err = list.ChooseInstance(sys) | |
83 | if err != nil { | |
84 | return false | |
85 | } | |
86 | key = manifestBigDataKey(manifestDigest) | |
87 | manifestBytes, err = store.ImageBigData(img.ID, key) | |
88 | if err != nil { | |
89 | return false | |
90 | } | |
91 | manifestType = manifest.GuessMIMEType(manifestBytes) | |
92 | } | |
93 | // Load the image's configuration blob. | |
94 | m, err := manifest.FromBlob(manifestBytes, manifestType) | |
95 | getConfig := func(blobInfo types.BlobInfo) ([]byte, error) { | |
96 | return store.ImageBigData(img.ID, blobInfo.Digest.String()) | |
97 | } | |
98 | ii, err := m.Inspect(getConfig) | |
99 | if err != nil { | |
100 | return false | |
101 | } | |
102 | // Build a dummy index containing one instance and information about | |
103 | // the image's target system from the image's configuration. | |
104 | index := manifest.OCI1IndexFromComponents([]imgspecv1.Descriptor{{ | |
105 | MediaType: imgspecv1.MediaTypeImageManifest, | |
106 | Digest: manifestDigest, | |
107 | Size: int64(len(manifestBytes)), | |
108 | Platform: &imgspecv1.Platform{ | |
109 | OS: ii.Os, | |
110 | Architecture: ii.Architecture, | |
111 | }, | |
112 | }}, nil) | |
113 | // Check that ChooseInstance() would select this image for this system, | |
114 | // from a list of images. | |
115 | instanceDigest, err := index.ChooseInstance(sys) | |
116 | if err != nil { | |
117 | return false | |
118 | } | |
119 | // Double-check that we can read the runnable image's manifest from the | |
120 | // image record. | |
121 | key = manifestBigDataKey(instanceDigest) | |
122 | _, err = store.ImageBigData(img.ID, key) | |
123 | return err == nil | |
124 | } | |
125 | ||
53 | 126 | // Resolve the reference's name to an image ID in the store, if there's already |
54 | 127 | // one present with the same name or ID, and return the image. |
55 | func (s *storageReference) resolveImage() (*storage.Image, error) { | |
128 | func (s *storageReference) resolveImage(sys *types.SystemContext) (*storage.Image, error) { | |
56 | 129 | var loadedImage *storage.Image |
57 | 130 | if s.id == "" && s.named != nil { |
58 | 131 | // Look for an image that has the expanded reference name as an explicit Name value. |
71 | 144 | if err == nil && len(images) > 0 { |
72 | 145 | for _, image := range images { |
73 | 146 | if imageMatchesRepo(image, s.named) { |
74 | loadedImage = image | |
75 | s.id = image.ID | |
76 | break | |
147 | if loadedImage == nil || imageMatchesSystemContext(s.transport.store, image, digested.Digest(), sys) { | |
148 | loadedImage = image | |
149 | s.id = image.ID | |
150 | } | |
77 | 151 | } |
78 | 152 | } |
79 | 153 | } |
201 | 275 | } |
202 | 276 | |
203 | 277 | func (s storageReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { |
204 | img, err := s.resolveImage() | |
278 | img, err := s.resolveImage(sys) | |
205 | 279 | if err != nil { |
206 | 280 | return err |
207 | 281 | } |
216 | 290 | } |
217 | 291 | |
218 | 292 | func (s storageReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { |
219 | return newImageSource(s) | |
293 | return newImageSource(ctx, sys, s) | |
220 | 294 | } |
221 | 295 | |
222 | 296 | func (s storageReference) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) { |
13 | 13 | "io/ioutil" |
14 | 14 | "os" |
15 | 15 | "path/filepath" |
16 | "reflect" | |
16 | 17 | "strings" |
17 | 18 | "testing" |
18 | 19 | "time" |
19 | 20 | |
21 | imanifest "github.com/containers/image/v4/manifest" | |
20 | 22 | "github.com/containers/image/v4/pkg/blobinfocache/memory" |
21 | 23 | "github.com/containers/image/v4/types" |
22 | 24 | "github.com/containers/storage" |
269 | 271 | } |
270 | 272 | if n != len(buf) { |
271 | 273 | t.Fatalf("Short write writing tar header: %d < %d", n, len(buf)) |
274 | } | |
275 | err = twriter.Flush() | |
276 | if err != nil { | |
277 | t.Fatalf("Error flushing output to tar archive: %v", err) | |
272 | 278 | } |
273 | 279 | }() |
274 | 280 | _, err = io.Copy(&tbuffer, preader) |
393 | 399 | manifest = strings.Replace(manifest, "%li", li, -1) |
394 | 400 | manifest = strings.Replace(manifest, "%ci", sum.Hex(), -1) |
395 | 401 | t.Logf("this manifest is %q", manifest) |
396 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
402 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
397 | 403 | t.Fatalf("Error saving manifest to destination: %v", err) |
398 | 404 | } |
399 | if err := dest.PutSignatures(context.Background(), signatures); err != nil { | |
405 | if err := dest.PutSignatures(context.Background(), signatures, nil); err != nil { | |
400 | 406 | t.Fatalf("Error saving signatures to destination: %v", err) |
401 | 407 | } |
402 | if err := dest.Commit(context.Background()); err != nil { | |
408 | unparsedToplevel := unparsedImage{ | |
409 | imageReference: nil, | |
410 | manifestBytes: []byte(manifest), | |
411 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
412 | signatures: signatures, | |
413 | } | |
414 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
403 | 415 | t.Fatalf("Error committing changes to destination: %v", err) |
404 | 416 | } |
405 | 417 | dest.Close() |
453 | 465 | t.Fatalf("GetManifest(%q) returned error %v", ref.StringWithinTransport(), err) |
454 | 466 | } |
455 | 467 | t.Logf("this manifest's type appears to be %q", manifestType) |
456 | sum = ddigest.SHA256.FromBytes([]byte(manifest)) | |
457 | _, _, err = src.GetManifest(context.Background(), &sum) | |
458 | if err == nil { | |
459 | t.Fatalf("GetManifest(%q) with an instanceDigest is supposed to fail", ref.StringWithinTransport()) | |
468 | sum, err = imanifest.Digest([]byte(manifest)) | |
469 | if err != nil { | |
470 | t.Fatalf("manifest.Digest() returned error %v", err) | |
471 | } | |
472 | retrieved, _, err := src.GetManifest(context.Background(), &sum) | |
473 | if err != nil { | |
474 | t.Fatalf("GetManifest(%q) with an instanceDigest is supposed to succeed", ref.StringWithinTransport()) | |
475 | } | |
476 | if string(retrieved) != string(manifest) { | |
477 | t.Fatalf("GetManifest(%q) with an instanceDigest retrieved a different manifest", ref.StringWithinTransport()) | |
460 | 478 | } |
461 | 479 | sigs, err := src.GetSignatures(context.Background(), nil) |
462 | 480 | if err != nil { |
473 | 491 | t.Fatalf("Signature %d was corrupted", i) |
474 | 492 | } |
475 | 493 | } |
476 | _, err = src.GetSignatures(context.Background(), &sum) | |
477 | if err == nil { | |
478 | t.Fatalf("GetSignatures(%q) with instanceDigest is supposed to fail", ref.StringWithinTransport()) | |
494 | sigs2, err := src.GetSignatures(context.Background(), &sum) | |
495 | if err != nil { | |
496 | t.Fatalf("GetSignatures(%q) with instance %s returned error %v", ref.StringWithinTransport(), sum.String(), err) | |
497 | } | |
498 | if !reflect.DeepEqual(sigs, sigs2) { | |
499 | t.Fatalf("GetSignatures(%q) with instance %s returned a different result", ref.StringWithinTransport(), sum.String()) | |
479 | 500 | } |
480 | 501 | for _, layerInfo := range layerInfos { |
481 | 502 | buf := bytes.Buffer{} |
492 | 513 | t.Fatalf("Error decompressing layer %q from %q", layerInfo.Digest, ref.StringWithinTransport()) |
493 | 514 | } |
494 | 515 | n, err := io.Copy(&buf, decompressed) |
516 | layer.Close() | |
495 | 517 | if layerInfo.Size >= 0 && compressed.Count != layerInfo.Size { |
496 | 518 | t.Fatalf("Blob size is different than expected: %d != %d, read %d", compressed.Count, layerInfo.Size, n) |
497 | 519 | } |
555 | 577 | ] |
556 | 578 | } |
557 | 579 | `, digest, size) |
558 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
580 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
559 | 581 | t.Fatalf("Error storing manifest to destination: %v", err) |
560 | 582 | } |
561 | if err := dest.Commit(context.Background()); err != nil { | |
583 | unparsedToplevel := unparsedImage{ | |
584 | imageReference: nil, | |
585 | manifestBytes: []byte(manifest), | |
586 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
587 | signatures: nil, | |
588 | } | |
589 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
562 | 590 | t.Fatalf("Error committing changes to destination, first pass: %v", err) |
563 | 591 | } |
564 | 592 | dest.Close() |
590 | 618 | ] |
591 | 619 | } |
592 | 620 | `, digest, size) |
593 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
621 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
594 | 622 | t.Fatalf("Error storing manifest to destination: %v", err) |
595 | 623 | } |
596 | if err := dest.Commit(context.Background()); err != nil { | |
624 | unparsedToplevel = unparsedImage{ | |
625 | imageReference: nil, | |
626 | manifestBytes: []byte(manifest), | |
627 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
628 | signatures: nil, | |
629 | } | |
630 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
597 | 631 | t.Fatalf("Error committing changes to destination, second pass: %v", err) |
598 | 632 | } |
599 | 633 | dest.Close() |
642 | 676 | ] |
643 | 677 | } |
644 | 678 | `, digest, size) |
645 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
679 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
646 | 680 | t.Fatalf("Error storing manifest to destination: %v", err) |
647 | 681 | } |
648 | if err := dest.Commit(context.Background()); err != nil { | |
682 | unparsedToplevel := unparsedImage{ | |
683 | imageReference: nil, | |
684 | manifestBytes: []byte(manifest), | |
685 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
686 | signatures: nil, | |
687 | } | |
688 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
649 | 689 | t.Fatalf("Error committing changes to destination, first pass: %v", err) |
650 | 690 | } |
651 | 691 | dest.Close() |
677 | 717 | ] |
678 | 718 | } |
679 | 719 | `, digest, size) |
680 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
720 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
681 | 721 | t.Fatalf("Error storing manifest to destination: %v", err) |
682 | 722 | } |
683 | if err := dest.Commit(context.Background()); errors.Cause(err) != storage.ErrDuplicateID { | |
723 | unparsedToplevel = unparsedImage{ | |
724 | imageReference: nil, | |
725 | manifestBytes: []byte(manifest), | |
726 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
727 | signatures: nil, | |
728 | } | |
729 | if err := dest.Commit(context.Background(), &unparsedToplevel); errors.Cause(err) != storage.ErrDuplicateID { | |
684 | 730 | if err != nil { |
685 | 731 | t.Fatalf("Wrong error committing changes to destination, second pass: %v", err) |
686 | 732 | } |
732 | 778 | ] |
733 | 779 | } |
734 | 780 | `, digest, size) |
735 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
781 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
736 | 782 | t.Fatalf("Error storing manifest to destination: %v", err) |
737 | 783 | } |
738 | if err := dest.Commit(context.Background()); err != nil { | |
784 | unparsedToplevel := unparsedImage{ | |
785 | imageReference: nil, | |
786 | manifestBytes: []byte(manifest), | |
787 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
788 | signatures: nil, | |
789 | } | |
790 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
739 | 791 | t.Fatalf("Error committing changes to destination, first pass: %v", err) |
740 | 792 | } |
741 | 793 | dest.Close() |
767 | 819 | ] |
768 | 820 | } |
769 | 821 | `, digest, size) |
770 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
822 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
771 | 823 | t.Fatalf("Error storing manifest to destination: %v", err) |
772 | 824 | } |
773 | if err := dest.Commit(context.Background()); errors.Cause(err) != storage.ErrDuplicateID { | |
825 | unparsedToplevel = unparsedImage{ | |
826 | imageReference: nil, | |
827 | manifestBytes: []byte(manifest), | |
828 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
829 | signatures: nil, | |
830 | } | |
831 | if err := dest.Commit(context.Background(), &unparsedToplevel); errors.Cause(err) != storage.ErrDuplicateID { | |
774 | 832 | if err != nil { |
775 | 833 | t.Fatalf("Wrong error committing changes to destination, second pass: %v", err) |
776 | 834 | } |
888 | 946 | ] |
889 | 947 | } |
890 | 948 | `, configInfo.Size, configInfo.Digest, digest1, size1, digest2, size2) |
891 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
949 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
892 | 950 | t.Fatalf("Error storing manifest to destination: %v", err) |
893 | 951 | } |
894 | if err := dest.Commit(context.Background()); err != nil { | |
952 | unparsedToplevel := unparsedImage{ | |
953 | imageReference: nil, | |
954 | manifestBytes: []byte(manifest), | |
955 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
956 | signatures: nil, | |
957 | } | |
958 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
895 | 959 | t.Fatalf("Error committing changes to destination: %v", err) |
896 | 960 | } |
897 | 961 | dest.Close() |
904 | 968 | if usize == -1 || err != nil { |
905 | 969 | t.Fatalf("Error calculating image size: %v", err) |
906 | 970 | } |
907 | if int(usize) != len(config)+int(usize1)+int(usize2)+len(manifest) { | |
908 | t.Fatalf("Unexpected image size: %d != %d + %d + %d + %d", usize, len(config), usize1, usize2, len(manifest)) | |
971 | if int(usize) != len(config)+int(usize1)+int(usize2)+2*len(manifest) { | |
972 | t.Fatalf("Unexpected image size: %d != %d + %d + %d + %d (%d)", usize, len(config), usize1, usize2, len(manifest), len(config)+int(usize1)+int(usize2)+2*len(manifest)) | |
909 | 973 | } |
910 | 974 | img.Close() |
911 | 975 | } |
999 | 1063 | ] |
1000 | 1064 | } |
1001 | 1065 | `, configInfo.Size, configInfo.Digest, digest1, size1, digest2, size2, digest1, size1, digest2, size2) |
1002 | if err := dest.PutManifest(context.Background(), []byte(manifest)); err != nil { | |
1066 | if err := dest.PutManifest(context.Background(), []byte(manifest), nil); err != nil { | |
1003 | 1067 | t.Fatalf("Error storing manifest to destination: %v", err) |
1004 | 1068 | } |
1005 | if err := dest.Commit(context.Background()); err != nil { | |
1069 | unparsedToplevel := unparsedImage{ | |
1070 | imageReference: nil, | |
1071 | manifestBytes: []byte(manifest), | |
1072 | manifestType: imanifest.GuessMIMEType([]byte(manifest)), | |
1073 | signatures: nil, | |
1074 | } | |
1075 | if err := dest.Commit(context.Background(), &unparsedToplevel); err != nil { | |
1006 | 1076 | t.Fatalf("Error committing changes to destination: %v", err) |
1007 | 1077 | } |
1008 | 1078 | dest.Close() |
1046 | 1116 | src.Close() |
1047 | 1117 | img.Close() |
1048 | 1118 | } |
1119 | ||
1120 | type unparsedImage struct { | |
1121 | imageReference types.ImageReference | |
1122 | manifestBytes []byte | |
1123 | manifestType string | |
1124 | signatures [][]byte | |
1125 | } | |
1126 | ||
1127 | func (u *unparsedImage) Reference() types.ImageReference { | |
1128 | return u.imageReference | |
1129 | } | |
1130 | func (u *unparsedImage) Manifest(context.Context) ([]byte, string, error) { | |
1131 | return u.manifestBytes, u.manifestType, nil | |
1132 | } | |
1133 | func (u *unparsedImage) Signatures(context.Context) ([][]byte, error) { | |
1134 | return u.signatures, nil | |
1135 | } |
287 | 287 | } |
288 | 288 | if sref, ok := ref.(*storageReference); ok { |
289 | 289 | tmpRef := *sref |
290 | if img, err := tmpRef.resolveImage(); err == nil { | |
290 | if img, err := tmpRef.resolveImage(&types.SystemContext{}); err == nil { | |
291 | 291 | return img, nil |
292 | 292 | } |
293 | 293 | } |
247 | 247 | } |
248 | 248 | |
249 | 249 | // GetSignatures returns the image's signatures. It may use a remote (= slow) service. |
250 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve signatures for | |
251 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
252 | // (e.g. if the source never returns manifest lists). | |
250 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, | |
251 | // as there can be no secondary manifests. | |
253 | 252 | func (*tarballImageSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { |
254 | 253 | if instanceDigest != nil { |
255 | 254 | return nil, fmt.Errorf("manifest lists are not supported by the %q transport", transportName) |
261 | 260 | return &is.reference |
262 | 261 | } |
263 | 262 | |
264 | // LayerInfosForCopy() returns updated layer info that should be used when reading, in preference to values in the manifest, if specified. | |
265 | func (*tarballImageSource) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) { | |
263 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
264 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
265 | // to read the image's layers. | |
266 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
267 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
268 | // (e.g. if the source never returns manifest lists). | |
269 | // The Digest field is guaranteed to be provided; Size may be -1. | |
270 | // WARNING: The list may contain duplicates, and they are semantically relevant. | |
271 | func (*tarballImageSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { | |
266 | 272 | return nil, nil |
267 | 273 | } |
226 | 226 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list |
227 | 227 | // (e.g. if the source never returns manifest lists). |
228 | 228 | GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) |
229 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer blobsums that are listed in the image's manifest. | |
229 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer | |
230 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() | |
231 | // to read the image's layers. | |
232 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve BlobInfos for | |
233 | // (when the primary manifest is a manifest list); this never happens if the primary manifest is not a manifest list | |
234 | // (e.g. if the source never returns manifest lists). | |
230 | 235 | // The Digest field is guaranteed to be provided; Size may be -1. |
231 | 236 | // WARNING: The list may contain duplicates, and they are semantically relevant. |
232 | LayerInfosForCopy(ctx context.Context) ([]BlobInfo, error) | |
237 | LayerInfosForCopy(ctx context.Context, instanceDigest *digest.Digest) ([]BlobInfo, error) | |
233 | 238 | } |
234 | 239 | |
235 | 240 | // ImageDestination is a service, possibly remote (= slow), to store components of a single image. |
285 | 290 | // May use and/or update cache. |
286 | 291 | TryReusingBlob(ctx context.Context, info BlobInfo, cache BlobInfoCache, canSubstitute bool) (bool, BlobInfo, error) |
287 | 292 | // PutManifest writes manifest to the destination. |
293 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write the manifest for | |
294 | // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
295 | // It is expected but not enforced that the instanceDigest, when specified, matches the digest of `manifest` as generated | |
296 | // by `manifest.Digest()`. | |
288 | 297 | // FIXME? This should also receive a MIME type if known, to differentiate between schema versions. |
289 | 298 | // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), |
290 | 299 | // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. |
291 | PutManifest(ctx context.Context, manifest []byte) error | |
292 | PutSignatures(ctx context.Context, signatures [][]byte) error | |
300 | PutManifest(ctx context.Context, manifest []byte, instanceDigest *digest.Digest) error | |
301 | // PutSignatures writes a set of signatures to the destination. | |
302 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to write or overwrite the signatures for | |
303 | // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. | |
304 | // MUST be called after PutManifest (signatures may reference manifest contents). | |
305 | PutSignatures(ctx context.Context, signatures [][]byte, instanceDigest *digest.Digest) error | |
293 | 306 | // Commit marks the process of storing the image as successful and asks for the image to be persisted. |
294 | 307 | // WARNING: This does not have any transactional semantics: |
295 | 308 | // - Uploaded data MAY be visible to others before Commit() is called |
296 | 309 | // - Uploaded data MAY be removed or MAY remain around if Close() is called without Commit() (i.e. rollback is allowed but not guaranteed) |
297 | Commit(ctx context.Context) error | |
310 | Commit(ctx context.Context, unparsedToplevel UnparsedImage) error | |
298 | 311 | } |
299 | 312 | |
300 | 313 | // ManifestTypeRejectedError is returned by ImageDestination.PutManifest if the destination is in principle available, |