diff --git a/api/v1/testkube.yaml b/api/v1/testkube.yaml index ce53502308a..2e85f9fcebc 100644 --- a/api/v1/testkube.yaml +++ b/api/v1/testkube.yaml @@ -6849,6 +6849,13 @@ components: example: env: "prod" app: "backend" + disabled: + type: boolean + description: whether webhook is disabled + default: false + example: + - true + - false Event: description: Event data diff --git a/cmd/kubectl-testkube/commands/webhooks/common.go b/cmd/kubectl-testkube/commands/webhooks/common.go index c818cc62035..b4eb92d4497 100644 --- a/cmd/kubectl-testkube/commands/webhooks/common.go +++ b/cmd/kubectl-testkube/commands/webhooks/common.go @@ -56,6 +56,13 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW PayloadTemplateReference: payloadTemplateReference, } + if cmd.Flag("enable").Changed { + options.Disabled = false + } + if cmd.Flag("disable").Changed { + options.Disabled = true + } + return options, nil } @@ -137,5 +144,18 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW options.Headers = &headers } + if cmd.Flag("enable").Changed { + options.Disabled = new(bool) + *options.Disabled = false + } + if cmd.Flag("disable").Changed { + options.Disabled = new(bool) + *options.Disabled = true + } + return options, nil } + +func isBothEnabledAndDisabledSet(cmd *cobra.Command) bool { + return cmd.Flag("enable").Changed && cmd.Flag("disable").Changed +} diff --git a/cmd/kubectl-testkube/commands/webhooks/create.go b/cmd/kubectl-testkube/commands/webhooks/create.go index 046cd2c75a1..81a130d2675 100644 --- a/cmd/kubectl-testkube/commands/webhooks/create.go +++ b/cmd/kubectl-testkube/commands/webhooks/create.go @@ -38,6 +38,10 @@ func NewCreateWebhookCmd() *cobra.Command { ui.Failf("pass valid name (in '--name' flag)") } + if isBothEnabledAndDisabledSet(cmd) { + ui.Failf("both --enable and --disable flags are set, please use only one") + } + namespace := cmd.Flag("namespace").Value.String() var client apiv1.Client if !crdOnly { @@ -45,6 +49,13 @@ func NewCreateWebhookCmd() *cobra.Command { ui.ExitOnError("getting client", err) webhook, _ := client.GetWebhook(name) + if cmd.Flag("enable").Changed { + webhook.Disabled = false + } + if cmd.Flag("disable").Changed { + webhook.Disabled = true + } + if name == webhook.Name { if cmd.Flag("update").Changed { if !update { @@ -99,6 +110,8 @@ func NewCreateWebhookCmd() *cobra.Command { cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair (golang template supported): --header Content-Type=application/xml") cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") cmd.Flags().BoolVar(&update, "update", false, "update, if webhook already exists") + cmd.Flags().Bool("disable", false, "disable webhook") + cmd.Flags().Bool("enable", false, "enable webhook") return cmd } diff --git a/cmd/kubectl-testkube/commands/webhooks/update.go b/cmd/kubectl-testkube/commands/webhooks/update.go index 8f0c524d026..4fda059cc1a 100644 --- a/cmd/kubectl-testkube/commands/webhooks/update.go +++ b/cmd/kubectl-testkube/commands/webhooks/update.go @@ -17,6 +17,8 @@ func UpdateWebhookCmd() *cobra.Command { payloadTemplate string headers map[string]string payloadTemplateReference string + enable bool + disable bool ) cmd := &cobra.Command{ @@ -29,6 +31,10 @@ func UpdateWebhookCmd() *cobra.Command { ui.Failf("pass valid name (in '--name' flag)") } + if isBothEnabledAndDisabledSet(cmd) { + ui.Failf("both --enable and --disable flags are set, please use only one") + } + client, namespace, err := common.GetClient(cmd) ui.ExitOnError("getting client", err) @@ -56,6 +62,8 @@ func UpdateWebhookCmd() *cobra.Command { cmd.Flags().StringVarP(&payloadTemplate, "payload-template", "", "", "if webhook needs to send a custom notification, then a path to template file should be provided") cmd.Flags().StringToStringVarP(&headers, "header", "", nil, "webhook header value pair (golang template supported): --header Content-Type=application/xml") cmd.Flags().StringVar(&payloadTemplateReference, "payload-template-reference", "", "reference to payload template to use for the webhook") + cmd.Flags().BoolVar(&disable, "disable", false, "disable webhook") + cmd.Flags().BoolVar(&enable, "enable", false, "enable webhook") return cmd } diff --git a/docs/docs/articles/webhooks.mdx b/docs/docs/articles/webhooks.mdx index 01edda609ef..0d6dd915640 100644 --- a/docs/docs/articles/webhooks.mdx +++ b/docs/docs/articles/webhooks.mdx @@ -450,3 +450,82 @@ If you are just getting started and want to test your webhook configuration, you 2. [Webhook.Site](https://webhook.site/): Webhook.Site provides an easy way to capture HTTP payloads, review the headers and body for the incoming requests, and automatically respond with a `200 OK` status code. By using these services, you can quickly set up an HTTP endpoint to receive webhook payloads during your testing/development process. + +## Disabling Webhooks + +Disabling webhooks can be helpful to make sure your team does not get spammed with notifications during development. Testkube lets you disable them via the CLI, the API or modifying the CRD directly. By default, webhooks are enabled on creation. + + + + +The easiest way to operate on webhooks in Testkube is to use the official CLI. + +#### Set on Creation + +To disable: + +```bash +testkube create webhook --disable --name "${WEBHOOK_NAME}" ... +``` + +To enable it explicitly: + +```bash +testkube create webhook --enable --name "${WEBHOOK_NAME}" ... +``` + +#### Set on Update + +To disable: + +```bash +testkube update webhook --disable --name "${WEBHOOK_NAME}" +``` + +To enable: + +```bash +testkube update webhook --enable --name "${WEBHOOK_NAME}" +``` + + + + + +When automating your operations on webhooks, you can also use the API. For more details, please consult the "Core OSS OpenAPI Definion" page, respectively the "OpenAPI Specification" for Testkube Pro and Enterprise, under the Reference tab on the left. The field specifying this on the webhooks is called `disabled` and is expecting a value of type `boolean`. + +Keep in mind, that enabling the webhook again from the API will require specifying `"disabled":false`, simply deleting the field from the object is interpreted as not setting any value on it. + + + + + +Using the webhook from the example in creation, you can add the field `disabled` set to `"true"` to the specification: + +```yaml title="webhook.yaml" +apiVersion: executor.testkube.io/v1 +kind: Webhook +metadata: + name: example-webhook + namespace: testkube +spec: + uri: + events: + - start-test + - end-test-success + - end-test-failed + selector: "" + disabled: "true" +``` + +Apply with: + +```sh +kubectl apply -f webhook.yaml +``` + +To enable a webhook again, you can either set the field `disabled` to `"false"` explicitly, or delete it altogether from the specification. + + + + diff --git a/docs/docs/cli/testkube_create_webhook.md b/docs/docs/cli/testkube_create_webhook.md index db0eaf84f91..94c6b85e24d 100644 --- a/docs/docs/cli/testkube_create_webhook.md +++ b/docs/docs/cli/testkube_create_webhook.md @@ -13,6 +13,8 @@ testkube create webhook [flags] ### Options ``` + --disable disable webhook + --enable enable webhook -e, --events stringArray event types handled by webhook e.g. start-test|end-test --header stringToString webhook header value pair (golang template supported): --header Content-Type=application/xml (default []) -h, --help help for webhook diff --git a/docs/docs/cli/testkube_update_webhook.md b/docs/docs/cli/testkube_update_webhook.md index 4886f90c89a..f21fe9d5894 100644 --- a/docs/docs/cli/testkube_update_webhook.md +++ b/docs/docs/cli/testkube_update_webhook.md @@ -13,6 +13,8 @@ testkube update webhook [flags] ### Options ``` + --disable disable webhook + --enable enable webhook -e, --events stringArray event types handled by webhook e.g. start-test|end-test --header stringToString webhook header value pair (golang template supported): --header Content-Type=application/xml (default []) -h, --help help for webhook diff --git a/go.mod b/go.mod index 47e73be45cc..c75f458578c 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/kelseyhightower/envconfig v1.4.0 github.com/kubepug/kubepug v1.7.1 - github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240520124846-07a4da84c97e + github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240521110543-f5f8bc7df098 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 e9c479b74b2..9ea4e1bae76 100644 --- a/go.sum +++ b/go.sum @@ -368,6 +368,8 @@ github.com/kubepug/kubepug v1.7.1 h1:LKhfSxS8Y5mXs50v+3Lpyec+cogErDLcV7CMUuiaisw github.com/kubepug/kubepug v1.7.1/go.mod h1:lv+HxD0oTFL7ZWjj0u6HKhMbbTIId3eG7aWIW0gyF8g= github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240520124846-07a4da84c97e h1:q6lJqg6BfZlvogXRhC4iuPlQsZ1gk9Ng8JkYALFL6kA= github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240520124846-07a4da84c97e/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240521110543-f5f8bc7df098 h1:Dwu0AWX+pPgKYjlDXHf+7TRP3FiPzEcciBIqbUIg3dI= +github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240521110543-f5f8bc7df098/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk= 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_webhook.go b/pkg/api/v1/testkube/model_webhook.go index 92685ccc19e..1302ed9b2f0 100644 --- a/pkg/api/v1/testkube/model_webhook.go +++ b/pkg/api/v1/testkube/model_webhook.go @@ -27,4 +27,6 @@ type Webhook struct { Headers map[string]string `json:"headers,omitempty"` // webhook labels Labels map[string]string `json:"labels,omitempty"` + // whether webhook is disabled + Disabled bool `json:"disabled,omitempty"` } diff --git a/pkg/api/v1/testkube/model_webhook_create_request.go b/pkg/api/v1/testkube/model_webhook_create_request.go index ed8387ced36..d98e35a0710 100644 --- a/pkg/api/v1/testkube/model_webhook_create_request.go +++ b/pkg/api/v1/testkube/model_webhook_create_request.go @@ -27,4 +27,6 @@ type WebhookCreateRequest struct { Headers map[string]string `json:"headers,omitempty"` // webhook labels Labels map[string]string `json:"labels,omitempty"` + // whether webhook is disabled + Disabled bool `json:"disabled,omitempty"` } diff --git a/pkg/api/v1/testkube/model_webhook_extended.go b/pkg/api/v1/testkube/model_webhook_extended.go index d95f78e436a..c937aca7f5e 100644 --- a/pkg/api/v1/testkube/model_webhook_extended.go +++ b/pkg/api/v1/testkube/model_webhook_extended.go @@ -14,7 +14,7 @@ import "fmt" type Webhooks []Webhook func (list Webhooks) Table() (header []string, output [][]string) { - header = []string{"Name", "URI", "Events", "Selector", "Labels"} + header = []string{"Name", "URI", "Events", "Selector", "Labels", "Disabled"} for _, e := range list { output = append(output, []string{ @@ -23,6 +23,7 @@ func (list Webhooks) Table() (header []string, output [][]string) { fmt.Sprintf("%v", e.Events), e.Selector, MapToString(e.Labels), + fmt.Sprint(e.Disabled), }) } diff --git a/pkg/api/v1/testkube/model_webhook_update_request.go b/pkg/api/v1/testkube/model_webhook_update_request.go index 22e2ec886a7..8ec6795be9e 100644 --- a/pkg/api/v1/testkube/model_webhook_update_request.go +++ b/pkg/api/v1/testkube/model_webhook_update_request.go @@ -27,4 +27,6 @@ type WebhookUpdateRequest struct { Headers *map[string]string `json:"headers,omitempty"` // webhook labels Labels *map[string]string `json:"labels,omitempty"` + // whether webhook is disabled + Disabled *bool `json:"disabled,omitempty"` } diff --git a/pkg/event/kind/webhook/listener.go b/pkg/event/kind/webhook/listener.go index 55d3b9a4c7d..571605419f0 100644 --- a/pkg/event/kind/webhook/listener.go +++ b/pkg/event/kind/webhook/listener.go @@ -22,7 +22,7 @@ import ( var _ common.Listener = (*WebhookListener)(nil) func NewWebhookListener(name, uri, selector string, events []testkube.EventType, - payloadObjectField, payloadTemplate string, headers map[string]string) *WebhookListener { + payloadObjectField, payloadTemplate string, headers map[string]string, disabled bool) *WebhookListener { return &WebhookListener{ name: name, Uri: uri, @@ -33,6 +33,7 @@ func NewWebhookListener(name, uri, selector string, events []testkube.EventType, payloadObjectField: payloadObjectField, payloadTemplate: payloadTemplate, headers: headers, + disabled: disabled, } } @@ -46,6 +47,7 @@ type WebhookListener struct { payloadObjectField string payloadTemplate string headers map[string]string + disabled bool } func (l *WebhookListener) Name() string { @@ -68,6 +70,7 @@ func (l *WebhookListener) Metadata() map[string]string { "payloadObjectField": l.payloadObjectField, "payloadTemplate": l.payloadTemplate, "headers": fmt.Sprintf("%v", l.headers), + "disabled": fmt.Sprint(l.disabled), } } @@ -83,7 +86,16 @@ func (l *WebhookListener) Headers() map[string]string { return l.headers } +func (l *WebhookListener) Disabled() bool { + return l.disabled +} + func (l *WebhookListener) Notify(event testkube.Event) (result testkube.EventResult) { + if l.disabled { + l.Log.With(event.Log()...).Debug("webhook listener is disabled") + return testkube.NewSuccessEventResult(event.Id, "webhook listener is disabled") + } + body := bytes.NewBuffer([]byte{}) log := l.Log.With(event.Log()...) diff --git a/pkg/event/kind/webhook/listener_test.go b/pkg/event/kind/webhook/listener_test.go index 974fa6aa4d2..5fb7a786fbd 100644 --- a/pkg/event/kind/webhook/listener_test.go +++ b/pkg/event/kind/webhook/listener_test.go @@ -33,7 +33,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false) // when r := l.Notify(testkube.Event{ @@ -55,7 +55,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "", nil, false) // when r := l.Notify(testkube.Event{ @@ -72,7 +72,7 @@ func TestWebhookListener_Notify(t *testing.T) { t.Parallel() // given - s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil) + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, false) // when r := s.Notify(testkube.Event{ @@ -105,7 +105,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "field", "", nil, false) // when r := l.Notify(testkube.Event{ @@ -131,7 +131,7 @@ func TestWebhookListener_Notify(t *testing.T) { svr := httptest.NewServer(testHandler) defer svr.Close() - l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "{\"id\": \"{{ .Id }}\"}", map[string]string{"Content-Type": "application/json"}) + l := NewWebhookListener("l1", svr.URL, "", testEventTypes, "", "{\"id\": \"{{ .Id }}\"}", map[string]string{"Content-Type": "application/json"}, false) // when r := l.Notify(testkube.Event{ @@ -143,6 +143,22 @@ func TestWebhookListener_Notify(t *testing.T) { assert.Equal(t, "", r.Error()) }) + + t.Run("send event disabled webhook", func(t *testing.T) { + t.Parallel() + // given + + s := NewWebhookListener("l1", "http://baduri.badbadbad", "", testEventTypes, "", "", nil, true) + + // when + r := s.Notify(testkube.Event{ + Type_: testkube.EventStartTest, + TestExecution: exampleExecution(), + }) + + // then + assert.Equal(t, "", r.Error()) + }) } func exampleExecution() *testkube.Execution { diff --git a/pkg/event/kind/webhook/loader.go b/pkg/event/kind/webhook/loader.go index 9b9ebf7d701..9410eb140ae 100644 --- a/pkg/event/kind/webhook/loader.go +++ b/pkg/event/kind/webhook/loader.go @@ -66,7 +66,7 @@ func (r WebhooksLoader) Load() (listeners common.Listeners, err error) { types := webhooks.MapEventArrayToCRDEvents(webhook.Spec.Events) name := fmt.Sprintf("%s.%s", webhook.ObjectMeta.Namespace, webhook.ObjectMeta.Name) - listeners = append(listeners, NewWebhookListener(name, webhook.Spec.Uri, webhook.Spec.Selector, types, webhook.Spec.PayloadObjectField, payloadTemplate, webhook.Spec.Headers)) + listeners = append(listeners, NewWebhookListener(name, webhook.Spec.Uri, webhook.Spec.Selector, types, webhook.Spec.PayloadObjectField, payloadTemplate, webhook.Spec.Headers, webhook.Spec.Disabled)) } return listeners, nil diff --git a/pkg/mapper/webhooks/mapper.go b/pkg/mapper/webhooks/mapper.go index 35d96c38d38..36da188012d 100644 --- a/pkg/mapper/webhooks/mapper.go +++ b/pkg/mapper/webhooks/mapper.go @@ -20,6 +20,7 @@ func MapCRDToAPI(item executorv1.Webhook) testkube.Webhook { PayloadTemplate: item.Spec.PayloadTemplate, PayloadTemplateReference: item.Spec.PayloadTemplateReference, Headers: item.Spec.Headers, + Disabled: item.Spec.Disabled, } } @@ -55,6 +56,7 @@ func MapAPIToCRD(request testkube.WebhookCreateRequest) executorv1.Webhook { PayloadTemplate: request.PayloadTemplate, PayloadTemplateReference: request.PayloadTemplateReference, Headers: request.Headers, + Disabled: request.Disabled, }, } } @@ -121,6 +123,10 @@ func MapUpdateToSpec(request testkube.WebhookUpdateRequest, webhook *executorv1. webhook.Spec.Headers = *request.Headers } + if request.Disabled != nil { + webhook.Spec.Disabled = *request.Disabled + } + return webhook } @@ -169,6 +175,7 @@ func MapSpecToUpdate(webhook *executorv1.Webhook) (request testkube.WebhookUpdat request.Labels = &webhook.Labels request.Headers = &webhook.Spec.Headers + request.Disabled = &webhook.Spec.Disabled return request }