Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple HTTP methods, assertions, and request body in controller #58

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion api/checkly/v1alpha1/apicheck_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ import (

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
type Assertion struct {
// Source of the assertion (e.g., STATUS_CODE, JSON_BODY, etc.)
Source string `json:"source"`

// Property to validate, e.g., a JSONPath expression like $.result (optional)
Property string `json:"property,omitempty"`

// Comparison operation (e.g., EQUALS, NOT_NULL, etc.)
Comparison string `json:"comparison"`

// Target value for the comparison (optional)
Target string `json:"target,omitempty"`
}

// ApiCheckSpec defines the desired state of ApiCheck
type ApiCheckSpec struct {
Expand All @@ -38,13 +51,25 @@ type ApiCheckSpec struct {
Endpoint string `json:"endpoint"`

// Success determines the returned success code, ex. 200
Success string `json:"success"`
Success string `json:"success,omitempty"`

// MaxResponseTime determines what the maximum number of miliseconds can pass before the check fails, default 15000
MaxResponseTime int `json:"maxresponsetime,omitempty"`

// Method defines the HTTP method to use for the check, e.g., GET, POST, PUT (default is GET)
Method string `json:"method,omitempty"`

// Group determines in which group does the check belong to
Group string `json:"group"`

// Body defines the request payload for the check
Body string `json:"body,omitempty"`

// BodyType specifies the format of the request payload, e.g., json, graphql, raw data (default is NONE)
BodyType string `json:"bodyType,omitempty"`

// Assertions define the validation conditions for the check
Assertions []Assertion `json:"assertions,omitempty"`
}

// ApiCheckStatus defines the observed state of ApiCheck
Expand Down
22 changes: 21 additions & 1 deletion api/checkly/v1alpha1/zz_generated.deepcopy.go

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

38 changes: 37 additions & 1 deletion config/crd/bases/k8s.checklyhq.com_apichecks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ spec:
spec:
description: ApiCheckSpec defines the desired state of ApiCheck
properties:
assertions:
description: Assertions define the validation conditions for the check
items:
description: |-
EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
properties:
comparison:
description: Comparison operation (e.g., EQUALS, NOT_NULL, etc.)
type: string
property:
description: Property to validate, e.g., a JSONPath expression
like $.result (optional)
type: string
source:
description: Source of the assertion (e.g., STATUS_CODE, JSON_BODY,
etc.)
type: string
target:
description: Target value for the comparison (optional)
type: string
required:
- comparison
- source
type: object
type: array
body:
description: Body defines the request payload for the check
type: string
bodyType:
description: BodyType specifies the format of the request payload,
e.g., json, graphql, raw data (default is NONE)
type: string
endpoint:
description: Endpoint determines which URL to monitor, ex. https://foo.bar/baz
type: string
Expand All @@ -72,6 +105,10 @@ spec:
description: MaxResponseTime determines what the maximum number of
miliseconds can pass before the check fails, default 15000
type: integer
method:
description: Method defines the HTTP method to use for the check,
e.g., GET, POST, PUT (default is GET)
type: string
muted:
description: Muted determines if the created alert is muted or not,
default false
Expand All @@ -82,7 +119,6 @@ spec:
required:
- endpoint
- group
- success
type: object
status:
description: ApiCheckStatus defines the observed state of ApiCheck
Expand Down
26 changes: 22 additions & 4 deletions docs/api-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
See the [official checkly docs](https://www.checklyhq.com/docs/api-checks/) on what API checks are.

> ***Warning***
> We currently only support GET requests for API Checks.
> The default HTTP method is GET for API Checks. Other methods like POST, PUT, and DELETE are supported but must be explicitly specified in the configuration.

API Checks resources are namespace scoped, meaning they need to be unique inside a namespace and you need to add a `metadata.namespace` field to them.

Expand All @@ -26,10 +26,14 @@ Any `metadata.labels` specified will be transformed into tags, for example `envi
|--------------|-----------|------------|
| `endpoint` | String; Endpoint to run the check against | none (*required) |
| `success` | String; The expected success code | none (*required) |
Copy link

@sorccu sorccu Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this would no longer be required. However it should be kept for B/C and marked deprecated.

| `group` | String; Name of the group to which the check belongs; Kubernetes `Group` resource name` | none (*required)|
| `frequency` | Integer; Frequency of minutes between each check, possible values: 1,2,5,10,15,30,60,120,180 | `5`|
| `group` | String; Name of the group to which the check belongs; Kubernetes `Group` resource name | none (*required) |
| `frequency` | Integer; Frequency of minutes between each check, possible values: 1,2,5,10,15,30,60,120,180 | `5` |
| `muted` | Bool; Is the check muted or not | `false` |
| `maxresponsetime` | Integer; Number of milliseconds to wait for a response | `15000` |
| `method` | String; HTTP method to use (e.g., GET, POST, PUT, DELETE) | `GET` |
| `body` | String; Payload for the HTTP request, if applicable | `""` (empty) |
| `bodyType` | String; Format of the body (e.g., json, graphql, raw data) | `""` (none) |
| `assertions` | Array; A list of conditions to validate the check’s response | none (*optional) |

### Example

Expand All @@ -47,6 +51,16 @@ spec:
frequency: 10 # Default 5
muted: true # Default "false"
group: "checkly-operator-test-group"
method: "POST"
body: '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}'
bodyType: "json"
assertions:
- source: "STATUS_CODE"
comparison: "EQUALS"
target: "200"
- source: "JSON_BODY"
property: "$.status"
comparison: "NOT_NULL"
---
apiVersion: k8s.checklyhq.com/v1alpha1
kind: ApiCheck
Expand All @@ -59,4 +73,8 @@ spec:
endpoint: "https://foo.bar/baaz"
success: "200"
group: "checkly-operator-test-group"
```
method: "GET"
assertions:
- source: "STATUS_CODE"
comparison: "EQUALS"
target: "200"
136 changes: 75 additions & 61 deletions external/checkly/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package external

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"

"github.com/checkly/checkly-go-sdk"
Expand All @@ -32,23 +34,20 @@ type Check struct {
Frequency int
MaxResponseTime int
Endpoint string
SuccessCode string
GroupID int64
ID string
Muted bool
Labels map[string]string
Assertions []checkly.Assertion
Method string
Body string
BodyType string
}

func checklyCheck(apiCheck Check) (check checkly.Check, err error) {

shouldFail, err := shouldFail(apiCheck.SuccessCode)
if err != nil {
return
}

tags := getTags(apiCheck.Labels)
tags = append(tags, "checkly-operator")
tags = append(tags, apiCheck.Namespace)
tags = append(tags, "checkly-operator", apiCheck.Namespace)

alertSettings := checkly.AlertSettings{
EscalationType: checkly.RunBased,
Expand All @@ -67,49 +66,76 @@ func checklyCheck(apiCheck Check) (check checkly.Check, err error) {
},
}

shouldFail := false
assertions := apiCheck.Assertions
if len(assertions) == 0 {
assertions = []checkly.Assertion{
{
Source: checkly.StatusCode,
Comparison: checkly.Equals,
Target: "200",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would change this to apiCheck.SuccessCode, and revert its removal for backwards compat reasons. It should be marked deprecated though.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that is done, then shouldFail should also be updated.

},
}
} else {
for _, assertion := range assertions {
if assertion.Source == checkly.StatusCode && assertion.Comparison == checkly.Equals && assertion.Target >= "400" {
shouldFail = true
break
}
}
}

method := http.MethodGet
if apiCheck.Method != "" {
method = apiCheck.Method
}

body := apiCheck.Body
bodyType := strings.ToUpper(apiCheck.BodyType)
if bodyType == "" {
bodyType = "NONE"
}

if bodyType == "JSON" {
var jsonBody map[string]interface{}
err := json.Unmarshal([]byte(body), &jsonBody)
if err != nil {
return check, fmt.Errorf("invalid JSON body: %w", err)
}

formattedBody, err := json.Marshal(jsonBody)
if err != nil {
return check, fmt.Errorf("failed to format JSON body: %w", err)
}

body = string(formattedBody)
}

check = checkly.Check{
Name: apiCheck.Name,
Type: checkly.TypeAPI,
Frequency: checkValueInt(apiCheck.Frequency, 5),
DegradedResponseTime: 5000,
MaxResponseTime: checkValueInt(apiCheck.MaxResponseTime, 15000),
Activated: true,
Muted: apiCheck.Muted, // muted for development
ShouldFail: shouldFail,
DoubleCheck: false,
SSLCheck: false,
LocalSetupScript: "",
LocalTearDownScript: "",
Locations: []string{},
Tags: tags,
AlertSettings: alertSettings,
UseGlobalAlertSettings: false,
GroupID: apiCheck.GroupID,
Name: apiCheck.Name,
Type: checkly.TypeAPI,
Frequency: checkValueInt(apiCheck.Frequency, 5),
DegradedResponseTime: 5000,
MaxResponseTime: checkValueInt(apiCheck.MaxResponseTime, 15000),
Activated: true,
Muted: apiCheck.Muted,
ShouldFail: shouldFail,
DoubleCheck: false,
SSLCheck: false,
AlertSettings: alertSettings,
Locations: []string{},
Tags: tags,
Request: checkly.Request{
Method: http.MethodGet,
URL: apiCheck.Endpoint,
Headers: []checkly.KeyValue{
// {
// Key: "X-Test",
// Value: "foo",
// },
},
QueryParameters: []checkly.KeyValue{
// {
// Key: "query",
// Value: "foo",
// },
},
Assertions: []checkly.Assertion{
{
Source: checkly.StatusCode,
Comparison: checkly.Equals,
Target: apiCheck.SuccessCode,
},
},
Body: "",
BodyType: "NONE",
Method: method,
URL: apiCheck.Endpoint,
Assertions: assertions,
Headers: []checkly.KeyValue{},
QueryParameters: []checkly.KeyValue{},
Body: body,
BodyType: bodyType,
},
UseGlobalAlertSettings: false,
GroupID: apiCheck.GroupID,
}

return
Expand Down Expand Up @@ -162,15 +188,3 @@ func Delete(ID string, client checkly.Client) (err error) {

return
}

func shouldFail(successCode string) (bool, error) {
code, err := strconv.Atoi(successCode)
if err != nil {
return false, err
}
if code < 400 {
return false, nil
} else {
return true, nil
}
}
Loading