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

     1  package postgres
     2  
     3  import (
     4  	"context"
     5  	"encoding/binary"
     6  	"errors"
     7  	"fmt"
     8  	"hash/fnv"
     9  	"strconv"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/google/go-cmp/cmp"
    14  	"github.com/google/go-cmp/cmp/cmpopts"
    15  	"github.com/google/uuid"
    16  	"github.com/jackc/pgx/v4/pgxpool"
    17  	"github.com/quay/zlog"
    18  
    19  	"github.com/quay/claircore"
    20  	"github.com/quay/claircore/datastore"
    21  	"github.com/quay/claircore/libvuln/driver"
    22  	"github.com/quay/claircore/test"
    23  	"github.com/quay/claircore/test/integration"
    24  	pgtest "github.com/quay/claircore/test/postgres"
    25  )
    26  
    27  // TestUpdateE2E performs an end to end test of update operations and diffing
    28  func TestUpdateE2E(t *testing.T) {
    29  	integration.NeedDB(t)
    30  	ctx := zlog.Test(context.Background(), t)
    31  
    32  	cases := []updateE2e{
    33  		{
    34  			Name:    "10Add2",
    35  			Insert:  10,
    36  			Updates: 2,
    37  		},
    38  		{
    39  			Name:    "100Add2",
    40  			Insert:  100,
    41  			Updates: 2,
    42  		},
    43  		{
    44  			Name:    "10Add20",
    45  			Insert:  10,
    46  			Updates: 20,
    47  		},
    48  	}
    49  	for _, tc := range cases {
    50  		c := &tc
    51  		t.Run(c.Name, c.Run(ctx))
    52  	}
    53  }
    54  
    55  // UpdateE2e implements a multi-phase test ensuring an update operation and
    56  // diff works end to end.
    57  type updateE2e struct {
    58  	Name    string
    59  	Insert  int
    60  	Updates int
    61  
    62  	// These are all computed values or results that need to hang around between
    63  	// tests.
    64  	updater   string
    65  	s         datastore.MatcherStore
    66  	pool      *pgxpool.Pool
    67  	updateOps []driver.UpdateOperation
    68  }
    69  
    70  func (e *updateE2e) Run(ctx context.Context) func(*testing.T) {
    71  	h := fnv.New64a()
    72  	h.Write([]byte(e.Name))
    73  	binary.Write(h, binary.BigEndian, int64(e.Insert))
    74  	binary.Write(h, binary.BigEndian, int64(e.Updates))
    75  	e.updater = strconv.FormatUint(h.Sum64(), 36)
    76  	order := []struct {
    77  		Name string
    78  		Test func(context.Context) func(*testing.T)
    79  	}{
    80  		{"Update", e.Update},
    81  		{"GetUpdateOperations", e.GetUpdateOperations},
    82  		{"recordUpdaterStatus", e.recordUpdaterStatus},
    83  		{"Diff", e.Diff},
    84  		{"DeleteUpdateOperations", e.DeleteUpdateOperations},
    85  	}
    86  	return func(t *testing.T) {
    87  		ctx := zlog.Test(ctx, t)
    88  		pool := pgtest.TestMatcherDB(ctx, t)
    89  		e.pool = pool
    90  		e.s = NewMatcherStore(pool)
    91  		for _, sub := range order {
    92  			if !t.Run(sub.Name, sub.Test(ctx)) {
    93  				t.FailNow()
    94  			}
    95  		}
    96  	}
    97  }
    98  
    99  const (
   100  	opStep = 10
   101  )
   102  
   103  func (e *updateE2e) vulns() [][]*claircore.Vulnerability {
   104  	sz := e.Insert + (opStep * e.Updates)
   105  	vs := test.GenUniqueVulnerabilities(sz, e.updater)
   106  	r := make([][]*claircore.Vulnerability, e.Updates)
   107  	for i := 0; i < e.Updates; i++ {
   108  		off := i * opStep
   109  		r[i] = vs[off : off+e.Insert]
   110  	}
   111  	return r
   112  }
   113  
   114  var updateOpCmp = cmpopts.IgnoreFields(driver.UpdateOperation{}, "Date")
   115  
   116  // Update confirms multiple updates to the vulstore
   117  // do the correct things.
   118  func (e *updateE2e) Update(ctx context.Context) func(*testing.T) {
   119  	fp := driver.Fingerprint(uuid.New().String())
   120  	return func(t *testing.T) {
   121  		ctx := zlog.Test(ctx, t)
   122  		e.updateOps = make([]driver.UpdateOperation, 0, e.Updates)
   123  		for _, vs := range e.vulns() {
   124  			ref, err := e.s.UpdateVulnerabilities(ctx, e.updater, fp, vs)
   125  			if err != nil {
   126  				t.Fatalf("failed to perform update: %v", err)
   127  			}
   128  
   129  			// attach generated UpdateOperations to test retrieval
   130  			// date can be ignored. add in stack order to compare
   131  			e.updateOps = append(e.updateOps, driver.UpdateOperation{
   132  				Ref:         ref,
   133  				Fingerprint: fp,
   134  				Updater:     e.updater,
   135  			})
   136  
   137  			checkInsertedVulns(ctx, t, e.pool, ref, vs)
   138  		}
   139  		t.Log("ok")
   140  	}
   141  }
   142  
   143  // GetUpdateOperations confirms retrieving an update
   144  // operation returns the expected results.
   145  func (e *updateE2e) GetUpdateOperations(ctx context.Context) func(*testing.T) {
   146  	return func(t *testing.T) {
   147  		ctx := zlog.Test(ctx, t)
   148  		out, err := e.s.GetUpdateOperations(ctx, driver.VulnerabilityKind, e.updater)
   149  		if err != nil {
   150  			t.Fatalf("failed to get UpdateOperations: %v", err)
   151  		}
   152  		// confirm number of update operations
   153  		if got, want := len(out[e.updater]), e.Updates; got != want {
   154  			t.Fatalf("wrong number of update operations: got: %d, want: %d", got, want)
   155  		}
   156  		// confirm retrieved update operations match
   157  		// test generated values
   158  		for i := 0; i < e.Updates; i++ {
   159  			ri := e.Updates - i - 1
   160  			want, got := e.updateOps[ri], out[e.updater][i]
   161  			if !cmp.Equal(want, got, updateOpCmp) {
   162  				t.Fatal(cmp.Diff(want, got, updateOpCmp))
   163  			}
   164  		}
   165  		t.Log("ok")
   166  	}
   167  }
   168  
   169  type update struct {
   170  	UpdaterName            string             `json:"updater_name"`
   171  	LastAttempt            time.Time          `json:"last_attempt"`
   172  	LastSuccess            *time.Time         `json:"last_success"`
   173  	LastRunSucceeded       bool               `json:"last_run_succeeded"`
   174  	LastAttemptFingerprint driver.Fingerprint `json:"last_attempt_fingerprint"`
   175  	LastError              *string            `json:"last_error"`
   176  }
   177  
   178  // recordUpdaterStatus confirms multiple updates to record last update times
   179  // and then an update to an whole updater set
   180  func (e *updateE2e) recordUpdaterStatus(ctx context.Context) func(*testing.T) {
   181  	return func(t *testing.T) {
   182  		ctx := zlog.Test(ctx, t)
   183  		errorText := "test error"
   184  		firstUpdateDate := time.Date(2020, time.Month(1), 22, 2, 10, 30, 0, time.UTC)
   185  		secondUpdateDate := time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC)
   186  		var emptyFingerprint driver.Fingerprint
   187  		updates := []update{
   188  			{
   189  				UpdaterName:            "test-updater-1",
   190  				LastAttempt:            firstUpdateDate,
   191  				LastSuccess:            &firstUpdateDate,
   192  				LastRunSucceeded:       true,
   193  				LastAttemptFingerprint: driver.Fingerprint(uuid.New().String()),
   194  			},
   195  			{
   196  				UpdaterName:            "test-updater-1",
   197  				LastAttempt:            secondUpdateDate,
   198  				LastSuccess:            &secondUpdateDate,
   199  				LastRunSucceeded:       true,
   200  				LastAttemptFingerprint: driver.Fingerprint(uuid.New().String()),
   201  			},
   202  			{
   203  				UpdaterName:            "test-updater-2",
   204  				LastAttempt:            firstUpdateDate,
   205  				LastSuccess:            &firstUpdateDate,
   206  				LastRunSucceeded:       true,
   207  				LastAttemptFingerprint: emptyFingerprint,
   208  			},
   209  			{
   210  				UpdaterName:            "test-updater-3",
   211  				LastAttempt:            firstUpdateDate,
   212  				LastRunSucceeded:       false,
   213  				LastAttemptFingerprint: driver.Fingerprint(uuid.New().String()),
   214  				LastError:              &errorText,
   215  			},
   216  		}
   217  		expectedTableContents := make(map[string]update)
   218  		for _, update := range updates {
   219  			var updateError error
   220  			if update.LastError != nil {
   221  				updateError = errors.New(*update.LastError)
   222  			}
   223  			err := e.s.RecordUpdaterStatus(ctx, update.UpdaterName, update.LastAttempt, update.LastAttemptFingerprint, updateError)
   224  			if err != nil {
   225  				t.Fatalf("failed to perform update: %v", err)
   226  			}
   227  			expectedTableContents[update.UpdaterName] = update
   228  		}
   229  		checkUpdateTimes(ctx, t, e.pool, expectedTableContents)
   230  
   231  		newUpdaterSetTime := time.Date(2021, time.Month(2), 25, 1, 10, 30, 0, time.UTC)
   232  		e.s.RecordUpdaterSetStatus(ctx, "test", newUpdaterSetTime)
   233  		for updater, row := range expectedTableContents {
   234  			row.LastAttempt = newUpdaterSetTime
   235  			row.LastSuccess = &newUpdaterSetTime
   236  			row.LastRunSucceeded = true
   237  			expectedTableContents[updater] = row
   238  		}
   239  		checkUpdateTimes(ctx, t, e.pool, expectedTableContents)
   240  		t.Log("ok")
   241  	}
   242  }
   243  
   244  var vulnCmp = cmp.Options{
   245  	cmpopts.IgnoreFields(claircore.Vulnerability{}, "ID", "Package.ID", "Dist.ID", "Repo.ID"),
   246  }
   247  
   248  func orNoIndex(a int) string {
   249  	if a < 0 {
   250  		return "no index"
   251  	}
   252  	return fmt.Sprintf("index %d", a)
   253  }
   254  
   255  // Diff fetches Operation diffs from the database and compares them against
   256  // independently calculated diffs.
   257  func (e *updateE2e) Diff(ctx context.Context) func(t *testing.T) {
   258  	return func(t *testing.T) {
   259  		ctx := zlog.Test(ctx, t)
   260  		for n := range e.vulns() {
   261  			// This does a bunch of checks so that the first operation is
   262  			// compared appropriately.
   263  			prev := uuid.Nil
   264  			if n != 0 {
   265  				prev = e.updateOps[n-1].Ref
   266  			}
   267  			cur := e.updateOps[n].Ref
   268  			t.Logf("comparing %v (%s) and %v (index %d)", prev, orNoIndex(n-1), cur, n)
   269  
   270  			diff, err := e.s.GetUpdateDiff(ctx, prev, cur)
   271  			if err != nil {
   272  				t.Fatalf("received error getting UpdateDiff: %v", err)
   273  			}
   274  
   275  			expectSz := opStep
   276  			if n == 0 {
   277  				expectSz = e.Insert
   278  			}
   279  			if l := len(diff.Added); l != expectSz {
   280  				t.Fatalf("got: len == %d, want len == %d", l, expectSz)
   281  			}
   282  			if n == 0 {
   283  				expectSz = 0
   284  			}
   285  			if l := len(diff.Removed); l != expectSz {
   286  				t.Fatalf("got: len == %d, want len == %d", l, expectSz)
   287  			}
   288  
   289  			// make sure update operations match generated test values
   290  			if prev != diff.Prev.Ref {
   291  				t.Errorf("want: %v, got: %v", diff.Prev.Ref, prev)
   292  			}
   293  			if cur != diff.Cur.Ref {
   294  				t.Errorf("want: %v, got: %v", diff.Cur.Ref, cur)
   295  			}
   296  
   297  			// confirm removed and added vulnerabilities are the ones we expect
   298  			pair := e.calcDiff(n)
   299  			if n == 0 {
   300  				pair[0] = []*claircore.Vulnerability{}
   301  			}
   302  			// I can't figure out how to make a cmp.Option that does this.
   303  			added := make([]*claircore.Vulnerability, len(pair[1]))
   304  			for i := range diff.Added {
   305  				added[i] = &diff.Added[i]
   306  			}
   307  			if want, got := pair[1], added; !cmp.Equal(got, want, vulnCmp) {
   308  				t.Error(cmp.Diff(got, want, vulnCmp))
   309  			}
   310  
   311  			removed := make([]*claircore.Vulnerability, len(pair[0]))
   312  			for i := range diff.Removed {
   313  				removed[i] = &diff.Removed[i]
   314  			}
   315  			if want, got := pair[0], removed; !cmp.Equal(want, got, vulnCmp) {
   316  				t.Error(cmp.Diff(want, got, vulnCmp))
   317  			}
   318  		}
   319  		t.Log("ok")
   320  	}
   321  }
   322  
   323  func (e *updateE2e) calcDiff(i int) [2][]*claircore.Vulnerability {
   324  	if i >= e.Updates {
   325  		panic(fmt.Sprintf("update %d out of bounds (%d)", i, e.Updates))
   326  	}
   327  	sz := e.Insert + (opStep * e.Updates)
   328  	vs := test.GenUniqueVulnerabilities(sz, e.updater)
   329  	if i == 0 {
   330  		return [...][]*claircore.Vulnerability{{}, vs[:e.Insert]}
   331  	}
   332  	loff, lend := (i-1)*opStep, i*opStep
   333  	roff, rend := loff+e.Insert, lend+e.Insert
   334  	return [...][]*claircore.Vulnerability{vs[loff:lend], vs[roff:rend]}
   335  }
   336  
   337  // DeleteUpdateOperations performs a deletion of all UpdateOperations used in
   338  // the test and confirms both the UpdateOperation and vulnerabilities are
   339  // removed from the vulnstore.
   340  func (e *updateE2e) DeleteUpdateOperations(ctx context.Context) func(*testing.T) {
   341  	return func(t *testing.T) {
   342  		const (
   343  			opExists    = `SELECT EXISTS(SELECT 1 FROM update_operation WHERE ref = $1::uuid);`
   344  			assocExists = `SELECT EXISTS(SELECT 1 FROM uo_vuln JOIN update_operation uo ON (uo_vuln.uo = uo.id) WHERE uo.ref = $1::uuid);`
   345  		)
   346  		var exists bool
   347  		ctx := zlog.Test(ctx, t)
   348  		for _, op := range e.updateOps {
   349  			_, err := e.s.DeleteUpdateOperations(ctx, op.Ref)
   350  			if err != nil {
   351  				t.Fatalf("failed to get delete UpdateOperation: %v", err)
   352  			}
   353  
   354  			// Check that the update_operation is removed from the table.
   355  			if err := e.pool.QueryRow(ctx, opExists, op.Ref).Scan(&exists); err != nil {
   356  				t.Errorf("query failed: %v", err)
   357  			}
   358  			t.Logf("operation %v exists: %v", op.Ref, exists)
   359  			if exists {
   360  				t.Error()
   361  			}
   362  
   363  			// This really shouldn't happen because of the foreign constraint.
   364  			if err := e.pool.QueryRow(ctx, assocExists, op.Ref).Scan(&exists); err != nil {
   365  				t.Errorf("query failed: %v", err)
   366  			}
   367  			t.Logf("operation %v exists: %v", op.Ref, exists)
   368  			if exists {
   369  				t.Error()
   370  			}
   371  		}
   372  		t.Log("ok")
   373  	}
   374  }
   375  
   376  // checkInsertedVulns confirms vulnerabilitiles are inserted into the database correctly when
   377  // store.UpdateVulnerabilities is called.
   378  func checkInsertedVulns(ctx context.Context, t *testing.T, pool *pgxpool.Pool, id uuid.UUID, vulns []*claircore.Vulnerability) {
   379  	const query = `SELECT
   380  	vuln.hash_kind,
   381  	vuln.hash,
   382  	vuln.updater,
   383  	vuln.id,
   384  	vuln.name,
   385  	vuln.description,
   386  	vuln.issued,
   387  	vuln.links,
   388  	vuln.normalized_severity,
   389  	vuln.severity,
   390  	vuln.package_name,
   391  	vuln.package_version,
   392  	vuln.package_module,
   393  	vuln.package_arch,
   394  	vuln.package_kind,
   395  	vuln.dist_id,
   396  	vuln.dist_name,
   397  	vuln.dist_version,
   398  	vuln.dist_version_code_name,
   399  	vuln.dist_version_id,
   400  	vuln.dist_arch,
   401  	vuln.dist_cpe,
   402  	vuln.dist_pretty_name,
   403  	vuln.arch_operation,
   404  	vuln.repo_name,
   405  	vuln.repo_key,
   406  	vuln.repo_uri,
   407  	vuln.fixed_in_version
   408  FROM uo_vuln
   409  JOIN vuln ON vuln.id = uo_vuln.vuln
   410  JOIN update_operation uo ON uo.id = uo_vuln.uo
   411  WHERE uo.ref = $1::uuid;`
   412  	expectedVulns := map[string]*claircore.Vulnerability{}
   413  	for _, vuln := range vulns {
   414  		expectedVulns[vuln.Name] = vuln
   415  	}
   416  	rows, err := pool.Query(ctx, query, id)
   417  	if err != nil {
   418  		t.Fatalf("query failed: %v", err)
   419  	}
   420  	defer rows.Close()
   421  
   422  	queriedVulns := map[string]*claircore.Vulnerability{}
   423  	for rows.Next() {
   424  		var id int64
   425  		var hashKind string
   426  		var hash []byte
   427  		vuln := claircore.Vulnerability{
   428  			Package: &claircore.Package{},
   429  			Dist:    &claircore.Distribution{},
   430  			Repo:    &claircore.Repository{},
   431  		}
   432  		err := rows.Scan(
   433  			&hashKind,
   434  			&hash,
   435  			&vuln.Updater,
   436  			&id,
   437  			&vuln.Name,
   438  			&vuln.Description,
   439  			&vuln.Issued,
   440  			&vuln.Links,
   441  			&vuln.NormalizedSeverity,
   442  			&vuln.Severity,
   443  			&vuln.Package.Name,
   444  			&vuln.Package.Version,
   445  			&vuln.Package.Module,
   446  			&vuln.Package.Arch,
   447  			&vuln.Package.Kind,
   448  			&vuln.Dist.DID,
   449  			&vuln.Dist.Name,
   450  			&vuln.Dist.Version,
   451  			&vuln.Dist.VersionCodeName,
   452  			&vuln.Dist.VersionID,
   453  			&vuln.Dist.Arch,
   454  			&vuln.Dist.CPE,
   455  			&vuln.Dist.PrettyName,
   456  			&vuln.ArchOperation,
   457  			&vuln.Repo.Name,
   458  			&vuln.Repo.Key,
   459  			&vuln.Repo.URI,
   460  			&vuln.FixedInVersion,
   461  		)
   462  		vuln.ID = strconv.FormatInt(id, 10)
   463  		if err != nil {
   464  			t.Fatalf("failed to scan vulnerability: %v", err)
   465  		}
   466  		// confirm a hash was generated
   467  		if hashKind == "" || len(hash) == 0 {
   468  			t.Fatalf("failed to identify hash for inserted vulnerability %+v", vuln)
   469  		}
   470  		queriedVulns[vuln.Name] = &vuln
   471  	}
   472  	if err := rows.Err(); err != nil {
   473  		t.Error(err)
   474  	}
   475  
   476  	// confirm we did not receive unexpected vulns or bad fields
   477  	for name, got := range queriedVulns {
   478  		if want, ok := expectedVulns[name]; !ok {
   479  			t.Fatalf("received unexpected vuln: %v", got.Name)
   480  		} else {
   481  			// compare vuln fields. ignore id's
   482  			if !cmp.Equal(want, got, vulnCmp) {
   483  				t.Fatal(cmp.Diff(want, got, vulnCmp))
   484  			}
   485  		}
   486  	}
   487  
   488  	// confirm queriedVulns contain all expected vulns
   489  	for name := range expectedVulns {
   490  		if _, ok := queriedVulns[name]; !ok {
   491  			t.Fatalf("expected vuln %v was not found in query", name)
   492  		}
   493  	}
   494  }
   495  
   496  // checkUpdateTimes confirms updater update times are upserted into the database correctly when
   497  // store.RecordUpaterUptdateTime is called.
   498  func checkUpdateTimes(ctx context.Context, t *testing.T, pool *pgxpool.Pool, updates map[string]update) {
   499  	const query = `SELECT updater_name, last_attempt, last_success, last_run_succeeded, last_attempt_fingerprint, last_error
   500  FROM updater_status`
   501  
   502  	rows, err := pool.Query(ctx, query)
   503  	if err != nil {
   504  		t.Fatalf("query failed: %v", err)
   505  	}
   506  	defer rows.Close()
   507  
   508  	queriedUpdates := make(map[string]update)
   509  	for rows.Next() {
   510  		var updateEntry update
   511  		err := rows.Scan(
   512  			&updateEntry.UpdaterName,
   513  			&updateEntry.LastAttempt,
   514  			&updateEntry.LastSuccess,
   515  			&updateEntry.LastRunSucceeded,
   516  			&updateEntry.LastAttemptFingerprint,
   517  			&updateEntry.LastError,
   518  		)
   519  		if err != nil {
   520  			t.Fatalf("failed to scan update: %v", err)
   521  		}
   522  		queriedUpdates[updateEntry.UpdaterName] = updateEntry
   523  	}
   524  	if err := rows.Err(); err != nil {
   525  		t.Error(err)
   526  	}
   527  
   528  	// confirm we did not receive unexpected updates
   529  	for name, got := range queriedUpdates {
   530  		if want, ok := updates[name]; !ok {
   531  			t.Fatalf("received unexpected update: %s %v", name, got)
   532  		} else {
   533  			if !cmp.Equal(want, got) {
   534  				t.Fatal(cmp.Diff(want, got))
   535  			}
   536  		}
   537  	}
   538  
   539  	// confirm queriedUpdates contain all expected updates
   540  	for name := range updates {
   541  		if _, ok := queriedUpdates[name]; !ok {
   542  			t.Fatalf("expected update %v was not found in query", name)
   543  		}
   544  	}
   545  }