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 }