github.com/kubeshop/testkube@v1.17.23/pkg/tcl/apitcl/v1/testworkflows.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 "context" 13 "fmt" 14 "net/http" 15 "os" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/gofiber/fiber/v2" 21 "github.com/pkg/errors" 22 "go.mongodb.org/mongo-driver/bson/primitive" 23 "sigs.k8s.io/kustomize/kyaml/yaml" 24 25 testworkflowsv1 "github.com/kubeshop/testkube-operator/api/testworkflows/v1" 26 "github.com/kubeshop/testkube/internal/common" 27 "github.com/kubeshop/testkube/pkg/api/v1/testkube" 28 "github.com/kubeshop/testkube/pkg/tcl/expressionstcl" 29 testworkflowmappers "github.com/kubeshop/testkube/pkg/tcl/mapperstcl/testworkflows" 30 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor" 31 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowprocessor/constants" 32 "github.com/kubeshop/testkube/pkg/tcl/testworkflowstcl/testworkflowresolver" 33 ) 34 35 func (s *apiTCL) ListTestWorkflowsHandler() fiber.Handler { 36 errPrefix := "failed to list test workflows" 37 return func(c *fiber.Ctx) (err error) { 38 workflows, err := s.getFilteredTestWorkflowList(c) 39 if err != nil { 40 return s.BadGateway(c, errPrefix, "client problem", err) 41 } 42 err = SendResourceList(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapTestWorkflowKubeToAPI, workflows.Items...) 43 if err != nil { 44 return s.InternalError(c, errPrefix, "serialization problem", err) 45 } 46 return 47 } 48 } 49 50 func (s *apiTCL) GetTestWorkflowHandler() fiber.Handler { 51 return func(c *fiber.Ctx) (err error) { 52 name := c.Params("id") 53 errPrefix := fmt.Sprintf("failed to get test workflow '%s'", name) 54 workflow, err := s.TestWorkflowsClient.Get(name) 55 if err != nil { 56 return s.ClientError(c, errPrefix, err) 57 } 58 err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, workflow) 59 if err != nil { 60 return s.InternalError(c, errPrefix, "serialization problem", err) 61 } 62 return 63 } 64 } 65 66 func (s *apiTCL) DeleteTestWorkflowHandler() fiber.Handler { 67 return func(c *fiber.Ctx) error { 68 name := c.Params("id") 69 errPrefix := fmt.Sprintf("failed to delete test workflow '%s'", name) 70 err := s.TestWorkflowsClient.Delete(name) 71 s.Metrics.IncDeleteTestWorkflow(err) 72 if err != nil { 73 return s.ClientError(c, errPrefix, err) 74 } 75 skipExecutions := c.Query("skipDeleteExecutions", "") 76 if skipExecutions != "true" { 77 err = s.TestWorkflowResults.DeleteByTestWorkflow(context.Background(), name) 78 if err != nil { 79 return s.ClientError(c, "deleting executions", err) 80 } 81 } 82 return c.SendStatus(http.StatusNoContent) 83 } 84 } 85 86 func (s *apiTCL) DeleteTestWorkflowsHandler() fiber.Handler { 87 errPrefix := "failed to delete test workflows" 88 return func(c *fiber.Ctx) error { 89 selector := c.Query("selector") 90 workflows, err := s.TestWorkflowsClient.List(selector) 91 if err != nil { 92 return s.BadGateway(c, errPrefix, "client problem", err) 93 } 94 95 // Delete 96 err = s.TestWorkflowsClient.DeleteByLabels(selector) 97 if err != nil { 98 return s.ClientError(c, errPrefix, err) 99 } 100 101 // Mark as deleted 102 for range workflows.Items { 103 s.Metrics.IncDeleteTestWorkflow(err) 104 } 105 106 // Delete the executions 107 skipExecutions := c.Query("skipDeleteExecutions", "") 108 if skipExecutions != "true" { 109 names := common.MapSlice(workflows.Items, func(t testworkflowsv1.TestWorkflow) string { 110 return t.Name 111 }) 112 err = s.TestWorkflowResults.DeleteByTestWorkflows(context.Background(), names) 113 if err != nil { 114 return s.ClientError(c, "deleting executions", err) 115 } 116 } 117 118 return c.SendStatus(http.StatusNoContent) 119 } 120 } 121 122 func (s *apiTCL) CreateTestWorkflowHandler() fiber.Handler { 123 errPrefix := "failed to create test workflow" 124 return func(c *fiber.Ctx) (err error) { 125 // Deserialize resource 126 obj := new(testworkflowsv1.TestWorkflow) 127 if HasYAML(c) { 128 err = common.DeserializeCRD(obj, c.Body()) 129 if err != nil { 130 return s.BadRequest(c, errPrefix, "invalid body", err) 131 } 132 } else { 133 var v *testkube.TestWorkflow 134 err = c.BodyParser(&v) 135 if err != nil { 136 return s.BadRequest(c, errPrefix, "invalid body", err) 137 } 138 obj = testworkflowmappers.MapAPIToKube(v) 139 } 140 141 // Validate resource 142 if obj == nil || obj.Name == "" { 143 return s.BadRequest(c, errPrefix, "invalid body", errors.New("name is required")) 144 } 145 obj.Namespace = s.Namespace 146 147 // Create the resource 148 obj, err = s.TestWorkflowsClient.Create(obj) 149 s.Metrics.IncCreateTestWorkflow(err) 150 if err != nil { 151 return s.BadRequest(c, errPrefix, "client error", err) 152 } 153 s.sendCreateWorkflowTelemetry(c.Context(), obj) 154 155 err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj) 156 if err != nil { 157 return s.InternalError(c, errPrefix, "serialization problem", err) 158 } 159 return 160 } 161 } 162 163 func (s *apiTCL) UpdateTestWorkflowHandler() fiber.Handler { 164 errPrefix := "failed to update test workflow" 165 return func(c *fiber.Ctx) (err error) { 166 name := c.Params("id") 167 168 // Deserialize resource 169 obj := new(testworkflowsv1.TestWorkflow) 170 if HasYAML(c) { 171 err = common.DeserializeCRD(obj, c.Body()) 172 if err != nil { 173 return s.BadRequest(c, errPrefix, "invalid body", err) 174 } 175 } else { 176 var v *testkube.TestWorkflow 177 err = c.BodyParser(&v) 178 if err != nil { 179 return s.BadRequest(c, errPrefix, "invalid body", err) 180 } 181 obj = testworkflowmappers.MapAPIToKube(v) 182 } 183 184 // Read existing resource 185 workflow, err := s.TestWorkflowsClient.Get(name) 186 if err != nil { 187 return s.ClientError(c, errPrefix, err) 188 } 189 190 // Validate resource 191 if obj == nil { 192 return s.BadRequest(c, errPrefix, "invalid body", errors.New("body is required")) 193 } 194 obj.Namespace = workflow.Namespace 195 obj.Name = workflow.Name 196 obj.ResourceVersion = workflow.ResourceVersion 197 198 // Update the resource 199 obj, err = s.TestWorkflowsClient.Update(obj) 200 s.Metrics.IncUpdateTestWorkflow(err) 201 if err != nil { 202 return s.BadRequest(c, errPrefix, "client error", err) 203 } 204 205 err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj) 206 if err != nil { 207 return s.InternalError(c, errPrefix, "serialization problem", err) 208 } 209 return 210 } 211 } 212 213 func (s *apiTCL) PreviewTestWorkflowHandler() fiber.Handler { 214 errPrefix := "failed to resolve test workflow" 215 return func(c *fiber.Ctx) (err error) { 216 // Check if it should inline templates 217 inline, _ := strconv.ParseBool(c.Query("inline")) 218 219 // Deserialize resource 220 obj := new(testworkflowsv1.TestWorkflow) 221 if HasYAML(c) { 222 err = common.DeserializeCRD(obj, c.Body()) 223 if err != nil { 224 return s.BadRequest(c, errPrefix, "invalid body", err) 225 } 226 } else { 227 var v *testkube.TestWorkflow 228 err = c.BodyParser(&v) 229 if err != nil { 230 return s.BadRequest(c, errPrefix, "invalid body", err) 231 } 232 obj = testworkflowmappers.MapAPIToKube(v) 233 } 234 235 // Validate resource 236 if obj == nil { 237 return s.BadRequest(c, errPrefix, "invalid body", errors.New("name is required")) 238 } 239 obj.Namespace = s.Namespace 240 241 if inline { 242 // Fetch the templates 243 tpls := testworkflowresolver.ListTemplates(obj) 244 tplsMap := make(map[string]testworkflowsv1.TestWorkflowTemplate, len(tpls)) 245 for name := range tpls { 246 tpl, err := s.TestWorkflowTemplatesClient.Get(name) 247 if err != nil { 248 return s.BadRequest(c, errPrefix, "fetching error", err) 249 } 250 tplsMap[name] = *tpl 251 } 252 253 // Resolve the TestWorkflow 254 err = testworkflowresolver.ApplyTemplates(obj, tplsMap) 255 if err != nil { 256 return s.BadRequest(c, errPrefix, "resolving error", err) 257 } 258 } 259 260 err = SendResource(c, "TestWorkflow", testworkflowsv1.GroupVersion, testworkflowmappers.MapKubeToAPI, obj) 261 if err != nil { 262 return s.InternalError(c, errPrefix, "serialization problem", err) 263 } 264 return 265 } 266 } 267 268 // TODO: Add metrics 269 func (s *apiTCL) ExecuteTestWorkflowHandler() fiber.Handler { 270 return func(c *fiber.Ctx) (err error) { 271 ctx := c.Context() 272 name := c.Params("id") 273 errPrefix := fmt.Sprintf("failed to execute test workflow '%s'", name) 274 workflow, err := s.TestWorkflowsClient.Get(name) 275 if err != nil { 276 return s.ClientError(c, errPrefix, err) 277 } 278 279 // Delete unnecessary data 280 delete(workflow.Annotations, "kubectl.kubernetes.io/last-applied-configuration") 281 282 // Preserve initial workflow 283 initialWorkflow := workflow.DeepCopy() 284 285 // Load the execution request 286 var request testkube.TestWorkflowExecutionRequest 287 err = c.BodyParser(&request) 288 if err != nil && !errors.Is(err, fiber.ErrUnprocessableEntity) { 289 return s.BadRequest(c, errPrefix, "invalid body", err) 290 } 291 292 // Fetch the templates 293 tpls := testworkflowresolver.ListTemplates(workflow) 294 tplsMap := make(map[string]testworkflowsv1.TestWorkflowTemplate, len(tpls)) 295 for tplName := range tpls { 296 tpl, err := s.TestWorkflowTemplatesClient.Get(tplName) 297 if err != nil { 298 return s.BadRequest(c, errPrefix, "fetching error", err) 299 } 300 tplsMap[tplName] = *tpl 301 } 302 303 // Fetch the global template 304 globalTemplateStr := "" 305 if s.GlobalTemplateName != "" { 306 internalName := testworkflowresolver.GetInternalTemplateName(s.GlobalTemplateName) 307 displayName := testworkflowresolver.GetDisplayTemplateName(s.GlobalTemplateName) 308 309 if _, ok := tplsMap[internalName]; !ok { 310 globalTemplatePtr, err := s.TestWorkflowTemplatesClient.Get(internalName) 311 if err != nil && !IsNotFound(err) { 312 return s.BadRequest(c, errPrefix, "global template error", err) 313 } else if err == nil { 314 tplsMap[internalName] = *globalTemplatePtr 315 } 316 } 317 if _, ok := tplsMap[internalName]; ok { 318 workflow.Spec.Use = append([]testworkflowsv1.TemplateRef{{Name: displayName}}, workflow.Spec.Use...) 319 b, err := yaml.Marshal(tplsMap[internalName]) 320 if err == nil { 321 globalTemplateStr = string(b) 322 } 323 } 324 } 325 326 // Apply the configuration 327 _, err = testworkflowresolver.ApplyWorkflowConfig(workflow, testworkflowmappers.MapConfigValueAPIToKube(request.Config)) 328 if err != nil { 329 return s.BadRequest(c, errPrefix, "configuration", err) 330 } 331 332 // Resolve the TestWorkflow 333 err = testworkflowresolver.ApplyTemplates(workflow, tplsMap) 334 if err != nil { 335 return s.BadRequest(c, errPrefix, "resolving error", err) 336 } 337 338 // Build the basic Execution data 339 id := primitive.NewObjectID().Hex() 340 now := time.Now() 341 machine := expressionstcl.NewMachine(). 342 RegisterStringMap("internal", map[string]string{ 343 "storage.url": os.Getenv("STORAGE_ENDPOINT"), 344 "storage.accessKey": os.Getenv("STORAGE_ACCESSKEYID"), 345 "storage.secretKey": os.Getenv("STORAGE_SECRETACCESSKEY"), 346 "storage.region": os.Getenv("STORAGE_REGION"), 347 "storage.bucket": os.Getenv("STORAGE_BUCKET"), 348 "storage.token": os.Getenv("STORAGE_TOKEN"), 349 "storage.ssl": common.GetOr(os.Getenv("STORAGE_SSL"), "false"), 350 "storage.skipVerify": common.GetOr(os.Getenv("STORAGE_SKIP_VERIFY"), "false"), 351 "storage.certFile": os.Getenv("STORAGE_CERT_FILE"), 352 "storage.keyFile": os.Getenv("STORAGE_KEY_FILE"), 353 "storage.caFile": os.Getenv("STORAGE_CA_FILE"), 354 355 "cloud.enabled": strconv.FormatBool(os.Getenv("TESTKUBE_PRO_API_KEY") != "" || os.Getenv("TESTKUBE_CLOUD_API_KEY") != ""), 356 "cloud.api.key": common.GetOr(os.Getenv("TESTKUBE_PRO_API_KEY"), os.Getenv("TESTKUBE_CLOUD_API_KEY")), 357 "cloud.api.tlsInsecure": common.GetOr(os.Getenv("TESTKUBE_PRO_TLS_INSECURE"), os.Getenv("TESTKUBE_CLOUD_TLS_INSECURE"), "false"), 358 "cloud.api.skipVerify": common.GetOr(os.Getenv("TESTKUBE_PRO_SKIP_VERIFY"), os.Getenv("TESTKUBE_CLOUD_SKIP_VERIFY"), "false"), 359 "cloud.api.url": common.GetOr(os.Getenv("TESTKUBE_PRO_URL"), os.Getenv("TESTKUBE_CLOUD_URL")), 360 361 "dashboard.url": os.Getenv("TESTKUBE_DASHBOARD_URI"), 362 "api.url": s.ApiUrl, 363 "namespace": s.Namespace, 364 "globalTemplate": globalTemplateStr, 365 366 "images.init": constants.DefaultInitImage, 367 "images.toolkit": constants.DefaultToolkitImage, 368 }). 369 RegisterStringMap("workflow", map[string]string{ 370 "name": workflow.Name, 371 }). 372 RegisterStringMap("execution", map[string]string{ 373 "id": id, 374 }) 375 376 // Preserve resolved TestWorkflow 377 resolvedWorkflow := workflow.DeepCopy() 378 379 // Process the TestWorkflow 380 bundle, err := testworkflowprocessor.NewFullFeatured(s.ImageInspector). 381 Bundle(c.Context(), workflow, machine) 382 if err != nil { 383 return s.BadRequest(c, errPrefix, "processing error", err) 384 } 385 386 // Load execution identifier data 387 // TODO: Consider if that should not be shared (as now it is between Tests and Test Suites) 388 number, _ := s.ExecutionResults.GetNextExecutionNumber(context.Background(), workflow.Name) 389 executionName := request.Name 390 if executionName == "" { 391 executionName = fmt.Sprintf("%s-%d", workflow.Name, number) 392 } 393 394 // Ensure it is unique name 395 // TODO: Consider if we shouldn't make name unique across all TestWorkflows 396 next, _ := s.TestWorkflowResults.GetByNameAndTestWorkflow(ctx, executionName, workflow.Name) 397 if next.Name == executionName { 398 return s.BadRequest(c, errPrefix, "execution name already exists", errors.New(executionName)) 399 } 400 401 // Build Execution entity 402 // TODO: Consider storing "config" as well 403 execution := testkube.TestWorkflowExecution{ 404 Id: id, 405 Name: executionName, 406 Number: number, 407 ScheduledAt: now, 408 StatusAt: now, 409 Signature: testworkflowprocessor.MapSignatureListToInternal(bundle.Signature), 410 Result: &testkube.TestWorkflowResult{ 411 Status: common.Ptr(testkube.QUEUED_TestWorkflowStatus), 412 PredictedStatus: common.Ptr(testkube.PASSED_TestWorkflowStatus), 413 Initialization: &testkube.TestWorkflowStepResult{ 414 Status: common.Ptr(testkube.QUEUED_TestWorkflowStepStatus), 415 }, 416 Steps: testworkflowprocessor.MapSignatureListToStepResults(bundle.Signature), 417 }, 418 Output: []testkube.TestWorkflowOutput{}, 419 Workflow: testworkflowmappers.MapKubeToAPI(initialWorkflow), 420 ResolvedWorkflow: testworkflowmappers.MapKubeToAPI(resolvedWorkflow), 421 } 422 err = s.TestWorkflowResults.Insert(ctx, execution) 423 if err != nil { 424 return s.InternalError(c, errPrefix, "inserting execution to storage", err) 425 } 426 427 // Schedule the execution 428 s.TestWorkflowExecutor.Schedule(bundle, execution) 429 s.sendRunWorkflowTelemetry(c.Context(), workflow) 430 431 return c.JSON(execution) 432 } 433 } 434 435 func (s *apiTCL) getFilteredTestWorkflowList(c *fiber.Ctx) (*testworkflowsv1.TestWorkflowList, error) { 436 crWorkflows, err := s.TestWorkflowsClient.List(c.Query("selector")) 437 if err != nil { 438 return nil, err 439 } 440 441 search := c.Query("textSearch") 442 if search != "" { 443 // filter items array 444 for i := len(crWorkflows.Items) - 1; i >= 0; i-- { 445 if !strings.Contains(crWorkflows.Items[i].Name, search) { 446 crWorkflows.Items = append(crWorkflows.Items[:i], crWorkflows.Items[i+1:]...) 447 } 448 } 449 } 450 451 return crWorkflows, nil 452 }