github.com/kubeshop/testkube@v1.17.23/internal/app/api/v1/testtriggers.go (about)

     1  package v1
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"strings"
     8  
     9  	"github.com/gofiber/fiber/v2"
    10  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    11  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    12  	"k8s.io/apimachinery/pkg/labels"
    13  	"k8s.io/apimachinery/pkg/util/yaml"
    14  
    15  	testtriggersv1 "github.com/kubeshop/testkube-operator/api/testtriggers/v1"
    16  	"github.com/kubeshop/testkube/pkg/api/v1/testkube"
    17  	"github.com/kubeshop/testkube/pkg/crd"
    18  	"github.com/kubeshop/testkube/pkg/keymap/triggers"
    19  	triggerskeymapmapper "github.com/kubeshop/testkube/pkg/mapper/keymap/triggers"
    20  	testtriggersmapper "github.com/kubeshop/testkube/pkg/mapper/testtriggers"
    21  	"github.com/kubeshop/testkube/pkg/utils"
    22  )
    23  
    24  const testTriggerMaxNameLength = 57
    25  
    26  // CreateTestTriggerHandler is a handler for creating test trigger objects
    27  func (s *TestkubeAPI) CreateTestTriggerHandler() fiber.Handler {
    28  	return func(c *fiber.Ctx) error {
    29  		errPrefix := "failed to create test trigger"
    30  		var testTrigger testtriggersv1.TestTrigger
    31  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
    32  			testTriggerSpec := string(c.Body())
    33  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testTriggerSpec), len(testTriggerSpec))
    34  			if err := decoder.Decode(&testTrigger); err != nil {
    35  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
    36  			}
    37  		} else {
    38  			var request testkube.TestTriggerUpsertRequest
    39  			err := c.BodyParser(&request)
    40  			if err != nil {
    41  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json request: %w", errPrefix, err))
    42  			}
    43  
    44  			testTrigger = testtriggersmapper.MapTestTriggerUpsertRequestToTestTriggerCRD(request)
    45  			// default namespace if not defined in upsert request
    46  			if testTrigger.Namespace == "" {
    47  				testTrigger.Namespace = s.Namespace
    48  			}
    49  			// default trigger name if not defined in upsert request
    50  			if testTrigger.Name == "" {
    51  				testTrigger.Name = generateTestTriggerName(&testTrigger)
    52  			}
    53  
    54  			if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
    55  				data, err := crd.GenerateYAML(crd.TemplateTestTrigger, []testkube.TestTrigger{testtriggersmapper.MapCRDToAPI(&testTrigger)})
    56  				return s.getCRDs(c, data, err)
    57  			}
    58  		}
    59  
    60  		errPrefix = errPrefix + " " + testTrigger.Name
    61  
    62  		s.Log.Infow("creating test trigger", "testTrigger", testTrigger)
    63  
    64  		created, err := s.TestKubeClientset.TestsV1().TestTriggers(s.Namespace).Create(c.UserContext(), &testTrigger, v1.CreateOptions{})
    65  
    66  		s.Metrics.IncCreateTestTrigger(err)
    67  
    68  		if err != nil {
    69  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not create test trigger: %w", errPrefix, err))
    70  		}
    71  
    72  		c.Status(http.StatusCreated)
    73  		return c.JSON(testtriggersmapper.MapCRDToAPI(created))
    74  	}
    75  }
    76  
    77  // UpdateTestTriggerHandler is a handler for updates an existing TestTrigger CRD based on TestTrigger content
    78  func (s *TestkubeAPI) UpdateTestTriggerHandler() fiber.Handler {
    79  	return func(c *fiber.Ctx) error {
    80  		errPrefix := "failed to update test trigger"
    81  		var request testkube.TestTriggerUpsertRequest
    82  		if string(c.Request().Header.ContentType()) == mediaTypeYAML {
    83  			var testTrigger testtriggersv1.TestTrigger
    84  			testTriggerSpec := string(c.Body())
    85  			decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(testTriggerSpec), len(testTriggerSpec))
    86  			if err := decoder.Decode(&testTrigger); err != nil {
    87  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse yaml request: %w", errPrefix, err))
    88  			}
    89  
    90  			request = testtriggersmapper.MapTestTriggerCRDToTestTriggerUpsertRequest(testTrigger)
    91  		} else {
    92  			err := c.BodyParser(&request)
    93  			if err != nil {
    94  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse json request: %w", errPrefix, err))
    95  			}
    96  		}
    97  
    98  		namespace := s.Namespace
    99  		if request.Namespace != "" {
   100  			namespace = request.Namespace
   101  		}
   102  		errPrefix = errPrefix + " " + request.Name
   103  
   104  		// we need to get resource first and load its metadata.ResourceVersion
   105  		testTrigger, err := s.TestKubeClientset.TestsV1().TestTriggers(namespace).Get(c.UserContext(), request.Name, v1.GetOptions{})
   106  		if err != nil {
   107  			if k8serrors.IsNotFound(err) {
   108  				return s.Error(c, http.StatusNotFound, fmt.Errorf("%s: client could not find test trigger: %w", errPrefix, err))
   109  			}
   110  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test trigger: %w", errPrefix, err))
   111  		}
   112  
   113  		// map TestSuite but load spec only to not override metadata.ResourceVersion
   114  		crdTestTrigger := testtriggersmapper.MapTestTriggerUpsertRequestToTestTriggerCRD(request)
   115  		testTrigger.Spec = crdTestTrigger.Spec
   116  		testTrigger.Labels = request.Labels
   117  		testTrigger, err = s.TestKubeClientset.TestsV1().TestTriggers(namespace).Update(c.UserContext(), testTrigger, v1.UpdateOptions{})
   118  
   119  		s.Metrics.IncUpdateTestTrigger(err)
   120  
   121  		if err != nil {
   122  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not update test trigger: %w", errPrefix, err))
   123  		}
   124  
   125  		return c.JSON(testtriggersmapper.MapCRDToAPI(testTrigger))
   126  	}
   127  }
   128  
   129  // BulkUpdateTestTriggersHandler is a handler for bulk updates an existing TestTrigger CRDs based on array of TestTrigger content
   130  func (s *TestkubeAPI) BulkUpdateTestTriggersHandler() fiber.Handler {
   131  	return func(c *fiber.Ctx) error {
   132  		errPrefix := "failed to bulk update test triggers"
   133  
   134  		var request []testkube.TestTriggerUpsertRequest
   135  		err := c.BodyParser(&request)
   136  		if err != nil {
   137  			return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: could not parse request: %w", errPrefix, err))
   138  		}
   139  
   140  		namespaces := make(map[string]struct{}, 0)
   141  		for _, upsertRequest := range request {
   142  			namespace := s.Namespace
   143  			if upsertRequest.Namespace != "" {
   144  				namespace = upsertRequest.Namespace
   145  			}
   146  
   147  			namespaces[namespace] = struct{}{}
   148  		}
   149  
   150  		for namespace := range namespaces {
   151  			err = s.TestKubeClientset.
   152  				TestsV1().
   153  				TestTriggers(namespace).
   154  				DeleteCollection(c.UserContext(), v1.DeleteOptions{}, v1.ListOptions{})
   155  			if err != nil && !k8serrors.IsNotFound(err) {
   156  				return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: error cleaning triggers before reapply", errPrefix))
   157  			}
   158  		}
   159  
   160  		s.Metrics.IncBulkDeleteTestTrigger(nil)
   161  
   162  		testTriggers := make([]testkube.TestTrigger, 0, len(request))
   163  
   164  		for _, upsertRequest := range request {
   165  			namespace := s.Namespace
   166  			if upsertRequest.Namespace != "" {
   167  				namespace = upsertRequest.Namespace
   168  			}
   169  			var testTrigger *testtriggersv1.TestTrigger
   170  			crdTestTrigger := testtriggersmapper.MapTestTriggerUpsertRequestToTestTriggerCRD(upsertRequest)
   171  			// default trigger name if not defined in upsert request
   172  			if crdTestTrigger.Name == "" {
   173  				crdTestTrigger.Name = generateTestTriggerName(&crdTestTrigger)
   174  			}
   175  			testTrigger, err = s.TestKubeClientset.
   176  				TestsV1().
   177  				TestTriggers(namespace).
   178  				Create(c.UserContext(), &crdTestTrigger, v1.CreateOptions{})
   179  
   180  			s.Metrics.IncCreateTestTrigger(err)
   181  
   182  			if err != nil {
   183  				return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: error reapplying triggers after clean", errPrefix))
   184  			}
   185  
   186  			testTriggers = append(testTriggers, testtriggersmapper.MapCRDToAPI(testTrigger))
   187  		}
   188  
   189  		s.Metrics.IncBulkUpdateTestTrigger(nil)
   190  
   191  		return c.JSON(testTriggers)
   192  	}
   193  }
   194  
   195  // GetTestTriggerHandler is a handler for getting TestTrigger object
   196  func (s *TestkubeAPI) GetTestTriggerHandler() fiber.Handler {
   197  	return func(c *fiber.Ctx) error {
   198  		namespace := c.Query("namespace", s.Namespace)
   199  		name := c.Params("id")
   200  		errPrefix := fmt.Sprintf("failed to get test trigger %s", name)
   201  
   202  		testTrigger, err := s.TestKubeClientset.TestsV1().TestTriggers(namespace).Get(c.UserContext(), name, v1.GetOptions{})
   203  		if err != nil {
   204  			if k8serrors.IsNotFound(err) {
   205  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: client could not find test trigger: %w", errPrefix, err))
   206  			}
   207  
   208  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not get test trigger: %w", errPrefix, err))
   209  		}
   210  
   211  		c.Status(http.StatusOK)
   212  
   213  		apiTestTrigger := testtriggersmapper.MapCRDToAPI(testTrigger)
   214  
   215  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   216  			data, err := crd.GenerateYAML(crd.TemplateTestTrigger, []testkube.TestTrigger{apiTestTrigger})
   217  			return s.getCRDs(c, data, err)
   218  		}
   219  
   220  		return c.JSON(apiTestTrigger)
   221  	}
   222  }
   223  
   224  // DeleteTestTriggerHandler is a handler for deleting TestTrigger by id
   225  func (s *TestkubeAPI) DeleteTestTriggerHandler() fiber.Handler {
   226  	return func(c *fiber.Ctx) error {
   227  		namespace := c.Query("namespace", s.Namespace)
   228  		name := c.Params("id")
   229  		errPrefix := fmt.Sprintf("failed to delete test trigger %s", name)
   230  
   231  		err := s.TestKubeClientset.TestsV1().TestTriggers(namespace).Delete(c.UserContext(), name, v1.DeleteOptions{})
   232  
   233  		s.Metrics.IncDeleteTestTrigger(err)
   234  
   235  		if err != nil {
   236  			if k8serrors.IsNotFound(err) {
   237  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: client could not find test trigger: %w", errPrefix, err))
   238  			}
   239  
   240  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not delete test trigger: %w", errPrefix, err))
   241  		}
   242  
   243  		return c.SendStatus(http.StatusNoContent)
   244  	}
   245  }
   246  
   247  // DeleteTestTriggersHandler is a handler for deleting all or selected TestTriggers
   248  func (s *TestkubeAPI) DeleteTestTriggersHandler() fiber.Handler {
   249  	return func(c *fiber.Ctx) error {
   250  		errPrefix := "failed to delete test triggers"
   251  
   252  		namespace := c.Query("namespace", s.Namespace)
   253  		selector := c.Query("selector")
   254  		if selector != "" {
   255  			_, err := labels.Parse(selector)
   256  			if err != nil {
   257  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: error validating selector: %w", errPrefix, err))
   258  			}
   259  		}
   260  		listOpts := v1.ListOptions{LabelSelector: selector}
   261  		err := s.TestKubeClientset.TestsV1().TestTriggers(namespace).DeleteCollection(c.UserContext(), v1.DeleteOptions{}, listOpts)
   262  
   263  		s.Metrics.IncBulkDeleteTestTrigger(err)
   264  
   265  		if err != nil {
   266  			if k8serrors.IsNotFound(err) {
   267  				return s.Warn(c, http.StatusNotFound, fmt.Errorf("%s: client could not find test trigger: %w", errPrefix, err))
   268  			}
   269  
   270  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not bulk delete test triggers: %w", errPrefix, err))
   271  		}
   272  
   273  		return c.SendStatus(http.StatusNoContent)
   274  	}
   275  }
   276  
   277  // ListTestTriggersHandler is a handler for listing all available TestTriggers
   278  func (s *TestkubeAPI) ListTestTriggersHandler() fiber.Handler {
   279  	return func(c *fiber.Ctx) error {
   280  		errPrefix := "failed to delete test triggers"
   281  
   282  		namespace := c.Query("namespace", s.Namespace)
   283  		selector := c.Query("selector")
   284  		if selector != "" {
   285  			_, err := labels.Parse(selector)
   286  			if err != nil {
   287  				return s.Error(c, http.StatusBadRequest, fmt.Errorf("%s: error validating selector: %w", errPrefix, err))
   288  			}
   289  		}
   290  		opts := v1.ListOptions{LabelSelector: selector}
   291  		testTriggers, err := s.TestKubeClientset.TestsV1().TestTriggers(namespace).List(c.UserContext(), opts)
   292  		if err != nil {
   293  			return s.Error(c, http.StatusBadGateway, fmt.Errorf("%s: client could not list test triggers: %w", errPrefix, err))
   294  		}
   295  
   296  		c.Status(http.StatusOK)
   297  
   298  		apiTestTriggers := testtriggersmapper.MapTestTriggerListKubeToAPI(testTriggers)
   299  
   300  		if c.Accepts(mediaTypeJSON, mediaTypeYAML) == mediaTypeYAML {
   301  			data, err := crd.GenerateYAML(crd.TemplateTestTrigger, apiTestTriggers)
   302  			return s.getCRDs(c, data, err)
   303  		}
   304  
   305  		return c.JSON(apiTestTriggers)
   306  	}
   307  }
   308  
   309  // GetTestTriggerKeyMapHandler is a handler for listing supported TestTrigger field combinations
   310  func (s *TestkubeAPI) GetTestTriggerKeyMapHandler() fiber.Handler {
   311  	return func(c *fiber.Ctx) error {
   312  		return c.JSON(triggerskeymapmapper.MapTestTriggerKeyMapToAPI(triggers.NewKeyMap()))
   313  	}
   314  }
   315  
   316  // generateTestTriggerName function generates a trigger name from the TestTrigger spec
   317  // function also takes care of name collisions, not exceeding k8s max object name (63 characters) and not ending with a hyphen '-'
   318  func generateTestTriggerName(t *testtriggersv1.TestTrigger) string {
   319  	name := fmt.Sprintf("trigger-%s-%s-%s-%s", t.Spec.Resource, t.Spec.Event, t.Spec.Action, t.Spec.Execution)
   320  	if len(name) > testTriggerMaxNameLength {
   321  		name = name[:testTriggerMaxNameLength-1]
   322  	}
   323  	name = strings.TrimSuffix(name, "-")
   324  	name = fmt.Sprintf("%s-%s", name, utils.RandAlphanum(5))
   325  	return name
   326  }