Signed-off-by: CrazyMax <1951866+crazy-max@users.noreply.github.com>
| 1 | 1 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,41 @@ |
| 0 |
+package identitycache |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "time" |
|
| 5 |
+ |
|
| 6 |
+ imagetypes "github.com/moby/moby/api/types/image" |
|
| 7 |
+) |
|
| 8 |
+ |
|
| 9 |
+// Entry contains a persisted image signature cache record. |
|
| 10 |
+type Entry struct {
|
|
| 11 |
+ CachedAt time.Time |
|
| 12 |
+ ExpiresAt time.Time |
|
| 13 |
+ Signature *imagetypes.SignatureIdentity |
|
| 14 |
+} |
|
| 15 |
+ |
|
| 16 |
+// Backend is a persistent storage backend for image signature cache entries. |
|
| 17 |
+type Backend interface {
|
|
| 18 |
+ Load(ctx context.Context, cacheKey string, now time.Time) (Entry, bool, error) |
|
| 19 |
+ Store(ctx context.Context, cacheKey string, entry Entry, now time.Time) error |
|
| 20 |
+ Close() error |
|
| 21 |
+} |
|
| 22 |
+ |
|
| 23 |
+type nopBackend struct{}
|
|
| 24 |
+ |
|
| 25 |
+// NewNopBackend returns a backend that never persists or returns entries. |
|
| 26 |
+func NewNopBackend() Backend {
|
|
| 27 |
+ return nopBackend{}
|
|
| 28 |
+} |
|
| 29 |
+ |
|
| 30 |
+func (nopBackend) Load(context.Context, string, time.Time) (Entry, bool, error) {
|
|
| 31 |
+ return Entry{}, false, nil
|
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+func (nopBackend) Store(context.Context, string, Entry, time.Time) error {
|
|
| 35 |
+ return nil |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 38 |
+func (nopBackend) Close() error {
|
|
| 39 |
+ return nil |
|
| 40 |
+} |
| 0 | 41 |
new file mode 100644 |
| ... | ... |
@@ -0,0 +1,180 @@ |
| 0 |
+package identitycache |
|
| 1 |
+ |
|
| 2 |
+import ( |
|
| 3 |
+ "context" |
|
| 4 |
+ "crypto/rand" |
|
| 5 |
+ "encoding/json" |
|
| 6 |
+ "math/big" |
|
| 7 |
+ "os" |
|
| 8 |
+ "path/filepath" |
|
| 9 |
+ "sync" |
|
| 10 |
+ "time" |
|
| 11 |
+ |
|
| 12 |
+ boltdb "github.com/moby/buildkit/util/db" |
|
| 13 |
+ "github.com/moby/buildkit/util/db/boltutil" |
|
| 14 |
+ bolt "go.etcd.io/bbolt" |
|
| 15 |
+) |
|
| 16 |
+ |
|
| 17 |
+var bboltCacheBucket = []byte("image-identity-cache-v1")
|
|
| 18 |
+ |
|
| 19 |
+const ( |
|
| 20 |
+ // 15m matches the shortest cache TTL (imageIdentityErrorCacheTTL), so |
|
| 21 |
+ // prune won't lag far behind shortest-lived entries. |
|
| 22 |
+ pruneIntervalMin = 15 * time.Minute |
|
| 23 |
+ pruneIntervalSpread = 15 * time.Minute |
|
| 24 |
+) |
|
| 25 |
+ |
|
| 26 |
+type boltBackend struct {
|
|
| 27 |
+ db boltdb.DB |
|
| 28 |
+ closeOnce sync.Once |
|
| 29 |
+ closeErr error |
|
| 30 |
+ stopPrune chan struct{}
|
|
| 31 |
+ pruneDone chan struct{}
|
|
| 32 |
+} |
|
| 33 |
+ |
|
| 34 |
+// NewBoltDBBackend creates a bbolt-backed persistent cache backend. |
|
| 35 |
+func NewBoltDBBackend(root string) (Backend, error) {
|
|
| 36 |
+ if root == "" {
|
|
| 37 |
+ return NewNopBackend(), nil |
|
| 38 |
+ } |
|
| 39 |
+ cacheDir := filepath.Join(root, "image") |
|
| 40 |
+ if err := os.MkdirAll(cacheDir, 0o700); err != nil {
|
|
| 41 |
+ return nil, err |
|
| 42 |
+ } |
|
| 43 |
+ db, err := boltutil.SafeOpen(filepath.Join(cacheDir, "identity-cache.db"), 0o600, nil) |
|
| 44 |
+ if err != nil {
|
|
| 45 |
+ return nil, err |
|
| 46 |
+ } |
|
| 47 |
+ b := &boltBackend{db: db}
|
|
| 48 |
+ if err := b.db.Update(func(tx *bolt.Tx) error {
|
|
| 49 |
+ _, err := tx.CreateBucketIfNotExists(bboltCacheBucket) |
|
| 50 |
+ return err |
|
| 51 |
+ }); err != nil {
|
|
| 52 |
+ _ = db.Close() |
|
| 53 |
+ return nil, err |
|
| 54 |
+ } |
|
| 55 |
+ b.startPrune() |
|
| 56 |
+ return b, nil |
|
| 57 |
+} |
|
| 58 |
+ |
|
| 59 |
+func (b *boltBackend) Load(_ context.Context, cacheKey string, now time.Time) (Entry, bool, error) {
|
|
| 60 |
+ var ( |
|
| 61 |
+ entry Entry |
|
| 62 |
+ payload []byte |
|
| 63 |
+ ) |
|
| 64 |
+ err := b.db.View(func(tx *bolt.Tx) error {
|
|
| 65 |
+ bucket := tx.Bucket(bboltCacheBucket) |
|
| 66 |
+ if bucket == nil {
|
|
| 67 |
+ return nil |
|
| 68 |
+ } |
|
| 69 |
+ value := bucket.Get([]byte(cacheKey)) |
|
| 70 |
+ if value == nil {
|
|
| 71 |
+ return nil |
|
| 72 |
+ } |
|
| 73 |
+ payload = append([]byte(nil), value...) |
|
| 74 |
+ return nil |
|
| 75 |
+ }) |
|
| 76 |
+ if err != nil {
|
|
| 77 |
+ return Entry{}, false, err
|
|
| 78 |
+ } |
|
| 79 |
+ if len(payload) == 0 {
|
|
| 80 |
+ return Entry{}, false, nil
|
|
| 81 |
+ } |
|
| 82 |
+ if err := json.Unmarshal(payload, &entry); err != nil {
|
|
| 83 |
+ _ = b.delete(cacheKey) |
|
| 84 |
+ return Entry{}, false, nil
|
|
| 85 |
+ } |
|
| 86 |
+ if now.After(entry.ExpiresAt) {
|
|
| 87 |
+ if err := b.delete(cacheKey); err != nil {
|
|
| 88 |
+ return Entry{}, false, err
|
|
| 89 |
+ } |
|
| 90 |
+ return Entry{}, false, nil
|
|
| 91 |
+ } |
|
| 92 |
+ return entry, true, nil |
|
| 93 |
+} |
|
| 94 |
+ |
|
| 95 |
+func (b *boltBackend) Store(_ context.Context, cacheKey string, entry Entry, _ time.Time) error {
|
|
| 96 |
+ payload, err := json.Marshal(entry) |
|
| 97 |
+ if err != nil {
|
|
| 98 |
+ return err |
|
| 99 |
+ } |
|
| 100 |
+ return b.db.Update(func(tx *bolt.Tx) error {
|
|
| 101 |
+ bucket, err := tx.CreateBucketIfNotExists(bboltCacheBucket) |
|
| 102 |
+ if err != nil {
|
|
| 103 |
+ return err |
|
| 104 |
+ } |
|
| 105 |
+ return bucket.Put([]byte(cacheKey), payload) |
|
| 106 |
+ }) |
|
| 107 |
+} |
|
| 108 |
+ |
|
| 109 |
+func (b *boltBackend) Close() error {
|
|
| 110 |
+ if b == nil || b.db == nil {
|
|
| 111 |
+ return nil |
|
| 112 |
+ } |
|
| 113 |
+ b.closeOnce.Do(func() {
|
|
| 114 |
+ if b.stopPrune != nil {
|
|
| 115 |
+ close(b.stopPrune) |
|
| 116 |
+ } |
|
| 117 |
+ if b.pruneDone != nil {
|
|
| 118 |
+ <-b.pruneDone |
|
| 119 |
+ } |
|
| 120 |
+ b.closeErr = b.db.Close() |
|
| 121 |
+ }) |
|
| 122 |
+ return b.closeErr |
|
| 123 |
+} |
|
| 124 |
+ |
|
| 125 |
+func (b *boltBackend) delete(cacheKey string) error {
|
|
| 126 |
+ return b.db.Update(func(tx *bolt.Tx) error {
|
|
| 127 |
+ bucket := tx.Bucket(bboltCacheBucket) |
|
| 128 |
+ if bucket == nil {
|
|
| 129 |
+ return nil |
|
| 130 |
+ } |
|
| 131 |
+ return bucket.Delete([]byte(cacheKey)) |
|
| 132 |
+ }) |
|
| 133 |
+} |
|
| 134 |
+ |
|
| 135 |
+func (b *boltBackend) startPrune() {
|
|
| 136 |
+ b.stopPrune = make(chan struct{})
|
|
| 137 |
+ b.pruneDone = make(chan struct{})
|
|
| 138 |
+ go func() {
|
|
| 139 |
+ defer close(b.pruneDone) |
|
| 140 |
+ timer := time.NewTimer(nextPruneDelay()) |
|
| 141 |
+ defer timer.Stop() |
|
| 142 |
+ for {
|
|
| 143 |
+ select {
|
|
| 144 |
+ case <-b.stopPrune: |
|
| 145 |
+ return |
|
| 146 |
+ case <-timer.C: |
|
| 147 |
+ _ = b.pruneExpiredEntries(time.Now().UTC()) |
|
| 148 |
+ timer.Reset(nextPruneDelay()) |
|
| 149 |
+ } |
|
| 150 |
+ } |
|
| 151 |
+ }() |
|
| 152 |
+} |
|
| 153 |
+ |
|
| 154 |
+func nextPruneDelay() time.Duration {
|
|
| 155 |
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(pruneIntervalSpread))) |
|
| 156 |
+ if err != nil {
|
|
| 157 |
+ return pruneIntervalMin |
|
| 158 |
+ } |
|
| 159 |
+ return pruneIntervalMin + time.Duration(n.Int64()) |
|
| 160 |
+} |
|
| 161 |
+ |
|
| 162 |
+func (b *boltBackend) pruneExpiredEntries(now time.Time) error {
|
|
| 163 |
+ return b.db.Update(func(tx *bolt.Tx) error {
|
|
| 164 |
+ bucket := tx.Bucket(bboltCacheBucket) |
|
| 165 |
+ if bucket == nil {
|
|
| 166 |
+ return nil |
|
| 167 |
+ } |
|
| 168 |
+ cursor := bucket.Cursor() |
|
| 169 |
+ for key, value := cursor.First(); key != nil; key, value = cursor.Next() {
|
|
| 170 |
+ var entry Entry |
|
| 171 |
+ if err := json.Unmarshal(value, &entry); err != nil || now.After(entry.ExpiresAt) {
|
|
| 172 |
+ if err := cursor.Delete(); err != nil {
|
|
| 173 |
+ return err |
|
| 174 |
+ } |
|
| 175 |
+ } |
|
| 176 |
+ } |
|
| 177 |
+ return nil |
|
| 178 |
+ }) |
|
| 179 |
+} |
| ... | ... |
@@ -4,34 +4,121 @@ import ( |
| 4 | 4 |
"cmp" |
| 5 | 5 |
"context" |
| 6 | 6 |
"encoding/json" |
| 7 |
+ "net" |
|
| 8 |
+ "net/url" |
|
| 7 | 9 |
"slices" |
| 8 | 10 |
"strings" |
| 11 |
+ "sync" |
|
| 9 | 12 |
"time" |
| 10 | 13 |
|
| 11 | 14 |
"github.com/containerd/containerd/v2/core/content" |
| 15 |
+ c8dimages "github.com/containerd/containerd/v2/core/images" |
|
| 12 | 16 |
"github.com/containerd/containerd/v2/core/remotes" |
| 13 | 17 |
"github.com/containerd/containerd/v2/pkg/labels" |
| 14 | 18 |
"github.com/containerd/log" |
| 19 |
+ "github.com/containerd/platforms" |
|
| 15 | 20 |
"github.com/distribution/reference" |
| 16 | 21 |
imagetypes "github.com/moby/moby/api/types/image" |
| 22 |
+ "github.com/moby/moby/v2/daemon/containerd/identitycache" |
|
| 17 | 23 |
"github.com/moby/moby/v2/daemon/internal/builder-next/exporter" |
| 18 | 24 |
policyimage "github.com/moby/policy-helpers/image" |
| 19 | 25 |
policytypes "github.com/moby/policy-helpers/types" |
| 20 | 26 |
"github.com/opencontainers/go-digest" |
| 21 | 27 |
ocispec "github.com/opencontainers/image-spec/specs-go/v1" |
| 22 | 28 |
"github.com/pkg/errors" |
| 29 |
+ "golang.org/x/sync/singleflight" |
|
| 23 | 30 |
) |
| 24 | 31 |
|
| 32 |
+const ( |
|
| 33 |
+ imageIdentityCacheTTL = 48 * time.Hour |
|
| 34 |
+ imageIdentityErrorCacheTTL = 15 * time.Minute |
|
| 35 |
+ imageIdentityWarmupTimeout = 2 * time.Minute |
|
| 36 |
+) |
|
| 37 |
+ |
|
| 38 |
+type imageIdentityCacheEntry = identitycache.Entry |
|
| 39 |
+ |
|
| 40 |
+type imageIdentityState struct {
|
|
| 41 |
+ flight singleflight.Group |
|
| 42 |
+ cacheMu sync.Mutex |
|
| 43 |
+ cache map[string]imageIdentityCacheEntry |
|
| 44 |
+ cacheStore identitycache.Backend |
|
| 45 |
+} |
|
| 46 |
+ |
|
| 25 | 47 |
func (i *ImageService) imageIdentity(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.Identity, error) {
|
| 48 |
+ return i.imageIdentityWithCachePolicy(ctx, desc, multi, true) |
|
| 49 |
+} |
|
| 50 |
+ |
|
| 51 |
+func (i *ImageService) imageIdentityFromCache(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.Identity, error) {
|
|
| 52 |
+ return i.imageIdentityWithCachePolicy(ctx, desc, multi, false) |
|
| 53 |
+} |
|
| 54 |
+ |
|
| 55 |
+func (i *ImageService) imageIdentityWithCachePolicy(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary, computeOnCacheMiss bool) (*imagetypes.Identity, error) {
|
|
| 26 | 56 |
info, err := i.content.Info(ctx, desc.Digest) |
| 27 | 57 |
if err != nil {
|
| 28 | 58 |
return nil, err |
| 29 | 59 |
} |
| 30 |
- identity := &imagetypes.Identity{}
|
|
| 31 | 60 |
|
| 61 |
+ identity := imageIdentityFromLabels(ctx, info.Labels) |
|
| 62 |
+ bestDigest, bestPlatform := imageIdentityBestMatch(multi) |
|
| 63 |
+ cacheKey := imageIdentityCacheKey(desc.Digest.String(), bestDigest, bestPlatform) |
|
| 64 |
+ signature, ok, err := i.imageSignatureIdentityFromCache(ctx, cacheKey) |
|
| 65 |
+ if err != nil {
|
|
| 66 |
+ log.G(ctx).WithError(err).WithField("image", desc.Digest).Debug("failed to load image identity cache entry")
|
|
| 67 |
+ } |
|
| 68 |
+ if !ok && computeOnCacheMiss {
|
|
| 69 |
+ v, err, _ := i.identity.flight.Do(cacheKey, func() (any, error) {
|
|
| 70 |
+ if cached, ok, err := i.imageSignatureIdentityFromCache(ctx, cacheKey); err == nil && ok {
|
|
| 71 |
+ return cached, nil |
|
| 72 |
+ } else if err != nil {
|
|
| 73 |
+ log.G(ctx).WithError(err).WithField("image", desc.Digest).Debug("failed to refresh image identity cache entry")
|
|
| 74 |
+ } |
|
| 75 |
+ |
|
| 76 |
+ 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 |
+ |
|
| 87 |
+ return computedSignature, nil |
|
| 88 |
+ }) |
|
| 89 |
+ if err != nil {
|
|
| 90 |
+ return nil, err |
|
| 91 |
+ } |
|
| 92 |
+ cachedSignature, ok := v.(*imagetypes.SignatureIdentity) |
|
| 93 |
+ if !ok {
|
|
| 94 |
+ return nil, errors.Errorf("unexpected cached signature identity type %T", v)
|
|
| 95 |
+ } |
|
| 96 |
+ signature = cachedSignature |
|
| 97 |
+ } |
|
| 98 |
+ |
|
| 99 |
+ if signature != nil {
|
|
| 100 |
+ identity.Signature = append(identity.Signature, *signature) |
|
| 101 |
+ } |
|
| 102 |
+ |
|
| 103 |
+ if len(identity.Build) == 0 && len(identity.Pull) == 0 && len(identity.Signature) == 0 {
|
|
| 104 |
+ return nil, nil |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ return identity, nil |
|
| 108 |
+} |
|
| 109 |
+ |
|
| 110 |
+func imageIdentityBestMatch(multi *multiPlatformSummary) (bestDigest string, bestPlatform string) {
|
|
| 111 |
+ if multi == nil || multi.Best == nil {
|
|
| 112 |
+ return "", "" |
|
| 113 |
+ } |
|
| 114 |
+ return multi.Best.Target().Digest.String(), platforms.FormatAll(multi.BestPlatform) |
|
| 115 |
+} |
|
| 116 |
+ |
|
| 117 |
+func imageIdentityFromLabels(ctx context.Context, labelsByDigest map[string]string) *imagetypes.Identity {
|
|
| 118 |
+ identity := &imagetypes.Identity{}
|
|
| 32 | 119 |
seenRepos := make(map[string]struct{})
|
| 33 | 120 |
|
| 34 |
- for k, v := range info.Labels {
|
|
| 121 |
+ for k, v := range labelsByDigest {
|
|
| 35 | 122 |
if ref, ok := strings.CutPrefix(k, exporter.BuildRefLabel); ok {
|
| 36 | 123 |
var val exporter.BuildRefLabelValue |
| 37 | 124 |
if err := json.Unmarshal([]byte(v), &val); err == nil {
|
| ... | ... |
@@ -64,26 +151,185 @@ func (i *ImageService) imageIdentity(ctx context.Context, desc ocispec.Descripto |
| 64 | 64 |
} |
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
- if multi.Best != nil {
|
|
| 68 |
- si, err := i.signatureIdentity(ctx, desc, multi.Best, multi.BestPlatform) |
|
| 69 |
- if err != nil {
|
|
| 70 |
- log.G(ctx).WithError(err).Error("failed to validate image signature")
|
|
| 67 |
+ slices.SortFunc(identity.Build, func(a, b imagetypes.BuildIdentity) int {
|
|
| 68 |
+ return cmp.Compare(a.Ref, b.Ref) |
|
| 69 |
+ }) |
|
| 70 |
+ |
|
| 71 |
+ return identity |
|
| 72 |
+} |
|
| 73 |
+ |
|
| 74 |
+func (i *ImageService) computeSignatureIdentity(ctx context.Context, desc ocispec.Descriptor, multi *multiPlatformSummary) (*imagetypes.SignatureIdentity, bool) {
|
|
| 75 |
+ if multi == nil || multi.Best == nil {
|
|
| 76 |
+ return nil, false |
|
| 77 |
+ } |
|
| 78 |
+ |
|
| 79 |
+ signatureIdentity, err := i.signatureIdentity(ctx, desc, multi.Best, multi.BestPlatform) |
|
| 80 |
+ if err != nil {
|
|
| 81 |
+ log.G(ctx).WithError(err).Error("failed to validate image signature")
|
|
| 82 |
+ return nil, signatureVerificationErrorIsTransient(err) |
|
| 83 |
+ } |
|
| 84 |
+ |
|
| 85 |
+ // verification errors are represented as a payload field. Treat only |
|
| 86 |
+ // network-like errors as transient so deterministic verification failures |
|
| 87 |
+ // remain cached with the normal TTL |
|
| 88 |
+ hasTransientVerificationError := signatureIdentity != nil && signatureVerificationMessageIsTransient(signatureIdentity.Error) |
|
| 89 |
+ return signatureIdentity, hasTransientVerificationError |
|
| 90 |
+} |
|
| 91 |
+ |
|
| 92 |
+func signatureVerificationErrorIsTransient(err error) bool {
|
|
| 93 |
+ if err == nil {
|
|
| 94 |
+ return false |
|
| 95 |
+ } |
|
| 96 |
+ if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
|
|
| 97 |
+ return true |
|
| 98 |
+ } |
|
| 99 |
+ |
|
| 100 |
+ var netErr net.Error |
|
| 101 |
+ if errors.As(err, &netErr) {
|
|
| 102 |
+ if netErr.Timeout() {
|
|
| 103 |
+ return true |
|
| 71 | 104 |
} |
| 72 |
- if si != nil {
|
|
| 73 |
- identity.Signature = append(identity.Signature, *si) |
|
| 105 |
+ } |
|
| 106 |
+ |
|
| 107 |
+ var dnsErr *net.DNSError |
|
| 108 |
+ if errors.As(err, &dnsErr) {
|
|
| 109 |
+ return true |
|
| 110 |
+ } |
|
| 111 |
+ |
|
| 112 |
+ var urlErr *url.Error |
|
| 113 |
+ if errors.As(err, &urlErr) {
|
|
| 114 |
+ return signatureVerificationErrorIsTransient(urlErr.Err) |
|
| 115 |
+ } |
|
| 116 |
+ |
|
| 117 |
+ return signatureVerificationMessageIsTransient(err.Error()) |
|
| 118 |
+} |
|
| 119 |
+ |
|
| 120 |
+func signatureVerificationMessageIsTransient(msg string) bool {
|
|
| 121 |
+ msg = strings.ToLower(msg) |
|
| 122 |
+ // TODO: replace message-based transient detection with structured error |
|
| 123 |
+ // classification from policy-helpers / signature verification (e.g. typed |
|
| 124 |
+ // retryable errors), so cache TTL decisions do not depend on string |
|
| 125 |
+ // matching. |
|
| 126 |
+ for _, transient := range []string{
|
|
| 127 |
+ "context deadline exceeded", |
|
| 128 |
+ "i/o timeout", |
|
| 129 |
+ "tls handshake timeout", |
|
| 130 |
+ "no such host", |
|
| 131 |
+ "temporary failure in name resolution", |
|
| 132 |
+ "connection refused", |
|
| 133 |
+ "connection reset by peer", |
|
| 134 |
+ "network is unreachable", |
|
| 135 |
+ "dial tcp", |
|
| 136 |
+ } {
|
|
| 137 |
+ if strings.Contains(msg, transient) {
|
|
| 138 |
+ return true |
|
| 74 | 139 |
} |
| 75 | 140 |
} |
| 141 |
+ return false |
|
| 142 |
+} |
|
| 76 | 143 |
|
| 77 |
- // return nil if there is no identity information |
|
| 78 |
- if len(identity.Build) == 0 && len(identity.Pull) == 0 && len(identity.Signature) == 0 {
|
|
| 79 |
- return nil, nil |
|
| 144 |
+func imageIdentityCacheKey(imageDigest, bestDigest, bestPlatform string) string {
|
|
| 145 |
+ return strings.Join([]string{imageDigest, bestDigest, bestPlatform}, "|")
|
|
| 146 |
+} |
|
| 147 |
+ |
|
| 148 |
+func (i *ImageService) imageSignatureIdentityFromCache(ctx context.Context, cacheKey string) (*imagetypes.SignatureIdentity, bool, error) {
|
|
| 149 |
+ now := time.Now() |
|
| 150 |
+ |
|
| 151 |
+ i.identity.cacheMu.Lock() |
|
| 152 |
+ if cached, ok := i.identity.cache[cacheKey]; ok {
|
|
| 153 |
+ if now.After(cached.ExpiresAt) {
|
|
| 154 |
+ delete(i.identity.cache, cacheKey) |
|
| 155 |
+ } else {
|
|
| 156 |
+ i.identity.cacheMu.Unlock() |
|
| 157 |
+ return cloneSignatureIdentity(cached.Signature), true, nil |
|
| 158 |
+ } |
|
| 80 | 159 |
} |
| 160 |
+ i.identity.cacheMu.Unlock() |
|
| 81 | 161 |
|
| 82 |
- slices.SortFunc(identity.Build, func(a, b imagetypes.BuildIdentity) int {
|
|
| 83 |
- return cmp.Compare(a.Ref, b.Ref) |
|
| 84 |
- }) |
|
| 162 |
+ if i.identity.cacheStore == nil {
|
|
| 163 |
+ return nil, false, nil |
|
| 164 |
+ } |
|
| 165 |
+ cached, ok, err := i.identity.cacheStore.Load(ctx, cacheKey, now) |
|
| 166 |
+ if err != nil {
|
|
| 167 |
+ return nil, false, err |
|
| 168 |
+ } |
|
| 169 |
+ if !ok {
|
|
| 170 |
+ return nil, false, nil |
|
| 171 |
+ } |
|
| 85 | 172 |
|
| 86 |
- return identity, nil |
|
| 173 |
+ i.identity.cacheMu.Lock() |
|
| 174 |
+ if i.identity.cache == nil {
|
|
| 175 |
+ i.identity.cache = map[string]imageIdentityCacheEntry{}
|
|
| 176 |
+ } |
|
| 177 |
+ i.identity.cache[cacheKey] = cached |
|
| 178 |
+ i.identity.cacheMu.Unlock() |
|
| 179 |
+ return cloneSignatureIdentity(cached.Signature), true, nil |
|
| 180 |
+} |
|
| 181 |
+ |
|
| 182 |
+func (i *ImageService) updateImageIdentityCache(ctx context.Context, cacheKey string, signature *imagetypes.SignatureIdentity, ttl time.Duration) error {
|
|
| 183 |
+ if ttl <= 0 {
|
|
| 184 |
+ return nil |
|
| 185 |
+ } |
|
| 186 |
+ |
|
| 187 |
+ now := time.Now() |
|
| 188 |
+ entry := imageIdentityCacheEntry{
|
|
| 189 |
+ CachedAt: now, |
|
| 190 |
+ ExpiresAt: now.Add(ttl), |
|
| 191 |
+ Signature: cloneSignatureIdentity(signature), |
|
| 192 |
+ } |
|
| 193 |
+ |
|
| 194 |
+ i.identity.cacheMu.Lock() |
|
| 195 |
+ if i.identity.cache == nil {
|
|
| 196 |
+ i.identity.cache = map[string]imageIdentityCacheEntry{}
|
|
| 197 |
+ } |
|
| 198 |
+ i.identity.cache[cacheKey] = entry |
|
| 199 |
+ pruneImageIdentityCacheEntries(i.identity.cache, now) |
|
| 200 |
+ i.identity.cacheMu.Unlock() |
|
| 201 |
+ |
|
| 202 |
+ if i.identity.cacheStore == nil {
|
|
| 203 |
+ return nil |
|
| 204 |
+ } |
|
| 205 |
+ return i.identity.cacheStore.Store(ctx, cacheKey, entry, now) |
|
| 206 |
+} |
|
| 207 |
+ |
|
| 208 |
+func cloneSignatureIdentity(s *imagetypes.SignatureIdentity) *imagetypes.SignatureIdentity {
|
|
| 209 |
+ if s == nil {
|
|
| 210 |
+ return nil |
|
| 211 |
+ } |
|
| 212 |
+ out := *s |
|
| 213 |
+ out.Timestamps = slices.Clone(s.Timestamps) |
|
| 214 |
+ out.Warnings = slices.Clone(s.Warnings) |
|
| 215 |
+ if s.Signer != nil {
|
|
| 216 |
+ signer := *s.Signer |
|
| 217 |
+ out.Signer = &signer |
|
| 218 |
+ } |
|
| 219 |
+ return &out |
|
| 220 |
+} |
|
| 221 |
+ |
|
| 222 |
+func pruneImageIdentityCacheEntries(entries map[string]imageIdentityCacheEntry, now time.Time) {
|
|
| 223 |
+ for key, entry := range entries {
|
|
| 224 |
+ if now.After(entry.ExpiresAt) {
|
|
| 225 |
+ delete(entries, key) |
|
| 226 |
+ } |
|
| 227 |
+ } |
|
| 228 |
+} |
|
| 229 |
+ |
|
| 230 |
+func (i *ImageService) warmImageIdentityCache(ctx context.Context, img c8dimages.Image) {
|
|
| 231 |
+ if i.policyVerifier == nil {
|
|
| 232 |
+ return |
|
| 233 |
+ } |
|
| 234 |
+ go func() {
|
|
| 235 |
+ warmCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), imageIdentityWarmupTimeout) |
|
| 236 |
+ defer cancel() |
|
| 237 |
+ multi, err := i.multiPlatformSummary(warmCtx, img, matchAnyWithPreference(platforms.Default(), nil)) |
|
| 238 |
+ if err != nil {
|
|
| 239 |
+ log.G(warmCtx).WithError(err).WithField("image", img.Name).Debug("failed to build image identity cache in background")
|
|
| 240 |
+ return |
|
| 241 |
+ } |
|
| 242 |
+ if _, err := i.imageIdentity(warmCtx, img.Target, multi); err != nil {
|
|
| 243 |
+ log.G(warmCtx).WithError(err).WithField("image", img.Name).Debug("failed to build image identity cache in background")
|
|
| 244 |
+ } |
|
| 245 |
+ }() |
|
| 87 | 246 |
} |
| 88 | 247 |
|
| 89 | 248 |
func (i *ImageService) signatureIdentity(ctx context.Context, desc ocispec.Descriptor, img *ImageManifest, platform ocispec.Platform) (*imagetypes.SignatureIdentity, error) {
|
| ... | ... |
@@ -101,6 +347,10 @@ func (i *ImageService) signatureIdentity(ctx context.Context, desc ocispec.Descr |
| 101 | 101 |
return nil, nil |
| 102 | 102 |
} |
| 103 | 103 |
|
| 104 |
+ if i.policyVerifier == nil {
|
|
| 105 |
+ return nil, nil |
|
| 106 |
+ } |
|
| 107 |
+ |
|
| 104 | 108 |
v, err := i.policyVerifier() |
| 105 | 109 |
if err != nil {
|
| 106 | 110 |
return nil, err |
| ... | ... |
@@ -15,6 +15,7 @@ import ( |
| 15 | 15 |
"github.com/containerd/log" |
| 16 | 16 |
"github.com/containerd/platforms" |
| 17 | 17 |
"github.com/moby/moby/v2/daemon/container" |
| 18 |
+ "github.com/moby/moby/v2/daemon/containerd/identitycache" |
|
| 18 | 19 |
daemonevents "github.com/moby/moby/v2/daemon/events" |
| 19 | 20 |
dimages "github.com/moby/moby/v2/daemon/images" |
| 20 | 21 |
"github.com/moby/moby/v2/daemon/internal/distribution" |
| ... | ... |
@@ -42,6 +43,7 @@ type ImageService struct {
|
| 42 | 42 |
refCountMounter snapshotter.Mounter |
| 43 | 43 |
idMapping user.IdentityMapping |
| 44 | 44 |
policyVerifier func() (*policyverifier.Verifier, error) |
| 45 |
+ identity imageIdentityState |
|
| 45 | 46 |
|
| 46 | 47 |
// defaultPlatformOverride is used in tests to override the host platform. |
| 47 | 48 |
defaultPlatformOverride platforms.MatchComparer |
| ... | ... |
@@ -51,6 +53,7 @@ type ImageServiceConfig struct {
|
| 51 | 51 |
Client *containerd.Client |
| 52 | 52 |
Containers container.Store |
| 53 | 53 |
Snapshotter string |
| 54 |
+ IdentityCacheBackend identitycache.Backend |
|
| 54 | 55 |
RegistryHosts docker.RegistryHosts |
| 55 | 56 |
Registry distribution.RegistryResolver |
| 56 | 57 |
EventsService *daemonevents.Events |
| ... | ... |
@@ -76,6 +79,15 @@ func NewService(config ImageServiceConfig) *ImageService {
|
| 76 | 76 |
refCountMounter: config.RefCountMounter, |
| 77 | 77 |
idMapping: config.IDMapping, |
| 78 | 78 |
policyVerifier: config.PolicyVerifierProvider, |
| 79 |
+ identity: imageIdentityState{
|
|
| 80 |
+ cache: make(map[string]imageIdentityCacheEntry), |
|
| 81 |
+ cacheStore: func() identitycache.Backend {
|
|
| 82 |
+ if config.IdentityCacheBackend != nil {
|
|
| 83 |
+ return config.IdentityCacheBackend |
|
| 84 |
+ } |
|
| 85 |
+ return identitycache.NewNopBackend() |
|
| 86 |
+ }(), |
|
| 87 |
+ }, |
|
| 79 | 88 |
} |
| 80 | 89 |
} |
| 81 | 90 |
|
| ... | ... |
@@ -132,6 +144,9 @@ func (i *ImageService) GetLayerMountID(cid string) (string, error) {
|
| 132 | 132 |
// Cleanup resources before the process is shutdown. |
| 133 | 133 |
// called from daemon.go Daemon.Shutdown() |
| 134 | 134 |
func (i *ImageService) Cleanup() error {
|
| 135 |
+ if i.identity.cacheStore != nil {
|
|
| 136 |
+ return i.identity.cacheStore.Close() |
|
| 137 |
+ } |
|
| 135 | 138 |
return nil |
| 136 | 139 |
} |
| 137 | 140 |
|
| ... | ... |
@@ -53,6 +53,7 @@ import ( |
| 53 | 53 |
"github.com/moby/moby/v2/daemon/config" |
| 54 | 54 |
"github.com/moby/moby/v2/daemon/container" |
| 55 | 55 |
ctrd "github.com/moby/moby/v2/daemon/containerd" |
| 56 |
+ "github.com/moby/moby/v2/daemon/containerd/identitycache" |
|
| 56 | 57 |
"github.com/moby/moby/v2/daemon/containerd/migration" |
| 57 | 58 |
"github.com/moby/moby/v2/daemon/events" |
| 58 | 59 |
_ "github.com/moby/moby/v2/daemon/graphdriver/register" // register graph drivers |
| ... | ... |
@@ -1273,11 +1274,17 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S |
| 1273 | 1273 |
if err := configureKernelSecuritySupport(&cfgStore.Config, driverName); err != nil {
|
| 1274 | 1274 |
return nil, err |
| 1275 | 1275 |
} |
| 1276 |
+ identityCacheBackend, err := identitycache.NewBoltDBBackend(config.Root) |
|
| 1277 |
+ if err != nil {
|
|
| 1278 |
+ log.G(ctx).WithError(err).Warn("failed to initialize image identity bbolt cache backend")
|
|
| 1279 |
+ identityCacheBackend = identitycache.NewNopBackend() |
|
| 1280 |
+ } |
|
| 1276 | 1281 |
d.usesSnapshotter = true |
| 1277 | 1282 |
d.imageService = ctrd.NewService(ctrd.ImageServiceConfig{
|
| 1278 | 1283 |
Client: d.containerdClient, |
| 1279 | 1284 |
Containers: d.containers, |
| 1280 | 1285 |
Snapshotter: driverName, |
| 1286 |
+ IdentityCacheBackend: identityCacheBackend, |
|
| 1281 | 1287 |
RegistryHosts: d.RegistryHosts, |
| 1282 | 1288 |
Registry: d.registryService, |
| 1283 | 1289 |
EventsService: d.EventsService, |