@@ -266,24 +266,27 @@ func (r *Registry) displayEvent(ev remotes.FixupEvent) {
266266}
267267
268268// GetCachedImage returns information about an image from local docker cache.
269- func (r * Registry ) GetCachedImage (ctx context.Context , ref cnab.OCIReference ) (ImageSummary , error ) {
269+ func (r * Registry ) GetCachedImage (ctx context.Context , ref cnab.OCIReference ) (ImageMetadata , error ) {
270270 image := ref .String ()
271271 ctx , log := tracing .StartSpan (ctx , attribute .String ("reference" , image ))
272272 defer log .EndSpan ()
273273
274274 cli , err := docker .GetDockerClient ()
275275 if err != nil {
276- return ImageSummary {}, log .Error (err )
276+ return ImageMetadata {}, log .Error (err )
277277 }
278278
279279 result , _ , err := cli .Client ().ImageInspectWithRaw (ctx , image )
280280 if err != nil {
281- return ImageSummary {}, log .Error (fmt .Errorf ("failed to find image in docker cache: %w" , ErrNotFound {Reference : ref }))
281+ err = fmt .Errorf ("failed to find image in docker cache: %w" , ErrNotFound {Reference : ref })
282+ // log as debug because this isn't a terminal error
283+ log .Debugf (err .Error ())
284+ return ImageMetadata {}, err
282285 }
283286
284- summary , err := NewImageSummary ( image , result )
287+ summary , err := NewImageSummaryFromInspect ( ref , result )
285288 if err != nil {
286- return ImageSummary {}, log .Error (fmt .Errorf ("failed to extract image %s in docker cache: %w" , image , err ))
289+ return ImageMetadata {}, log .Error (fmt .Errorf ("failed to extract image %s in docker cache: %w" , image , err ))
287290 }
288291
289292 return summary , nil
@@ -331,6 +334,41 @@ func (r *Registry) GetBundleMetadata(ctx context.Context, ref cnab.OCIReference,
331334 }, nil
332335}
333336
337+ // GetImageMetadata returns information about an image in a registry
338+ // Use ErrNotFound to detect if the error is because the image is not in the registry.
339+ func (r * Registry ) GetImageMetadata (ctx context.Context , ref cnab.OCIReference , opts RegistryOptions ) (ImageMetadata , error ) {
340+ ctx , span := tracing .StartSpan (ctx , attribute .String ("reference" , ref .String ()))
341+ defer span .EndSpan ()
342+
343+ // Check if we already have the image in the Docker cache
344+ cachedResult , err := r .GetCachedImage (ctx , ref )
345+ if err != nil {
346+ if ! errors .Is (err , ErrNotFound {}) {
347+ return ImageMetadata {}, err
348+ }
349+ }
350+
351+ // Check if we have the repository digest cached for the referenced image
352+ if cachedDigest , err := cachedResult .GetRepositoryDigest (); err == nil {
353+ span .SetAttributes (attribute .String ("cached-digest" , cachedDigest .String ()))
354+ return cachedResult , nil
355+ }
356+
357+ // Do a HEAD against the registry to retrieve image metadata without pulling the entire image contents
358+ desc , err := crane .Head (ref .String (), opts .toCraneOptions ()... )
359+ if err != nil {
360+ if notFoundErr := asNotFoundError (err , ref ); notFoundErr != nil {
361+ return ImageMetadata {}, span .Error (notFoundErr )
362+ }
363+ return ImageMetadata {}, span .Errorf ("error fetching image metadata for %s: %w" , ref , err )
364+ }
365+
366+ repoDigest := digest .NewDigestFromHex (desc .Digest .Algorithm , desc .Digest .Hex )
367+ span .SetAttributes (attribute .String ("fetched-digest" , repoDigest .String ()))
368+
369+ return NewImageSummaryFromDigest (ref , repoDigest )
370+ }
371+
334372// asNotFoundError checks if the error is an HTTP 404 not found error, and if so returns a corresponding ErrNotFound instance.
335373func asNotFoundError (err error , ref cnab.OCIReference ) error {
336374 var httpError * transport.Error
@@ -343,49 +381,57 @@ func asNotFoundError(err error, ref cnab.OCIReference) error {
343381 return nil
344382}
345383
346- // ImageSummary contains information about an OCI image.
347- type ImageSummary struct {
348- types. ImageInspect
349- imageRef cnab. OCIReference
384+ // ImageMetadata contains information about an OCI image.
385+ type ImageMetadata struct {
386+ Reference cnab. OCIReference
387+ RepoDigests [] string
350388}
351389
352- func NewImageSummary (imageRef string , sum types.ImageInspect ) (ImageSummary , error ) {
353- ref , err := cnab .ParseOCIReference (imageRef )
354- if err != nil {
355- return ImageSummary {}, err
356- }
357-
358- img := ImageSummary {
359- imageRef : ref ,
360- ImageInspect : sum ,
390+ func NewImageSummaryFromInspect (ref cnab.OCIReference , sum types.ImageInspect ) (ImageMetadata , error ) {
391+ img := ImageMetadata {
392+ Reference : ref ,
393+ RepoDigests : sum .RepoDigests ,
361394 }
362395 if img .IsZero () {
363- return ImageSummary {}, fmt .Errorf ("invalid image summary for image reference %s" , imageRef )
396+ return ImageMetadata {}, fmt .Errorf ("invalid image summary for image reference %s" , ref )
364397 }
365398
366399 return img , nil
367400}
368401
369- func (i ImageSummary ) GetImageReference () cnab.OCIReference {
370- return i .imageRef
402+ func NewImageSummaryFromDigest (ref cnab.OCIReference , repoDigest digest.Digest ) (ImageMetadata , error ) {
403+ digestedRef , err := ref .WithDigest (repoDigest )
404+ if err != nil {
405+ return ImageMetadata {}, fmt .Errorf ("error building an OCI reference from image %s and digest %s" , ref .Repository (), ref .Digest ())
406+ }
407+
408+ return ImageMetadata {
409+ Reference : ref ,
410+ RepoDigests : []string {digestedRef .String ()},
411+ }, nil
412+ }
413+
414+ func (i ImageMetadata ) String () string {
415+ return i .Reference .String ()
371416}
372417
373- func (i ImageSummary ) IsZero () bool {
374- return i .ID == ""
418+ func (i ImageMetadata ) IsZero () bool {
419+ return i .String () == ""
375420}
376421
377- // Digest returns the image digest for the image reference.
378- func (i ImageSummary ) Digest () (digest.Digest , error ) {
422+ // GetRepositoryDigest finds the repository digest associated with the original
423+ // image reference used to create this ImageMetadata.
424+ func (i ImageMetadata ) GetRepositoryDigest () (digest.Digest , error ) {
379425 if len (i .RepoDigests ) == 0 {
380- return "" , fmt .Errorf ("failed to get digest for image: %s" , i . imageRef . String () )
426+ return "" , fmt .Errorf ("failed to get digest for image: %s" , i )
381427 }
382428 var imgDigest digest.Digest
383429 for _ , rd := range i .RepoDigests {
384430 imgRef , err := cnab .ParseOCIReference (rd )
385431 if err != nil {
386432 return "" , err
387433 }
388- if imgRef .Repository () != i .imageRef .Repository () {
434+ if imgRef .Repository () != i .Reference .Repository () {
389435 continue
390436 }
391437
@@ -398,7 +444,7 @@ func (i ImageSummary) Digest() (digest.Digest, error) {
398444 }
399445
400446 if imgDigest == "" {
401- return "" , fmt .Errorf ("cannot find image digest for desired repo %s" , i . imageRef . String () )
447+ return "" , fmt .Errorf ("cannot find image digest for desired repo %s" , i )
402448 }
403449
404450 if err := imgDigest .Validate (); err != nil {
0 commit comments