| package identitycache | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "os" | |
| "path/filepath" | |
| "sync" | |
| "time" | |
| "github.com/containerd/log" | |
| bolt "go.etcd.io/bbolt" | |
| ) | |
| var bboltCacheBucket = []byte("image-identity-cache-v1") | |
| type boltBackend struct { | |
| db *bolt.DB | |
| closeOnce sync.Once | |
| closeErr error | |
| } | |
| // NewBoltDBBackend creates a bbolt-backed persistent cache backend. | |
| func NewBoltDBBackend(root string) (Backend, error) { | |
| if root == "" { | |
| return NewNopBackend(), nil | |
| } | |
| cacheDir := filepath.Join(root, "image") | |
| if err := os.MkdirAll(cacheDir, 0o700); err != nil { | |
| return nil, err | |
| } | |
| db, err := safeOpen(filepath.Join(cacheDir, "identity-cache.db"), 0o600, nil) | |
| if err != nil { | |
| return nil, err | |
| } | |
| b := &boltBackend{db: db} | |
| if err := b.db.Update(func(tx *bolt.Tx) error { | |
| _, err := tx.CreateBucketIfNotExists(bboltCacheBucket) | |
| return err | |
| }); err != nil { | |
| _ = db.Close() | |
| return nil, err | |
| } | |
| return b, nil | |
| } | |
| func (b *boltBackend) Load(_ context.Context, cacheKey string, now time.Time) (Entry, bool, error) { | |
| var ( | |
| entry Entry | |
| payload []byte | |
| ) | |
| err := b.db.View(func(tx *bolt.Tx) error { | |
| bucket := tx.Bucket(bboltCacheBucket) | |
| if bucket == nil { | |
| return nil | |
| } | |
| value := bucket.Get([]byte(cacheKey)) | |
| if value == nil { | |
| return nil | |
| } | |
| payload = append([]byte(nil), value...) | |
| return nil | |
| }) | |
| if err != nil { | |
| return Entry{}, false, err | |
| } | |
| if len(payload) == 0 { | |
| return Entry{}, false, nil | |
| } | |
| if err := json.Unmarshal(payload, &entry); err != nil { | |
| _ = b.delete(cacheKey) | |
| return Entry{}, false, nil | |
| } | |
| if now.After(entry.ExpiresAt) { | |
| return Entry{}, false, nil | |
| } | |
| return entry, true, nil | |
| } | |
| func (b *boltBackend) Store(_ context.Context, cacheKey string, entry Entry, _ time.Time) error { | |
| payload, err := json.Marshal(entry) | |
| if err != nil { | |
| return err | |
| } | |
| return b.db.Update(func(tx *bolt.Tx) error { | |
| bucket, err := tx.CreateBucketIfNotExists(bboltCacheBucket) | |
| if err != nil { | |
| return err | |
| } | |
| return bucket.Put([]byte(cacheKey), payload) | |
| }) | |
| } | |
| func (b *boltBackend) Walk(ctx context.Context, _ time.Time, fn func(cacheKey string, entry Entry) error) error { | |
| return b.db.View(func(tx *bolt.Tx) error { | |
| bucket := tx.Bucket(bboltCacheBucket) | |
| if bucket == nil { | |
| return nil | |
| } | |
| cursor := bucket.Cursor() | |
| for key, value := cursor.First(); key != nil; key, value = cursor.Next() { | |
| if err := ctx.Err(); err != nil { | |
| return err | |
| } | |
| var entry Entry | |
| if err := json.Unmarshal(value, &entry); err != nil { | |
| continue | |
| } | |
| if err := fn(string(key), entry); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| }) | |
| } | |
| func (b *boltBackend) PruneExpired(_ context.Context, now time.Time) error { | |
| return b.pruneExpiredEntries(now) | |
| } | |
| func (b *boltBackend) Close() error { | |
| if b == nil || b.db == nil { | |
| return nil | |
| } | |
| b.closeOnce.Do(func() { | |
| b.closeErr = b.db.Close() | |
| }) | |
| return b.closeErr | |
| } | |
| func (b *boltBackend) delete(cacheKey string) error { | |
| return b.db.Update(func(tx *bolt.Tx) error { | |
| bucket := tx.Bucket(bboltCacheBucket) | |
| if bucket == nil { | |
| return nil | |
| } | |
| return bucket.Delete([]byte(cacheKey)) | |
| }) | |
| } | |
| func (b *boltBackend) pruneExpiredEntries(now time.Time) error { | |
| return b.db.Update(func(tx *bolt.Tx) error { | |
| bucket := tx.Bucket(bboltCacheBucket) | |
| if bucket == nil { | |
| return nil | |
| } | |
| cursor := bucket.Cursor() | |
| for key, value := cursor.First(); key != nil; key, value = cursor.Next() { | |
| var entry Entry | |
| if err := json.Unmarshal(value, &entry); err != nil || now.After(entry.ExpiresAt) { | |
| if err := cursor.Delete(); err != nil { | |
| return err | |
| } | |
| } | |
| } | |
| return nil | |
| }) | |
| } | |
| // safeOpen opens a bolt database with automatic recovery from corruption. | |
| // If the database file is corrupted, it backs up the corrupted file and creates | |
| // a new empty database. | |
| func safeOpen(dbPath string, mode os.FileMode, opts *bolt.Options) (db *bolt.DB, err error) { | |
| defer func() { | |
| if r := recover(); r != nil { | |
| err = fmt.Errorf("%v", r) | |
| } | |
| // If we fail opening the DB, but can read a non-empty file, try resetting it. | |
| if err != nil && fileHasContent(dbPath) { | |
| db, err = fallbackOpen(dbPath, mode, opts, err) | |
| } | |
| }() | |
| return openDB(dbPath, mode, opts) | |
| } | |
| func openDB(dbPath string, mode os.FileMode, opts *bolt.Options) (*bolt.DB, error) { | |
| bdb, err := bolt.Open(dbPath, mode, opts) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return bdb, nil | |
| } | |
| func fallbackOpen(dbPath string, mode os.FileMode, opts *bolt.Options, openErr error) (*bolt.DB, error) { | |
| backupPath := dbPath + "." + fmt.Sprintf("%d", time.Now().UnixNano()) + ".bak" | |
| log.L.Errorf("failed to open moby image identity cache database %s, resetting to empty. "+ | |
| "Old database is backed up to %s. This usually means dockerd crashed or was terminated abruptly, leaving the cache DB corrupted. "+ | |
| "If this keeps happening, please report at https://github.com/moby/moby/issues. original error: %v", | |
| dbPath, backupPath, openErr) | |
| if err := os.Rename(dbPath, backupPath); err != nil { | |
| return nil, fmt.Errorf("failed to rename database file %s to %s: %w", dbPath, backupPath, err) | |
| } | |
| // Second open should create a new database; failure here is permanent. | |
| return openDB(dbPath, mode, opts) | |
| } | |
| func fileHasContent(dbPath string) bool { | |
| st, err := os.Stat(dbPath) | |
| return err == nil && st.Size() > 0 | |
| } |