github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/executions.go (about) 1 package v1 2 3 import ( 4 "bufio" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "os" 12 "strconv" 13 "strings" 14 15 "k8s.io/apimachinery/pkg/api/errors" 16 17 "github.com/kubeshop/testkube/pkg/logs/events" 18 "github.com/kubeshop/testkube/pkg/repository/result" 19 20 "github.com/gofiber/fiber/v2" 21 "github.com/gofiber/websocket/v2" 22 "go.mongodb.org/mongo-driver/mongo" 23 24 testsv3 "github.com/kubeshop/testkube-operator/api/tests/v3" 25 "github.com/kubeshop/testkube/internal/common" 26 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 27 "github.com/kubeshop/testkube/pkg/executor/client" 28 "github.com/kubeshop/testkube/pkg/executor/output" 29 "github.com/kubeshop/testkube/pkg/scheduler" 30 "github.com/kubeshop/testkube/pkg/storage" 31 "github.com/kubeshop/testkube/pkg/storage/minio" 32 "github.com/kubeshop/testkube/pkg/types" 33 "github.com/kubeshop/testkube/pkg/workerpool" 34 ) 35 36 const ( 37 // latestExecutionNo defines the number of relevant latest executions 38 latestExecutions = 5 39 40 containerType = "container" 41 ) 42 43 // ExecuteTestsHandler calls particular executor based on execution request content and type 44 func (s *TestkubeAPI) ExecuteTestsHandler() fiber.Handler { 45 return func(c *fiber.Ctx) error { 46 ctx := c.Context() 47 errPrefix := "failed to execute test" 48 49 var request testkube.ExecutionRequest 50 err := c.BodyParser(&request) 51 if err != nil { 52 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: test request body invalid: %w", errPrefix, err)) 53 } 54 55 id := c.Params("id") 56 57 var tests []testsv3.Test 58 if id != "" { 59 test, err := s.TestsClient.Get(id) 60 if err != nil { 61 if errors.IsNotFound(err) { 62 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: client found no test: %w", errPrefix, err)) 63 } 64 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: can't get test: %w", errPrefix, err)) 65 } 66 67 tests = append(tests, *test) 68 } else { 69 testList, err := s.TestsClient.List(c.Query("selector")) 70 if err != nil { 71 return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: can't get tests: %w", errPrefix, err)) 72 } 73 74 tests = append(tests, testList.Items...) 75 } 76 77 l := s.Log.With("testID", id) 78 79 if len(tests) != 0 { 80 l.Infow("executing test", "test", tests[0]) 81 } 82 var results []testkube.Execution 83 if len(tests) != 0 { 84 request.TestExecutionName = strings.Clone(c.Query("testExecutionName")) 85 concurrencyLevel, err := strconv.Atoi(c.Query("concurrency", strconv.Itoa(scheduler.DefaultConcurrencyLevel))) 86 if err != nil { 87 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: can't detect concurrency level: %w", errPrefix, err)) 88 } 89 90 workerpoolService := workerpool.New[testkube.Test, testkube.ExecutionRequest, testkube.Execution](concurrencyLevel) 91 92 go workerpoolService.SendRequests(s.scheduler.PrepareTestRequests(tests, request)) 93 go workerpoolService.Run(ctx) 94 95 for r := range workerpoolService.GetResponses() { 96 results = append(results, r.Result) 97 } 98 } 99 100 if id != "" && len(results) != 0 { 101 if results[0].ExecutionResult.IsFailed() { 102 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: execution failed: %s", errPrefix, results[0].ExecutionResult.ErrorMessage)) 103 } 104 105 c.Status(http.StatusCreated) 106 return c.JSON(results[0]) 107 } 108 109 c.Status(http.StatusCreated) 110 return c.JSON(results) 111 } 112 } 113 114 // ListExecutionsHandler returns array of available test executions 115 func (s *TestkubeAPI) ListExecutionsHandler() fiber.Handler { 116 return func(c *fiber.Ctx) error { 117 errPrefix := "failed to list executions" 118 // TODO refactor into some Services (based on some abstraction for CRDs at least / CRUD) 119 // should we split this to separate endpoint? currently this one handles 120 // endpoints from /executions and from /tests/{id}/executions 121 // or should id be a query string as it's some kind of filter? 122 123 filter := getFilterFromRequest(c) 124 125 executions, err := s.ExecutionResults.GetExecutions(c.Context(), filter) 126 if err != nil { 127 if err == mongo.ErrNoDocuments { 128 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: db found no execution results: %w", errPrefix, err)) 129 } 130 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db client failed to get execution results: %w", errPrefix, err)) 131 } 132 133 executionTotals, err := s.ExecutionResults.GetExecutionTotals(c.Context(), false, filter) 134 if err != nil { 135 if err == mongo.ErrNoDocuments { 136 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: db client found no total execution results: %w", errPrefix, err)) 137 } 138 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db client failed to get total execution results: %w", errPrefix, err)) 139 } 140 141 filteredTotals, err := s.ExecutionResults.GetExecutionTotals(c.Context(), true, filter) 142 if err != nil { 143 if err == mongo.ErrNoDocuments { 144 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: db found no total filtered execution results: %w", errPrefix, err)) 145 } 146 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db client failed to get total filtered execution results: %w", errPrefix, err)) 147 } 148 results := testkube.ExecutionsResult{ 149 Totals: &executionTotals, 150 Filtered: &filteredTotals, 151 Results: mapExecutionsToExecutionSummary(executions), 152 } 153 154 return c.JSON(results) 155 } 156 } 157 158 func (s *TestkubeAPI) GetLogsStream(ctx context.Context, executionID string) (chan output.Output, error) { 159 execution, err := s.ExecutionResults.Get(ctx, executionID) 160 if err != nil { 161 return nil, fmt.Errorf("can't find execution %s: %w", executionID, err) 162 } 163 executor, err := s.getExecutorByTestType(execution.TestType) 164 if err != nil { 165 return nil, fmt.Errorf("can't get executor for test type %s: %w", execution.TestType, err) 166 } 167 168 logs, err := executor.Logs(ctx, executionID, execution.TestNamespace) 169 if err != nil { 170 return nil, fmt.Errorf("can't get executor logs: %w", err) 171 } 172 173 return logs, nil 174 } 175 176 func (s *TestkubeAPI) ExecutionLogsStreamHandler() fiber.Handler { 177 return websocket.New(func(c *websocket.Conn) { 178 if s.featureFlags.LogsV2 { 179 return 180 } 181 182 executionID := c.Params("executionID") 183 l := s.Log.With("executionID", executionID) 184 185 l.Debugw("getting pod logs and passing to websocket", "id", c.Params("id"), "locals", c.Locals, "remoteAddr", c.RemoteAddr(), "localAddr", c.LocalAddr()) 186 187 defer c.Conn.Close() 188 189 logs, err := s.GetLogsStream(context.Background(), executionID) 190 if err != nil { 191 l.Errorw("can't get pod logs", "error", err) 192 return 193 } 194 for logLine := range logs { 195 l.Debugw("sending log line to websocket", "line", logLine) 196 _ = c.WriteJSON(logLine) 197 } 198 }) 199 } 200 201 func (s *TestkubeAPI) ExecutionLogsStreamHandlerV2() fiber.Handler { 202 return websocket.New(func(c *websocket.Conn) { 203 if !s.featureFlags.LogsV2 { 204 return 205 } 206 207 executionID := c.Params("executionID") 208 l := s.Log.With("executionID", executionID) 209 210 l.Debugw("getting logs from grpc log server and passing to websocket", 211 "id", c.Params("id"), "locals", c.Locals, "remoteAddr", c.RemoteAddr(), "localAddr", c.LocalAddr()) 212 213 defer c.Conn.Close() 214 215 logs, err := s.logGrpcClient.Get(context.Background(), executionID) 216 if err != nil { 217 l.Errorw("can't get logs fom grpc", "error", err) 218 return 219 } 220 221 for logLine := range logs { 222 if logLine.Error != nil { 223 l.Errorw("can't get log line", "error", logLine.Error) 224 continue 225 } 226 227 l.Debugw("sending log line to websocket", "line", logLine.Log) 228 _ = c.WriteJSON(logLine.Log) 229 } 230 231 l.Debug("stream stopped in v2 logs handler") 232 }) 233 } 234 235 // ExecutionLogsHandler streams the logs from a test execution 236 func (s *TestkubeAPI) ExecutionLogsHandler() fiber.Handler { 237 return func(c *fiber.Ctx) error { 238 if s.featureFlags.LogsV2 { 239 return nil 240 } 241 242 executionID := c.Params("executionID") 243 244 s.Log.Debug("getting logs", "executionID", executionID) 245 246 ctx := c.Context() 247 248 ctx.SetContentType("text/event-stream") 249 ctx.Response.Header.Set("Cache-Control", "no-cache") 250 ctx.Response.Header.Set("Connection", "keep-alive") 251 ctx.Response.Header.Set("Transfer-Encoding", "chunked") 252 253 ctx.SetBodyStreamWriter(func(w *bufio.Writer) { 254 s.Log.Debug("start streaming logs") 255 _ = w.Flush() 256 257 execution, err := s.ExecutionResults.Get(ctx, executionID) 258 if err != nil { 259 output.PrintError(os.Stdout, fmt.Errorf("could not get execution result for ID %s: %w", executionID, err)) 260 s.Log.Errorw("getting execution error", "error", err) 261 _ = w.Flush() 262 return 263 } 264 265 if execution.ExecutionResult.IsCompleted() { 266 err := s.streamLogsFromResult(execution.ExecutionResult, w) 267 if err != nil { 268 output.PrintError(os.Stdout, fmt.Errorf("could not get execution result for ID %s: %w", executionID, err)) 269 s.Log.Errorw("getting execution error", "error", err) 270 _ = w.Flush() 271 } 272 return 273 } 274 275 s.streamLogsFromJob(ctx, executionID, execution.TestType, execution.TestNamespace, w) 276 }) 277 278 return nil 279 } 280 } 281 282 // ExecutionLogsHandlerV2 streams the logs from a test execution version 2 283 func (s *TestkubeAPI) ExecutionLogsHandlerV2() fiber.Handler { 284 return func(c *fiber.Ctx) error { 285 if !s.featureFlags.LogsV2 { 286 return nil 287 } 288 289 executionID := c.Params("executionID") 290 291 s.Log.Debugw("getting logs", "executionID", executionID) 292 293 ctx := c.Context() 294 295 ctx.SetContentType("text/event-stream") 296 ctx.Response.Header.Set("Cache-Control", "no-cache") 297 ctx.Response.Header.Set("Connection", "keep-alive") 298 ctx.Response.Header.Set("Transfer-Encoding", "chunked") 299 300 ctx.SetBodyStreamWriter(func(w *bufio.Writer) { 301 s.Log.Debug("start streaming logs") 302 _ = w.Flush() 303 304 s.Log.Infow("getting logs from grpc log server") 305 logs, err := s.logGrpcClient.Get(ctx, executionID) 306 if err != nil { 307 s.Log.Errorw("can't get logs from grpc", "error", err) 308 return 309 } 310 311 s.streamLogsFromLogServer(logs, w) 312 }) 313 314 return nil 315 } 316 } 317 318 // GetExecutionHandler returns test execution object for given test and execution id/name 319 func (s *TestkubeAPI) GetExecutionHandler() fiber.Handler { 320 return func(c *fiber.Ctx) error { 321 ctx := c.Context() 322 id := c.Params("id", "") 323 executionID := c.Params("executionID") 324 325 var execution testkube.Execution 326 var err error 327 328 if id == "" { 329 execution, err = s.ExecutionResults.Get(ctx, executionID) 330 if err == mongo.ErrNoDocuments { 331 return s.Error(c, http.StatusNotFound, fmt.Errorf("execution %s not found (test:%s)", executionID, id)) 332 } 333 if err != nil { 334 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("db client was unable to get execution %s (test:%s): %w", executionID, id, err)) 335 } 336 } else { 337 execution, err = s.ExecutionResults.GetByNameAndTest(ctx, executionID, id) 338 if err == mongo.ErrNoDocuments { 339 return s.Error(c, http.StatusNotFound, fmt.Errorf("test %s not found for execution %s", id, executionID)) 340 } 341 if err != nil { 342 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("can't get test (%s) for execution %s: %w", id, executionID, err)) 343 } 344 } 345 346 execution.Duration = types.FormatDuration(execution.Duration) 347 348 testSecretMap := make(map[string]string) 349 if execution.TestSecretUUID != "" { 350 testSecretMap, err = s.TestsClient.GetSecretTestVars(execution.TestName, execution.TestSecretUUID) 351 if err != nil { 352 return s.Error(c, http.StatusBadGateway, fmt.Errorf("client was unable to get test secrets: %w", err)) 353 } 354 } 355 356 testSuiteSecretMap := make(map[string]string) 357 if execution.TestSuiteSecretUUID != "" { 358 testSuiteSecretMap, err = s.TestsSuitesClient.GetSecretTestSuiteVars(execution.TestSuiteName, execution.TestSuiteSecretUUID) 359 if err != nil { 360 return s.Error(c, http.StatusBadGateway, fmt.Errorf("client was unable to get test suite secrets: %w", err)) 361 } 362 } 363 364 for key, value := range testSuiteSecretMap { 365 testSecretMap[key] = value 366 } 367 368 for key, value := range testSecretMap { 369 if variable, ok := execution.Variables[key]; ok && value != "" { 370 variable.Value = value 371 variable.SecretRef = nil 372 execution.Variables[key] = variable 373 } 374 } 375 376 s.Log.Debugw("get test execution request - debug", "execution", execution) 377 378 return c.JSON(execution) 379 } 380 } 381 382 func (s *TestkubeAPI) AbortExecutionHandler() fiber.Handler { 383 return func(c *fiber.Ctx) error { 384 ctx := c.Context() 385 executionID := c.Params("executionID") 386 errPrefix := "failed to abort execution %s" 387 388 s.Log.Infow("aborting execution", "executionID", executionID) 389 execution, err := s.ExecutionResults.Get(ctx, executionID) 390 if err != nil { 391 if err == mongo.ErrNoDocuments { 392 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test with execution id %s not found", errPrefix, executionID)) 393 } 394 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get test %v", errPrefix, err)) 395 } 396 397 res, err := s.Executor.Abort(ctx, &execution) 398 if err != nil { 399 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not abort execution: %v", errPrefix, err)) 400 } 401 s.Metrics.IncAbortTest(execution.TestType, res.IsFailed()) 402 403 return c.JSON(res) 404 } 405 } 406 407 func (s *TestkubeAPI) GetArtifactHandler() fiber.Handler { 408 return func(c *fiber.Ctx) error { 409 executionID := c.Params("executionID") 410 fileName := c.Params("filename") 411 errPrefix := fmt.Sprintf("failed to get artifact %s for execution %s", fileName, executionID) 412 413 // TODO fix this someday :) we don't know 15 mins before release why it's working this way 414 // remember about CLI client and Dashboard client too! 415 unescaped, err := url.QueryUnescape(fileName) 416 if err == nil { 417 fileName = unescaped 418 } 419 420 unescaped, err = url.QueryUnescape(fileName) 421 if err == nil { 422 fileName = unescaped 423 } 424 425 //// quickfix end 426 427 execution, err := s.ExecutionResults.Get(c.Context(), executionID) 428 if err == mongo.ErrNoDocuments { 429 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test with execution id/name %s not found", errPrefix, executionID)) 430 } 431 if err != nil { 432 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db could not get execution result: %w", errPrefix, err)) 433 } 434 435 var file io.Reader 436 var bucket string 437 artifactsStorage := s.ArtifactsStorage 438 folder := execution.Id 439 if execution.ArtifactRequest != nil { 440 bucket = execution.ArtifactRequest.StorageBucket 441 if execution.ArtifactRequest.OmitFolderPerExecution { 442 folder = "" 443 } 444 } 445 446 if bucket != "" { 447 artifactsStorage, err = s.getArtifactStorage(bucket) 448 if err != nil { 449 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get artifact storage: %w", errPrefix, err)) 450 } 451 } 452 453 file, err = artifactsStorage.DownloadFile(c.Context(), fileName, folder, execution.TestName, execution.TestSuiteName, "") 454 if err != nil { 455 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not download file: %w", errPrefix, err)) 456 } 457 458 // SendStream promises to close file using io.Close() method 459 return c.SendStream(file) 460 } 461 } 462 463 // GetArtifactArchiveHandler returns artifact archive 464 func (s *TestkubeAPI) GetArtifactArchiveHandler() fiber.Handler { 465 return func(c *fiber.Ctx) error { 466 executionID := c.Params("executionID") 467 query := c.Request().URI().QueryString() 468 errPrefix := fmt.Sprintf("failed to get artifact archive for execution %s", executionID) 469 470 values, err := url.ParseQuery(string(query)) 471 if err != nil { 472 return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse query string: %w", errPrefix, err)) 473 } 474 475 execution, err := s.ExecutionResults.Get(c.Context(), executionID) 476 if err == mongo.ErrNoDocuments { 477 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test with execution id/name %s not found", errPrefix, executionID)) 478 } 479 if err != nil { 480 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db could not get execution result: %w", errPrefix, err)) 481 } 482 483 var archive io.Reader 484 var bucket string 485 artifactsStorage := s.ArtifactsStorage 486 folder := execution.Id 487 if execution.ArtifactRequest != nil { 488 bucket = execution.ArtifactRequest.StorageBucket 489 if execution.ArtifactRequest.OmitFolderPerExecution { 490 folder = "" 491 } 492 } 493 494 if bucket != "" { 495 artifactsStorage, err = s.getArtifactStorage(bucket) 496 if err != nil { 497 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get artifact storage: %w", errPrefix, err)) 498 } 499 } 500 501 archive, err = artifactsStorage.DownloadArchive(c.Context(), folder, values["mask"]) 502 if err != nil { 503 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not download artifact archive: %w", errPrefix, err)) 504 } 505 506 // SendStream promises to close archive using io.Close() method 507 return c.SendStream(archive) 508 } 509 } 510 511 // ListArtifactsHandler returns list of files in the given bucket 512 func (s *TestkubeAPI) ListArtifactsHandler() fiber.Handler { 513 return func(c *fiber.Ctx) error { 514 515 executionID := c.Params("executionID") 516 errPrefix := fmt.Sprintf("failed to list artifacts for execution %s", executionID) 517 518 execution, err := s.ExecutionResults.Get(c.Context(), executionID) 519 if err == mongo.ErrNoDocuments { 520 return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: test with execution id/name %s not found", errPrefix, executionID)) 521 } 522 if err != nil { 523 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: db could not get test with execution: %s", errPrefix, err)) 524 } 525 526 var files []testkube.Artifact 527 var bucket string 528 artifactsStorage := s.ArtifactsStorage 529 folder := execution.Id 530 if execution.ArtifactRequest != nil { 531 bucket = execution.ArtifactRequest.StorageBucket 532 if execution.ArtifactRequest.OmitFolderPerExecution { 533 folder = "" 534 } 535 } 536 537 if bucket != "" { 538 artifactsStorage, err = s.getArtifactStorage(bucket) 539 if err != nil { 540 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: could not get artifact storage: %w", errPrefix, err)) 541 } 542 } 543 544 files, err = artifactsStorage.ListFiles(c.Context(), folder, execution.TestName, execution.TestSuiteName, "") 545 if err != nil { 546 return s.Error(c, http.StatusInternalServerError, fmt.Errorf("%s: storage client could not list files %w", errPrefix, err)) 547 } 548 549 return c.JSON(files) 550 } 551 } 552 553 // streamLogsFromResult writes logs from the output of executionResult to the writer 554 func (s *TestkubeAPI) streamLogsFromResult(executionResult *testkube.ExecutionResult, w *bufio.Writer) error { 555 enc := json.NewEncoder(w) 556 _, _ = fmt.Fprintf(w, "data: ") 557 s.Log.Debug("using logs from result") 558 output := testkube.ExecutorOutput{ 559 Type_: output.TypeResult, 560 Content: executionResult.Output, 561 Result: executionResult, 562 } 563 564 if executionResult.ErrorMessage != "" { 565 output.Content = output.Content + "\n" + executionResult.ErrorMessage 566 } 567 568 err := enc.Encode(output) 569 if err != nil { 570 s.Log.Infow("Encode", "error", err) 571 return err 572 } 573 _, _ = fmt.Fprintf(w, "\n") 574 _ = w.Flush() 575 return nil 576 } 577 578 // streamLogsFromJob streams logs in chunks to writer from the running execution 579 func (s *TestkubeAPI) streamLogsFromJob(ctx context.Context, executionID, testType, namespace string, w *bufio.Writer) { 580 enc := json.NewEncoder(w) 581 s.Log.Infow("getting logs from Kubernetes job") 582 583 executor, err := s.getExecutorByTestType(testType) 584 if err != nil { 585 output.PrintError(os.Stdout, err) 586 s.Log.Errorw("getting logs error", "error", err) 587 _ = w.Flush() 588 return 589 } 590 591 logs, err := executor.Logs(ctx, executionID, namespace) 592 s.Log.Debugw("waiting for jobs channel", "channelSize", len(logs)) 593 if err != nil { 594 output.PrintError(os.Stdout, err) 595 s.Log.Errorw("getting logs error", "error", err) 596 _ = w.Flush() 597 return 598 } 599 600 s.Log.Infow("looping through logs channel") 601 // loop through pods log lines - it's blocking channel 602 // and pass single log output as sse data chunk 603 for out := range logs { 604 s.Log.Debugw("got log line from pod", "out", out) 605 _, _ = fmt.Fprintf(w, "data: ") 606 err = enc.Encode(out) 607 if err != nil { 608 s.Log.Infow("Encode", "error", err) 609 } 610 // enc.Encode adds \n and we need \n\n after `data: {}` chunk 611 _, _ = fmt.Fprintf(w, "\n") 612 _ = w.Flush() 613 } 614 } 615 616 func mapExecutionsToExecutionSummary(executions []testkube.Execution) []testkube.ExecutionSummary { 617 res := make([]testkube.ExecutionSummary, len(executions)) 618 619 for i, execution := range executions { 620 res[i] = testkube.ExecutionSummary{ 621 Id: execution.Id, 622 Name: execution.Name, 623 Number: execution.Number, 624 TestName: execution.TestName, 625 TestType: execution.TestType, 626 Status: execution.ExecutionResult.Status, 627 StartTime: execution.StartTime, 628 EndTime: execution.EndTime, 629 Duration: types.FormatDuration(execution.Duration), 630 DurationMs: types.FormatDurationMs(execution.Duration), 631 Labels: execution.Labels, 632 } 633 } 634 635 return res 636 } 637 638 // GetLatestExecutionLogs returns the latest executions' logs 639 func (s *TestkubeAPI) GetLatestExecutionLogs(ctx context.Context) (map[string][]string, error) { 640 latestExecutions, err := s.getNewestExecutions(ctx) 641 if err != nil { 642 return nil, fmt.Errorf("could not list executions: %w", err) 643 } 644 645 executionLogs := map[string][]string{} 646 for _, e := range latestExecutions { 647 logs, err := s.getExecutionLogs(ctx, e) 648 if err != nil { 649 return nil, fmt.Errorf("could not get logs: %w", err) 650 } 651 executionLogs[e.Id] = logs 652 } 653 654 return executionLogs, nil 655 } 656 657 // getNewestExecutions returns the latest Testkube executions 658 func (s *TestkubeAPI) getNewestExecutions(ctx context.Context) ([]testkube.Execution, error) { 659 f := result.NewExecutionsFilter().WithPage(1).WithPageSize(latestExecutions) 660 executions, err := s.ExecutionResults.GetExecutions(ctx, f) 661 if err != nil { 662 return []testkube.Execution{}, fmt.Errorf("could not get executions from repo: %w", err) 663 } 664 return executions, nil 665 } 666 667 // getExecutionLogs returns logs from an execution 668 func (s *TestkubeAPI) getExecutionLogs(ctx context.Context, execution testkube.Execution) ([]string, error) { 669 var res []string 670 671 if s.featureFlags.LogsV2 { 672 logs, err := s.logGrpcClient.Get(ctx, execution.Id) 673 if err != nil { 674 return []string{}, fmt.Errorf("could not get logs for grpc %s: %w", execution.Id, err) 675 } 676 677 for out := range logs { 678 if out.Error != nil { 679 s.Log.Errorw("can't get log line", "error", out.Error) 680 continue 681 } 682 683 res = append(res, out.Log.Content) 684 } 685 686 return res, nil 687 } 688 689 if execution.ExecutionResult.IsCompleted() { 690 return append(res, execution.ExecutionResult.Output), nil 691 } 692 693 logs, err := s.Executor.Logs(ctx, execution.Id, execution.TestNamespace) 694 if err != nil { 695 return []string{}, fmt.Errorf("could not get logs for execution %s: %w", execution.Id, err) 696 } 697 698 for out := range logs { 699 res = append(res, out.Result.Output) 700 } 701 702 return res, nil 703 } 704 705 func (s *TestkubeAPI) getExecutorByTestType(testType string) (client.Executor, error) { 706 executorCR, err := s.ExecutorsClient.GetByType(testType) 707 if err != nil { 708 return nil, fmt.Errorf("can't get executor spec: %w", err) 709 } 710 switch executorCR.Spec.ExecutorType { 711 case containerType: 712 return s.ContainerExecutor, nil 713 default: 714 return s.Executor, nil 715 } 716 } 717 718 func (s *TestkubeAPI) getArtifactStorage(bucket string) (storage.ArtifactsStorage, error) { 719 if s.mode == common.ModeAgent { 720 return s.ArtifactsStorage, nil 721 } 722 723 opts := minio.GetTLSOptions(s.storageParams.SSL, s.storageParams.SkipVerify, s.storageParams.CertFile, s.storageParams.KeyFile, s.storageParams.CAFile) 724 minioClient := minio.NewClient( 725 s.storageParams.Endpoint, 726 s.storageParams.AccessKeyId, 727 s.storageParams.SecretAccessKey, 728 s.storageParams.Region, 729 s.storageParams.Token, 730 bucket, 731 opts..., 732 ) 733 if err := minioClient.Connect(); err != nil { 734 return nil, err 735 } 736 737 return minio.NewMinIOArtifactClient(minioClient), nil 738 } 739 740 // streamLogsFromLogServer writes logs from the output of log server to the writer 741 func (s *TestkubeAPI) streamLogsFromLogServer(logs chan events.LogResponse, w *bufio.Writer) { 742 enc := json.NewEncoder(w) 743 s.Log.Infow("looping through logs channel") 744 // loop through grpc server log lines - it's blocking channel 745 // and pass single log output as sse data chunk 746 for out := range logs { 747 if out.Error != nil { 748 s.Log.Errorw("can't get log line", "error", out.Error) 749 continue 750 } 751 752 s.Log.Debugw("got log line from grpc log server", "out", out.Log) 753 _, _ = fmt.Fprintf(w, "data: ") 754 err := enc.Encode(out.Log) 755 if err != nil { 756 s.Log.Infow("Encode", "error", err) 757 } 758 // enc.Encode adds \n and we need \n\n after `data: {}` chunk 759 _, _ = fmt.Fprintf(w, "\n") 760 _ = w.Flush() 761 } 762 763 s.Log.Debugw("logs streaming stopped") 764 }