github.com/kubeshop/testkube@v1.17.23/pkg/tcl/repositorytcl/testworkflow/mongo.go (about) 1 // Copyright 2024 Testkube. 2 // 3 // Licensed as a Testkube Pro file under the Testkube Community 4 // License (the "License"); you may not use this file except in compliance with 5 // the License. You may obtain a copy of the License at 6 // 7 // https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt 8 9 package testworkflow 10 11 import ( 12 "context" 13 "strings" 14 "time" 15 16 "github.com/kubeshop/testkube/pkg/repository/common" 17 18 "go.mongodb.org/mongo-driver/bson" 19 "go.mongodb.org/mongo-driver/bson/primitive" 20 "go.mongodb.org/mongo-driver/mongo" 21 "go.mongodb.org/mongo-driver/mongo/options" 22 23 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 24 ) 25 26 var _ Repository = (*MongoRepository)(nil) 27 28 const CollectionName = "testworkflowresults" 29 30 func NewMongoRepository(db *mongo.Database, allowDiskUse bool, opts ...MongoRepositoryOpt) *MongoRepository { 31 r := &MongoRepository{ 32 db: db, 33 Coll: db.Collection(CollectionName), 34 allowDiskUse: allowDiskUse, 35 } 36 37 for _, opt := range opts { 38 opt(r) 39 } 40 41 return r 42 } 43 44 type MongoRepository struct { 45 db *mongo.Database 46 Coll *mongo.Collection 47 allowDiskUse bool 48 } 49 50 func WithMongoRepositoryCollection(collection *mongo.Collection) MongoRepositoryOpt { 51 return func(r *MongoRepository) { 52 r.Coll = collection 53 } 54 } 55 56 type MongoRepositoryOpt func(*MongoRepository) 57 58 func (r *MongoRepository) Get(ctx context.Context, id string) (result testkube.TestWorkflowExecution, err error) { 59 err = r.Coll.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": id}, bson.M{"name": id}}}).Decode(&result) 60 return *result.UnscapeDots(), err 61 } 62 63 func (r *MongoRepository) GetByNameAndTestWorkflow(ctx context.Context, name, workflowName string) (result testkube.TestWorkflowExecution, err error) { 64 err = r.Coll.FindOne(ctx, bson.M{"$or": bson.A{bson.M{"id": name}, bson.M{"name": name}}, "workflow.name": workflowName}).Decode(&result) 65 return *result.UnscapeDots(), err 66 } 67 68 func (r *MongoRepository) GetLatestByTestWorkflow(ctx context.Context, workflowName string) (*testkube.TestWorkflowExecution, error) { 69 opts := options.Aggregate() 70 pipeline := []bson.M{ 71 {"$sort": bson.M{"statusat": -1}}, 72 {"$match": bson.M{"workflow.name": workflowName}}, 73 {"$limit": 1}, 74 } 75 cursor, err := r.Coll.Aggregate(ctx, pipeline, opts) 76 if err != nil { 77 return nil, err 78 } 79 var items []testkube.TestWorkflowExecution 80 err = cursor.All(ctx, &items) 81 if err != nil { 82 return nil, err 83 } 84 if len(items) == 0 { 85 return nil, mongo.ErrNoDocuments 86 } 87 return items[0].UnscapeDots(), err 88 } 89 90 func (r *MongoRepository) GetLatestByTestWorkflows(ctx context.Context, workflowNames []string) (executions []testkube.TestWorkflowExecutionSummary, err error) { 91 if len(workflowNames) == 0 { 92 return executions, nil 93 } 94 95 documents := bson.A{} 96 for _, workflowName := range workflowNames { 97 documents = append(documents, bson.M{"workflow.name": workflowName}) 98 } 99 100 pipeline := []bson.M{ 101 {"$sort": bson.M{"statusat": -1}}, 102 {"$project": bson.M{ 103 "_id": 0, 104 "output": 0, 105 "signature": 0, 106 "result.steps": 0, 107 "result.initialization": 0, 108 "workflow.spec": 0, 109 "resolvedWorkflow": 0, 110 }}, 111 {"$match": bson.M{"$or": documents}}, 112 {"$group": bson.M{"_id": "$workflow.name", "execution": bson.M{"$first": "$$ROOT"}}}, 113 {"$replaceRoot": bson.M{"newRoot": "$execution"}}, 114 } 115 116 opts := options.Aggregate() 117 if r.allowDiskUse { 118 opts.SetAllowDiskUse(r.allowDiskUse) 119 } 120 121 cursor, err := r.Coll.Aggregate(ctx, pipeline, opts) 122 if err != nil { 123 return nil, err 124 } 125 err = cursor.All(ctx, &executions) 126 if err != nil { 127 return nil, err 128 } 129 130 if len(executions) == 0 { 131 return executions, nil 132 } 133 134 for i := range executions { 135 executions[i].UnscapeDots() 136 } 137 return executions, nil 138 } 139 140 // TODO: Add limit? 141 func (r *MongoRepository) GetRunning(ctx context.Context) (result []testkube.TestWorkflowExecution, err error) { 142 result = make([]testkube.TestWorkflowExecution, 0) 143 opts := &options.FindOptions{} 144 opts.SetSort(bson.D{{Key: "_id", Value: -1}}) 145 if r.allowDiskUse { 146 opts.SetAllowDiskUse(r.allowDiskUse) 147 } 148 149 cursor, err := r.Coll.Find(ctx, bson.M{ 150 "$or": bson.A{ 151 bson.M{"result.status": testkube.RUNNING_TestWorkflowStatus}, 152 bson.M{"result.status": testkube.QUEUED_TestWorkflowStatus}, 153 }, 154 }, opts) 155 if err != nil { 156 return result, err 157 } 158 err = cursor.All(ctx, &result) 159 160 for i := range result { 161 result[i].UnscapeDots() 162 } 163 return 164 } 165 166 func (r *MongoRepository) GetExecutionsTotals(ctx context.Context, filter ...Filter) (totals testkube.ExecutionsTotals, err error) { 167 var result []struct { 168 Status string `bson:"_id"` 169 Count int `bson:"count"` 170 } 171 172 query := bson.M{} 173 if len(filter) > 0 { 174 query, _ = composeQueryAndOpts(filter[0]) 175 } 176 177 pipeline := []bson.D{{{Key: "$match", Value: query}}} 178 if len(filter) > 0 { 179 pipeline = append(pipeline, bson.D{{Key: "$sort", Value: bson.D{{Key: "statusat", Value: -1}}}}) 180 pipeline = append(pipeline, bson.D{{Key: "$skip", Value: int64(filter[0].Page() * filter[0].PageSize())}}) 181 pipeline = append(pipeline, bson.D{{Key: "$limit", Value: int64(filter[0].PageSize())}}) 182 } 183 184 pipeline = append(pipeline, bson.D{{Key: "$group", Value: bson.D{{Key: "_id", Value: "$result.status"}, 185 {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}}}}) 186 187 opts := options.Aggregate() 188 if r.allowDiskUse { 189 opts.SetAllowDiskUse(r.allowDiskUse) 190 } 191 192 cursor, err := r.Coll.Aggregate(ctx, pipeline, opts) 193 if err != nil { 194 return totals, err 195 } 196 err = cursor.All(ctx, &result) 197 if err != nil { 198 return totals, err 199 } 200 201 var sum int32 202 203 for _, o := range result { 204 sum += int32(o.Count) 205 switch testkube.TestWorkflowStatus(o.Status) { 206 case testkube.QUEUED_TestWorkflowStatus: 207 totals.Queued = int32(o.Count) 208 case testkube.RUNNING_TestWorkflowStatus: 209 totals.Running = int32(o.Count) 210 case testkube.PASSED_TestWorkflowStatus: 211 totals.Passed = int32(o.Count) 212 case testkube.FAILED_TestWorkflowStatus, testkube.ABORTED_TestWorkflowStatus: 213 totals.Failed = int32(o.Count) 214 } 215 } 216 totals.Results = sum 217 218 return 219 } 220 221 func (r *MongoRepository) GetExecutions(ctx context.Context, filter Filter) (result []testkube.TestWorkflowExecution, err error) { 222 result = make([]testkube.TestWorkflowExecution, 0) 223 query, opts := composeQueryAndOpts(filter) 224 if r.allowDiskUse { 225 opts.SetAllowDiskUse(r.allowDiskUse) 226 } 227 228 cursor, err := r.Coll.Find(ctx, query, opts) 229 if err != nil { 230 return 231 } 232 err = cursor.All(ctx, &result) 233 234 for i := range result { 235 result[i].UnscapeDots() 236 } 237 return 238 } 239 240 func (r *MongoRepository) GetExecutionsSummary(ctx context.Context, filter Filter) (result []testkube.TestWorkflowExecutionSummary, err error) { 241 result = make([]testkube.TestWorkflowExecutionSummary, 0) 242 query, opts := composeQueryAndOpts(filter) 243 if r.allowDiskUse { 244 opts.SetAllowDiskUse(r.allowDiskUse) 245 } 246 247 opts = opts.SetProjection(bson.M{ 248 "_id": 0, 249 "output": 0, 250 "signature": 0, 251 "result.steps": 0, 252 "result.initialization": 0, 253 "workflow.spec": 0, 254 "resolvedWorkflow": 0, 255 }) 256 cursor, err := r.Coll.Find(ctx, query, opts) 257 if err != nil { 258 return 259 } 260 err = cursor.All(ctx, &result) 261 262 for i := range result { 263 result[i].UnscapeDots() 264 } 265 return 266 } 267 268 func (r *MongoRepository) Insert(ctx context.Context, result testkube.TestWorkflowExecution) (err error) { 269 result.EscapeDots() 270 _, err = r.Coll.InsertOne(ctx, result) 271 return 272 } 273 274 func (r *MongoRepository) Update(ctx context.Context, result testkube.TestWorkflowExecution) (err error) { 275 result.EscapeDots() 276 _, err = r.Coll.ReplaceOne(ctx, bson.M{"id": result.Id}, result) 277 return 278 } 279 280 func (r *MongoRepository) UpdateResult(ctx context.Context, id string, result *testkube.TestWorkflowResult) (err error) { 281 data := bson.M{"result": result} 282 if !result.FinishedAt.IsZero() { 283 data["statusat"] = result.FinishedAt 284 } 285 _, err = r.Coll.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": data}) 286 return 287 } 288 289 func (r *MongoRepository) UpdateOutput(ctx context.Context, id string, refs []testkube.TestWorkflowOutput) (err error) { 290 _, err = r.Coll.UpdateOne(ctx, bson.M{"id": id}, bson.M{"$set": bson.M{"output": refs}}) 291 return 292 } 293 294 func composeQueryAndOpts(filter Filter) (bson.M, *options.FindOptions) { 295 query := bson.M{} 296 opts := options.Find() 297 startTimeQuery := bson.M{} 298 299 if filter.NameDefined() { 300 query["workflow.name"] = filter.Name() 301 } 302 303 if filter.TextSearchDefined() { 304 query["name"] = bson.M{"$regex": primitive.Regex{Pattern: filter.TextSearch(), Options: "i"}} 305 } 306 307 if filter.LastNDaysDefined() { 308 startTimeQuery["$gte"] = time.Now().Add(-time.Duration(filter.LastNDays()) * 24 * time.Hour) 309 } 310 311 if filter.StartDateDefined() { 312 startTimeQuery["$gte"] = filter.StartDate() 313 } 314 315 if filter.EndDateDefined() { 316 startTimeQuery["$lte"] = filter.EndDate() 317 } 318 319 if len(startTimeQuery) > 0 { 320 query["scheduledat"] = startTimeQuery 321 } 322 323 if filter.StatusesDefined() { 324 statuses := filter.Statuses() 325 if len(statuses) == 1 { 326 query["result.status"] = statuses[0] 327 } else { 328 var conditions bson.A 329 for _, status := range statuses { 330 conditions = append(conditions, bson.M{"result.status": status}) 331 } 332 333 query["$or"] = conditions 334 } 335 } 336 337 if filter.Selector() != "" { 338 items := strings.Split(filter.Selector(), ",") 339 for _, item := range items { 340 elements := strings.Split(item, "=") 341 if len(elements) == 2 { 342 query["workflow.labels."+elements[0]] = elements[1] 343 } else if len(elements) == 1 { 344 query["workflow.labels."+elements[0]] = bson.M{"$exists": true} 345 } 346 } 347 } 348 349 opts.SetSkip(int64(filter.Page() * filter.PageSize())) 350 opts.SetLimit(int64(filter.PageSize())) 351 opts.SetSort(bson.D{{Key: "scheduledat", Value: -1}}) 352 353 return query, opts 354 } 355 356 // DeleteByTestWorkflow deletes execution results by workflow 357 func (r *MongoRepository) DeleteByTestWorkflow(ctx context.Context, workflowName string) (err error) { 358 _, err = r.Coll.DeleteMany(ctx, bson.M{"workflow.name": workflowName}) 359 return 360 } 361 362 // DeleteAll deletes all execution results 363 func (r *MongoRepository) DeleteAll(ctx context.Context) (err error) { 364 _, err = r.Coll.DeleteMany(ctx, bson.M{}) 365 return 366 } 367 368 // DeleteByTestWorkflows deletes execution results by workflows 369 func (r *MongoRepository) DeleteByTestWorkflows(ctx context.Context, workflowNames []string) (err error) { 370 if len(workflowNames) == 0 { 371 return nil 372 } 373 374 conditions := bson.A{} 375 for _, workflowName := range workflowNames { 376 conditions = append(conditions, bson.M{"workflow.name": workflowName}) 377 } 378 379 filter := bson.M{"$or": conditions} 380 381 _, err = r.Coll.DeleteMany(ctx, filter) 382 return 383 } 384 385 // TODO: Avoid calculating for all executions in memory (same for tests/test suites) 386 // GetTestWorkflowMetrics returns test executions metrics 387 func (r *MongoRepository) GetTestWorkflowMetrics(ctx context.Context, name string, limit, last int) (metrics testkube.ExecutionsMetrics, err error) { 388 query := bson.M{"workflow.name": name} 389 390 if last > 0 { 391 query["scheduledat"] = bson.M{"$gte": time.Now().Add(-time.Duration(last) * 24 * time.Hour)} 392 } 393 394 pipeline := []bson.M{ 395 {"$sort": bson.M{"scheduledat": -1}}, 396 {"$match": query}, 397 {"$project": bson.M{ 398 "status": "$result.status", 399 "duration": "$result.duration", 400 "starttime": "$scheduledat", 401 "name": 1, 402 }}, 403 } 404 405 opts := options.Aggregate() 406 if r.allowDiskUse { 407 opts.SetAllowDiskUse(r.allowDiskUse) 408 } 409 410 cursor, err := r.Coll.Aggregate(ctx, pipeline, opts) 411 if err != nil { 412 return metrics, err 413 } 414 415 var executions []testkube.ExecutionsMetricsExecutions 416 err = cursor.All(ctx, &executions) 417 418 if err != nil { 419 return metrics, err 420 } 421 422 metrics = common.CalculateMetrics(executions) 423 if limit > 0 && limit < len(metrics.Executions) { 424 metrics.Executions = metrics.Executions[:limit] 425 } 426 427 return metrics, nil 428 }