Skip to content

Commit

Permalink
feat: onStateChange on webhooks (#5601)
Browse files Browse the repository at this point in the history
  • Loading branch information
vLia authored Jun 20, 2024
1 parent 61cfaa6 commit 53b67f1
Show file tree
Hide file tree
Showing 53 changed files with 563 additions and 117 deletions.
7 changes: 7 additions & 0 deletions api/v1/testkube.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6895,6 +6895,13 @@ components:
example:
- true
- false
onStateChange:
type: boolean
description: whether webhook is triggered on state change only
default: false
example:
- true
- false

Event:
description: Event data
Expand Down
14 changes: 14 additions & 0 deletions cmd/kubectl-testkube/commands/webhooks/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW
return options, err
}

onStateChange, err := cmd.Flags().GetBool("on-state-change")
if err != nil {
return options, err
}

payloadTemplateReference := cmd.Flag("payload-template-reference").Value.String()
options = apiv1.CreateWebhookOptions{
Name: name,
Expand All @@ -54,6 +59,7 @@ func NewCreateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.CreateW
PayloadTemplate: payloadTemplateContent,
Headers: headers,
PayloadTemplateReference: payloadTemplateReference,
OnStateChange: onStateChange,
}

if cmd.Flag("enable").Changed {
Expand Down Expand Up @@ -153,6 +159,14 @@ func NewUpdateWebhookOptionsFromFlags(cmd *cobra.Command) (options apiv1.UpdateW
*options.Disabled = true
}

if cmd.Flag("on-state-change").Changed {
onStateChange, err := cmd.Flags().GetBool("on-state-change")
if err != nil {
return options, err
}
options.OnStateChange = &onStateChange
}

return options, nil
}

Expand Down
2 changes: 2 additions & 0 deletions cmd/kubectl-testkube/commands/webhooks/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func NewCreateWebhookCmd() *cobra.Command {
headers map[string]string
payloadTemplateReference string
update bool
onStateChange bool
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -112,6 +113,7 @@ func NewCreateWebhookCmd() *cobra.Command {
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")
cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change")

return cmd
}
2 changes: 2 additions & 0 deletions cmd/kubectl-testkube/commands/webhooks/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func UpdateWebhookCmd() *cobra.Command {
payloadTemplateReference string
enable bool
disable bool
onStateChange bool
)

cmd := &cobra.Command{
Expand Down Expand Up @@ -64,6 +65,7 @@ func UpdateWebhookCmd() *cobra.Command {
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")
cmd.Flags().BoolVar(&onStateChange, "on-state-change", false, "specify whether webhook should be triggered only on a state change")

return cmd
}
54 changes: 54 additions & 0 deletions docs/docs/articles/webhooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -890,3 +890,57 @@ Example usage:
</Tabs>

The examples show only the `create` operations, however, these instructions work the same for `update` and `run` as well.

## Enable Webhooks on State Changes

It is possible to get notified by webhooks only when the latest execution's outcome differs from the previous one. This way you can see instantly when one of your scheduled tests was healed or when they got broken. It could also help prevent alert fatigue by deduplicating the alerts and therefore decreasing the load on your monitoring systems. Enable this by setting `onStateChange` to true directly on the webhook.

<Tabs>

<TabItem value="cli" label="CLI">

```bash
testkube update webhook --name example-webhook --on-state-change
```

</TabItem>

<TabItem value="api" label="API">

```json
{
"name": "example-webhook",
"namespace": "testkube",
"uri": "https://webhook.url/",
"events": [
"start-test",
"end-test-success",
"end-test-failed"
],
"disabled": false,
"onStateChange": true,
}
```

</TabItem>

<TabItem value="crd" label="Custom Resource">

```yaml
apiVersion: executor.testkube.io/v1
kind: Webhook
metadata:
name: example-webhook-enabled
namespace: testkube
spec:
events:
- start-test
- end-test-success
- end-test-failed
onStateChange: true
uri: https://webhook.url/
```

</TabItem>

</Tabs>
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,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.20240607075650-f76a84665689
github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240612160826-d6bd7a493885
github.com/minio/minio-go/v7 v7.0.47
github.com/montanaflynn/stats v0.6.6
github.com/moogar0880/problems v0.1.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.20240607075650-f76a84665689 h1:YVG675sarzcCDYcPH81tM5FpYReGgDoSbIShbpw7NNg=
github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240607075650-f76a84665689/go.mod h1:P47tw1nKQFufdsZndyq2HG2MSa0zK/lU0XpRfZtEmIk=
github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240612160826-d6bd7a493885 h1:nudyF4kv10vtvR/0QYGUg33agMWU60bb4R/+TZPTsjQ=
github.com/kubeshop/testkube-operator v1.15.2-beta1.0.20240612160826-d6bd7a493885/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=
Expand Down
1 change: 0 additions & 1 deletion goreleaser_files/.goreleaser-docker-build-api.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: 2
project_name: testkube-api-server

env:
Expand Down
4 changes: 4 additions & 0 deletions internal/app/api/v1/executions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ func (r MockExecutionResultsRepository) GetNextExecutionNumber(ctx context.Conte
panic("not implemented")
}

func (r MockExecutionResultsRepository) GetPreviousFinishedState(ctx context.Context, testName string, date time.Time) (testkube.ExecutionStatus, error) {
panic("not implemented")
}

func (r MockExecutionResultsRepository) Insert(ctx context.Context, result testkube.Execution) error {
panic("not implemented")
}
Expand Down
6 changes: 3 additions & 3 deletions internal/app/api/v1/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ const (
func NewTestkubeAPI(
namespace string,
testExecutionResults result.Repository,
testsuiteExecutionsResults testresult.Repository,
testSuiteExecutionsResults testresult.Repository,
testWorkflowResults testworkflow.Repository,
testWorkflowOutput testworkflow.OutputRepository,
testsClient *testsclientv3.TestsClient,
Expand Down Expand Up @@ -125,7 +125,7 @@ func NewTestkubeAPI(

s := TestkubeAPI{
HTTPServer: server.NewServer(httpConfig),
TestExecutionResults: testsuiteExecutionsResults,
TestExecutionResults: testSuiteExecutionsResults,
ExecutionResults: testExecutionResults,
TestWorkflowResults: testWorkflowResults,
TestWorkflowOutput: testWorkflowOutput,
Expand Down Expand Up @@ -169,7 +169,7 @@ func NewTestkubeAPI(
// will be reused in websockets handler
s.WebsocketLoader = ws.NewWebsocketLoader()

s.Events.Loader.Register(webhook.NewWebhookLoader(s.Log, webhookClient, templatesClient))
s.Events.Loader.Register(webhook.NewWebhookLoader(s.Log, webhookClient, templatesClient, testExecutionResults, testSuiteExecutionsResults, testWorkflowResults))
s.Events.Loader.Register(s.WebsocketLoader)
s.Events.Loader.Register(s.slackLoader)

Expand Down
5 changes: 2 additions & 3 deletions internal/graphql/gen/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion internal/graphql/services/mock_executors.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions pkg/api/v1/testkube/model_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ type Webhook struct {
Labels map[string]string `json:"labels,omitempty"`
// whether webhook is disabled
Disabled bool `json:"disabled,omitempty"`
// whether webhook is triggered on state change only
OnStateChange bool `json:"onStateChange,omitempty"`
}
2 changes: 2 additions & 0 deletions pkg/api/v1/testkube/model_webhook_create_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ type WebhookCreateRequest struct {
Labels map[string]string `json:"labels,omitempty"`
// whether webhook is disabled
Disabled bool `json:"disabled,omitempty"`
// whether webhook is triggered on state change only
OnStateChange bool `json:"onStateChange,omitempty"`
}
3 changes: 2 additions & 1 deletion pkg/api/v1/testkube/model_webhook_extended.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import "fmt"
type Webhooks []Webhook

func (list Webhooks) Table() (header []string, output [][]string) {
header = []string{"Name", "URI", "Events", "Selector", "Labels", "Disabled"}
header = []string{"Name", "URI", "Events", "Selector", "Labels", "Disabled", "On State Change"}

for _, e := range list {
output = append(output, []string{
Expand All @@ -24,6 +24,7 @@ func (list Webhooks) Table() (header []string, output [][]string) {
e.Selector,
MapToString(e.Labels),
fmt.Sprint(e.Disabled),
fmt.Sprint(e.OnStateChange),
})
}

Expand Down
2 changes: 2 additions & 0 deletions pkg/api/v1/testkube/model_webhook_update_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ type WebhookUpdateRequest struct {
Labels *map[string]string `json:"labels,omitempty"`
// whether webhook is disabled
Disabled *bool `json:"disabled,omitempty"`
// whether webhook is triggered on state change only
OnStateChange *bool `json:"onStateChange,omitempty"`
}
41 changes: 21 additions & 20 deletions pkg/cloud/data/result/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ package result
import "github.com/kubeshop/testkube/pkg/cloud/data/executor"

const (
CmdResultGetNextExecutionNumber executor.Command = "result_get_next_execution_number"
CmdResultGet executor.Command = "result_get"
CmdResultGetByNameAndTest executor.Command = "result_get_by_name_and_test"
CmdResultGetLatestByTest executor.Command = "result_get_latest_by_test"
CmdResultGetLatestByTests executor.Command = "result_get_latest_by_tests"
CmdResultGetExecutions executor.Command = "result_get_executions"
CmdResultGetExecutionTotals executor.Command = "result_get_execution_totals"
CmdResultInsert executor.Command = "result_insert"
CmdResultUpdate executor.Command = "result_update"
CmdResultUpdateResult executor.Command = "result_update_result"
CmdResultStartExecution executor.Command = "result_start_execution"
CmdResultEndExecution executor.Command = "result_end_execution"
CmdResultGetLabels executor.Command = "result_get_labels"
CmdResultDeleteByTest executor.Command = "result_delete_by_test"
CmdResultDeleteByTestSuite executor.Command = "result_delete_by_test_suite"
CmdResultDeleteAll executor.Command = "result_delete_all"
CmdResultDeleteByTests executor.Command = "result_delete_by_tests"
CmdResultDeleteByTestSuites executor.Command = "result_delete_by_test_suites"
CmdResultDeleteForAllTestSuites executor.Command = "result_delete_for_all_test_suites"
CmdResultGetTestMetrics executor.Command = "result_get_test_metrics"
CmdResultGetNextExecutionNumber executor.Command = "result_get_next_execution_number"
CmdResultGet executor.Command = "result_get"
CmdResultGetByNameAndTest executor.Command = "result_get_by_name_and_test"
CmdResultGetLatestByTest executor.Command = "result_get_latest_by_test"
CmdResultGetLatestByTests executor.Command = "result_get_latest_by_tests"
CmdResultGetExecutions executor.Command = "result_get_executions"
CmdResultGetExecutionTotals executor.Command = "result_get_execution_totals"
CmdResultGetPreviousFinishedState executor.Command = "result_get_previous_finished_state"
CmdResultInsert executor.Command = "result_insert"
CmdResultUpdate executor.Command = "result_update"
CmdResultUpdateResult executor.Command = "result_update_result"
CmdResultStartExecution executor.Command = "result_start_execution"
CmdResultEndExecution executor.Command = "result_end_execution"
CmdResultGetLabels executor.Command = "result_get_labels"
CmdResultDeleteByTest executor.Command = "result_delete_by_test"
CmdResultDeleteByTestSuite executor.Command = "result_delete_by_test_suite"
CmdResultDeleteAll executor.Command = "result_delete_all"
CmdResultDeleteByTests executor.Command = "result_delete_by_tests"
CmdResultDeleteByTestSuites executor.Command = "result_delete_by_test_suites"
CmdResultDeleteForAllTestSuites executor.Command = "result_delete_for_all_test_suites"
CmdResultGetTestMetrics executor.Command = "result_get_test_metrics"
)
14 changes: 14 additions & 0 deletions pkg/cloud/data/result/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,17 @@ func (r *CloudRepository) GetTestMetrics(ctx context.Context, name string, limit
func (r *CloudRepository) Count(ctx context.Context, filter result.Filter) (int64, error) {
return 0, nil
}

// GetPreviousFinishedState gets previous finished execution state by test
func (r *CloudRepository) GetPreviousFinishedState(ctx context.Context, testName string, date time.Time) (testkube.ExecutionStatus, error) {
req := GetPreviousFinishedStateRequest{TestName: testName, Date: date}
response, err := r.executor.Execute(ctx, CmdResultGetPreviousFinishedState, req)
if err != nil {
return "", err
}
var commandResponse GetPreviousFinishedStateResponse
if err := json.Unmarshal(response, &commandResponse); err != nil {
return "", err
}
return commandResponse.Result, nil
}
9 changes: 9 additions & 0 deletions pkg/cloud/data/result/result_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,15 @@ type GetExecutionTotalsResponse struct {
Result testkube.ExecutionsTotals `json:"result"`
}

type GetPreviousFinishedStateRequest struct {
TestName string
Date time.Time
}

type GetPreviousFinishedStateResponse struct {
Result testkube.ExecutionStatus
}

type InsertRequest struct {
Result testkube.Execution `json:"result"`
}
Expand Down
24 changes: 24 additions & 0 deletions pkg/cloud/data/result/result_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,30 @@ func TestCloudResultRepository_GetLatestByTest(t *testing.T) {
assert.Equal(t, &endExecution, result)
}

func TestCloudResultRepository_GetPreviousFinishedState(t *testing.T) {
t.Parallel()

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

mockExecutor := executor.NewMockExecutor(mockCtrl)
repo := &CloudRepository{executor: mockExecutor}

testName := "test_name"
date := time.Date(2023, 5, 5, 0, 0, 0, 0, time.UTC)
expectedStatus := testkube.PASSED_ExecutionStatus
response, _ := json.Marshal(GetPreviousFinishedStateResponse{Result: expectedStatus})

mockExecutor.
EXPECT().
Execute(ctx, CmdResultGetPreviousFinishedState, GetPreviousFinishedStateRequest{TestName: testName, Date: date}).
Return(response, nil)

status, err := repo.GetPreviousFinishedState(ctx, testName, date)
assert.NoError(t, err)
assert.Equal(t, expectedStatus, status)
}

func TestCloudResultRepository_Insert(t *testing.T) {
t.Parallel()

Expand Down
Loading

0 comments on commit 53b67f1

Please sign in to comment.