github.com/quay/claircore@v1.5.28/datastore/postgres/enrichment.go (about)

     1  package postgres
     2  
     3  import (
     4  	"context"
     5  	"crypto/md5"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"sort"
    10  	"strconv"
    11  	"time"
    12  
    13  	"github.com/google/uuid"
    14  	"github.com/jackc/pgx/v4"
    15  	"github.com/jackc/pgx/v4/pgxpool"
    16  	"github.com/prometheus/client_golang/prometheus"
    17  	"github.com/prometheus/client_golang/prometheus/promauto"
    18  	"github.com/quay/zlog"
    19  
    20  	"github.com/quay/claircore/datastore"
    21  	"github.com/quay/claircore/libvuln/driver"
    22  	"github.com/quay/claircore/pkg/microbatch"
    23  )
    24  
    25  var (
    26  	updateEnrichmentsCounter = promauto.NewCounterVec(
    27  		prometheus.CounterOpts{
    28  			Namespace: "claircore",
    29  			Subsystem: "vulnstore",
    30  			Name:      "updateenrichments_total",
    31  			Help:      "Total number of database queries issued in the UpdateEnrichments method.",
    32  		},
    33  		[]string{"query"},
    34  	)
    35  	updateEnrichmentsDuration = promauto.NewHistogramVec(
    36  		prometheus.HistogramOpts{
    37  			Namespace: "claircore",
    38  			Subsystem: "vulnstore",
    39  			Name:      "updateenrichments_duration_seconds",
    40  			Help:      "Duration of all queries issued in the UpdateEnrichments method.",
    41  		},
    42  		[]string{"query"},
    43  	)
    44  	getEnrichmentsCounter = promauto.NewCounterVec(
    45  		prometheus.CounterOpts{
    46  			Namespace: "claircore",
    47  			Subsystem: "vulnstore",
    48  			Name:      "getenrichments_total",
    49  			Help:      "Total number of database queries issued in the get method.",
    50  		},
    51  		[]string{"query", "success"},
    52  	)
    53  	getEnrichmentsDuration = promauto.NewHistogramVec(
    54  		prometheus.HistogramOpts{
    55  			Namespace: "claircore",
    56  			Subsystem: "vulnstore",
    57  			Name:      "getenrichments_duration_seconds",
    58  			Help:      "Duration of all queries issued in the get method.",
    59  		},
    60  		[]string{"query", "success"},
    61  	)
    62  )
    63  
    64  func (s *MatcherStore) UpdateEnrichmentsIter(ctx context.Context, updater string, fp driver.Fingerprint, it datastore.EnrichmentIter) (uuid.UUID, error) {
    65  	ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/MatcherStore.UpdateEnrichmentsIter")
    66  	return s.updateEnrichments(ctx, updater, fp, it)
    67  }
    68  
    69  // UpdateEnrichments creates a new UpdateOperation, inserts the provided
    70  // EnrichmentRecord(s), and ensures enrichments from previous updates are not
    71  // queried by clients.
    72  func (s *MatcherStore) UpdateEnrichments(ctx context.Context, updater string, fp driver.Fingerprint, es []driver.EnrichmentRecord) (uuid.UUID, error) {
    73  	ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/MatcherStore.UpdateEnrichments")
    74  	enIter := func(yield func(record *driver.EnrichmentRecord, err error) bool) {
    75  		for i := range es {
    76  			if !yield(&es[i], nil) {
    77  				break
    78  			}
    79  		}
    80  	}
    81  	return s.updateEnrichments(ctx, updater, fp, enIter)
    82  }
    83  
    84  func (s *MatcherStore) updateEnrichments(ctx context.Context, name string, fp driver.Fingerprint, it datastore.EnrichmentIter) (uuid.UUID, error) {
    85  	const (
    86  		create = `
    87  INSERT
    88  INTO
    89  	update_operation (updater, fingerprint, kind)
    90  VALUES
    91  	($1, $2, 'enrichment')
    92  RETURNING
    93  	id, ref;`
    94  		insert = `
    95  INSERT
    96  INTO
    97  	enrichment (hash_kind, hash, updater, tags, data)
    98  VALUES
    99  	($1, $2, $3, $4, $5)
   100  ON CONFLICT
   101  	(hash_kind, hash)
   102  DO
   103  	NOTHING;`
   104  		assoc = `
   105  INSERT
   106  INTO
   107  	uo_enrich (enrich, updater, uo, date)
   108  VALUES
   109  	(
   110  		(
   111  			SELECT
   112  				id
   113  			FROM
   114  				enrichment
   115  			WHERE
   116  				hash_kind = $1
   117  				AND hash = $2
   118  				AND updater = $3
   119  		),
   120  		$3,
   121  		$4,
   122  		transaction_timestamp()
   123  	)
   124  ON CONFLICT
   125  DO
   126  	NOTHING;`
   127  		refreshView = `REFRESH MATERIALIZED VIEW CONCURRENTLY latest_update_operations;`
   128  	)
   129  	ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/UpdateEnrichments")
   130  
   131  	var id uint64
   132  	var ref uuid.UUID
   133  
   134  	start := time.Now()
   135  
   136  	if err := s.pool.QueryRow(ctx, create, name, string(fp)).Scan(&id, &ref); err != nil {
   137  		return uuid.Nil, fmt.Errorf("failed to create update_operation: %w", err)
   138  	}
   139  
   140  	updateEnrichmentsCounter.WithLabelValues("create").Add(1)
   141  	updateEnrichmentsDuration.WithLabelValues("create").Observe(time.Since(start).Seconds())
   142  
   143  	tx, err := s.pool.Begin(ctx)
   144  	if err != nil {
   145  		return uuid.Nil, fmt.Errorf("unable to start transaction: %w", err)
   146  	}
   147  	defer tx.Rollback(ctx)
   148  
   149  	zlog.Debug(ctx).
   150  		Str("ref", ref.String()).
   151  		Msg("update_operation created")
   152  
   153  	batch := microbatch.NewInsert(tx, 2000, time.Minute)
   154  	start = time.Now()
   155  	enCt := 0
   156  	it(func(en *driver.EnrichmentRecord, iterErr error) bool {
   157  		if iterErr != nil {
   158  			err = iterErr
   159  			return false
   160  		}
   161  		enCt++
   162  		hashKind, hash := hashEnrichment(en)
   163  		err = batch.Queue(ctx, insert,
   164  			hashKind, hash, name, en.Tags, en.Enrichment,
   165  		)
   166  		if err != nil {
   167  			err = fmt.Errorf("failed to queue enrichment: %w", err)
   168  			return false
   169  		}
   170  		if err := batch.Queue(ctx, assoc, hashKind, hash, name, id); err != nil {
   171  			err = fmt.Errorf("failed to queue association: %w", err)
   172  			return false
   173  		}
   174  		return true
   175  	})
   176  	if err != nil {
   177  		return uuid.Nil, fmt.Errorf("iterating on enrichments: %w", err)
   178  	}
   179  	if err := batch.Done(ctx); err != nil {
   180  		return uuid.Nil, fmt.Errorf("failed to finish batch enrichment insert: %w", err)
   181  	}
   182  	updateEnrichmentsCounter.WithLabelValues("insert_batch").Add(1)
   183  	updateEnrichmentsDuration.WithLabelValues("insert_batch").Observe(time.Since(start).Seconds())
   184  
   185  	if err := tx.Commit(ctx); err != nil {
   186  		return uuid.Nil, fmt.Errorf("failed to commit transaction: %w", err)
   187  	}
   188  	if _, err = s.pool.Exec(ctx, refreshView); err != nil {
   189  		return uuid.Nil, fmt.Errorf("could not refresh latest_update_operations: %w", err)
   190  	}
   191  	zlog.Debug(ctx).
   192  		Stringer("ref", ref).
   193  		Int("inserted", enCt).
   194  		Msg("update_operation committed")
   195  	return ref, nil
   196  }
   197  
   198  func hashEnrichment(r *driver.EnrichmentRecord) (k string, d []byte) {
   199  	h := md5.New()
   200  	sort.Strings(r.Tags)
   201  	for _, t := range r.Tags {
   202  		io.WriteString(h, t)
   203  		h.Write([]byte("\x00"))
   204  	}
   205  	h.Write(r.Enrichment)
   206  	return "md5", h.Sum(nil)
   207  }
   208  
   209  func (s *MatcherStore) GetEnrichment(ctx context.Context, name string, tags []string) (res []driver.EnrichmentRecord, err error) {
   210  	const query = `
   211  WITH
   212  	latest
   213  		AS (
   214  			SELECT
   215  				id
   216  			FROM
   217  				latest_update_operations
   218  			WHERE
   219  				updater = $1
   220  			AND
   221  				kind = 'enrichment'
   222  			LIMIT 1
   223  		)
   224  SELECT
   225  	e.tags, e.data
   226  FROM
   227  	enrichment AS e,
   228  	uo_enrich AS uo,
   229  	latest
   230  WHERE
   231  	uo.uo = latest.id
   232  	AND uo.enrich = e.id
   233  	AND e.tags && $2::text[];`
   234  
   235  	ctx = zlog.ContextWithValues(ctx, "component", "datastore/postgres/GetEnrichment")
   236  	timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
   237  		getEnrichmentsDuration.WithLabelValues("query", strconv.FormatBool(errors.Is(err, nil))).Observe(v)
   238  	}))
   239  	defer timer.ObserveDuration()
   240  	defer func() {
   241  		getEnrichmentsCounter.WithLabelValues("query", strconv.FormatBool(errors.Is(err, nil))).Inc()
   242  	}()
   243  	var (
   244  		c    *pgxpool.Conn
   245  		rows pgx.Rows
   246  	)
   247  	c, err = s.pool.Acquire(ctx)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	defer c.Release()
   252  	res = make([]driver.EnrichmentRecord, 0, 8) // Guess at capacity.
   253  	rows, err = c.Query(ctx, query, name, tags)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	defer rows.Close()
   258  	for rows.Next() {
   259  		i := len(res)
   260  		res = append(res, driver.EnrichmentRecord{})
   261  		r := &res[i]
   262  		err = rows.Scan(&r.Tags, &r.Enrichment)
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  	}
   267  	err = rows.Err()
   268  	return res, err
   269  }