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  }