Skip to content

Commit

Permalink
add db metadata store
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
  • Loading branch information
wagoodman committed Sep 27, 2024
1 parent 2264f13 commit d3b3966
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 3 deletions.
17 changes: 14 additions & 3 deletions grype/db/internal/gormadapter/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var writerStatements = []string{
// on my box it reduces the time to write from 10 minutes to 10 seconds (with ~1GB memory utilization spikes)
`PRAGMA synchronous = OFF`,
`PRAGMA journal_mode = MEMORY`,
`PRAGMA cache_size = 100000`,
`PRAGMA mmap_size = 268435456`, // 256 MB
}

var readOptions = []string{
Expand All @@ -23,9 +25,18 @@ var readOptions = []string{

// Open a new connection to a sqlite3 database file
func Open(path string, write bool) (*gorm.DB, error) {
if write {
// the file may or may not exist, so we ignore the error explicitly
_ = os.Remove(path)
memory := len(path) == 0

if write && !memory {
if _, err := os.Stat(path); err == nil {
if err := os.Remove(path); err != nil {
return nil, fmt.Errorf("unable to remove existing DB file: %w", err)
}
}
}

if memory {
path = ":memory:"
}

connStr, err := connectionString(path)
Expand Down
52 changes: 52 additions & 0 deletions grype/db/v6/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package v6

import (
"io"
"path/filepath"
)

const (
VulnerabilityDBFileName = "vulnerability.db"

// We follow SchemaVer semantics (see https://snowplow.io/blog/introducing-schemaver-for-semantic-versioning-of-schemas)

// ModelVersion indicates how many breaking schema changes there have been (which will prevent interaction with any historical data)
// note: this must ALWAYS be "6" in the context of this package.
ModelVersion = 6

// Revision indicates how many changes have been introduced which **may** prevent interaction with some historical data
Revision = 0

// Addition indicates how many changes have been introduced that are compatible with all historical data
Addition = 0
)

type ReadWriter interface {
Reader
Writer
}

type Reader interface {
DBMetadataStoreReader
}

type Writer interface {
DBMetadataStoreWriter
io.Closer
}

type Config struct {
DBDirPath string
}

func (c *Config) DBFilePath() string {
return filepath.Join(c.DBDirPath, VulnerabilityDBFileName)
}

func NewReader(cfg Config) (Reader, error) {
return newStore(cfg, false)
}

func NewWriter(cfg Config) (ReadWriter, error) {
return newStore(cfg, true)
}
59 changes: 59 additions & 0 deletions grype/db/v6/db_metadata_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package v6

import (
"fmt"
"time"

"gorm.io/gorm"

"github.com/anchore/grype/internal/log"
)

type DBMetadataStoreWriter interface {
SetDBMetadata() error
}

type DBMetadataStoreReader interface {
GetDBMetadata() (*DBMetadata, error)
}

type dbMetadataStore struct {
db *gorm.DB
}

func newDBMetadataStore(db *gorm.DB) *dbMetadataStore {
return &dbMetadataStore{
db: db,
}
}

func (s *dbMetadataStore) GetDBMetadata() (*DBMetadata, error) {
log.Trace("fetching DB metadata record")

var model DBMetadata

result := s.db.First(&model)
return &model, result.Error
}

func (s *dbMetadataStore) SetDBMetadata() error {
log.Trace("writing DB metadata record")

if err := s.db.Unscoped().Where("true").Delete(&DBMetadata{}).Error; err != nil {
return fmt.Errorf("failed to delete existing DB metadata record: %w", err)
}

ts := time.Now().UTC()
instance := &DBMetadata{
BuildTimestamp: &ts,
Model: ModelVersion,
Revision: Revision,
Addition: Addition,
}

if err := s.db.Create(instance).Error; err != nil {
return fmt.Errorf("failed to create DB metadata record: %w", err)
}

return nil
}
51 changes: 51 additions & 0 deletions grype/db/v6/db_metadata_store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package v6

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)

func TestDbMetadataStore_empty(t *testing.T) {
s := newDBMetadataStore(setupTestDB(t))

// attempt to fetch a non-existent record
actualMetadata, err := s.GetDBMetadata()
require.ErrorIs(t, err, gorm.ErrRecordNotFound)
require.NotNil(t, actualMetadata)
}

func TestDbMetadataStore(t *testing.T) {
s := newDBMetadataStore(setupTestDB(t))

require.NoError(t, s.SetDBMetadata())

// fetch the record
actualMetadata, err := s.GetDBMetadata()
require.NoError(t, err)
require.NotNil(t, actualMetadata)

assert.NotZero(t, *actualMetadata.BuildTimestamp) // a timestamp was set
name, _ := actualMetadata.BuildTimestamp.Zone()
assert.Equal(t, "UTC", name) // the timestamp is in UTC

actualMetadata.BuildTimestamp = nil // value not under test

assert.Equal(t, DBMetadata{
BuildTimestamp: nil,
// expect the correct version info
Model: ModelVersion,
Revision: Revision,
Addition: Addition,
}, *actualMetadata)
}

func setupTestDB(t *testing.T) *gorm.DB {
// note: empty path means in-memory db
s, err := newStore(Config{}, true)
require.NoError(t, err)

return s.db
}
19 changes: 19 additions & 0 deletions grype/db/v6/models.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package v6

import "time"

func models() []any {
return []any{
// non-domain info
&DBMetadata{},
}
}

// non-domain info //////////////////////////////////////////////////////

type DBMetadata struct {
BuildTimestamp *time.Time `gorm:"column:build_timestamp;not null"`
Model int `gorm:"column:model;not null"`
Revision int `gorm:"column:revision;not null"`
Addition int `gorm:"column:addition;not null"`
}
61 changes: 61 additions & 0 deletions grype/db/v6/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package v6

import (
"fmt"

"gorm.io/gorm"

"github.com/anchore/grype/grype/db/internal/gormadapter"
"github.com/anchore/grype/internal/log"
)

type store struct {
*dbMetadataStore
db *gorm.DB
config Config
write bool
}

func newStore(cfg Config, write bool) (*store, error) {
db, err := gormadapter.Open(cfg.DBFilePath(), write)
if err != nil {
return nil, err
}

db.Exec("PRAGMA foreign_keys = ON")

if write {
if err := migrate(db, models()...); err != nil {
return nil, fmt.Errorf("unable to migrate: %w", err)
}
}

return &store{
dbMetadataStore: newDBMetadataStore(db),
db: db,
config: cfg,
write: write,
}, nil
}

func migrate(db *gorm.DB, models ...any) error {
if err := db.AutoMigrate(models...); err != nil {
return fmt.Errorf("unable to migrate: %w", err)
}

return nil
}

func (s *store) Close() error {
log.Debug("closing store")
if !s.write {
return nil
}

err := s.db.Exec("VACUUM").Error
if err != nil {
return fmt.Errorf("failed to vacuum: %w", err)
}

return nil
}

0 comments on commit d3b3966

Please sign in to comment.