From dac9fad4ce92fdb368ddac504f86e284a2cbbea1 Mon Sep 17 00:00:00 2001 From: Simo Kinnunen Date: Tue, 28 Jan 2025 19:03:50 +0900 Subject: [PATCH] feat: add support for TCP checks (#124) --- checkly.go | 104 ++++++++++++++++++++++++++++++++++-- checkly_integration_test.go | 54 +++++++++++++++++++ types.go | 61 +++++++++++++++++++++ 3 files changed, 215 insertions(+), 4 deletions(-) diff --git a/checkly.go b/checkly.go index 2bf51b1..3dceef2 100644 --- a/checkly.go +++ b/checkly.go @@ -185,14 +185,19 @@ func (c *client) CreateCheck( return nil, err } var checkType string - if check.Type == "BROWSER" { + switch check.Type { + case "BROWSER": checkType = "checks/browser" - } else if check.Type == "API" { + case "API": checkType = "checks/api" - } else if check.Type == "HEARTBEAT" { + case "HEARTBEAT": checkType = "checks/heartbeat" - } else if check.Type == "MULTI_STEP" { + case "MULTI_STEP": checkType = "checks/multistep" + case "TCP": + return nil, fmt.Errorf("user error: use CreateTCPCheck to create TCP checks") + default: + return nil, fmt.Errorf("unknown check type: %s", checkType) } status, res, err := c.apiCall( ctx, @@ -240,6 +245,33 @@ func (c *client) CreateHeartbeat( return &result, nil } +func (c *client) CreateTCPCheck( + ctx context.Context, + check TCPCheck, +) (*TCPCheck, error) { + data, err := json.Marshal(check) + if err != nil { + return nil, err + } + status, res, err := c.apiCall( + ctx, + http.MethodPost, + withAutoAssignAlertsFlag("checks/tcp"), + data, + ) + if err != nil { + return nil, err + } + if status != http.StatusCreated { + return nil, fmt.Errorf("unexpected response status %d: %q", status, res) + } + var result TCPCheck + if err = json.NewDecoder(strings.NewReader(res)).Decode(&result); err != nil { + return nil, fmt.Errorf("decoding error for data %s: %v", res, err) + } + return &result, nil +} + // Update updates an existing check with the specified details. It returns the // updated check, or an error. func (c *client) UpdateCheck( @@ -300,6 +332,44 @@ func (c *client) UpdateHeartbeat( return &result, nil } +func (c *client) UpdateTCPCheck( + ctx context.Context, + ID string, + check TCPCheck, +) (*TCPCheck, error) { + // Unfortunately `checkType` is required for this endpoint, so sneak it in + // using an anonymous struct. + payload := struct { + TCPCheck + Type string `json:"checkType"` + }{ + TCPCheck: check, + Type: "TCP", + } + data, err := json.Marshal(payload) + if err != nil { + return nil, err + } + status, res, err := c.apiCall( + ctx, + http.MethodPut, + withAutoAssignAlertsFlag(fmt.Sprintf("checks/tcp/%s", ID)), + data, + ) + if err != nil { + return nil, err + } + if status != http.StatusOK { + return nil, fmt.Errorf("unexpected response status %d: %q", status, res) + } + var result TCPCheck + err = json.NewDecoder(strings.NewReader(res)).Decode(&result) + if err != nil { + return nil, fmt.Errorf("decoding error for data %s: %v", res, err) + } + return &result, nil +} + // Delete deletes the check with the specified ID. func (c *client) DeleteCheck( ctx context.Context, @@ -372,6 +442,32 @@ func (c *client) GetHeartbeatCheck( return &result, nil } +// GetTCPCheck takes the ID of an existing TCP check, and returns the check +// parameters, or an error. +func (c *client) GetTCPCheck( + ctx context.Context, + ID string, +) (*TCPCheck, error) { + status, res, err := c.apiCall( + ctx, + http.MethodGet, + fmt.Sprintf("checks/%s", ID), + nil, + ) + if err != nil { + return nil, err + } + if status != http.StatusOK { + return nil, fmt.Errorf("unexpected response status %d: %q", status, res) + } + var result TCPCheck + err = json.NewDecoder(strings.NewReader(res)).Decode(&result) + if err != nil { + return nil, fmt.Errorf("decoding error for data %s: %v", res, err) + } + return &result, nil +} + // CreateGroup creates a new check group with the specified details. It returns // the newly-created group, or an error. func (c *client) CreateGroup( diff --git a/checkly_integration_test.go b/checkly_integration_test.go index a4af737..3abebb7 100644 --- a/checkly_integration_test.go +++ b/checkly_integration_test.go @@ -325,3 +325,57 @@ func TestGetPrivateLocationIntegration(t *testing.T) { t.Error(cmp.Diff(testPrivateLocation, *gotPrivateLocation, ignorePrivateLocationFields)) } } + +func TestTCPCheckCRUD(t *testing.T) { + ctx := context.TODO() + + client := setupClient(t) + + pendingCheck := checkly.TCPCheck{ + Name: "TestTCPCheckCRUD", + Muted: false, + Locations: []string{"eu-west-1"}, + Request: checkly.TCPRequest{ + Hostname: "api.checklyhq.com", + Port: 443, + }, + } + + createdCheck, err := client.CreateTCPCheck(ctx, pendingCheck) + if err != nil { + t.Fatalf("failed to create TCP check: %v", err) + } + var didDelete bool + defer func() { + if !didDelete { + _ = client.DeleteCheck(ctx, createdCheck.ID) + } + }() + + if createdCheck.Muted != false { + t.Fatalf("expected Muted to be false after creation") + } + + _, err = client.GetTCPCheck(ctx, createdCheck.ID) + if err != nil { + t.Fatalf("failed to get TCP check: %v", err) + } + + updateCheck := *createdCheck + updateCheck.Muted = true + + updatedCheck, err := client.UpdateTCPCheck(ctx, createdCheck.ID, updateCheck) + if err != nil { + t.Fatalf("failed to update TCP check: %v", err) + } + + if updatedCheck.Muted != true { + t.Fatalf("expected Muted to be true after update") + } + + didDelete = true + err = client.DeleteCheck(ctx, createdCheck.ID) + if err != nil { + t.Fatalf("failed to delete TCP check: %v", err) + } +} diff --git a/types.go b/types.go index a2850ab..98849f8 100644 --- a/types.go +++ b/types.go @@ -72,6 +72,13 @@ type Client interface { check HeartbeatCheck, ) (*HeartbeatCheck, error) + // CreateTCPCheck creates a new TCP check with the specified details. + // It returns the newly-created check, or an error. + CreateTCPCheck( + ctx context.Context, + check TCPCheck, + ) (*TCPCheck, error) + // Update updates an existing check with the specified details. // It returns the updated check, or an error. UpdateCheck( @@ -88,6 +95,14 @@ type Client interface { check HeartbeatCheck, ) (*HeartbeatCheck, error) + // UpdateTCPCheck updates an existing TCP check with the specified details. + // It returns the updated check, or an error. + UpdateTCPCheck( + ctx context.Context, + ID string, + check TCPCheck, + ) (*TCPCheck, error) + // Delete deletes the check with the specified ID. DeleteCheck( ctx context.Context, @@ -101,6 +116,13 @@ type Client interface { ID string, ) (*Check, error) + // Get takes the ID of an existing TCP check, and returns the check + // parameters, or an error. + GetTCPCheck( + ctx context.Context, + ID string, + ) (*TCPCheck, error) + // CreateGroup creates a new check group with the specified details. // It returns the newly-created group, or an error. CreateGroup( @@ -412,6 +434,9 @@ const Headers = "HEADERS" // ResponseTime identifies the response time as an assertion source. const ResponseTime = "RESPONSE_TIME" +// ResponseData identifies the response data of a TCP check as an assertion source. +const ResponseData = "RESPONSE_DATA" + // Assertion comparison constants // Equals asserts that the source and target are equal. @@ -524,6 +549,33 @@ type HeartbeatCheck struct { UpdatedAt time.Time `json:"updatedAt"` } +// TCPCheck represents a TCP check. +type TCPCheck struct { + ID string `json:"id,omitempty"` + Name string `json:"name"` + Frequency int `json:"frequency,omitempty"` + FrequencyOffset int `json:"frequencyOffset,omitempty"` + Activated bool `json:"activated"` + Muted bool `json:"muted"` + ShouldFail bool `json:"shouldFail"` + RunParallel bool `json:"runParallel"` + Locations []string `json:"locations,omitempty"` + DegradedResponseTime int `json:"degradedResponseTime,omitempty"` + MaxResponseTime int `json:"maxResponseTime,omitempty"` + Tags []string `json:"tags,omitempty"` + AlertSettings *AlertSettings `json:"alertSettings,omitempty"` + UseGlobalAlertSettings bool `json:"useGlobalAlertSettings"` + Request TCPRequest `json:"request"` + GroupID int64 `json:"groupId,omitempty"` + GroupOrder int `json:"groupOrder,omitempty"` + AlertChannelSubscriptions []AlertChannelSubscription `json:"alertChannelSubscriptions,omitempty"` + PrivateLocations *[]string `json:"privateLocations,omitempty"` + RuntimeID *string `json:"runtimeId"` + RetryStrategy *RetryStrategy `json:"retryStrategy,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty"` + UpdatedAt time.Time `json:"updated_at,omitempty"` +} + // Heartbeat represents the parameter for the heartbeat check. type Heartbeat struct { Period int `json:"period"` @@ -575,6 +627,15 @@ type KeyValue struct { Locked bool `json:"locked"` } +// TCPRequest represents the parameters for a TCP check's connection. +type TCPRequest struct { + Hostname string `json:"hostname"` + Port uint16 `json:"port"` + Data string `json:"data,omitempty"` + Assertions []Assertion `json:"assertions,omitempty"` + IPFamily string `json:"ipFamily,omitempty"` +} + // EnvironmentVariable represents a key-value pair for setting environment // values during check execution. type EnvironmentVariable struct {