diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index 2f540d5210..7cf5df049f 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -5473,6 +5473,10 @@ components: type: string description: kubernetes resource name selector example: nginx + nameRegex: + type: string + description: kubernetes resource name regex + example: nginx.* namespace: type: string description: resource namespace diff --git a/docs/docs/articles/test-triggers.mdx b/docs/docs/articles/test-triggers.mdx index f309c691c9..eeadcdd084 100644 --- a/docs/docs/articles/test-triggers.mdx +++ b/docs/docs/articles/test-triggers.mdx @@ -69,6 +69,7 @@ Name selectors are used when we want to select a specific resource in a specific ```yaml selector: name: Kubernetes object name + nameRegex: Kubernetes object name regex (for example, "test.*") namespace: Kubernetes object namespace (default is **testkube**) ``` diff --git a/go.mod b/go.mod index 80e33bb5c5..beba0c96f2 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/joshdk/go-junit v1.0.0 github.com/kelseyhightower/envconfig v1.4.0 - github.com/kubeshop/testkube-operator v1.10.8-0.20231005124234-aa5afde3a8f8 + github.com/kubeshop/testkube-operator v1.10.8-0.20231020122730-3ec11798a62f github.com/minio/minio-go/v7 v7.0.47 github.com/montanaflynn/stats v0.6.6 github.com/moogar0880/problems v0.1.1 diff --git a/go.sum b/go.sum index 54965c5961..14e292051f 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubeshop/testkube-operator v1.10.8-0.20231005124234-aa5afde3a8f8 h1:dDhvegOPuvkvRsSEH8H7t3puBRFXSTsS6RYR/kmeShg= -github.com/kubeshop/testkube-operator v1.10.8-0.20231005124234-aa5afde3a8f8/go.mod h1:iwzgZriFxOzstinAqWB32g9iAMSORiQvGYWzX0FWbQk= +github.com/kubeshop/testkube-operator v1.10.8-0.20231020122730-3ec11798a62f h1:ce4ifn6U9c442YSdkoDLcqwCNS6c84gLj/Pbl8mEygY= +github.com/kubeshop/testkube-operator v1.10.8-0.20231020122730-3ec11798a62f/go.mod h1:iwzgZriFxOzstinAqWB32g9iAMSORiQvGYWzX0FWbQk= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= diff --git a/pkg/api/v1/testkube/model_test_trigger_selector.go b/pkg/api/v1/testkube/model_test_trigger_selector.go index 23639759cb..c1174bdc25 100644 --- a/pkg/api/v1/testkube/model_test_trigger_selector.go +++ b/pkg/api/v1/testkube/model_test_trigger_selector.go @@ -12,6 +12,8 @@ package testkube type TestTriggerSelector struct { // kubernetes resource name selector Name string `json:"name,omitempty"` + // kubernetes resource name regex + NameRegex string `json:"nameRegex,omitempty"` // resource namespace Namespace string `json:"namespace,omitempty"` LabelSelector *IoK8sApimachineryPkgApisMetaV1LabelSelector `json:"labelSelector,omitempty"` diff --git a/pkg/crd/templates/testtrigger.tmpl b/pkg/crd/templates/testtrigger.tmpl index 327475c26f..22236bd30e 100644 --- a/pkg/crd/templates/testtrigger.tmpl +++ b/pkg/crd/templates/testtrigger.tmpl @@ -18,6 +18,9 @@ spec: {{- if .ResourceSelector.Name }} name: {{ .ResourceSelector.Name }} {{- end }} + {{- if .ResourceSelector.NameRegex }} + nameRegex: {{ .ResourceSelector.NameRegex }} + {{- end }} {{- if .ResourceSelector.Namespace }} namespace: {{ .ResourceSelector.Namespace }} {{- end }} @@ -111,6 +114,9 @@ spec: {{- if .TestSelector.Name }} name: {{ .TestSelector.Name }} {{- end }} + {{- if .TestSelector.NameRegex }} + nameRegex: {{ .TestSelector.NameRegex }} + {{- end }} {{- if .TestSelector.Namespace }} namespace: {{ .TestSelector.Namespace }} {{- end }} diff --git a/pkg/mapper/testtriggers/kube_openapi.go b/pkg/mapper/testtriggers/kube_openapi.go index 236e4097ca..a8631c457f 100644 --- a/pkg/mapper/testtriggers/kube_openapi.go +++ b/pkg/mapper/testtriggers/kube_openapi.go @@ -47,6 +47,7 @@ func mapSelectorFromCRD(selector testsv1.TestTriggerSelector) *testkube.TestTrig } return &testkube.TestTriggerSelector{ Name: selector.Name, + NameRegex: selector.NameRegex, Namespace: selector.Namespace, LabelSelector: labelSelector, } diff --git a/pkg/mapper/testtriggers/openapi_kube.go b/pkg/mapper/testtriggers/openapi_kube.go index 6db39ef272..6fb4bada83 100644 --- a/pkg/mapper/testtriggers/openapi_kube.go +++ b/pkg/mapper/testtriggers/openapi_kube.go @@ -40,6 +40,7 @@ func mapSelectorToCRD(selector *testkube.TestTriggerSelector) testsv1.TestTrigge } return testsv1.TestTriggerSelector{ Name: selector.Name, + NameRegex: selector.NameRegex, Namespace: selector.Namespace, LabelSelector: labelSelector, } diff --git a/pkg/triggers/executor.go b/pkg/triggers/executor.go index d97c5c93ae..16b0541928 100644 --- a/pkg/triggers/executor.go +++ b/pkg/triggers/executor.go @@ -2,6 +2,7 @@ package triggers import ( "context" + "regexp" "time" "github.com/pkg/errors" @@ -118,6 +119,26 @@ func (s *Service) getTests(t *testtriggersv1.TestTrigger) ([]testsv3.Test, error } tests = append(tests, *test) } + + if t.Spec.TestSelector.NameRegex != "" { + s.logger.Debugf("trigger service: executor component: fetching testsv3.Test with name regex %s", t.Spec.TestSelector.NameRegex) + testList, err := s.testsClient.List("") + if err != nil { + return nil, err + } + + re, err := regexp.Compile(t.Spec.TestSelector.NameRegex) + if err != nil { + return nil, err + } + + for i := range testList.Items { + if re.MatchString(testList.Items[i].Name) { + tests = append(tests, testList.Items[i]) + } + } + } + if t.Spec.TestSelector.LabelSelector != nil { selector, err := metav1.LabelSelectorAsSelector(t.Spec.TestSelector.LabelSelector) if err != nil { @@ -144,6 +165,26 @@ func (s *Service) getTestSuites(t *testtriggersv1.TestTrigger) ([]testsuitesv3.T } testSuites = append(testSuites, *testSuite) } + + if t.Spec.TestSelector.NameRegex != "" { + s.logger.Debugf("trigger service: executor component: fetching testsuitesv3.TestSuite with name regex %s", t.Spec.TestSelector.NameRegex) + testSuitesList, err := s.testSuitesClient.List("") + if err != nil { + return nil, err + } + + re, err := regexp.Compile(t.Spec.TestSelector.NameRegex) + if err != nil { + return nil, err + } + + for i := range testSuitesList.Items { + if re.MatchString(testSuitesList.Items[i].Name) { + testSuites = append(testSuites, testSuitesList.Items[i]) + } + } + } + if t.Spec.TestSelector.LabelSelector != nil { selector, err := metav1.LabelSelectorAsSelector(t.Spec.TestSelector.LabelSelector) if err != nil { diff --git a/pkg/triggers/matcher.go b/pkg/triggers/matcher.go index dc1bf718ab..2917d79e15 100644 --- a/pkg/triggers/matcher.go +++ b/pkg/triggers/matcher.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "regexp" "sync" "time" @@ -112,6 +113,18 @@ func matchSelector(selector *testtriggersv1.TestTriggerSelector, namespace strin isSameTestTriggerNamespace := selector.Namespace == "" && namespace == event.namespace return isSameName && (isSameNamespace || isSameTestTriggerNamespace) } + if selector.NameRegex != "" { + re, err := regexp.Compile(selector.NameRegex) + if err != nil { + logger.Errorf("error compiling %v name regex: %v", selector.NameRegex, err) + return false + } + + isSameName := re.MatchString(event.name) + isSameNamespace := selector.Namespace == event.namespace + isSameTestTriggerNamespace := selector.Namespace == "" && namespace == event.namespace + return isSameName && (isSameNamespace || isSameTestTriggerNamespace) + } if selector.LabelSelector != nil && len(event.labels) > 0 { k8sSelector, err := v1.LabelSelectorAsSelector(selector.LabelSelector) if err != nil { diff --git a/pkg/triggers/matcher_test.go b/pkg/triggers/matcher_test.go index b1cc5a0355..0d2969f074 100644 --- a/pkg/triggers/matcher_test.go +++ b/pkg/triggers/matcher_test.go @@ -415,6 +415,56 @@ func TestService_match(t *testing.T) { assert.NoError(t, err) } +func TestService_matchRegex(t *testing.T) { + t.Parallel() + + e := &watcherEvent{ + resource: "deployment", + name: "test-deployment", + namespace: "testkube", + labels: nil, + object: nil, + eventType: "modified", + causes: nil, + } + + srv1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + defer srv1.Close() + + testTrigger1 := &testtriggersv1.TestTrigger{ + ObjectMeta: metav1.ObjectMeta{Namespace: "testkube", Name: "test-trigger-1"}, + Spec: testtriggersv1.TestTriggerSpec{ + Resource: "deployment", + ResourceSelector: testtriggersv1.TestTriggerSelector{NameRegex: "test.*"}, + Event: "modified", + Action: "run", + Execution: "test", + ConcurrencyPolicy: "allow", + TestSelector: testtriggersv1.TestTriggerSelector{NameRegex: "some.*"}, + }, + } + statusKey1 := newStatusKey(testTrigger1.Namespace, testTrigger1.Name) + triggerStatus1 := &triggerStatus{testTrigger: testTrigger1} + s := &Service{ + defaultConditionsCheckBackoff: defaultConditionsCheckBackoff, + defaultConditionsCheckTimeout: defaultConditionsCheckTimeout, + defaultProbesCheckBackoff: defaultProbesCheckBackoff, + defaultProbesCheckTimeout: defaultProbesCheckTimeout, + triggerExecutor: func(ctx context.Context, trigger *testtriggersv1.TestTrigger) error { + assert.Equal(t, "testkube", trigger.Namespace) + assert.Equal(t, "test-trigger-1", trigger.Name) + return nil + }, + triggerStatus: map[statusKey]*triggerStatus{statusKey1: triggerStatus1}, + logger: log.DefaultLogger, + httpClient: http.DefaultClient, + } + + err := s.match(context.Background(), e) + assert.NoError(t, err) +} + func TestService_noMatch(t *testing.T) { t.Parallel()