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 }