Skip to content

Commit

Permalink
Add min_delay setting
Browse files Browse the repository at this point in the history
  • Loading branch information
julienduchesne committed Nov 17, 2021
1 parent 6463e7c commit f6cfe04
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
```
44 changes: 38 additions & 6 deletions pkg/handlers/launch.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"

"github.com/grafana/flagger-k6-webhook/pkg/k6"
Expand All @@ -23,14 +24,24 @@ var outputRegex = regexp.MustCompile(`output: cloud \((?P<url>https:\/\/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"`
}

Expand Down Expand Up @@ -70,13 +81,22 @@ 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
}

type launchHandler struct {
client k6.Client
slackClient slack.Client

lastRunTime map[string]time.Time
lastRunTimeMutex sync.Mutex

// mockables
sleep func(time.Duration)
}
Expand All @@ -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
}
Expand All @@ -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 {
Expand Down
27 changes: 26 additions & 1 deletion pkg/handlers/launch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
Expand All @@ -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
}(),
},
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit f6cfe04

Please sign in to comment.