github.com/kubeshop/testkube@v1.17.23/pkg/tcl/apitcl/v1/testworkflowexecutions.go (about)

     1  // Copyright 2024 Testkube.
     2  //
     3  // Licensed as a Testkube Pro file under the Testkube Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //	https://github.com/kubeshop/testkube/blob/main/licenses/TCL.txt
     8  
     9  package v1
    10  
    11  import (
    12  	"bufio"
    13  	"context"
    14  	"encoding/json"
    15  	"fmt"
    16  	"io"
    17  	"math"
    18  	"net/http"
    19  	"net/url"
    20  	"strconv"
    21  
    22  	"github.com/gofiber/fiber/v2"
    23  	"github.com/gofiber/websocket/v2"
    24  	"github.com/pkg/errors"
    25  
    26  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    27  	"github.com/kubeshop/testkube/pkg/datefilter"
    28  	"github.com/kubeshop/testkube/pkg/tcl/repositorytcl/testworkflow"
    29  	"github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowcontroller"
    30  )
    31  
    32  func (s *apiTCL) StreamTestWorkflowExecutionNotificationsHandler() fiber.Handler {
    33  	return func(c *fiber.Ctx) error {
    34  		ctx := c.Context()
    35  		id := c.Params("executionID")
    36  		errPrefix := fmt.Sprintf("failed to stream test workflow execution notifications '%s'", id)
    37  
    38  		// Fetch execution from database
    39  		execution, err := s.TestWorkflowResults.Get(ctx, id)
    40  		if err != nil {
    41  			return s.ClientError(c, errPrefix, err)
    42  		}
    43  
    44  		// Check for the logs
    45  		ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
    46  		if err != nil {
    47  			return s.BadRequest(c, errPrefix, "fetching job", err)
    48  		}
    49  
    50  		// Initiate processing event stream
    51  		ctx.SetContentType("text/event-stream")
    52  		ctx.Response.Header.Set("Cache-Control", "no-cache")
    53  		ctx.Response.Header.Set("Connection", "keep-alive")
    54  		ctx.Response.Header.Set("Transfer-Encoding", "chunked")
    55  
    56  		// Stream the notifications
    57  		ctx.SetBodyStreamWriter(func(w *bufio.Writer) {
    58  			_ = w.Flush()
    59  			enc := json.NewEncoder(w)
    60  
    61  			for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
    62  				if n.Error == nil {
    63  					_ = enc.Encode(n.Value)
    64  					_, _ = fmt.Fprintf(w, "\n")
    65  					_ = w.Flush()
    66  				}
    67  			}
    68  		})
    69  
    70  		return nil
    71  	}
    72  }
    73  
    74  func (s *apiTCL) StreamTestWorkflowExecutionNotificationsWebSocketHandler() fiber.Handler {
    75  	return websocket.New(func(c *websocket.Conn) {
    76  		ctx, ctxCancel := context.WithCancel(context.Background())
    77  		id := c.Params("executionID")
    78  
    79  		// Stop reading when the WebSocket connection is already closed
    80  		originalClose := c.CloseHandler()
    81  		c.SetCloseHandler(func(code int, text string) error {
    82  			ctxCancel()
    83  			return originalClose(code, text)
    84  		})
    85  		defer c.Conn.Close()
    86  
    87  		// Fetch execution from database
    88  		execution, err := s.TestWorkflowResults.Get(ctx, id)
    89  		if err != nil {
    90  			return
    91  		}
    92  
    93  		// Check for the logs TODO: Load from the database if possible
    94  		ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
    95  		if err != nil {
    96  			return
    97  		}
    98  
    99  		for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
   100  			if n.Error == nil {
   101  				_ = c.WriteJSON(n.Value)
   102  			}
   103  		}
   104  	})
   105  }
   106  
   107  func (s *apiTCL) ListTestWorkflowExecutionsHandler() fiber.Handler {
   108  	return func(c *fiber.Ctx) error {
   109  		errPrefix := "failed to list test workflow executions"
   110  
   111  		filter := getWorkflowExecutionsFilterFromRequest(c)
   112  
   113  		executions, err := s.TestWorkflowResults.GetExecutionsSummary(c.Context(), filter)
   114  		if err != nil {
   115  			return s.ClientError(c, errPrefix+": get execution results", err)
   116  		}
   117  
   118  		executionTotals, err := s.TestWorkflowResults.GetExecutionsTotals(c.Context(), testworkflow.NewExecutionsFilter().WithName(filter.Name()))
   119  		if err != nil {
   120  			return s.ClientError(c, errPrefix+": get totals", err)
   121  		}
   122  
   123  		filterTotals := *filter.(*testworkflow.FilterImpl)
   124  		filterTotals.WithPage(0).WithPageSize(math.MaxInt32)
   125  		filteredTotals, err := s.TestWorkflowResults.GetExecutionsTotals(c.Context(), filterTotals)
   126  		if err != nil {
   127  			return s.ClientError(c, errPrefix+": get filtered totals", err)
   128  		}
   129  
   130  		results := testkube.TestWorkflowExecutionsResult{
   131  			Totals:   &executionTotals,
   132  			Filtered: &filteredTotals,
   133  			Results:  executions,
   134  		}
   135  		return c.JSON(results)
   136  	}
   137  }
   138  
   139  func (s *apiTCL) GetTestWorkflowMetricsHandler() fiber.Handler {
   140  	return func(c *fiber.Ctx) error {
   141  		workflowName := c.Params("id")
   142  
   143  		const DefaultLimit = 0
   144  		limit, err := strconv.Atoi(c.Query("limit", strconv.Itoa(DefaultLimit)))
   145  		if err != nil {
   146  			limit = DefaultLimit
   147  		}
   148  
   149  		const DefaultLastDays = 7
   150  		last, err := strconv.Atoi(c.Query("last", strconv.Itoa(DefaultLastDays)))
   151  		if err != nil {
   152  			last = DefaultLastDays
   153  		}
   154  
   155  		metrics, err := s.TestWorkflowResults.GetTestWorkflowMetrics(c.Context(), workflowName, limit, last)
   156  		if err != nil {
   157  			return s.ClientError(c, "get metrics for workflow", err)
   158  		}
   159  
   160  		return c.JSON(metrics)
   161  	}
   162  }
   163  
   164  func (s *apiTCL) GetTestWorkflowExecutionHandler() fiber.Handler {
   165  	return func(c *fiber.Ctx) error {
   166  		ctx := c.Context()
   167  		id := c.Params("id", "")
   168  		executionID := c.Params("executionID")
   169  
   170  		var execution testkube.TestWorkflowExecution
   171  		var err error
   172  		if id == "" {
   173  			execution, err = s.TestWorkflowResults.Get(ctx, executionID)
   174  		} else {
   175  			execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, id)
   176  		}
   177  		if err != nil {
   178  			return s.ClientError(c, "get execution", err)
   179  		}
   180  
   181  		return c.JSON(execution)
   182  	}
   183  }
   184  
   185  func (s *apiTCL) GetTestWorkflowExecutionLogsHandler() fiber.Handler {
   186  	return func(c *fiber.Ctx) error {
   187  		ctx := c.Context()
   188  		id := c.Params("id", "")
   189  		executionID := c.Params("executionID")
   190  
   191  		var execution testkube.TestWorkflowExecution
   192  		var err error
   193  		if id == "" {
   194  			execution, err = s.TestWorkflowResults.Get(ctx, executionID)
   195  		} else {
   196  			execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, id)
   197  		}
   198  		if err != nil {
   199  			return s.ClientError(c, "get execution", err)
   200  		}
   201  
   202  		reader, err := s.TestWorkflowOutput.ReadLog(ctx, executionID, execution.Workflow.Name)
   203  		if err != nil {
   204  			return s.InternalError(c, "can't get log", executionID, err)
   205  		}
   206  
   207  		c.Context().SetContentType(mediaTypePlainText)
   208  		_, err = io.Copy(c.Response().BodyWriter(), reader)
   209  		return err
   210  	}
   211  }
   212  
   213  func (s *apiTCL) AbortTestWorkflowExecutionHandler() fiber.Handler {
   214  	return func(c *fiber.Ctx) error {
   215  		ctx := c.Context()
   216  		name := c.Params("id")
   217  		executionID := c.Params("executionID")
   218  		errPrefix := fmt.Sprintf("failed to abort test workflow execution '%s'", executionID)
   219  
   220  		var execution testkube.TestWorkflowExecution
   221  		var err error
   222  		if name == "" {
   223  			execution, err = s.TestWorkflowResults.Get(ctx, executionID)
   224  		} else {
   225  			execution, err = s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionID, name)
   226  		}
   227  		if err != nil {
   228  			return s.ClientError(c, errPrefix, err)
   229  		}
   230  
   231  		if execution.Result != nil && execution.Result.IsFinished() {
   232  			return s.BadRequest(c, errPrefix, "checking execution", errors.New("execution already finished"))
   233  		}
   234  
   235  		// Obtain the controller
   236  		ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
   237  		if err != nil {
   238  			return s.BadRequest(c, errPrefix, "fetching job", err)
   239  		}
   240  
   241  		// Abort the execution
   242  		err = ctrl.Abort(context.Background())
   243  		if err != nil {
   244  			return s.ClientError(c, "aborting test workflow execution", err)
   245  		}
   246  
   247  		c.Status(http.StatusNoContent)
   248  
   249  		return nil
   250  	}
   251  }
   252  
   253  func (s *apiTCL) AbortAllTestWorkflowExecutionsHandler() fiber.Handler {
   254  	return func(c *fiber.Ctx) error {
   255  		ctx := c.Context()
   256  		name := c.Params("id")
   257  		errPrefix := fmt.Sprintf("failed to abort test workflow executions '%s'", name)
   258  
   259  		// Fetch executions
   260  		filter := testworkflow.NewExecutionsFilter().WithName(name).WithStatus(string(testkube.RUNNING_TestWorkflowStatus))
   261  		executions, err := s.TestWorkflowResults.GetExecutions(ctx, filter)
   262  		if err != nil {
   263  			if IsNotFound(err) {
   264  				c.Status(http.StatusNoContent)
   265  				return nil
   266  			}
   267  			return s.ClientError(c, errPrefix, err)
   268  		}
   269  
   270  		for _, execution := range executions {
   271  			// Obtain the controller
   272  			ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
   273  			if err != nil {
   274  				return s.BadRequest(c, errPrefix, "fetching job", err)
   275  			}
   276  
   277  			// Abort the execution
   278  			err = ctrl.Abort(context.Background())
   279  			if err != nil {
   280  				return s.ClientError(c, errPrefix, err)
   281  			}
   282  		}
   283  
   284  		c.Status(http.StatusNoContent)
   285  
   286  		return nil
   287  	}
   288  }
   289  
   290  func (s *apiTCL) ListTestWorkflowExecutionArtifactsHandler() fiber.Handler {
   291  	return func(c *fiber.Ctx) error {
   292  		executionID := c.Params("executionID")
   293  		errPrefix := fmt.Sprintf("failed to list artifacts for test workflow execution %s", executionID)
   294  
   295  		execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
   296  		if err != nil {
   297  			return s.ClientError(c, errPrefix, err)
   298  		}
   299  
   300  		files, err := s.ArtifactsStorage.ListFiles(c.Context(), execution.Id, "", "", execution.Workflow.Name)
   301  		if err != nil {
   302  			return s.InternalError(c, errPrefix, "storage client could not list test workflow files", err)
   303  		}
   304  
   305  		return c.JSON(files)
   306  	}
   307  }
   308  
   309  func (s *apiTCL) GetTestWorkflowArtifactHandler() fiber.Handler {
   310  	return func(c *fiber.Ctx) error {
   311  		executionID := c.Params("executionID")
   312  		fileName := c.Params("filename")
   313  		errPrefix := fmt.Sprintf("failed to get artifact %s for workflow execution %s", fileName, executionID)
   314  
   315  		// TODO fix this someday :) we don't know 15 mins before release why it's working this way
   316  		// remember about CLI client and Dashboard client too!
   317  		unescaped, err := url.QueryUnescape(fileName)
   318  		if err == nil {
   319  			fileName = unescaped
   320  		}
   321  		unescaped, err = url.QueryUnescape(fileName)
   322  		if err == nil {
   323  			fileName = unescaped
   324  		}
   325  		//// quickfix end
   326  
   327  		execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
   328  		if err != nil {
   329  			return s.ClientError(c, errPrefix, err)
   330  		}
   331  
   332  		file, err := s.ArtifactsStorage.DownloadFile(c.Context(), fileName, execution.Id, "", "", execution.Workflow.Name)
   333  		if err != nil {
   334  			return s.InternalError(c, errPrefix, "could not download file", err)
   335  		}
   336  
   337  		return c.SendStream(file)
   338  	}
   339  }
   340  
   341  func (s *apiTCL) GetTestWorkflowArtifactArchiveHandler() fiber.Handler {
   342  	return func(c *fiber.Ctx) error {
   343  		executionID := c.Params("executionID")
   344  		query := c.Request().URI().QueryString()
   345  		errPrefix := fmt.Sprintf("failed to get artifact archive for test workflow execution %s", executionID)
   346  
   347  		values, err := url.ParseQuery(string(query))
   348  		if err != nil {
   349  			return s.BadRequest(c, errPrefix, "could not parse query string", err)
   350  		}
   351  
   352  		execution, err := s.TestWorkflowResults.Get(c.Context(), executionID)
   353  		if err != nil {
   354  			return s.ClientError(c, errPrefix, err)
   355  		}
   356  
   357  		archive, err := s.ArtifactsStorage.DownloadArchive(c.Context(), execution.Id, values["mask"])
   358  		if err != nil {
   359  			return s.InternalError(c, errPrefix, "could not download workflow artifact archive", err)
   360  		}
   361  
   362  		return c.SendStream(archive)
   363  	}
   364  }
   365  
   366  func (s *apiTCL) GetTestWorkflowNotificationsStream(ctx context.Context, executionID string) (chan testkube.TestWorkflowExecutionNotification, error) {
   367  	// Load the execution
   368  	execution, err := s.TestWorkflowResults.Get(ctx, executionID)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	// Check for the logs
   374  	ctrl, err := testworkflowcontroller.New(ctx, s.Clientset, s.Namespace, execution.Id, execution.ScheduledAt)
   375  	if err != nil {
   376  		return nil, err
   377  	}
   378  
   379  	// Stream the notifications
   380  	ch := make(chan testkube.TestWorkflowExecutionNotification)
   381  	go func() {
   382  		for n := range ctrl.Watch(ctx).Stream(ctx).Channel() {
   383  			if n.Error == nil {
   384  				ch <- n.Value.ToInternal()
   385  			}
   386  		}
   387  		close(ch)
   388  	}()
   389  	return ch, nil
   390  }
   391  
   392  func getWorkflowExecutionsFilterFromRequest(c *fiber.Ctx) testworkflow.Filter {
   393  	filter := testworkflow.NewExecutionsFilter()
   394  	name := c.Params("id", "")
   395  	if name != "" {
   396  		filter = filter.WithName(name)
   397  	}
   398  
   399  	textSearch := c.Query("textSearch", "")
   400  	if textSearch != "" {
   401  		filter = filter.WithTextSearch(textSearch)
   402  	}
   403  
   404  	page, err := strconv.Atoi(c.Query("page", ""))
   405  	if err == nil {
   406  		filter = filter.WithPage(page)
   407  	}
   408  
   409  	pageSize, err := strconv.Atoi(c.Query("pageSize", ""))
   410  	if err == nil && pageSize != 0 {
   411  		filter = filter.WithPageSize(pageSize)
   412  	}
   413  
   414  	status := c.Query("status", "")
   415  	if status != "" {
   416  		filter = filter.WithStatus(status)
   417  	}
   418  
   419  	last, err := strconv.Atoi(c.Query("last", "0"))
   420  	if err == nil && last != 0 {
   421  		filter = filter.WithLastNDays(last)
   422  	}
   423  
   424  	dFilter := datefilter.NewDateFilter(c.Query("startDate", ""), c.Query("endDate", ""))
   425  	if dFilter.IsStartValid {
   426  		filter = filter.WithStartDate(dFilter.Start)
   427  	}
   428  
   429  	if dFilter.IsEndValid {
   430  		filter = filter.WithEndDate(dFilter.End)
   431  	}
   432  
   433  	selector := c.Query("selector")
   434  	if selector != "" {
   435  		filter = filter.WithSelector(selector)
   436  	}
   437  
   438  	return filter
   439  }