Files
trivy/pkg/cache/fs.go

224 lines
6.0 KiB
Go

package cache
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"time"
"github.com/hashicorp/go-multierror"
bolt "go.etcd.io/bbolt"
bberrors "go.etcd.io/bbolt/errors"
"golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/fanal/types"
)
const defaultFSCacheTimeout = 5 * time.Second
var _ Cache = &FSCache{}
type FSCache struct {
db *bolt.DB
directory string
}
func NewFSCache(cacheDir string) (FSCache, error) {
dir := filepath.Join(cacheDir, scanCacheDirName)
if err := os.MkdirAll(dir, 0o700); err != nil {
return FSCache{}, xerrors.Errorf("failed to create cache dir: %w", err)
}
db, err := bolt.Open(filepath.Join(dir, "fanal.db"), 0o600, &bolt.Options{
Timeout: defaultFSCacheTimeout,
})
if err != nil {
// Check if the error is due to timeout (database locked by another process)
if errors.Is(err, bberrors.ErrTimeout) {
return FSCache{}, xerrors.Errorf("cache may be in use by another process: %w", err)
}
return FSCache{}, xerrors.Errorf("unable to open cache DB: %w", err)
}
err = db.Update(func(tx *bolt.Tx) error {
for _, bucket := range []string{
artifactBucket,
blobBucket,
} {
if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil {
return xerrors.Errorf("unable to create %s bucket: %w", bucket, err)
}
}
return nil
})
if err != nil {
return FSCache{}, xerrors.Errorf("DB error: %w", err)
}
return FSCache{
db: db,
directory: dir,
}, nil
}
// GetBlob gets blob information such as layer data from local cache
func (fs FSCache) GetBlob(blobID string) (types.BlobInfo, error) {
var blobInfo types.BlobInfo
err := fs.db.View(func(tx *bolt.Tx) error {
var err error
blobBucket := tx.Bucket([]byte(blobBucket))
blobInfo, err = fs.getBlob(blobBucket, blobID)
if err != nil {
return xerrors.Errorf("failed to get blob from the cache: %w", err)
}
return nil
})
if err != nil {
return types.BlobInfo{}, xerrors.Errorf("DB error: %w", err)
}
return blobInfo, nil
}
func (fs FSCache) getBlob(blobBucket *bolt.Bucket, diffID string) (types.BlobInfo, error) {
b := blobBucket.Get([]byte(diffID))
var l types.BlobInfo
if err := json.Unmarshal(b, &l); err != nil {
return types.BlobInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err)
}
return l, nil
}
// PutBlob stores blob information such as layer information in local cache
func (fs FSCache) PutBlob(blobID string, blobInfo types.BlobInfo) error {
b, err := json.Marshal(blobInfo)
if err != nil {
return xerrors.Errorf("unable to marshal blob JSON (%s): %w", blobID, err)
}
err = fs.db.Update(func(tx *bolt.Tx) error {
blobBucket := tx.Bucket([]byte(blobBucket))
err = blobBucket.Put([]byte(blobID), b)
if err != nil {
return xerrors.Errorf("unable to store blob information in cache (%s): %w", blobID, err)
}
return nil
})
if err != nil {
return xerrors.Errorf("DB update error: %w", err)
}
return nil
}
// GetArtifact gets artifact information such as image metadata from local cache
func (fs FSCache) GetArtifact(artifactID string) (types.ArtifactInfo, error) {
var blob []byte
err := fs.db.View(func(tx *bolt.Tx) error {
artifactBucket := tx.Bucket([]byte(artifactBucket))
blob = artifactBucket.Get([]byte(artifactID))
return nil
})
if err != nil {
return types.ArtifactInfo{}, xerrors.Errorf("DB error: %w", err)
}
var info types.ArtifactInfo
if err := json.Unmarshal(blob, &info); err != nil {
return types.ArtifactInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err)
}
return info, nil
}
// DeleteBlobs removes blobs by IDs
func (fs FSCache) DeleteBlobs(blobIDs []string) error {
var errs error
err := fs.db.Update(func(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(blobBucket))
for _, blobID := range blobIDs {
if err := bucket.Delete([]byte(blobID)); err != nil {
errs = multierror.Append(errs, err)
}
}
return nil
})
if err != nil {
return xerrors.Errorf("DB delete error: %w", err)
}
return errs
}
// PutArtifact stores artifact information such as image metadata in local cache
func (fs FSCache) PutArtifact(artifactID string, artifactInfo types.ArtifactInfo) (err error) {
b, err := json.Marshal(artifactInfo)
if err != nil {
return xerrors.Errorf("unable to marshal artifact JSON (%s): %w", artifactID, err)
}
err = fs.db.Update(func(tx *bolt.Tx) error {
artifactBucket := tx.Bucket([]byte(artifactBucket))
err = artifactBucket.Put([]byte(artifactID), b)
if err != nil {
return xerrors.Errorf("unable to store artifact information in cache (%s): %w", artifactID, err)
}
return nil
})
if err != nil {
return xerrors.Errorf("DB update error: %w", err)
}
return nil
}
// MissingBlobs returns missing blob IDs such as layer IDs
func (fs FSCache) MissingBlobs(artifactID string, blobIDs []string) (bool, []string, error) {
var missingArtifact bool
var missingBlobIDs []string
err := fs.db.View(func(tx *bolt.Tx) error {
blobBucket := tx.Bucket([]byte(blobBucket))
for _, blobID := range blobIDs {
blobInfo, err := fs.getBlob(blobBucket, blobID)
if err != nil {
// error means cache missed blob info
missingBlobIDs = append(missingBlobIDs, blobID)
continue
}
if blobInfo.SchemaVersion != types.BlobJSONSchemaVersion {
missingBlobIDs = append(missingBlobIDs, blobID)
}
}
return nil
})
if err != nil {
return false, nil, xerrors.Errorf("DB error: %w", err)
}
// get artifact info
artifactInfo, err := fs.GetArtifact(artifactID)
if err != nil {
// error means cache missed artifact info
return true, missingBlobIDs, nil
}
if artifactInfo.SchemaVersion != types.ArtifactJSONSchemaVersion {
missingArtifact = true
}
return missingArtifact, missingBlobIDs, nil
}
// Close closes the database
func (fs FSCache) Close() error {
if err := fs.db.Close(); err != nil {
return xerrors.Errorf("unable to close DB: %w", err)
}
return nil
}
// Clear removes the database
func (fs FSCache) Clear() error {
if err := fs.Close(); err != nil {
return err
}
if err := os.RemoveAll(fs.directory); err != nil {
return xerrors.Errorf("failed to remove cache: %w", err)
}
return nil
}