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  }