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  }