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 }