77 "net"
88 "net/url"
99 "slices"
10+ "sort"
1011 "strings"
1112 "sync"
1213 "time"
@@ -26,13 +27,19 @@ import (
2627 "github.com/opencontainers/go-digest"
2728 ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2829 "github.com/pkg/errors"
30+ "golang.org/x/sync/errgroup"
2931 "golang.org/x/sync/singleflight"
3032)
3133
3234const (
3335 imageIdentityCacheTTL = 48 * time .Hour
3436 imageIdentityErrorCacheTTL = 15 * time .Minute
3537 imageIdentityWarmupTimeout = 2 * time .Minute
38+
39+ imageIdentityRefreshConcurrency = 4
40+ imageIdentityRefreshTimeout = 2 * time .Minute
41+ imageIdentityRefreshInterval = 30 * time .Minute
42+ imageIdentityRefreshAhead = imageIdentityRefreshInterval + imageIdentityRefreshTimeout
3643)
3744
3845type imageIdentityCacheEntry = identitycache.Entry
@@ -42,6 +49,10 @@ type imageIdentityState struct {
4249 cacheMu sync.Mutex
4350 cache map [string ]imageIdentityCacheEntry
4451 cacheStore identitycache.Backend
52+
53+ stopRefreshOnce sync.Once
54+ stopRefresh chan struct {}
55+ refreshDone chan struct {}
4556}
4657
4758func (i * ImageService ) imageIdentity (ctx context.Context , desc ocispec.Descriptor , multi * multiPlatformSummary ) (* imagetypes.Identity , error ) {
@@ -74,16 +85,7 @@ func (i *ImageService) imageIdentityWithCachePolicy(ctx context.Context, desc oc
7485 }
7586
7687 computedSignature , hasTransientVerificationError := i .computeSignatureIdentity (ctx , desc , multi )
77- ttl := imageIdentityCacheTTL
78- if hasTransientVerificationError {
79- // signature verification errors can be temporary (e.g. no network),
80- // so cache these for a shorter period
81- ttl = imageIdentityErrorCacheTTL
82- }
83- if err := i .updateImageIdentityCache (ctx , cacheKey , computedSignature , ttl ); err != nil {
84- log .G (ctx ).WithError (err ).WithField ("image" , desc .Digest ).Debug ("failed to update image identity cache entry" )
85- }
86-
88+ computedSignature = i .cacheComputedSignatureIdentity (ctx , cacheKey , desc , computedSignature , hasTransientVerificationError )
8789 return computedSignature , nil
8890 })
8991 if err != nil {
@@ -292,6 +294,19 @@ func (i *ImageService) updateImageIdentityCache(ctx context.Context, cacheKey st
292294 return i .identity .cacheStore .Store (ctx , cacheKey , entry , now )
293295}
294296
297+ func (i * ImageService ) cacheComputedSignatureIdentity (ctx context.Context , cacheKey string , desc ocispec.Descriptor , computedSignature * imagetypes.SignatureIdentity , hasTransientVerificationError bool ) * imagetypes.SignatureIdentity {
298+ ttl := imageIdentityCacheTTL
299+ if hasTransientVerificationError {
300+ // signature verification errors can be temporary (e.g. no network),
301+ // so cache these for a shorter period
302+ ttl = imageIdentityErrorCacheTTL
303+ }
304+ if err := i .updateImageIdentityCache (ctx , cacheKey , computedSignature , ttl ); err != nil {
305+ log .G (ctx ).WithError (err ).WithField ("image" , desc .Digest ).Debug ("failed to update image identity cache entry" )
306+ }
307+ return computedSignature
308+ }
309+
295310func cloneSignatureIdentity (s * imagetypes.SignatureIdentity ) * imagetypes.SignatureIdentity {
296311 if s == nil {
297312 return nil
@@ -314,6 +329,211 @@ func pruneImageIdentityCacheEntries(entries map[string]imageIdentityCacheEntry,
314329 }
315330}
316331
332+ func imageIdentityCacheEntryNeedsRefresh (entry imageIdentityCacheEntry , now time.Time ) bool {
333+ if entry .ExpiresAt .IsZero () {
334+ return false
335+ }
336+ return ! entry .ExpiresAt .After (now .Add (imageIdentityRefreshAhead ))
337+ }
338+
339+ func (i * ImageService ) imageIdentityCacheKeysToRefresh (ctx context.Context , now time.Time ) ([]string , error ) {
340+ seen := map [string ]struct {}{}
341+
342+ i .identity .cacheMu .Lock ()
343+ for key , entry := range i .identity .cache {
344+ if imageIdentityCacheEntryNeedsRefresh (entry , now ) {
345+ seen [key ] = struct {}{}
346+ }
347+ }
348+ i .identity .cacheMu .Unlock ()
349+
350+ if i .identity .cacheStore != nil {
351+ if err := i .identity .cacheStore .Walk (ctx , now , func (cacheKey string , entry imageIdentityCacheEntry ) error {
352+ if imageIdentityCacheEntryNeedsRefresh (entry , now ) {
353+ seen [cacheKey ] = struct {}{}
354+ }
355+ return nil
356+ }); err != nil {
357+ return nil , err
358+ }
359+ }
360+
361+ out := make ([]string , 0 , len (seen ))
362+ for key := range seen {
363+ out = append (out , key )
364+ }
365+ sort .Strings (out )
366+ return out , nil
367+ }
368+
369+ func (i * ImageService ) refreshImageIdentityCache (ctx context.Context , now time.Time ) {
370+ keys , err := i .imageIdentityCacheKeysToRefresh (ctx , now )
371+ if err != nil {
372+ log .G (ctx ).WithError (err ).Debug ("failed to list image identity cache refresh candidates" )
373+ return
374+ }
375+ if len (keys ) == 0 {
376+ return
377+ }
378+
379+ var g errgroup.Group
380+ g .SetLimit (imageIdentityRefreshConcurrency )
381+ for _ , key := range keys {
382+ g .Go (func () error {
383+ if err := i .refreshImageIdentityCacheKey (ctx , key ); err != nil {
384+ log .G (ctx ).WithError (err ).WithField ("cacheKey" , key ).Debug ("failed to refresh image identity cache entry" )
385+ }
386+ return nil
387+ })
388+ }
389+ _ = g .Wait ()
390+ }
391+
392+ func (i * ImageService ) pruneImageIdentityInMemoryCacheEntries (now time.Time ) {
393+ i .identity .cacheMu .Lock ()
394+ pruneImageIdentityCacheEntries (i .identity .cache , now )
395+ i .identity .cacheMu .Unlock ()
396+ }
397+
398+ func (i * ImageService ) pruneImageIdentityCacheStore (ctx context.Context , now time.Time ) {
399+ if i .identity .cacheStore == nil {
400+ return
401+ }
402+ if err := i .identity .cacheStore .PruneExpired (ctx , now ); err != nil {
403+ log .G (ctx ).WithError (err ).Debug ("failed to prune expired image identity cache entries" )
404+ }
405+ }
406+
407+ func (i * ImageService ) runImageIdentityCacheMaintenance (ctx context.Context , now time.Time ) {
408+ i .refreshImageIdentityCache (ctx , now )
409+ i .pruneImageIdentityInMemoryCacheEntries (now )
410+ i .pruneImageIdentityCacheStore (ctx , now )
411+ }
412+
413+ func (i * ImageService ) refreshImageIdentityCacheKey (ctx context.Context , cacheKey string ) error {
414+ imageDigest , bestDigest , bestPlatform , err := parseImageIdentityCacheKey (cacheKey )
415+ if err != nil {
416+ return err
417+ }
418+
419+ imgs , err := i .images .List (ctx , "target.digest==" + imageDigest .String ())
420+ if err != nil {
421+ return err
422+ }
423+ if len (imgs ) == 0 {
424+ return nil
425+ }
426+
427+ platformMatcher , err := imageIdentityPlatformMatcher (bestPlatform )
428+ if err != nil {
429+ return err
430+ }
431+
432+ for _ , img := range imgs {
433+ multi , err := i .multiPlatformSummary (ctx , img , platformMatcher )
434+ if err != nil {
435+ continue
436+ }
437+ if multi .Best == nil {
438+ continue
439+ }
440+ if bestDigest != "" && multi .Best .Target ().Digest != bestDigest {
441+ continue
442+ }
443+ computedSignature , hasTransientVerificationError := i .computeSignatureIdentity (ctx , img .Target , multi )
444+ _ = i .cacheComputedSignatureIdentity (ctx , cacheKey , img .Target , computedSignature , hasTransientVerificationError )
445+ return nil
446+ }
447+
448+ return nil
449+ }
450+
451+ func imageIdentityPlatformMatcher (platform string ) (platforms.MatchComparer , error ) {
452+ if platform == "" {
453+ return matchAnyWithPreference (platforms .Default (), nil ), nil
454+ }
455+ parsed , err := platforms .Parse (platform )
456+ if err != nil {
457+ return nil , err
458+ }
459+ return platforms .Only (parsed ), nil
460+ }
461+
462+ func parseImageIdentityCacheKey (cacheKey string ) (digest.Digest , digest.Digest , string , error ) {
463+ parts := strings .Split (cacheKey , "|" )
464+ if len (parts ) != 3 {
465+ return "" , "" , "" , errors .Errorf ("invalid image identity cache key %q" , cacheKey )
466+ }
467+
468+ imageDigest , err := digest .Parse (parts [0 ])
469+ if err != nil {
470+ return "" , "" , "" , errors .Wrapf (err , "invalid image digest in image identity cache key %q" , cacheKey )
471+ }
472+
473+ var bestDigest digest.Digest
474+ if parts [1 ] != "" {
475+ bestDigest , err = digest .Parse (parts [1 ])
476+ if err != nil {
477+ return "" , "" , "" , errors .Wrapf (err , "invalid best digest in image identity cache key %q" , cacheKey )
478+ }
479+ }
480+
481+ return imageDigest , bestDigest , parts [2 ], nil
482+ }
483+
484+ func (i * ImageService ) startImageIdentityCacheRefresh () {
485+ i .identity .cacheMu .Lock ()
486+ if i .identity .stopRefresh != nil {
487+ i .identity .cacheMu .Unlock ()
488+ return
489+ }
490+ i .identity .stopRefresh = make (chan struct {})
491+ i .identity .refreshDone = make (chan struct {})
492+ stopCh := i .identity .stopRefresh
493+ doneCh := i .identity .refreshDone
494+ i .identity .cacheMu .Unlock ()
495+
496+ go func () {
497+ defer close (doneCh )
498+ runMaintenance := func () {
499+ now := time .Now ()
500+
501+ maintenanceCtx , cancel := context .WithTimeout (context .Background (), imageIdentityRefreshTimeout )
502+ defer cancel ()
503+ i .runImageIdentityCacheMaintenance (maintenanceCtx , now )
504+ }
505+
506+ // Run one pass right after startup to refresh expired entries and prune stale ones.
507+ runMaintenance ()
508+
509+ timer := time .NewTicker (imageIdentityRefreshInterval )
510+ defer timer .Stop ()
511+ for {
512+ select {
513+ case <- stopCh :
514+ return
515+ case <- timer .C :
516+ runMaintenance ()
517+ }
518+ }
519+ }()
520+ }
521+
522+ func (i * ImageService ) stopImageIdentityCacheRefresh () {
523+ i .identity .stopRefreshOnce .Do (func () {
524+ i .identity .cacheMu .Lock ()
525+ stopCh := i .identity .stopRefresh
526+ doneCh := i .identity .refreshDone
527+ i .identity .cacheMu .Unlock ()
528+ if stopCh != nil {
529+ close (stopCh )
530+ }
531+ if doneCh != nil {
532+ <- doneCh
533+ }
534+ })
535+ }
536+
317537func (i * ImageService ) warmImageIdentityCache (ctx context.Context , img c8dimages.Image ) {
318538 if i .policyVerifier == nil {
319539 return
0 commit comments