zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/storage/cache/boltdb.go (about)

     1  package cache
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	godigest "github.com/opencontainers/go-digest"
    11  	"go.etcd.io/bbolt"
    12  
    13  	zerr "zotregistry.dev/zot/errors"
    14  	zlog "zotregistry.dev/zot/pkg/log"
    15  	"zotregistry.dev/zot/pkg/storage/constants"
    16  )
    17  
    18  type BoltDBDriver struct {
    19  	rootDir     string
    20  	db          *bbolt.DB
    21  	log         zlog.Logger
    22  	useRelPaths bool // whether or not to use relative paths, should be true for filesystem and false for s3
    23  }
    24  
    25  type BoltDBDriverParameters struct {
    26  	RootDir     string
    27  	Name        string
    28  	UseRelPaths bool
    29  }
    30  
    31  func NewBoltDBCache(parameters interface{}, log zlog.Logger) (*BoltDBDriver, error) {
    32  	properParameters, ok := parameters.(BoltDBDriverParameters)
    33  	if !ok {
    34  		log.Error().Err(zerr.ErrTypeAssertionFailed).Msgf("failed to cast type, expected type '%T' but got '%T'",
    35  			BoltDBDriverParameters{}, parameters)
    36  
    37  		return nil, zerr.ErrTypeAssertionFailed
    38  	}
    39  
    40  	err := os.MkdirAll(properParameters.RootDir, constants.DefaultDirPerms)
    41  	if err != nil {
    42  		log.Error().Err(err).Str("directory", properParameters.RootDir).Msg("failed to create directory for cache db")
    43  
    44  		return nil, err
    45  	}
    46  
    47  	dbPath := path.Join(properParameters.RootDir, properParameters.Name+constants.DBExtensionName)
    48  	dbOpts := &bbolt.Options{
    49  		Timeout:      constants.DBCacheLockCheckTimeout,
    50  		FreelistType: bbolt.FreelistArrayType,
    51  	}
    52  
    53  	cacheDB, err := bbolt.Open(dbPath, 0o600, dbOpts) //nolint:gomnd
    54  	if err != nil {
    55  		if strings.Contains(err.Error(), "timeout") {
    56  			err := fmt.Errorf("%w: %w, path '%s'", zerr.ErrTimeout, zerr.ErrDatabaseFileAlreadyInUse, dbPath)
    57  
    58  			log.Error().Err(err).Str("dbPath", dbPath).Msg("failed to create cache db")
    59  
    60  			return nil, err
    61  		}
    62  
    63  		log.Error().Err(err).Str("dbPath", dbPath).Msg("failed to create cache db")
    64  
    65  		return nil, err
    66  	}
    67  
    68  	if err := cacheDB.Update(func(tx *bbolt.Tx) error {
    69  		if _, err := tx.CreateBucketIfNotExists([]byte(constants.BlobsCache)); err != nil {
    70  			// this is a serious failure
    71  			log.Error().Err(err).Str("dbPath", dbPath).Msg("failed to create a root bucket")
    72  
    73  			return err
    74  		}
    75  
    76  		return nil
    77  	}); err != nil {
    78  		// something went wrong
    79  		log.Error().Err(err).Msg("failed to create a cache")
    80  
    81  		return nil, err
    82  	}
    83  
    84  	return &BoltDBDriver{
    85  		rootDir:     properParameters.RootDir,
    86  		db:          cacheDB,
    87  		useRelPaths: properParameters.UseRelPaths,
    88  		log:         log,
    89  	}, nil
    90  }
    91  
    92  func (d *BoltDBDriver) UsesRelativePaths() bool {
    93  	return d.useRelPaths
    94  }
    95  
    96  func (d *BoltDBDriver) Name() string {
    97  	return "boltdb"
    98  }
    99  
   100  func (d *BoltDBDriver) PutBlob(digest godigest.Digest, path string) error {
   101  	if path == "" {
   102  		d.log.Error().Err(zerr.ErrEmptyValue).Str("digest", digest.String()).
   103  			Msg("failed to put blob due to empty path being provided")
   104  
   105  		return zerr.ErrEmptyValue
   106  	}
   107  
   108  	// use only relative (to rootDir) paths on blobs
   109  	var err error
   110  	if d.useRelPaths {
   111  		path, err = filepath.Rel(d.rootDir, path)
   112  		if err != nil {
   113  			d.log.Error().Err(err).Str("path", path).Msg("failed to get relative path")
   114  		}
   115  	}
   116  
   117  	if err := d.db.Update(func(tx *bbolt.Tx) error {
   118  		root := tx.Bucket([]byte(constants.BlobsCache))
   119  		if root == nil {
   120  			// this is a serious failure
   121  			err := zerr.ErrCacheRootBucket
   122  			d.log.Error().Err(err).Msg("failed to access root bucket")
   123  
   124  			return err
   125  		}
   126  
   127  		bucket, err := root.CreateBucketIfNotExists([]byte(digest.String()))
   128  		if err != nil {
   129  			// this is a serious failure
   130  			d.log.Error().Err(err).Str("bucket", digest.String()).Msg("failed to create a bucket")
   131  
   132  			return err
   133  		}
   134  
   135  		// create nested deduped bucket where we store all the deduped blobs + original blob
   136  		deduped, err := bucket.CreateBucketIfNotExists([]byte(constants.DuplicatesBucket))
   137  		if err != nil {
   138  			// this is a serious failure
   139  			d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Msg("failed to create a bucket")
   140  
   141  			return err
   142  		}
   143  
   144  		if err := deduped.Put([]byte(path), nil); err != nil {
   145  			d.log.Error().Err(err).Str("bucket", constants.DuplicatesBucket).Str("value", path).Msg("failed to put record")
   146  
   147  			return err
   148  		}
   149  
   150  		// create origin bucket and insert only the original blob
   151  		origin := bucket.Bucket([]byte(constants.OriginalBucket))
   152  		if origin == nil {
   153  			// if the bucket doesn't exist yet then 'path' is the original blob
   154  			origin, err := bucket.CreateBucket([]byte(constants.OriginalBucket))
   155  			if err != nil {
   156  				// this is a serious failure
   157  				d.log.Error().Err(err).Str("bucket", constants.OriginalBucket).Msg("failed to create a bucket")
   158  
   159  				return err
   160  			}
   161  
   162  			if err := origin.Put([]byte(path), nil); err != nil {
   163  				d.log.Error().Err(err).Str("bucket", constants.OriginalBucket).Str("value", path).Msg("failed to put record")
   164  
   165  				return err
   166  			}
   167  		}
   168  
   169  		return nil
   170  	}); err != nil {
   171  		return err
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (d *BoltDBDriver) GetBlob(digest godigest.Digest) (string, error) {
   178  	var blobPath strings.Builder
   179  
   180  	if err := d.db.View(func(tx *bbolt.Tx) error {
   181  		root := tx.Bucket([]byte(constants.BlobsCache))
   182  		if root == nil {
   183  			// this is a serious failure
   184  			err := zerr.ErrCacheRootBucket
   185  			d.log.Error().Err(err).Msg("failed to access root bucket")
   186  
   187  			return err
   188  		}
   189  
   190  		bucket := root.Bucket([]byte(digest.String()))
   191  		if bucket != nil {
   192  			origin := bucket.Bucket([]byte(constants.OriginalBucket))
   193  			blobPath.Write(d.getOne(origin))
   194  
   195  			return nil
   196  		}
   197  
   198  		return zerr.ErrCacheMiss
   199  	}); err != nil {
   200  		return "", err
   201  	}
   202  
   203  	return blobPath.String(), nil
   204  }
   205  
   206  func (d *BoltDBDriver) HasBlob(digest godigest.Digest, blob string) bool {
   207  	if err := d.db.View(func(tx *bbolt.Tx) error {
   208  		root := tx.Bucket([]byte(constants.BlobsCache))
   209  		if root == nil {
   210  			// this is a serious failure
   211  			err := zerr.ErrCacheRootBucket
   212  			d.log.Error().Err(err).Msg("failed to access root bucket")
   213  
   214  			return err
   215  		}
   216  
   217  		bucket := root.Bucket([]byte(digest.String()))
   218  		if bucket == nil {
   219  			return zerr.ErrCacheMiss
   220  		}
   221  
   222  		origin := bucket.Bucket([]byte(constants.OriginalBucket))
   223  		if origin == nil {
   224  			return zerr.ErrCacheMiss
   225  		}
   226  
   227  		deduped := bucket.Bucket([]byte(constants.DuplicatesBucket))
   228  		if deduped == nil {
   229  			return zerr.ErrCacheMiss
   230  		}
   231  
   232  		if origin.Get([]byte(blob)) == nil {
   233  			if deduped.Get([]byte(blob)) == nil {
   234  				return zerr.ErrCacheMiss
   235  			}
   236  		}
   237  
   238  		return nil
   239  	}); err != nil {
   240  		return false
   241  	}
   242  
   243  	return true
   244  }
   245  
   246  func (d *BoltDBDriver) getOne(bucket *bbolt.Bucket) []byte {
   247  	if bucket != nil {
   248  		cursor := bucket.Cursor()
   249  		k, _ := cursor.First()
   250  
   251  		return k
   252  	}
   253  
   254  	return nil
   255  }
   256  
   257  func (d *BoltDBDriver) DeleteBlob(digest godigest.Digest, path string) error {
   258  	// use only relative (to rootDir) paths on blobs
   259  	var err error
   260  	if d.useRelPaths {
   261  		path, err = filepath.Rel(d.rootDir, path)
   262  		if err != nil {
   263  			d.log.Error().Err(err).Str("path", path).Msg("failed to get relative path")
   264  		}
   265  	}
   266  
   267  	if err := d.db.Update(func(tx *bbolt.Tx) error {
   268  		root := tx.Bucket([]byte(constants.BlobsCache))
   269  		if root == nil {
   270  			// this is a serious failure
   271  			err := zerr.ErrCacheRootBucket
   272  			d.log.Error().Err(err).Msg("failed to access root bucket")
   273  
   274  			return err
   275  		}
   276  
   277  		bucket := root.Bucket([]byte(digest.String()))
   278  		if bucket == nil {
   279  			return zerr.ErrCacheMiss
   280  		}
   281  
   282  		deduped := bucket.Bucket([]byte(constants.DuplicatesBucket))
   283  		if deduped == nil {
   284  			return zerr.ErrCacheMiss
   285  		}
   286  
   287  		if err := deduped.Delete([]byte(path)); err != nil {
   288  			d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.DuplicatesBucket).
   289  				Str("path", path).Msg("failed to delete")
   290  
   291  			return err
   292  		}
   293  
   294  		origin := bucket.Bucket([]byte(constants.OriginalBucket))
   295  		if origin != nil {
   296  			originBlob := d.getOne(origin)
   297  			if originBlob != nil {
   298  				if err := origin.Delete([]byte(path)); err != nil {
   299  					d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.OriginalBucket).
   300  						Str("path", path).Msg("failed to delete")
   301  
   302  					return err
   303  				}
   304  
   305  				// move next candidate to origin bucket, next GetKey will return this one and storage will move the content here
   306  				dedupedBlob := d.getOne(deduped)
   307  				if dedupedBlob != nil {
   308  					if err := origin.Put(dedupedBlob, nil); err != nil {
   309  						d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", constants.OriginalBucket).Str("path", path).
   310  							Msg("failed to put")
   311  
   312  						return err
   313  					}
   314  				}
   315  			}
   316  		}
   317  
   318  		// if no key in origin bucket then digest bucket is empty, remove it
   319  		k := d.getOne(origin)
   320  		if k == nil {
   321  			d.log.Debug().Str("digest", digest.String()).Str("path", path).Msg("deleting empty bucket")
   322  			if err := root.DeleteBucket([]byte(digest)); err != nil {
   323  				d.log.Error().Err(err).Str("digest", digest.String()).Str("bucket", digest.String()).Str("path", path).
   324  					Msg("failed to delete")
   325  
   326  				return err
   327  			}
   328  		}
   329  
   330  		return nil
   331  	}); err != nil {
   332  		return err
   333  	}
   334  
   335  	return nil
   336  }