github.com/kubeshop/testkube@v1.17.23/pkg/repository/result/mongo.go (about) 1 package result 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/kubeshop/testkube/pkg/repository/common" 10 11 "go.mongodb.org/mongo-driver/bson" 12 "go.mongodb.org/mongo-driver/bson/primitive" 13 "go.mongodb.org/mongo-driver/mongo" 14 "go.mongodb.org/mongo-driver/mongo/options" 15 "go.uber.org/zap" 16 17 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 18 "github.com/kubeshop/testkube/pkg/featureflags" 19 "github.com/kubeshop/testkube/pkg/log" 20 logsclient "github.com/kubeshop/testkube/pkg/logs/client" 21 "github.com/kubeshop/testkube/pkg/storage" 22 ) 23 24 var _ Repository = (*MongoRepository)(nil) 25 26 const ( 27 CollectionResults = "results" 28 CollectionSequences = "sequences" 29 // OutputPrefixSize is the size of the beginning of execution output in case this doesn't fit into Mongo 30 OutputPrefixSize = 1 * 1024 * 1024 31 // OutputMaxSize is the size of the execution output in case this doesn't fit into the 16 MB limited by Mongo 32 OutputMaxSize = 14 * 1024 * 1024 33 // OverflownOutputWarn is the message that lets the user know the output had to be trimmed 34 OverflownOutputWarn = "WARNING: Output was shortened in order to fit into MongoDB." 35 // StepMaxCount is the maximum number of steps saved into Mongo - due to the 16 MB document size limitation 36 StepMaxCount = 100 37 ) 38 39 // NewMongoRepository creates a new MongoRepository - used by other testkube components - use opts to extend the functionality 40 func NewMongoRepository(db *mongo.Database, allowDiskUse, isDocDb bool, opts ...MongoRepositoryOpt) *MongoRepository { 41 r := &MongoRepository{ 42 db: db, 43 ResultsColl: db.Collection(CollectionResults), 44 SequencesColl: db.Collection(CollectionSequences), 45 OutputRepository: NewMongoOutputRepository(db), 46 allowDiskUse: allowDiskUse, 47 isDocDb: isDocDb, 48 log: log.DefaultLogger, 49 } 50 51 for _, opt := range opts { 52 opt(r) 53 } 54 55 return r 56 } 57 58 func NewMongoRepositoryWithOutputRepository( 59 db *mongo.Database, 60 allowDiskUse bool, 61 outputRepository OutputRepository, 62 opts ...MongoRepositoryOpt, 63 ) *MongoRepository { 64 r := &MongoRepository{ 65 db: db, 66 ResultsColl: db.Collection(CollectionResults), 67 SequencesColl: db.Collection(CollectionSequences), 68 OutputRepository: outputRepository, 69 allowDiskUse: allowDiskUse, 70 log: log.DefaultLogger, 71 } 72 73 for _, opt := range opts { 74 opt(r) 75 } 76 77 return r 78 } 79 80 func NewMongoRepositoryWithMinioOutputStorage(db *mongo.Database, allowDiskUse bool, storageClient storage.Client, bucket string) *MongoRepository { 81 repo := MongoRepository{ 82 db: db, 83 ResultsColl: db.Collection(CollectionResults), 84 SequencesColl: db.Collection(CollectionSequences), 85 allowDiskUse: allowDiskUse, 86 log: log.DefaultLogger, 87 } 88 repo.OutputRepository = NewMinioOutputRepository(storageClient, repo.ResultsColl, bucket) 89 return &repo 90 } 91 92 type MongoRepository struct { 93 db *mongo.Database 94 ResultsColl *mongo.Collection 95 SequencesColl *mongo.Collection 96 OutputRepository OutputRepository 97 logGrpcClient logsclient.StreamGetter 98 allowDiskUse bool 99 isDocDb bool 100 features featureflags.FeatureFlags 101 log *zap.SugaredLogger 102 } 103 104 type MongoRepositoryOpt func(*MongoRepository) 105 106 func WithLogsClient(client logsclient.StreamGetter) MongoRepositoryOpt { 107 return func(r *MongoRepository) { 108 r.logGrpcClient = client 109 } 110 } 111 112 func WithFeatureFlags(features featureflags.FeatureFlags) MongoRepositoryOpt { 113 return func(r *MongoRepository) { 114 r.features = features 115 } 116 } 117 118 func WithMongoRepositoryResultCollection(collection *mongo.Collection) MongoRepositoryOpt { 119 return func(r *MongoRepository) { 120 r.ResultsColl = collection 121 } 122 } 123 124 func WithMongoRepositorySequenceCollection(collection *mongo.Collection) MongoRepositoryOpt { 125 return func(r *MongoRepository) { 126 r.SequencesColl = collection 127 } 128 } 129 130 func (r *MongoRepository) GetExecution(ctx context.Context, id string) (result testkube.Execution, err error) { 131 err = r.ResultsColl.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result) 132 if err != nil { 133 return 134 } 135 return *result.UnscapeDots(), err 136 } 137 138 func (r *MongoRepository) Get(ctx context.Context, id string) (result testkube.Execution, err error) { 139 err = r.ResultsColl.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result) 140 if err != nil { 141 return 142 } 143 144 err = r.attachOutput(ctx, &result) 145 if err != nil { 146 return 147 } 148 return *result.UnscapeDots(), err 149 } 150 151 func (r *MongoRepository) GetByNameAndTest(ctx context.Context, name, testName string) (result testkube.Execution, err error) { 152 err = r.ResultsColl.FindOne(ctx, bson.M{"name": name, "testname": testName}).Decode(&result) 153 if err != nil { 154 return 155 } 156 err = r.attachOutput(ctx, &result) 157 if err != nil { 158 return 159 } 160 161 return *result.UnscapeDots(), err 162 } 163 164 func (r *MongoRepository) attachOutput(ctx context.Context, result *testkube.Execution) (err error) { 165 if len(result.ExecutionResult.Output) == 0 && !r.features.LogsV2 { 166 result.ExecutionResult.Output, err = r.OutputRepository.GetOutput(ctx, result.Id, result.TestName, result.TestSuiteName) 167 if err == mongo.ErrNoDocuments { 168 err = nil 169 } 170 } 171 return err 172 } 173 174 func (r *MongoRepository) GetLatestTestNumber(ctx context.Context, testName string) (num int32, err error) { 175 var result struct { 176 Number int32 `bson:"number"` 177 } 178 opts := &options.FindOneOptions{Projection: bson.M{"_id": -1, "number": 1}, Sort: bson.M{"number": -1}} 179 err = r.ResultsColl.FindOne(ctx, bson.M{"testname": testName}, opts).Decode(&result) 180 if err != nil { 181 return 0, err 182 } 183 return result.Number, err 184 } 185 186 func (r *MongoRepository) slowGetLatestByTest(ctx context.Context, testName string) (*testkube.Execution, error) { 187 opts := options.Aggregate() 188 pipeline := []bson.M{ 189 {"$project": bson.M{"testname": 1, "starttime": 1, "endtime": 1}}, 190 {"$match": bson.M{"testname": testName}}, 191 192 {"$group": bson.D{ 193 {Key: "_id", Value: "$testname"}, 194 {Key: "doc", Value: bson.M{"$max": bson.D{ 195 {Key: "updatetime", Value: bson.M{"$max": bson.A{"$starttime", "$endtime"}}}, 196 {Key: "content", Value: "$$ROOT"}, 197 }}}, 198 }}, 199 {"$sort": bson.M{"doc.updatetime": -1}}, 200 {"$limit": 1}, 201 202 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "localField": "doc.content._id", "foreignField": "_id", "as": "execution"}}, 203 {"$replaceRoot": bson.M{"newRoot": bson.M{"$arrayElemAt": bson.A{"$execution", 0}}}}, 204 } 205 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 206 if err != nil { 207 return nil, err 208 } 209 var items []testkube.Execution 210 err = cursor.All(ctx, &items) 211 if err != nil { 212 return nil, err 213 } 214 if len(items) == 0 { 215 return nil, mongo.ErrNoDocuments 216 } 217 result := (&items[0]).UnscapeDots() 218 219 err = r.attachOutput(ctx, result) 220 if err != nil { 221 return result, err 222 } 223 return result, err 224 } 225 226 func (r *MongoRepository) GetLatestByTest(ctx context.Context, testName string) (*testkube.Execution, error) { 227 // Workaround, to use subset of MongoDB features available in AWS DocumentDB 228 if r.isDocDb { 229 return r.slowGetLatestByTest(ctx, testName) 230 } 231 232 opts := options.Aggregate() 233 pipeline := []bson.M{ 234 {"$group": bson.M{"_id": "$testname", "doc": bson.M{"$first": bson.M{}}}}, 235 {"$project": bson.M{"_id": 0, "name": "$_id"}}, 236 {"$match": bson.M{"name": testName}}, 237 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "let": bson.M{"name": "$name"}, "pipeline": []bson.M{ 238 {"$match": bson.M{"$expr": bson.M{"$eq": bson.A{"$testname", "$$name"}}}}, 239 {"$sort": bson.M{"starttime": -1}}, 240 {"$limit": 1}, 241 }, "as": "execution_by_start_time"}}, 242 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "let": bson.M{"name": "$name"}, "pipeline": []bson.M{ 243 {"$match": bson.M{"$expr": bson.M{"$eq": bson.A{"$testname", "$$name"}}}}, 244 {"$sort": bson.M{"endtime": -1}}, 245 {"$limit": 1}, 246 }, "as": "execution_by_end_time"}}, 247 {"$project": bson.M{"executions": bson.M{"$concatArrays": bson.A{"$execution_by_start_time", "$execution_by_end_time"}}}}, 248 {"$unwind": "$executions"}, 249 {"$replaceRoot": bson.M{"newRoot": "$executions"}}, 250 251 {"$group": bson.D{ 252 {Key: "_id", Value: "$testname"}, 253 {Key: "doc", Value: bson.M{"$max": bson.D{ 254 {Key: "updatetime", Value: bson.M{"$max": bson.A{"$starttime", "$endtime"}}}, 255 {Key: "content", Value: "$$ROOT"}, 256 }}}, 257 }}, 258 {"$sort": bson.M{"doc.updatetime": -1}}, 259 {"$replaceRoot": bson.M{"newRoot": "$doc.content"}}, 260 {"$limit": 1}, 261 } 262 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 263 if err != nil { 264 return nil, err 265 } 266 var items []testkube.Execution 267 err = cursor.All(ctx, &items) 268 if err != nil { 269 return nil, err 270 } 271 if len(items) == 0 { 272 return nil, mongo.ErrNoDocuments 273 } 274 result := (&items[0]).UnscapeDots() 275 276 err = r.attachOutput(ctx, result) 277 if err != nil { 278 return result, err 279 } 280 281 return result, err 282 } 283 284 func (r *MongoRepository) slowGetLatestByTests(ctx context.Context, testNames []string) (executions []testkube.Execution, err error) { 285 documents := bson.A{} 286 for _, testName := range testNames { 287 documents = append(documents, bson.M{"testname": testName}) 288 } 289 290 pipeline := []bson.M{ 291 {"$project": bson.M{"testname": 1, "starttime": 1, "endtime": 1}}, 292 {"$match": bson.M{"$or": documents}}, 293 294 {"$group": bson.D{ 295 {Key: "_id", Value: "$testname"}, 296 {Key: "doc", Value: bson.M{"$max": bson.D{ 297 {Key: "updatetime", Value: bson.M{"$max": bson.A{"$starttime", "$endtime"}}}, 298 {Key: "content", Value: "$$ROOT"}, 299 }}}, 300 }}, 301 {"$sort": bson.M{"doc.updatetime": -1}}, 302 303 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "localField": "doc.content._id", "foreignField": "_id", "as": "execution"}}, 304 {"$replaceRoot": bson.M{"newRoot": bson.M{"$arrayElemAt": bson.A{"$execution", 0}}}}, 305 } 306 307 opts := options.Aggregate() 308 if r.allowDiskUse { 309 opts.SetAllowDiskUse(r.allowDiskUse) 310 } 311 312 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 313 if err != nil { 314 return nil, err 315 } 316 err = cursor.All(ctx, &executions) 317 if err != nil { 318 return nil, err 319 } 320 321 for i := range executions { 322 executions[i].UnscapeDots() 323 } 324 return executions, nil 325 } 326 327 func (r *MongoRepository) GetLatestByTests(ctx context.Context, testNames []string) (executions []testkube.Execution, err error) { 328 if len(testNames) == 0 { 329 return executions, nil 330 } 331 332 // Workaround, to use subset of MongoDB features available in AWS DocumentDB 333 if r.isDocDb { 334 return r.slowGetLatestByTests(ctx, testNames) 335 } 336 337 documents := bson.A{} 338 for _, testName := range testNames { 339 documents = append(documents, bson.M{"name": testName}) 340 } 341 342 pipeline := []bson.M{ 343 {"$group": bson.M{"_id": "$testname", "doc": bson.M{"$first": bson.M{}}}}, 344 {"$project": bson.M{"_id": 0, "name": "$_id"}}, 345 {"$match": bson.M{"$or": documents}}, 346 347 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "let": bson.M{"name": "$name"}, "pipeline": []bson.M{ 348 {"$match": bson.M{"$expr": bson.M{"$eq": bson.A{"$testname", "$$name"}}}}, 349 {"$sort": bson.M{"starttime": -1}}, 350 {"$limit": 1}, 351 }, "as": "execution_by_start_time"}}, 352 {"$lookup": bson.M{"from": r.ResultsColl.Name(), "let": bson.M{"name": "$name"}, "pipeline": []bson.M{ 353 {"$match": bson.M{"$expr": bson.M{"$eq": bson.A{"$testname", "$$name"}}}}, 354 {"$sort": bson.M{"endtime": -1}}, 355 {"$limit": 1}, 356 }, "as": "execution_by_end_time"}}, 357 {"$project": bson.M{"executions": bson.M{"$concatArrays": bson.A{"$execution_by_start_time", "$execution_by_end_time"}}}}, 358 {"$unwind": "$executions"}, 359 {"$replaceRoot": bson.M{"newRoot": "$executions"}}, 360 361 {"$group": bson.D{ 362 {Key: "_id", Value: "$testname"}, 363 {Key: "doc", Value: bson.M{"$max": bson.D{ 364 {Key: "updatetime", Value: bson.M{"$max": bson.A{"$starttime", "$endtime"}}}, 365 {Key: "content", Value: "$$ROOT"}, 366 }}}, 367 }}, 368 {"$sort": bson.M{"doc.updatetime": -1}}, 369 {"$replaceRoot": bson.M{"newRoot": "$doc.content"}}, 370 } 371 372 opts := options.Aggregate() 373 if r.allowDiskUse { 374 opts.SetAllowDiskUse(r.allowDiskUse) 375 } 376 377 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 378 if err != nil { 379 return nil, err 380 } 381 err = cursor.All(ctx, &executions) 382 if err != nil { 383 return nil, err 384 } 385 386 for i := range executions { 387 executions[i].UnscapeDots() 388 } 389 return executions, nil 390 } 391 392 func (r *MongoRepository) GetNewestExecutions(ctx context.Context, limit int) (result []testkube.Execution, err error) { 393 result = make([]testkube.Execution, 0) 394 resultLimit := int64(limit) 395 opts := &options.FindOptions{Limit: &resultLimit} 396 opts.SetSort(bson.D{{Key: "_id", Value: -1}}) 397 398 if r.allowDiskUse { 399 opts.SetAllowDiskUse(r.allowDiskUse) 400 } 401 402 cursor, err := r.ResultsColl.Find(ctx, bson.M{}, opts) 403 if err != nil { 404 return result, err 405 } 406 err = cursor.All(ctx, &result) 407 408 for i := range result { 409 result[i].UnscapeDots() 410 } 411 return 412 } 413 414 func (r *MongoRepository) GetExecutions(ctx context.Context, filter Filter) (result []testkube.Execution, err error) { 415 result = make([]testkube.Execution, 0) 416 query, opts := composeQueryAndOpts(filter) 417 if r.allowDiskUse { 418 opts.SetAllowDiskUse(r.allowDiskUse) 419 } 420 421 cursor, err := r.ResultsColl.Find(ctx, query, opts) 422 if err != nil { 423 return 424 } 425 err = cursor.All(ctx, &result) 426 427 for i := range result { 428 result[i].UnscapeDots() 429 } 430 return 431 } 432 433 func (r *MongoRepository) Count(ctx context.Context, filter Filter) (count int64, err error) { 434 query, _ := composeQueryAndOpts(filter) 435 return r.ResultsColl.CountDocuments(ctx, query) 436 } 437 438 func (r *MongoRepository) GetExecutionTotals(ctx context.Context, paging bool, filter ...Filter) (totals testkube.ExecutionsTotals, err error) { 439 var result []struct { 440 Status string `bson:"_id"` 441 Count int `bson:"count"` 442 } 443 444 query := bson.M{} 445 if len(filter) > 0 { 446 query, _ = composeQueryAndOpts(filter[0]) 447 } 448 449 pipeline := []bson.D{ 450 {{Key: "$sort", Value: bson.M{"executionresult.status": 1}}}, 451 {{Key: "$match", Value: query}}, 452 } 453 if len(filter) > 0 { 454 pipeline = append(pipeline, bson.D{{Key: "$sort", Value: bson.D{{Key: "starttime", Value: -1}}}}) 455 if paging { 456 pipeline = append(pipeline, bson.D{{Key: "$skip", Value: int64(filter[0].Page() * filter[0].PageSize())}}) 457 pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(filter[0].PageSize())}}) 458 } 459 } else { 460 pipeline = append(pipeline, bson.D{{Key: "$sort", Value: bson.D{{Key: "executionresult.status", Value: 1}}}}) 461 } 462 463 pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$executionresult.status"}, 464 {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}) 465 466 opts := options.Aggregate() 467 if r.allowDiskUse { 468 opts.SetAllowDiskUse(r.allowDiskUse) 469 } 470 471 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 472 if err != nil { 473 return totals, err 474 } 475 err = cursor.All(ctx, &result) 476 if err != nil { 477 return totals, err 478 } 479 480 var sum int32 481 482 for _, o := range result { 483 sum += int32(o.Count) 484 switch testkube.TestSuiteExecutionStatus(o.Status) { 485 case testkube.QUEUED_TestSuiteExecutionStatus: 486 totals.Queued = int32(o.Count) 487 case testkube.RUNNING_TestSuiteExecutionStatus: 488 totals.Running = int32(o.Count) 489 case testkube.PASSED_TestSuiteExecutionStatus: 490 totals.Passed = int32(o.Count) 491 case testkube.FAILED_TestSuiteExecutionStatus: 492 totals.Failed = int32(o.Count) 493 } 494 } 495 totals.Results = sum 496 497 return 498 } 499 500 func (r *MongoRepository) GetLabels(ctx context.Context) (labels map[string][]string, err error) { 501 var result []struct { 502 Labels bson.M `bson:"labels"` 503 } 504 505 opts := options.Find() 506 if r.allowDiskUse { 507 opts.SetAllowDiskUse(r.allowDiskUse) 508 } 509 510 cursor, err := r.ResultsColl.Find(ctx, bson.M{}, opts) 511 if err != nil { 512 return nil, err 513 } 514 515 err = cursor.All(ctx, &result) 516 if err != nil { 517 return nil, err 518 } 519 520 labels = map[string][]string{} 521 for _, r := range result { 522 for key, value := range r.Labels { 523 if values, ok := labels[key]; !ok { 524 labels[key] = []string{fmt.Sprint(value)} 525 } else { 526 for _, v := range values { 527 if v == value { 528 continue 529 } 530 } 531 labels[key] = append(labels[key], fmt.Sprint(value)) 532 } 533 } 534 } 535 return labels, nil 536 } 537 538 func (r *MongoRepository) Insert(ctx context.Context, result testkube.Execution) (err error) { 539 output := result.ExecutionResult.Output 540 result.ExecutionResult.Output = "" 541 result.EscapeDots() 542 _, err = r.ResultsColl.InsertOne(ctx, result) 543 if err != nil { 544 return 545 } 546 547 if !r.features.LogsV2 { 548 err = r.OutputRepository.InsertOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output) 549 } 550 return 551 } 552 553 func (r *MongoRepository) Update(ctx context.Context, result testkube.Execution) (err error) { 554 output := result.ExecutionResult.Output 555 result.ExecutionResult.Output = "" 556 result.EscapeDots() 557 _, err = r.ResultsColl.ReplaceOne(ctx, bson.M{"id": result.Id}, result) 558 if err != nil { 559 return 560 } 561 562 if !r.features.LogsV2 { 563 err = r.OutputRepository.UpdateOutput(ctx, result.Id, result.TestName, result.TestSuiteName, output) 564 } 565 return 566 } 567 568 func (r *MongoRepository) UpdateResult(ctx context.Context, id string, result testkube.Execution) (err error) { 569 output := result.ExecutionResult.Output 570 result.ExecutionResult = result.ExecutionResult.GetDeepCopy() 571 result.ExecutionResult.Output = "" 572 result.ExecutionResult.Steps = cleanSteps(result.ExecutionResult.Steps) 573 574 var execution testkube.Execution 575 err = r.ResultsColl.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&execution) 576 if err != nil { 577 return err 578 } 579 580 errorMessage := "" 581 if execution.ExecutionResult != nil { 582 errorMessage = execution.ExecutionResult.ErrorMessage 583 } 584 585 if errorMessage != "" && result.ExecutionResult.ErrorMessage != "" { 586 errorMessage += "\n" 587 } 588 589 result.ExecutionResult.ErrorMessage = errorMessage + result.ExecutionResult.ErrorMessage 590 _, err = r.ResultsColl.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": bson.M{"executionresult": result.ExecutionResult}}) 591 if err != nil { 592 return err 593 } 594 595 if !r.features.LogsV2 { 596 err = r.OutputRepository.UpdateOutput(ctx, id, result.TestName, result.TestSuiteName, cleanOutput(output)) 597 } 598 return 599 } 600 601 // StartExecution updates execution start time 602 func (r *MongoRepository) StartExecution(ctx context.Context, id string, startTime time.Time) (err error) { 603 _, err = r.ResultsColl.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": bson.M{"starttime": startTime}}) 604 return 605 } 606 607 // EndExecution updates execution end time 608 func (r *MongoRepository) EndExecution(ctx context.Context, e testkube.Execution) (err error) { 609 _, err = r.ResultsColl.UpdateOne(ctx, bson.M{"id": e.Id}, bson.M{"$set": bson.M{"endtime": e.EndTime, "duration": e.Duration, "durationms": e.DurationMs}}) 610 return 611 } 612 613 func composeQueryAndOpts(filter Filter) (bson.M, *options.FindOptions) { 614 query := bson.M{} 615 conditions := bson.A{} 616 opts := options.Find() 617 startTimeQuery := bson.M{} 618 619 if filter.TextSearchDefined() { 620 conditions = append(conditions, bson.M{"$or": bson.A{ 621 bson.M{"testname": bson.M{"$regex": primitive.Regex{Pattern: filter.TextSearch(), Options: "i"}}}, 622 bson.M{"name": bson.M{"$regex": primitive.Regex{Pattern: filter.TextSearch(), Options: "i"}}}, 623 }}) 624 } 625 626 if filter.TestNameDefined() { 627 conditions = append(conditions, bson.M{"testname": filter.TestName()}) 628 } 629 630 if filter.LastNDaysDefined() { 631 startTimeQuery["$gte"] = time.Now().Add(-time.Duration(filter.LastNDays()) * 24 * time.Hour) 632 } 633 634 if filter.StartDateDefined() { 635 startTimeQuery["$gte"] = filter.StartDate() 636 } 637 638 if filter.EndDateDefined() { 639 startTimeQuery["$lte"] = filter.EndDate() 640 } 641 642 if len(startTimeQuery) > 0 { 643 conditions = append(conditions, bson.M{"starttime": startTimeQuery}) 644 } 645 646 if filter.StatusesDefined() { 647 statuses := filter.Statuses() 648 if len(statuses) == 1 { 649 conditions = append(conditions, bson.M{"executionresult.status": statuses[0]}) 650 } else { 651 expressions := bson.A{} 652 for _, status := range statuses { 653 expressions = append(expressions, bson.M{"executionresult.status": status}) 654 } 655 656 conditions = append(conditions, bson.M{"$or": expressions}) 657 } 658 } 659 660 if filter.Selector() != "" { 661 conditions = addSelectorConditions(filter.Selector(), "labels", conditions) 662 } 663 664 if filter.TypeDefined() { 665 conditions = append(conditions, bson.M{"testtype": filter.Type()}) 666 } 667 668 opts.SetSkip(int64(filter.Page() * filter.PageSize())) 669 opts.SetLimit(int64(filter.PageSize())) 670 opts.SetSort(bson.D{{Key: "starttime", Value: -1}}) 671 672 if len(conditions) > 0 { 673 query = bson.M{"$and": conditions} 674 } 675 676 return query, opts 677 } 678 679 func addSelectorConditions(selector string, tag string, conditions primitive.A) primitive.A { 680 items := strings.Split(selector, ",") 681 for _, item := range items { 682 elements := strings.Split(item, "=") 683 if len(elements) == 2 { 684 conditions = append(conditions, bson.M{tag + "." + elements[0]: elements[1]}) 685 } else if len(elements) == 1 { 686 conditions = append(conditions, bson.M{tag + "." + elements[0]: bson.M{"$exists": true}}) 687 } 688 } 689 return conditions 690 } 691 692 // DeleteByTest deletes execution results by test 693 func (r *MongoRepository) DeleteByTest(ctx context.Context, testName string) (err error) { 694 err = r.OutputRepository.DeleteOutputByTest(ctx, testName) 695 if err != nil { 696 return 697 } 698 err = r.DeleteExecutionNumber(ctx, testName) 699 if err != nil { 700 return 701 } 702 _, err = r.ResultsColl.DeleteMany(ctx, bson.M{"testname": testName}) 703 return 704 } 705 706 // DeleteByTestSuite deletes execution results by test suite 707 func (r *MongoRepository) DeleteByTestSuite(ctx context.Context, testSuiteName string) (err error) { 708 err = r.OutputRepository.DeleteOutputByTestSuite(ctx, testSuiteName) 709 if err != nil { 710 return 711 } 712 err = r.DeleteExecutionNumber(ctx, testSuiteName) 713 if err != nil { 714 return 715 } 716 _, err = r.ResultsColl.DeleteMany(ctx, bson.M{"testsuitename": testSuiteName}) 717 return 718 } 719 720 // DeleteAll deletes all execution results 721 func (r *MongoRepository) DeleteAll(ctx context.Context) (err error) { 722 err = r.OutputRepository.DeleteAllOutput(ctx) 723 if err != nil { 724 return 725 } 726 err = r.DeleteAllExecutionNumbers(ctx, false) 727 if err != nil { 728 return 729 } 730 _, err = r.ResultsColl.DeleteMany(ctx, bson.M{}) 731 return 732 } 733 734 // DeleteByTests deletes execution results by tests 735 func (r *MongoRepository) DeleteByTests(ctx context.Context, testNames []string) (err error) { 736 if len(testNames) == 0 { 737 return nil 738 } 739 740 var filter bson.M 741 if len(testNames) > 1 { 742 conditions := bson.A{} 743 for _, testName := range testNames { 744 conditions = append(conditions, bson.M{"testname": testName}) 745 } 746 747 filter = bson.M{"$or": conditions} 748 } else { 749 filter = bson.M{"testname": testNames[0]} 750 } 751 752 err = r.OutputRepository.DeleteOutputForTests(ctx, testNames) 753 if err != nil { 754 return 755 } 756 757 err = r.DeleteExecutionNumbers(ctx, testNames) 758 if err != nil { 759 return 760 } 761 _, err = r.ResultsColl.DeleteMany(ctx, filter) 762 return 763 } 764 765 // DeleteByTestSuites deletes execution results by test suites 766 func (r *MongoRepository) DeleteByTestSuites(ctx context.Context, testSuiteNames []string) (err error) { 767 if len(testSuiteNames) == 0 { 768 return nil 769 } 770 771 var filter bson.M 772 if len(testSuiteNames) > 1 { 773 conditions := bson.A{} 774 for _, testSuiteName := range testSuiteNames { 775 conditions = append(conditions, bson.M{"testsuitename": testSuiteName}) 776 } 777 778 filter = bson.M{"$or": conditions} 779 } else { 780 filter = bson.M{"testSuitename": testSuiteNames[0]} 781 } 782 783 err = r.OutputRepository.DeleteOutputForTestSuites(ctx, testSuiteNames) 784 if err != nil { 785 return 786 } 787 788 err = r.DeleteExecutionNumbers(ctx, testSuiteNames) 789 if err != nil { 790 return 791 } 792 793 _, err = r.ResultsColl.DeleteMany(ctx, filter) 794 return 795 } 796 797 // DeleteForAllTestSuites deletes execution results for all test suites 798 func (r *MongoRepository) DeleteForAllTestSuites(ctx context.Context) (err error) { 799 err = r.OutputRepository.DeleteOutputForAllTestSuite(ctx) 800 if err != nil { 801 return 802 } 803 804 err = r.DeleteAllExecutionNumbers(ctx, true) 805 if err != nil { 806 return 807 } 808 809 _, err = r.ResultsColl.DeleteMany(ctx, bson.M{"testsuitename": bson.M{"$ne": ""}}) 810 return 811 } 812 813 // GetTestMetrics returns test executions metrics limited to number of executions or last N days 814 func (r *MongoRepository) GetTestMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error) { 815 query := bson.M{"testname": name} 816 817 pipeline := []bson.D{} 818 if last > 0 { 819 query["starttime"] = bson.M{"$gte": time.Now().Add(-time.Duration(last) * 24 * time.Hour)} 820 } 821 822 pipeline = append(pipeline, bson.D{{Key: "$match", Value: query}}) 823 pipeline = append(pipeline, bson.D{{Key: "$sort", Value: bson.D{{Key: "starttime", Value: -1}}}}) 824 pipeline = append(pipeline, bson.D{ 825 { 826 Key: "$project", Value: bson.D{ 827 {Key: "status", Value: "$executionresult.status"}, 828 {Key: "duration", Value: 1}, 829 {Key: "starttime", Value: 1}, 830 {Key: "name", Value: 1}, 831 }, 832 }, 833 }) 834 835 opts := options.Aggregate() 836 if r.allowDiskUse { 837 opts.SetAllowDiskUse(r.allowDiskUse) 838 } 839 840 cursor, err := r.ResultsColl.Aggregate(ctx, pipeline, opts) 841 if err != nil { 842 return metrics, err 843 } 844 845 var executions []testkube.ExecutionsMetricsExecutions 846 err = cursor.All(ctx, &executions) 847 if err != nil { 848 return metrics, err 849 } 850 851 metrics = common.CalculateMetrics(executions) 852 if limit > 0 && limit < len(metrics.Executions) { 853 metrics.Executions = metrics.Executions[:limit] 854 } 855 856 return metrics, nil 857 } 858 859 // cleanOutput makes sure the output fits into the limits imposed by Mongo; 860 // if needed it trims the string 861 // it keeps the first OutputPrefixSize of strings in case there were errors on init 862 // it adds a warning that the logs were trimmed 863 // it adds the last OutputMaxSize-OutputPrefixSize-OverflownOutputWarnSize bytes to the end 864 func cleanOutput(output string) string { 865 if len(output) >= OutputMaxSize { 866 prefix := output[:OutputPrefixSize] 867 suffix := output[len(output)-OutputMaxSize+OutputPrefixSize+len(OverflownOutputWarn):] 868 output = fmt.Sprintf("%s\n%s\n%s", prefix, OverflownOutputWarn, suffix) 869 } 870 return output 871 } 872 873 // cleanSteps trims the list of ExecutionStepResults in case there's too many elements to make sure it fits into mongo 874 func cleanSteps(steps []testkube.ExecutionStepResult) []testkube.ExecutionStepResult { 875 l := len(steps) 876 if l > StepMaxCount { 877 steps = steps[l-StepMaxCount:] 878 } 879 return steps 880 }