From f6cfe041c50a5b0da47a9d06c81f4f6b9668530d Mon Sep 17 00:00:00 2001 From: Julien Duchesne Date: Wed, 17 Nov 2021 14:02:52 -0500 Subject: [PATCH] Add `min_delay` setting --- README.md | 3 +++ pkg/handlers/launch.go | 44 ++++++++++++++++++++++++++++++++----- pkg/handlers/launch_test.go | 27 ++++++++++++++++++++++- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e1bbb00d..527c4859 100644 --- a/README.md +++ b/README.md @@ -49,4 +49,7 @@ spec: } upload_to_cloud: "true" slack_channels: "channel1,channel2" + notification_context: "My Cluster: `dev-us-east-1`" # Additional context to be added to the end of messages + min_delay: "5m" # Fail all successive runs (keyed to the namespace + name + phase) within the given duration (defaults to 5m) + wait_for_results: "true" # Wait until the K6 analysis is completed before returning. This is required to fail/succeed on thresholds (defaults to true) ``` diff --git a/pkg/handlers/launch.go b/pkg/handlers/launch.go index 8f86eccf..2e4f50cd 100644 --- a/pkg/handlers/launch.go +++ b/pkg/handlers/launch.go @@ -10,6 +10,7 @@ import ( "regexp" "strconv" "strings" + "sync" "time" "github.com/grafana/flagger-k6-webhook/pkg/k6" @@ -23,14 +24,24 @@ var outputRegex = regexp.MustCompile(`output: cloud \((?Phttps:\/\/app\.k6\ type launchPayload struct { flaggerWebhook Metadata struct { - Script string `json:"script"` - UploadToCloudString string `json:"upload_to_cloud"` - UploadToCloud bool + Script string `json:"script"` + + // If true, the test results will be uploaded to cloud + UploadToCloudString string `json:"upload_to_cloud"` + UploadToCloud bool + + // If true, the handler will wait for the k6 run to be completed WaitForResultsString string `json:"wait_for_results"` WaitForResults bool - SlackChannelsString string `json:"slack_channels"` - SlackChannels []string - NotificationContext string `json:"notification_context"` + + // Notification settings. Context is added at the end of the message + SlackChannelsString string `json:"slack_channels"` + SlackChannels []string + NotificationContext string `json:"notification_context"` + + // Min delay between runs. All other runs will fail immediately + MinDelay time.Duration + MinDelayString string `json:"min_delay"` } `json:"metadata"` } @@ -70,6 +81,12 @@ func newLaunchPayload(req *http.Request) (*launchPayload, error) { payload.Metadata.SlackChannels = strings.Split(payload.Metadata.SlackChannelsString, ",") } + if payload.Metadata.MinDelayString == "" { + payload.Metadata.MinDelay = 5 * time.Minute + } else if payload.Metadata.MinDelay, err = time.ParseDuration(payload.Metadata.MinDelayString); err != nil { + return nil, fmt.Errorf("error parsing value for 'min_delay': %w", err) + } + return payload, nil } @@ -77,6 +94,9 @@ type launchHandler struct { client k6.Client slackClient slack.Client + lastRunTime map[string]time.Time + lastRunTimeMutex sync.Mutex + // mockables sleep func(time.Duration) } @@ -86,6 +106,7 @@ func NewLaunchHandler(client k6.Client, slackClient slack.Client) (http.Handler, return &launchHandler{ client: client, slackClient: slackClient, + lastRunTime: make(map[string]time.Time), sleep: time.Sleep, }, nil } @@ -97,6 +118,17 @@ func (h *launchHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { return } + runKey := payload.Namespace + "-" + payload.Name + "-" + payload.Phase + + h.lastRunTimeMutex.Lock() + if v, ok := h.lastRunTime[runKey]; ok && time.Since(v) < payload.Metadata.MinDelay { + logError(req, resp, "not enough time since last run", 400) + h.lastRunTimeMutex.Unlock() + return + } + h.lastRunTime[runKey] = time.Now() + h.lastRunTimeMutex.Unlock() + var buf bytes.Buffer cmd, err := h.client.Start(payload.Metadata.Script, payload.Metadata.UploadToCloud, &buf) if err != nil { diff --git a/pkg/handlers/launch_test.go b/pkg/handlers/launch_test.go index 47dcab27..362c1b2a 100644 --- a/pkg/handlers/launch_test.go +++ b/pkg/handlers/launch_test.go @@ -62,13 +62,14 @@ func TestNewLaunchPayload(t *testing.T) { p.Metadata.UploadToCloud = false p.Metadata.WaitForResults = true p.Metadata.SlackChannels = nil + p.Metadata.MinDelay = 5 * time.Minute return p }(), }, { name: "set values", request: &http.Request{ - Body: ioutil.NopCloser(strings.NewReader(`{"name": "test", "namespace": "test", "phase": "pre-rollout", "metadata": {"script": "my-script", "upload_to_cloud": "true", "wait_for_results": "false", "slack_channels": "test,test2"}}`)), + Body: ioutil.NopCloser(strings.NewReader(`{"name": "test", "namespace": "test", "phase": "pre-rollout", "metadata": {"script": "my-script", "upload_to_cloud": "true", "wait_for_results": "false", "slack_channels": "test,test2", "min_delay": "3m"}}`)), }, want: func() *launchPayload { p := &launchPayload{flaggerWebhook: flaggerWebhook{Name: "test", Namespace: "test", Phase: "pre-rollout"}} @@ -79,6 +80,8 @@ func TestNewLaunchPayload(t *testing.T) { p.Metadata.WaitForResults = false p.Metadata.SlackChannelsString = "test,test2" p.Metadata.SlackChannels = []string{"test", "test2"} + p.Metadata.MinDelay = 3 * time.Minute + p.Metadata.MinDelayString = "3m" return p }(), }, @@ -96,6 +99,13 @@ func TestNewLaunchPayload(t *testing.T) { }, wantErr: errors.New(`error parsing value for 'wait_for_results': strconv.ParseBool: parsing "bad": invalid syntax`), }, + { + name: "invalid min_delay", + request: &http.Request{ + Body: ioutil.NopCloser(strings.NewReader(`{"name": "test", "namespace": "test", "phase": "pre-rollout", "metadata": {"script": "my-script", "min_delay": "bad"}}`)), + }, + wantErr: errors.New(`error parsing value for 'min_delay': time: invalid duration "bad"`), + }, } for _, tc := range testCases { @@ -289,6 +299,21 @@ func TestLaunchAndWaitAndGetError(t *testing.T) { // Expected response assert.Equal(t, "failed to run: exit code 1\n", rr.Body.String()) assert.Equal(t, 400, rr.Result().StatusCode) + + // + // Run it again immediately to get the failure due to min_delay + // + + // Make request + request = &http.Request{ + Body: ioutil.NopCloser(strings.NewReader(`{"name": "test-name", "namespace": "test-space", "phase": "pre-rollout", "metadata": {"script": "my-script", "upload_to_cloud": "false", "slack_channels": "test,test2"}}`)), + } + rr = httptest.NewRecorder() + handler.ServeHTTP(rr, request) + + // Expected response + assert.Equal(t, "not enough time since last run\n", rr.Body.String()) + assert.Equal(t, 400, rr.Result().StatusCode) } func TestLaunchNeverStarted(t *testing.T) {