github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/testsuites.go (about) 1 package v1 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "math" 9 "net/http" 10 "sort" 11 "strconv" 12 "strings" 13 "time" 14 15 "github.com/gofiber/fiber/v2" 16 "go.mongodb.org/mongo-driver/mongo" 17 "k8s.io/apimachinery/pkg/api/errors" 18 "k8s.io/apimachinery/pkg/util/yaml" 19 20 testsuitesv3 "github.com/kubeshop/testkube-operator/api/testsuite/v3" 21 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 22 "github.com/kubeshop/testkube/pkg/crd" 23 "github.com/kubeshop/testkube/pkg/datefilter" 24 "github.com/kubeshop/testkube/pkg/event/bus" 25 testsmapper "github.com/kubeshop/testkube/pkg/mapper/tests" 26 testsuiteexecutionsmapper "github.com/kubeshop/testkube/pkg/mapper/testsuiteexecutions" 27 testsuitesmapper "github.com/kubeshop/testkube/pkg/mapper/testsuites" 28 "github.com/kubeshop/testkube/pkg/repository/testresult" 29 "github.com/kubeshop/testkube/pkg/scheduler" 30 "github.com/kubeshop/testkube/pkg/types" 31 "github.com/kubeshop/testkube/pkg/utils" 32 "github.com/kubeshop/testkube/pkg/workerpool" 33 ) 34 35 // CreateTestSuiteHandler for getting test object 36 func (s TestkubeAPI) CreateTestSuiteHandler() fiber.Handler { 37 return func(c *fiber.Ctx) error { 38 errPrefix := "failed to create test suite" 39 var testSuite testsuitesv3.TestSuite 40 if string(c.Request().Header.ContentType()) == mediaTypeYAML { 41 testSuiteSpec := string(c.Body()) 42 decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSuiteSpec), len(testSuiteSpec)) 43 if err := decoder.Decode(&testSuite); err != nil { 44 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err)) 45 } 46 errPrefix = errPrefix + " " + testSuite.Name 47 } else { 48 var request testkube.TestSuiteUpsertRequest 49 data := c.Body() 50 if string(c.Request().Header.ContentType()) != mediaTypeJSON { 51 return s.Error(c, http.StatusBadRequest, fiber.ErrUnprocessableEntity) 52 } 53 54 err := json.Unmarshal(data, &request) 55 if err != nil { 56 s.Log.Warnw("could not parse json request", "error", err) 57 } 58 errPrefix = errPrefix + " " + request.Name 59 60 emptyBatch := true 61 for _, step := range request.Steps { 62 if len(step.Execute) != 0 { 63 emptyBatch = false 64 break 65 } 66 } 67 68 if emptyBatch { 69 var requestV2 testkube.TestSuiteUpsertRequestV2 70 if err := json.Unmarshal(data, &requestV2); err != nil { 71 return s.Error(c, http.StatusBadRequest, err) 72 } 73 74 request = *requestV2.ToTestSuiteUpsertRequest() 75 } 76 77 if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { 78 request.QuoteTestSuiteTextFields() 79 data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuiteUpsertRequest{request}) 80 return s.getCRDs(c, data, err) 81 } 82 83 testSuite, err = testsuitesmapper.MapTestSuiteUpsertRequestToTestCRD(request) 84 if err != nil { 85 return s.Error(c, http.StatusBadRequest, err) 86 } 87 88 testSuite.Namespace = s.Namespace 89 } 90 91 s.Log.Infow("creating test suite", "testSuite", testSuite) 92 93 created, err := s.TestsSuitesClient.Create(&testSuite, s.disableSecretCreation) 94 95 s.Metrics.IncCreateTestSuite(err) 96 97 if err != nil { 98 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create test suite: %w", errPrefix, err)) 99 } 100 101 c.Status(http.StatusCreated) 102 return c.JSON(created) 103 } 104 } 105 106 // UpdateTestSuiteHandler updates an existing TestSuite CR based on TestSuite content 107 func (s TestkubeAPI) UpdateTestSuiteHandler() fiber.Handler { 108 return func(c *fiber.Ctx) error { 109 errPrefix := "failed to update test suite" 110 var request testkube.TestSuiteUpdateRequest 111 if string(c.Request().Header.ContentType()) == mediaTypeYAML { 112 var testSuite testsuitesv3.TestSuite 113 testSuiteSpec := string(c.Body()) 114 decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testSuiteSpec), len(testSuiteSpec)) 115 if err := decoder.Decode(&testSuite); err != nil { 116 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err)) 117 } 118 request = testsuitesmapper.MapTestSuiteTestCRDToUpdateRequest(&testSuite) 119 } else { 120 data := c.Body() 121 if string(c.Request().Header.ContentType()) != mediaTypeJSON { 122 return s.Error(c, http.StatusBadRequest, fiber.ErrUnprocessableEntity) 123 } 124 125 err := json.Unmarshal(data, &request) 126 if err != nil { 127 s.Log.Warnw("could not parse json request", "error", err) 128 } 129 130 if request.Steps != nil { 131 emptyBatch := true 132 for _, step := range *request.Steps { 133 if len(step.Execute) != 0 { 134 emptyBatch = false 135 break 136 } 137 } 138 139 if emptyBatch { 140 var requestV2 testkube.TestSuiteUpdateRequestV2 141 if err := json.Unmarshal(data, &requestV2); err != nil { 142 return s.Error(c, http.StatusBadRequest, err) 143 } 144 145 request = *requestV2.ToTestSuiteUpdateRequest() 146 } 147 } 148 } 149 150 var name string 151 if request.Name != nil { 152 name = *request.Name 153 } 154 errPrefix = errPrefix + " " + name 155 156 // we need to get resource first and load its metadata.ResourceVersion 157 testSuite, err := s.TestsSuitesClient.Get(name) 158 if err != nil { 159 if errors.IsNotFound(err) { 160 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 161 } 162 163 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err)) 164 } 165 166 // map TestSuite but load spec only to not override metadata.ResourceVersion 167 testSuiteSpec, err := testsuitesmapper.MapTestSuiteUpdateRequestToTestCRD(request, testSuite) 168 if err != nil { 169 return s.Error(c, http.StatusBadRequest, err) 170 } 171 172 updatedTestSuite, err := s.TestsSuitesClient.Update(testSuiteSpec, s.disableSecretCreation) 173 174 s.Metrics.IncUpdateTestSuite(err) 175 176 if err != nil { 177 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not update test suite: %w", errPrefix, err)) 178 } 179 180 return c.JSON(updatedTestSuite) 181 } 182 } 183 184 // GetTestSuiteHandler for getting TestSuite object 185 func (s TestkubeAPI) GetTestSuiteHandler() fiber.Handler { 186 return func(c *fiber.Ctx) error { 187 name := c.Params("id") 188 errPrefix := "failed to get test suite " + name 189 190 crTestSuite, err := s.TestsSuitesClient.Get(name) 191 if err != nil { 192 if errors.IsNotFound(err) { 193 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 194 } 195 196 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err)) 197 } 198 199 testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite) 200 if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { 201 testSuite.QuoteTestSuiteTextFields() 202 data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuite{testSuite}) 203 return s.getCRDs(c, data, err) 204 } 205 206 return c.JSON(testSuite) 207 } 208 } 209 210 // GetTestSuiteWithExecutionHandler for getting TestSuite object with execution 211 func (s TestkubeAPI) GetTestSuiteWithExecutionHandler() fiber.Handler { 212 return func(c *fiber.Ctx) error { 213 name := c.Params("id") 214 errPrefix := fmt.Sprintf("failed to get test suite %s with execution", name) 215 crTestSuite, err := s.TestsSuitesClient.Get(name) 216 if err != nil { 217 if errors.IsNotFound(err) { 218 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 219 } 220 221 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite: %w", errPrefix, err)) 222 } 223 224 testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite) 225 if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { 226 testSuite.QuoteTestSuiteTextFields() 227 data, err := crd.GenerateYAML(crd.TemplateTestSuite, []testkube.TestSuite{testSuite}) 228 return s.getCRDs(c, data, err) 229 } 230 231 ctx := c.Context() 232 execution, err := s.TestExecutionResults.GetLatestByTestSuite(ctx, name) 233 if err != nil && err != mongo.ErrNoDocuments { 234 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get execution: %w", errPrefix, err)) 235 } 236 237 return c.JSON(testkube.TestSuiteWithExecution{ 238 TestSuite: &testSuite, 239 LatestExecution: execution, 240 }) 241 } 242 } 243 244 // DeleteTestSuiteHandler for deleting a TestSuite with id 245 func (s TestkubeAPI) DeleteTestSuiteHandler() fiber.Handler { 246 return func(c *fiber.Ctx) error { 247 name := c.Params("id") 248 errPrefix := fmt.Sprintf("failed to delete test suite %s", name) 249 250 err := s.TestsSuitesClient.Delete(name) 251 if err != nil { 252 if errors.IsNotFound(err) { 253 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 254 } 255 256 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite: %w", errPrefix, err)) 257 } 258 259 // delete executions for test 260 if err = s.ExecutionResults.DeleteByTestSuite(c.Context(), name); err != nil { 261 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite test executions: %w", errPrefix, err)) 262 } 263 264 // delete executions for test suite 265 if err = s.TestExecutionResults.DeleteByTestSuite(c.Context(), name); err != nil { 266 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suite executions: %w", errPrefix, err)) 267 } 268 269 return c.SendStatus(http.StatusNoContent) 270 } 271 } 272 273 // DeleteTestSuitesHandler for deleting all TestSuites 274 func (s TestkubeAPI) DeleteTestSuitesHandler() fiber.Handler { 275 return func(c *fiber.Ctx) error { 276 errPrefix := "failed to delete test suites" 277 278 var err error 279 var testSuiteNames []string 280 selector := c.Query("selector") 281 if selector == "" { 282 err = s.TestsSuitesClient.DeleteAll() 283 } else { 284 var testSuiteList *testsuitesv3.TestSuiteList 285 testSuiteList, err = s.TestsSuitesClient.List(selector) 286 if err != nil { 287 if !errors.IsNotFound(err) { 288 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err)) 289 } 290 } else { 291 for _, item := range testSuiteList.Items { 292 testSuiteNames = append(testSuiteNames, item.Name) 293 } 294 } 295 296 err = s.TestsSuitesClient.DeleteByLabels(selector) 297 } 298 299 if err != nil { 300 if errors.IsNotFound(err) { 301 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 302 } 303 304 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test suites: %w", errPrefix, err)) 305 } 306 307 // delete all executions for tests 308 if selector == "" { 309 err = s.ExecutionResults.DeleteForAllTestSuites(c.Context()) 310 } else { 311 err = s.ExecutionResults.DeleteByTestSuites(c.Context(), testSuiteNames) 312 } 313 314 if err != nil { 315 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suite test executions: %w", errPrefix, err)) 316 } 317 318 // delete all executions for test suites 319 if selector == "" { 320 err = s.TestExecutionResults.DeleteAll(c.Context()) 321 } else { 322 err = s.TestExecutionResults.DeleteByTestSuites(c.Context(), testSuiteNames) 323 } 324 325 if err != nil { 326 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suite executions: %w", errPrefix, err)) 327 } 328 329 return c.SendStatus(http.StatusNoContent) 330 } 331 } 332 333 func (s TestkubeAPI) getFilteredTestSuitesList(c *fiber.Ctx) (*testsuitesv3.TestSuiteList, error) { 334 crTestSuites, err := s.TestsSuitesClient.List(c.Query("selector")) 335 if err != nil { 336 return nil, err 337 } 338 339 search := c.Query("textSearch") 340 if search != "" { 341 // filter items array 342 for i := len(crTestSuites.Items) - 1; i >= 0; i-- { 343 if !strings.Contains(crTestSuites.Items[i].Name, search) { 344 crTestSuites.Items = append(crTestSuites.Items[:i], crTestSuites.Items[i+1:]...) 345 } 346 } 347 } 348 349 return crTestSuites, nil 350 } 351 352 // ListTestSuitesHandler for getting list of all available TestSuites 353 func (s TestkubeAPI) ListTestSuitesHandler() fiber.Handler { 354 return func(c *fiber.Ctx) error { 355 errPrefix := "failed to list test suites" 356 357 crTestSuites, err := s.getFilteredTestSuitesList(c) 358 if err != nil { 359 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err)) 360 } 361 362 testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites) 363 if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { 364 for i := range testSuites { 365 testSuites[i].QuoteTestSuiteTextFields() 366 } 367 368 data, err := crd.GenerateYAML(crd.TemplateTestSuite, testSuites) 369 return s.getCRDs(c, data, err) 370 } 371 372 return c.JSON(testSuites) 373 } 374 } 375 376 // TestSuiteMetricsHandler returns basic metrics for given testsuite 377 func (s TestkubeAPI) TestSuiteMetricsHandler() fiber.Handler { 378 return func(c *fiber.Ctx) error { 379 errPrefix := "failed to get test suite metrics" 380 const ( 381 DefaultLastDays = 0 382 DefaultLimit = 0 383 ) 384 385 testSuiteName := c.Params("id") 386 387 limit, err := strconv.Atoi(c.Query("limit", strconv.Itoa(DefaultLimit))) 388 if err != nil { 389 limit = DefaultLimit 390 } 391 392 last, err := strconv.Atoi(c.Query("last", strconv.Itoa(DefaultLastDays))) 393 if err != nil { 394 last = DefaultLastDays 395 } 396 397 metrics, err := s.TestExecutionResults.GetTestSuiteMetrics(context.Background(), testSuiteName, limit, last) 398 if err != nil { 399 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: failed to get metrics from client: %w", errPrefix, err)) 400 } 401 402 return c.JSON(metrics) 403 } 404 } 405 406 // getLatestTestSuiteExecutions return latest test suite executions either by starttime or endtine for tests 407 func (s TestkubeAPI) getLatestTestSuiteExecutions(ctx context.Context, testSuiteNames []string) (map[string]testkube.TestSuiteExecution, error) { 408 executions, err := s.TestExecutionResults.GetLatestByTestSuites(ctx, testSuiteNames) 409 if err != nil && err != mongo.ErrNoDocuments { 410 return nil, err 411 } 412 413 executionMap := make(map[string]testkube.TestSuiteExecution, len(executions)) 414 for i := range executions { 415 if executions[i].TestSuite == nil { 416 continue 417 } 418 executionMap[executions[i].TestSuite.Name] = executions[i] 419 } 420 return executionMap, nil 421 } 422 423 // ListTestSuiteWithExecutionsHandler for getting list of all available TestSuite with latest executions 424 func (s TestkubeAPI) ListTestSuiteWithExecutionsHandler() fiber.Handler { 425 return func(c *fiber.Ctx) error { 426 errPrefix := "failed to list test suites with executions" 427 428 crTestSuites, err := s.getFilteredTestSuitesList(c) 429 if err != nil { 430 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test suites: %w", errPrefix, err)) 431 } 432 433 testSuites := testsuitesmapper.MapTestSuiteListKubeToAPI(*crTestSuites) 434 if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML { 435 for i := range testSuites { 436 testSuites[i].QuoteTestSuiteTextFields() 437 } 438 439 data, err := crd.GenerateYAML(crd.TemplateTestSuite, testSuites) 440 return s.getCRDs(c, data, err) 441 } 442 443 results := make([]testkube.TestSuiteWithExecutionSummary, 0, len(testSuites)) 444 testSuiteNames := make([]string, len(testSuites)) 445 for i := range testSuites { 446 testSuiteNames[i] = testSuites[i].Name 447 } 448 449 executionMap, err := s.getLatestTestSuiteExecutions(c.Context(), testSuiteNames) 450 if err != nil { 451 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not list test suite executions from db: %w", errPrefix, err)) 452 } 453 454 for i := range testSuites { 455 if execution, ok := executionMap[testSuites[i].Name]; ok { 456 results = append(results, testkube.TestSuiteWithExecutionSummary{ 457 TestSuite: &testSuites[i], 458 LatestExecution: testsuiteexecutionsmapper.MapToSummary(&execution), 459 }) 460 } else { 461 results = append(results, testkube.TestSuiteWithExecutionSummary{ 462 TestSuite: &testSuites[i], 463 }) 464 } 465 } 466 467 sort.Slice(results, func(i, j int) bool { 468 iTime := results[i].TestSuite.Created 469 if results[i].LatestExecution != nil { 470 iTime = results[i].LatestExecution.EndTime 471 if results[i].LatestExecution.StartTime.After(results[i].LatestExecution.EndTime) { 472 iTime = results[i].LatestExecution.StartTime 473 } 474 } 475 476 jTime := results[j].TestSuite.Created 477 if results[j].LatestExecution != nil { 478 jTime = results[j].LatestExecution.EndTime 479 if results[j].LatestExecution.StartTime.After(results[j].LatestExecution.EndTime) { 480 jTime = results[j].LatestExecution.StartTime 481 } 482 } 483 484 return iTime.After(jTime) 485 }) 486 487 status := c.Query("status") 488 if status != "" { 489 statusList, err := testkube.ParseTestSuiteExecutionStatusList(status, ",") 490 if err != nil { 491 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite execution status filter invalid: %w", errPrefix, err)) 492 } 493 494 statusMap := statusList.ToMap() 495 // filter items array 496 for i := len(results) - 1; i >= 0; i-- { 497 if results[i].LatestExecution != nil && results[i].LatestExecution.Status != nil { 498 if _, ok := statusMap[*results[i].LatestExecution.Status]; ok { 499 continue 500 } 501 } 502 503 results = append(results[:i], results[i+1:]...) 504 } 505 } 506 507 var page, pageSize int 508 pageParam := c.Query("page", "") 509 if pageParam != "" { 510 pageSize = testresult.PageDefaultLimit 511 page, err = strconv.Atoi(pageParam) 512 if err != nil { 513 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite page filter invalid: %w", errPrefix, err)) 514 } 515 } 516 517 pageSizeParam := c.Query("pageSize", "") 518 if pageSizeParam != "" { 519 pageSize, err = strconv.Atoi(pageSizeParam) 520 if err != nil { 521 s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test suite page size filter invalid: %w", errPrefix, err)) 522 } 523 } 524 525 if pageParam != "" || pageSizeParam != "" { 526 startPos := page * pageSize 527 endPos := (page + 1) * pageSize 528 if startPos < len(results) { 529 if endPos > len(results) { 530 endPos = len(results) 531 } 532 533 results = results[startPos:endPos] 534 } 535 } 536 537 return c.JSON(results) 538 } 539 } 540 541 func (s TestkubeAPI) ExecuteTestSuitesHandler() fiber.Handler { 542 return func(c *fiber.Ctx) error { 543 errPrefix := "failed to execute test suite" 544 var request testkube.TestSuiteExecutionRequest 545 err := c.BodyParser(&request) 546 if err != nil { 547 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test execution request body invalid: %w", errPrefix, err)) 548 } 549 550 name := c.Params("id") 551 selector := c.Query("selector") 552 s.Log.Debugw("getting test suite", "name", name, "selector", selector) 553 554 var testSuites []testsuitesv3.TestSuite 555 if name != "" { 556 errPrefix = errPrefix + " " + name 557 testSuite, err := s.TestsSuitesClient.Get(name) 558 if err != nil { 559 if errors.IsNotFound(err) { 560 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite not found: %w", errPrefix, err)) 561 } 562 563 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get test suite: %w", errPrefix, err)) 564 } 565 testSuites = append(testSuites, *testSuite) 566 } else { 567 testSuiteList, err := s.TestsSuitesClient.List(selector) 568 if err != nil { 569 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: can't list test suites: %w", errPrefix, err)) 570 } 571 572 testSuites = append(testSuites, testSuiteList.Items...) 573 } 574 575 var results []testkube.TestSuiteExecution 576 if len(testSuites) != 0 { 577 request.TestSuiteExecutionName = strings.Clone(c.Query("testSuiteExecutionName")) 578 concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel))) 579 if err != nil { 580 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err)) 581 } 582 583 workerpoolService := workerpool.New[testkube.TestSuite, testkube.TestSuiteExecutionRequest, testkube.TestSuiteExecution](concurrencyLevel) 584 585 go workerpoolService.SendRequests(s.scheduler.PrepareTestSuiteRequests(testSuites, request)) 586 go workerpoolService.Run(c.Context()) 587 588 for r := range workerpoolService.GetResponses() { 589 results = append(results, r.Result) 590 } 591 } 592 593 s.Log.Debugw("executing test", "name", name, "selector", selector) 594 if name != "" && len(results) != 0 { 595 if results[0].IsFailed() { 596 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: Test suite failed %v", errPrefix, name)) 597 } 598 599 c.Status(http.StatusCreated) 600 return c.JSON(results[0]) 601 } 602 603 c.Status(http.StatusCreated) 604 return c.JSON(results) 605 } 606 } 607 608 func (s TestkubeAPI) ListTestSuiteExecutionsHandler() fiber.Handler { 609 return func(c *fiber.Ctx) error { 610 611 now := time.Now() 612 var l = s.Log.With("handler", "ListTestSuiteExecutionsHandler", "id", utils.RandAlphanum(10)) 613 614 errPrefix := "failed to list test suite execution" 615 filter := getExecutionsFilterFromRequest(c) 616 617 ctx := c.Context() 618 executionsTotals, err := s.TestExecutionResults.GetExecutionsTotals(ctx, filter) 619 if err != nil { 620 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get executions totals: %w", errPrefix, err)) 621 } 622 l.Debugw("got executions totals", "totals", executionsTotals, "time", time.Since(now)) 623 filterAllTotals := *filter.(*testresult.FilterImpl) 624 filterAllTotals.WithPage(0).WithPageSize(math.MaxInt64) 625 allExecutionsTotals, err := s.TestExecutionResults.GetExecutionsTotals(ctx, filterAllTotals) 626 if err != nil { 627 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get all executions totals: %w", errPrefix, err)) 628 } 629 l.Debugw("got all executions totals", "totals", executionsTotals, "time", time.Since(now)) 630 631 executions, err := s.TestExecutionResults.GetExecutions(ctx, filter) 632 if err != nil { 633 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: client could not get executions: %w", errPrefix, err)) 634 } 635 l.Debugw("got executions", "time", time.Since(now)) 636 637 return c.JSON(testkube.TestSuiteExecutionsResult{ 638 Totals: &allExecutionsTotals, 639 Filtered: &executionsTotals, 640 Results: testsuitesmapper.MapToTestExecutionSummary(executions), 641 }) 642 } 643 } 644 645 func (s TestkubeAPI) GetTestSuiteExecutionHandler() fiber.Handler { 646 return func(c *fiber.Ctx) error { 647 id := c.Params("executionID") 648 errPrefix := fmt.Sprintf("failed to get test suite execution %s", id) 649 650 execution, err := s.TestExecutionResults.Get(c.Context(), id) 651 if err == mongo.ErrNoDocuments { 652 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id)) 653 } 654 if err != nil { 655 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get test suite executions from db: %w", errPrefix, err)) 656 } 657 658 execution.Duration = types.FormatDuration(execution.Duration) 659 660 secretMap := make(map[string]string) 661 if execution.SecretUUID != "" && execution.TestSuite != nil { 662 secretMap, err = s.TestsSuitesClient.GetSecretTestSuiteVars(execution.TestSuite.Name, execution.SecretUUID) 663 if err != nil { 664 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test suite secrets: %w", errPrefix, err)) 665 } 666 } 667 668 for key, value := range secretMap { 669 if variable, ok := execution.Variables[key]; ok && value != "" { 670 variable.Value = value 671 variable.SecretRef = nil 672 execution.Variables[key] = variable 673 } 674 } 675 676 return c.JSON(execution) 677 } 678 } 679 680 func (s TestkubeAPI) ListTestSuiteArtifactsHandler() fiber.Handler { 681 return func(c *fiber.Ctx) error { 682 s.Log.Infow("listing testsuite artifacts", "executionID", c.Params("executionID")) 683 id := c.Params("executionID") 684 errPrefix := fmt.Sprintf("failed to list test suite artifacts %s", id) 685 execution, err := s.TestExecutionResults.Get(c.Context(), id) 686 if err == mongo.ErrNoDocuments { 687 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id)) 688 } 689 if err != nil { 690 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get test suite execution from db: %w", errPrefix, err)) 691 } 692 693 var artifacts []testkube.Artifact 694 for _, stepResult := range execution.StepResults { 695 if stepResult.Execution == nil || stepResult.Execution.Id == "" { 696 continue 697 } 698 699 artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts) 700 if err != nil { 701 continue 702 } 703 } 704 705 for _, stepResults := range execution.ExecuteStepResults { 706 for _, stepResult := range stepResults.Execute { 707 if stepResult.Execution == nil || stepResult.Execution.Id == "" { 708 continue 709 } 710 711 artifacts, err = s.getExecutionArtfacts(c.Context(), stepResult.Execution, artifacts) 712 if err != nil { 713 continue 714 } 715 } 716 } 717 718 if err != nil { 719 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not list artifacts: %w", errPrefix, err)) 720 } 721 722 return c.JSON(artifacts) 723 } 724 } 725 726 func (s TestkubeAPI) getExecutionArtfacts(ctx context.Context, execution *testkube.Execution, 727 artifacts []testkube.Artifact) ([]testkube.Artifact, error) { 728 var stepArtifacts []testkube.Artifact 729 var bucket string 730 731 artifactsStorage := s.ArtifactsStorage 732 folder := execution.Id 733 if execution.ArtifactRequest != nil { 734 bucket = execution.ArtifactRequest.StorageBucket 735 if execution.ArtifactRequest.OmitFolderPerExecution { 736 folder = "" 737 } 738 } 739 740 var err error 741 if bucket != "" { 742 artifactsStorage, err = s.getArtifactStorage(bucket) 743 if err != nil { 744 s.Log.Warnw("can't get artifact storage", "executionID", execution.Id, "error", err) 745 return artifacts, err 746 } 747 } 748 749 stepArtifacts, err = artifactsStorage.ListFiles(ctx, folder, execution.TestName, execution.TestSuiteName, "") 750 if err != nil { 751 s.Log.Warnw("can't list artifacts", "executionID", execution.Id, "error", err) 752 return artifacts, err 753 } 754 755 s.Log.Debugw("listing artifacts for step", "executionID", execution.Id, "artifacts", stepArtifacts) 756 for i := range stepArtifacts { 757 stepArtifacts[i].ExecutionName = execution.Name 758 artifacts = append(artifacts, stepArtifacts[i]) 759 } 760 761 return artifacts, nil 762 } 763 764 // AbortTestSuiteHandler for aborting a TestSuite with id 765 func (s TestkubeAPI) AbortTestSuiteHandler() fiber.Handler { 766 return func(c *fiber.Ctx) error { 767 ctx := c.Context() 768 name := c.Params("id") 769 if name == "" { 770 return s.Error(c, http.StatusBadRequest, fmt.Errorf("failed to abort test suite: id cannot be empty")) 771 } 772 errPrefix := fmt.Sprintf("failed to abort test suite %s", name) 773 filter := testresult.NewExecutionsFilter().WithName(name).WithStatus(string(testkube.RUNNING_ExecutionStatus)) 774 executions, err := s.TestExecutionResults.GetExecutions(ctx, filter) 775 if err != nil { 776 if err == mongo.ErrNoDocuments { 777 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: executions with test syute name %s not found", errPrefix, name)) 778 } 779 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get executions: %w", errPrefix, err)) 780 } 781 782 for _, execution := range executions { 783 execution.Status = testkube.TestSuiteExecutionStatusAborting 784 s.Log.Infow("aborting test suite execution", "executionID", execution.Id) 785 err := s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution)) 786 787 if err != nil { 788 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err)) 789 } 790 791 s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID")) 792 } 793 794 return c.Status(http.StatusNoContent).SendString("") 795 } 796 } 797 798 func (s TestkubeAPI) AbortTestSuiteExecutionHandler() fiber.Handler { 799 return func(c *fiber.Ctx) error { 800 s.Log.Infow("aborting test suite execution", "executionID", c.Params("executionID")) 801 id := c.Params("executionID") 802 errPrefix := fmt.Sprintf("failed to abort test suite execution %s", id) 803 execution, err := s.TestExecutionResults.Get(c.Context(), id) 804 if err == mongo.ErrNoDocuments { 805 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test suite with execution id/name %s not found", errPrefix, id)) 806 } 807 if err != nil { 808 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not abort test suite execution: %w", errPrefix, err)) 809 } 810 811 execution.Status = testkube.TestSuiteExecutionStatusAborting 812 813 err = s.eventsBus.PublishTopic(bus.InternalPublishTopic, testkube.NewEventEndTestSuiteAborted(&execution)) 814 815 if err != nil { 816 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not sent test suite abortion event: %w", errPrefix, err)) 817 } 818 s.Log.Infow("test suite execution aborted, event sent", "executionID", c.Params("executionID")) 819 820 return c.Status(http.StatusNoContent).SendString("") 821 } 822 } 823 824 // ListTestSuiteTestsHandler for getting list of all available Tests for TestSuites 825 func (s TestkubeAPI) ListTestSuiteTestsHandler() fiber.Handler { 826 return func(c *fiber.Ctx) error { 827 name := c.Params("id") 828 errPrefix := fmt.Sprintf("failed to list tests for test suite %s", name) 829 crTestSuite, err := s.TestsSuitesClient.Get(name) 830 if err != nil { 831 if errors.IsNotFound(err) { 832 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite with id/name %s not found", errPrefix, name)) 833 } 834 835 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get test suite: %w", errPrefix, err)) 836 } 837 838 testSuite := testsuitesmapper.MapCRToAPI(*crTestSuite) 839 crTests, err := s.TestsClient.ListByNames(testSuite.GetTestNames()) 840 if err != nil { 841 if errors.IsNotFound(err) { 842 return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: test suite tests with id/name %s not found", errPrefix, testSuite.GetTestNames())) 843 } 844 845 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could get tests for test suite: %w", errPrefix, err)) 846 } 847 848 return c.JSON(testsmapper.MapTestArrayKubeToAPI(crTests)) 849 } 850 } 851 852 func getExecutionsFilterFromRequest(c *fiber.Ctx) testresult.Filter { 853 854 filter := testresult.NewExecutionsFilter() 855 name := c.Query("id", "") 856 if name != "" { 857 filter = filter.WithName(name) 858 } 859 860 textSearch := c.Query("textSearch", "") 861 if textSearch != "" { 862 filter = filter.WithTextSearch(textSearch) 863 } 864 865 page, err := strconv.Atoi(c.Query("page", "")) 866 if err == nil { 867 filter = filter.WithPage(page) 868 } 869 870 pageSize, err := strconv.Atoi(c.Query("pageSize", "")) 871 if err == nil && pageSize != 0 { 872 filter = filter.WithPageSize(pageSize) 873 } 874 875 status := c.Query("status", "") 876 if status != "" { 877 filter = filter.WithStatus(status) 878 } 879 880 last, err := strconv.Atoi(c.Query("last", "0")) 881 if err == nil && last != 0 { 882 filter = filter.WithLastNDays(last) 883 } 884 885 dFilter := datefilter.NewDateFilter(c.Query("startDate", ""), c.Query("endDate", "")) 886 if dFilter.IsStartValid { 887 filter = filter.WithStartDate(dFilter.Start) 888 } 889 890 if dFilter.IsEndValid { 891 filter = filter.WithEndDate(dFilter.End) 892 } 893 894 selector := c.Query("selector") 895 if selector != "" { 896 filter = filter.WithSelector(selector) 897 } 898 899 return filter 900 }