add towerops_check resource for service check management

- new towerops_check resource supporting HTTP, TCP, DNS, and ping checks
- flat schema with type-specific fields (url, host, port, hostname, etc.)
- client CRUD methods for /api/v1/checks endpoints
- resource documentation, examples, and provider index updated
- acceptance tests for all check types, update, import, and error handling
This commit is contained in:
Graham McIntire 2026-03-16 18:33:56 -05:00
parent 3ff5ea41b3
commit bc3dbfe419
No known key found for this signature in database
8 changed files with 1729 additions and 0 deletions

View file

@ -133,6 +133,24 @@ output "agent_token" {
} }
``` ```
### Service Checks
```terraform
resource "towerops_check" "web_health" {
name = "Web Health Check"
check_type = "http"
url = "https://example.com/health"
expected_status = 200
content_match = "\"status\":\"ok\""
}
resource "towerops_check" "gateway_ping" {
name = "Gateway Reachability"
check_type = "ping"
host = "10.0.0.1"
}
```
### Integration ### Integration
```terraform ```terraform

166
docs/resources/check.md Normal file
View file

@ -0,0 +1,166 @@
---
page_title: "towerops_check Resource - TowerOps"
description: |-
Manages a TowerOps service check.
---
# towerops_check (Resource)
Manages a TowerOps service check. Service checks monitor the availability and responsiveness of network services. Supported check types: HTTP, TCP, DNS, and ping.
Configuration fields are flattened into top-level attributes rather than nested inside a config block. Only the fields relevant to the chosen `check_type` are used.
## Example Usage
### HTTP Health Check
```terraform
resource "towerops_check" "web_health" {
name = "Web Health Check"
check_type = "http"
url = "https://example.com/health"
expected_status = 200
content_match = "\"status\":\"ok\""
}
```
### HTTP Check with Custom Settings
```terraform
resource "towerops_check" "api_check" {
name = "API Endpoint"
check_type = "http"
device_id = towerops_device.web_server.id
url = "https://api.example.com/v1/status"
method = "POST"
expected_status = 200
verify_ssl = true
follow_redirects = false
interval_seconds = 30
timeout_ms = 10000
}
```
### TCP Port Check
```terraform
resource "towerops_check" "radius_port" {
name = "FreeRADIUS Auth Port"
check_type = "tcp"
device_id = towerops_device.radius.id
host = "10.0.0.5"
port = 1812
}
```
### TCP Check with Send/Expect
```terraform
resource "towerops_check" "smtp_banner" {
name = "SMTP Banner Check"
check_type = "tcp"
host = "mail.example.com"
port = 25
expect_string = "220"
}
```
### DNS Resolution Check
```terraform
resource "towerops_check" "dns_resolution" {
name = "DNS Resolution"
check_type = "dns"
device_id = towerops_device.dns_server.id
hostname = "google.com"
dns_server = "10.0.0.1"
record_type = "A"
expected_result = "142.250.80.46"
}
```
### Ping Check
```terraform
resource "towerops_check" "gateway_ping" {
name = "Gateway Reachability"
check_type = "ping"
device_id = towerops_device.gateway.id
host = "10.0.0.1"
}
```
### Ping Check with Custom Count
```terraform
resource "towerops_check" "wan_ping" {
name = "WAN Link Ping"
check_type = "ping"
host = "8.8.8.8"
ping_count = 5
interval_seconds = 120
timeout_ms = 10000
}
```
## Schema
### Required
- `name` (String) - The name of the check.
- `check_type` (String) - The type of check: `http`, `tcp`, `dns`, or `ping`. Changing this forces a new resource.
### Optional
- `description` (String) - A description of the check.
- `enabled` (Boolean) - Whether the check is enabled. Default: `true`.
- `device_id` (String) - The ID of the device this check is associated with.
- `agent_token_id` (String) - The ID of the agent token that executes this check.
- `interval_seconds` (Number) - How often the check runs, in seconds. Default: `60`.
- `timeout_ms` (Number) - Timeout for check execution, in milliseconds. Default: `5000`.
- `retry_interval_seconds` (Number) - Interval between retries after a failure, in seconds. Default: `30`.
- `max_check_attempts` (Number) - Number of consecutive failures before transitioning to hard state. Default: `3`.
#### HTTP Check Fields (used when `check_type = "http"`)
- `url` (String) - The URL to check. Required for HTTP checks.
- `method` (String) - HTTP method to use. Default: `"GET"`.
- `expected_status` (Number) - Expected HTTP status code. Default: `200`.
- `verify_ssl` (Boolean) - Whether to verify SSL certificates. Default: `true`.
- `follow_redirects` (Boolean) - Whether to follow HTTP redirects. Default: `true`.
- `content_match` (String) - Regex pattern to match against the response body.
#### TCP Check Fields (used when `check_type = "tcp"`)
- `host` (String) - The host to connect to. Required for TCP checks.
- `port` (Number) - The TCP port to check. Required for TCP checks.
- `send_string` (String) - Data to send after establishing the TCP connection.
- `expect_string` (String) - Expected response string.
#### DNS Check Fields (used when `check_type = "dns"`)
- `hostname` (String) - The hostname to resolve. Required for DNS checks.
- `record_type` (String) - DNS record type (`A`, `AAAA`, `CNAME`, `MX`, `TXT`, `NS`, `PTR`). Default: `"A"`.
- `dns_server` (String) - DNS server to query. Uses system default if not set.
- `expected_result` (String) - Expected DNS resolution result.
#### Ping Check Fields (used when `check_type = "ping"`)
- `host` (String) - The host to ping. Required for ping checks.
- `ping_count` (Number) - Number of ping packets to send. Default: `3`.
### Read-Only
- `id` (String) - The unique identifier of the check.
- `current_state` (Number) - Current check state (0=OK, 1=WARNING, 2=CRITICAL, 3=UNKNOWN).
- `current_state_type` (String) - Current state type (`soft` or `hard`).
- `last_check_at` (String) - Timestamp of the last check execution.
- `inserted_at` (String) - The timestamp when the check was created.
## Import
Checks can be imported using their UUID:
```shell
terraform import towerops_check.web_health 7c9e6679-7425-40de-944b-e07fc1f90ae7
```

View file

@ -83,6 +83,29 @@ resource "towerops_maintenance_window" "network_upgrade" {
ends_at = "2024-03-15T06:00:00Z" ends_at = "2024-03-15T06:00:00Z"
} }
# Service checks
resource "towerops_check" "web_health" {
name = "Web Health Check"
check_type = "http"
url = "https://example.com/health"
expected_status = 200
content_match = "\"status\":\"ok\""
}
resource "towerops_check" "dns_resolution" {
name = "DNS Resolution"
check_type = "dns"
hostname = "google.com"
dns_server = "10.0.0.1"
record_type = "A"
}
resource "towerops_check" "gateway_ping" {
name = "Gateway Reachability"
check_type = "ping"
host = "10.0.0.1"
}
# Output the site ID # Output the site ID
output "site_id" { output "site_id" {
value = towerops_site.main_office.id value = towerops_site.main_office.id

View file

@ -0,0 +1,32 @@
# HTTP health check
resource "towerops_check" "web_health" {
name = "Web Health Check"
check_type = "http"
url = "https://example.com/health"
expected_status = 200
content_match = "\"status\":\"ok\""
}
# TCP port check
resource "towerops_check" "radius_port" {
name = "FreeRADIUS Auth Port"
check_type = "tcp"
host = "10.0.0.5"
port = 1812
}
# DNS resolution check
resource "towerops_check" "dns_resolution" {
name = "DNS Resolution"
check_type = "dns"
hostname = "google.com"
dns_server = "10.0.0.1"
record_type = "A"
}
# Ping check
resource "towerops_check" "gateway_ping" {
name = "Gateway Reachability"
check_type = "ping"
host = "10.0.0.1"
}

View file

@ -0,0 +1,660 @@
package provider
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &CheckResource{}
var _ resource.ResourceWithImportState = &CheckResource{}
// CheckResource defines the resource implementation.
type CheckResource struct {
client *Client
}
// CheckResourceModel describes the resource data model.
type CheckResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
CheckType types.String `tfsdk:"check_type"`
Description types.String `tfsdk:"description"`
Enabled types.Bool `tfsdk:"enabled"`
DeviceID types.String `tfsdk:"device_id"`
AgentTokenID types.String `tfsdk:"agent_token_id"`
IntervalSeconds types.Int64 `tfsdk:"interval_seconds"`
TimeoutMs types.Int64 `tfsdk:"timeout_ms"`
RetryIntervalSeconds types.Int64 `tfsdk:"retry_interval_seconds"`
MaxCheckAttempts types.Int64 `tfsdk:"max_check_attempts"`
// HTTP config
URL types.String `tfsdk:"url"`
Method types.String `tfsdk:"method"`
ExpectedStatus types.Int64 `tfsdk:"expected_status"`
VerifySSL types.Bool `tfsdk:"verify_ssl"`
FollowRedirects types.Bool `tfsdk:"follow_redirects"`
ContentMatch types.String `tfsdk:"content_match"`
// TCP config
Host types.String `tfsdk:"host"`
Port types.Int64 `tfsdk:"port"`
SendString types.String `tfsdk:"send_string"`
ExpectString types.String `tfsdk:"expect_string"`
// DNS config
Hostname types.String `tfsdk:"hostname"`
RecordType types.String `tfsdk:"record_type"`
DNSServer types.String `tfsdk:"dns_server"`
ExpectedResult types.String `tfsdk:"expected_result"`
// Ping config
PingCount types.Int64 `tfsdk:"ping_count"`
// Computed
CurrentState types.Int64 `tfsdk:"current_state"`
CurrentStateType types.String `tfsdk:"current_state_type"`
LastCheckAt types.String `tfsdk:"last_check_at"`
InsertedAt types.String `tfsdk:"inserted_at"`
}
// NewCheckResource creates a new check resource.
func NewCheckResource() resource.Resource {
return &CheckResource{}
}
func (r *CheckResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_check"
}
func (r *CheckResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a TowerOps service check. Supports HTTP, TCP, DNS, and ping check types.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier of the check.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the check.",
Required: true,
},
"check_type": schema.StringAttribute{
Description: "The type of check: http, tcp, dns, or ping.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"description": schema.StringAttribute{
Description: "A description of the check.",
Optional: true,
},
"enabled": schema.BoolAttribute{
Description: "Whether the check is enabled.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"device_id": schema.StringAttribute{
Description: "The ID of the device this check is associated with.",
Optional: true,
},
"agent_token_id": schema.StringAttribute{
Description: "The ID of the agent token that executes this check.",
Optional: true,
},
"interval_seconds": schema.Int64Attribute{
Description: "How often the check runs, in seconds.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(60),
},
"timeout_ms": schema.Int64Attribute{
Description: "Timeout for the check execution, in milliseconds.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(5000),
},
"retry_interval_seconds": schema.Int64Attribute{
Description: "Interval between retries after a failure, in seconds.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(30),
},
"max_check_attempts": schema.Int64Attribute{
Description: "Number of consecutive failures before transitioning to hard state.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3),
},
// HTTP config
"url": schema.StringAttribute{
Description: "The URL to check. Required for HTTP checks.",
Optional: true,
},
"method": schema.StringAttribute{
Description: "HTTP method to use. Defaults to GET.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("GET"),
},
"expected_status": schema.Int64Attribute{
Description: "Expected HTTP status code. Defaults to 200.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(200),
},
"verify_ssl": schema.BoolAttribute{
Description: "Whether to verify SSL certificates. Defaults to true.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"follow_redirects": schema.BoolAttribute{
Description: "Whether to follow HTTP redirects. Defaults to true.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"content_match": schema.StringAttribute{
Description: "Regex pattern to match against HTTP response body.",
Optional: true,
},
// TCP config
"host": schema.StringAttribute{
Description: "The host to check. Used for TCP and ping checks.",
Optional: true,
},
"port": schema.Int64Attribute{
Description: "The TCP port to check. Required for TCP checks.",
Optional: true,
},
"send_string": schema.StringAttribute{
Description: "Data to send after TCP connection.",
Optional: true,
},
"expect_string": schema.StringAttribute{
Description: "Expected response string for TCP checks.",
Optional: true,
},
// DNS config
"hostname": schema.StringAttribute{
Description: "The hostname to resolve. Required for DNS checks.",
Optional: true,
},
"record_type": schema.StringAttribute{
Description: "DNS record type (A, AAAA, CNAME, MX, TXT, NS, PTR). Defaults to A.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("A"),
},
"dns_server": schema.StringAttribute{
Description: "DNS server to query. Uses system default if not set.",
Optional: true,
},
"expected_result": schema.StringAttribute{
Description: "Expected DNS resolution result.",
Optional: true,
},
// Ping config
"ping_count": schema.Int64Attribute{
Description: "Number of ping packets to send. Defaults to 3.",
Optional: true,
Computed: true,
Default: int64default.StaticInt64(3),
},
// Computed
"current_state": schema.Int64Attribute{
Description: "Current check state (0=OK, 1=WARNING, 2=CRITICAL, 3=UNKNOWN).",
Computed: true,
},
"current_state_type": schema.StringAttribute{
Description: "Current state type (soft or hard).",
Computed: true,
},
"last_check_at": schema.StringAttribute{
Description: "Timestamp of the last check execution.",
Computed: true,
},
"inserted_at": schema.StringAttribute{
Description: "The timestamp when the check was created.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *CheckResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *Client, got: %T", req.ProviderData),
)
return
}
r.client = client
}
func (r *CheckResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data CheckResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
check := Check{
Name: data.Name.ValueString(),
CheckType: data.CheckType.ValueString(),
Config: buildConfig(data),
}
if !data.Description.IsNull() {
desc := data.Description.ValueString()
check.Description = &desc
}
if !data.Enabled.IsNull() {
enabled := data.Enabled.ValueBool()
check.Enabled = &enabled
}
if !data.DeviceID.IsNull() {
id := data.DeviceID.ValueString()
check.DeviceID = &id
}
if !data.AgentTokenID.IsNull() {
id := data.AgentTokenID.ValueString()
check.AgentTokenID = &id
}
if !data.IntervalSeconds.IsNull() {
v := int(data.IntervalSeconds.ValueInt64())
check.IntervalSeconds = &v
}
if !data.TimeoutMs.IsNull() {
v := int(data.TimeoutMs.ValueInt64())
check.TimeoutMs = &v
}
if !data.RetryIntervalSeconds.IsNull() {
v := int(data.RetryIntervalSeconds.ValueInt64())
check.RetryIntervalSeconds = &v
}
if !data.MaxCheckAttempts.IsNull() {
v := int(data.MaxCheckAttempts.ValueInt64())
check.MaxCheckAttempts = &v
}
created, err := r.client.CreateCheck(check)
if err != nil {
resp.Diagnostics.AddError("Failed to create check", err.Error())
return
}
mapCheckToState(created, &data)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *CheckResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data CheckResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
check, err := r.client.GetCheck(data.ID.ValueString())
if err != nil {
if errors.Is(err, ErrNotFound) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read check", err.Error())
return
}
mapCheckToState(check, &data)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *CheckResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data CheckResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
check := Check{
Name: data.Name.ValueString(),
CheckType: data.CheckType.ValueString(),
Config: buildConfig(data),
}
if !data.Description.IsNull() {
desc := data.Description.ValueString()
check.Description = &desc
}
if !data.Enabled.IsNull() {
enabled := data.Enabled.ValueBool()
check.Enabled = &enabled
}
if !data.DeviceID.IsNull() {
id := data.DeviceID.ValueString()
check.DeviceID = &id
}
if !data.AgentTokenID.IsNull() {
id := data.AgentTokenID.ValueString()
check.AgentTokenID = &id
}
if !data.IntervalSeconds.IsNull() {
v := int(data.IntervalSeconds.ValueInt64())
check.IntervalSeconds = &v
}
if !data.TimeoutMs.IsNull() {
v := int(data.TimeoutMs.ValueInt64())
check.TimeoutMs = &v
}
if !data.RetryIntervalSeconds.IsNull() {
v := int(data.RetryIntervalSeconds.ValueInt64())
check.RetryIntervalSeconds = &v
}
if !data.MaxCheckAttempts.IsNull() {
v := int(data.MaxCheckAttempts.ValueInt64())
check.MaxCheckAttempts = &v
}
updated, err := r.client.UpdateCheck(data.ID.ValueString(), check)
if err != nil {
if errors.Is(err, ErrNotFound) {
// Check deleted outside Terraform, recreate
created, createErr := r.client.CreateCheck(check)
if createErr != nil {
resp.Diagnostics.AddError("Failed to create check (after 404 on update)", createErr.Error())
return
}
mapCheckToState(created, &data)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
resp.Diagnostics.AddError("Failed to update check", err.Error())
return
}
mapCheckToState(updated, &data)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *CheckResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data CheckResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteCheck(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to delete check", err.Error())
return
}
}
func (r *CheckResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}
// buildConfig constructs the config map from flat Terraform attributes based on check_type.
func buildConfig(data CheckResourceModel) map[string]any {
config := map[string]any{}
checkType := data.CheckType.ValueString()
switch checkType {
case "http":
if !data.URL.IsNull() {
config["url"] = data.URL.ValueString()
}
if !data.Method.IsNull() {
config["method"] = data.Method.ValueString()
}
if !data.ExpectedStatus.IsNull() {
config["expected_status"] = int(data.ExpectedStatus.ValueInt64())
}
if !data.VerifySSL.IsNull() {
config["verify_ssl"] = data.VerifySSL.ValueBool()
}
if !data.FollowRedirects.IsNull() {
config["follow_redirects"] = data.FollowRedirects.ValueBool()
}
if !data.ContentMatch.IsNull() {
config["regex"] = data.ContentMatch.ValueString()
}
case "tcp":
if !data.Host.IsNull() {
config["host"] = data.Host.ValueString()
}
if !data.Port.IsNull() {
config["port"] = int(data.Port.ValueInt64())
}
if !data.SendString.IsNull() {
config["send"] = data.SendString.ValueString()
}
if !data.ExpectString.IsNull() {
config["expect"] = data.ExpectString.ValueString()
}
case "dns":
if !data.Hostname.IsNull() {
config["hostname"] = data.Hostname.ValueString()
}
if !data.RecordType.IsNull() {
config["record_type"] = data.RecordType.ValueString()
}
if !data.DNSServer.IsNull() {
config["server"] = data.DNSServer.ValueString()
}
if !data.ExpectedResult.IsNull() {
config["expected"] = data.ExpectedResult.ValueString()
}
case "ping":
if !data.Host.IsNull() {
config["host"] = data.Host.ValueString()
}
if !data.PingCount.IsNull() {
config["count"] = int(data.PingCount.ValueInt64())
}
}
return config
}
// mapCheckToState maps an API Check response back into the Terraform state model.
func mapCheckToState(check *Check, data *CheckResourceModel) {
data.ID = types.StringValue(check.ID)
data.Name = types.StringValue(check.Name)
data.CheckType = types.StringValue(check.CheckType)
data.InsertedAt = types.StringValue(check.InsertedAt)
if check.Description != nil {
data.Description = types.StringValue(*check.Description)
} else {
data.Description = types.StringNull()
}
if check.Enabled != nil {
data.Enabled = types.BoolValue(*check.Enabled)
}
if check.DeviceID != nil {
data.DeviceID = types.StringValue(*check.DeviceID)
} else {
data.DeviceID = types.StringNull()
}
if check.AgentTokenID != nil {
data.AgentTokenID = types.StringValue(*check.AgentTokenID)
} else {
data.AgentTokenID = types.StringNull()
}
if check.IntervalSeconds != nil {
data.IntervalSeconds = types.Int64Value(int64(*check.IntervalSeconds))
}
if check.TimeoutMs != nil {
data.TimeoutMs = types.Int64Value(int64(*check.TimeoutMs))
}
if check.RetryIntervalSeconds != nil {
data.RetryIntervalSeconds = types.Int64Value(int64(*check.RetryIntervalSeconds))
}
if check.MaxCheckAttempts != nil {
data.MaxCheckAttempts = types.Int64Value(int64(*check.MaxCheckAttempts))
}
if check.CurrentState != nil {
data.CurrentState = types.Int64Value(int64(*check.CurrentState))
} else {
data.CurrentState = types.Int64Null()
}
if check.CurrentStateType != nil {
data.CurrentStateType = types.StringValue(*check.CurrentStateType)
} else {
data.CurrentStateType = types.StringNull()
}
if check.LastCheckAt != nil {
data.LastCheckAt = types.StringValue(*check.LastCheckAt)
} else {
data.LastCheckAt = types.StringNull()
}
// Unpack config into flat fields based on check_type
unpackConfig(check.Config, check.CheckType, data)
}
// unpackConfig reads the config map and sets flat Terraform attributes.
func unpackConfig(config map[string]any, checkType string, data *CheckResourceModel) {
if config == nil {
return
}
switch checkType {
case "http":
if v, ok := config["url"].(string); ok {
data.URL = types.StringValue(v)
}
if v, ok := config["method"].(string); ok {
data.Method = types.StringValue(v)
}
if v, ok := configInt(config, "expected_status"); ok {
data.ExpectedStatus = types.Int64Value(int64(v))
}
if v, ok := config["verify_ssl"].(bool); ok {
data.VerifySSL = types.BoolValue(v)
}
if v, ok := config["follow_redirects"].(bool); ok {
data.FollowRedirects = types.BoolValue(v)
}
if v, ok := config["regex"].(string); ok {
data.ContentMatch = types.StringValue(v)
}
case "tcp":
if v, ok := config["host"].(string); ok {
data.Host = types.StringValue(v)
}
if v, ok := configInt(config, "port"); ok {
data.Port = types.Int64Value(int64(v))
}
if v, ok := config["send"].(string); ok {
data.SendString = types.StringValue(v)
}
if v, ok := config["expect"].(string); ok {
data.ExpectString = types.StringValue(v)
}
case "dns":
if v, ok := config["hostname"].(string); ok {
data.Hostname = types.StringValue(v)
}
if v, ok := config["record_type"].(string); ok {
data.RecordType = types.StringValue(v)
}
if v, ok := config["server"].(string); ok {
data.DNSServer = types.StringValue(v)
}
if v, ok := config["expected"].(string); ok {
data.ExpectedResult = types.StringValue(v)
}
case "ping":
if v, ok := config["host"].(string); ok {
data.Host = types.StringValue(v)
}
if v, ok := configInt(config, "count"); ok {
data.PingCount = types.Int64Value(int64(v))
}
}
}
// configInt extracts an integer from a config map, handling JSON's float64 encoding.
func configInt(config map[string]any, key string) (int, bool) {
v, ok := config[key]
if !ok {
return 0, false
}
switch n := v.(type) {
case float64:
return int(n), true
case int:
return n, true
case int64:
return int(n), true
default:
return 0, false
}
}

View file

@ -0,0 +1,756 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"sync"
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)
func TestAccCheckResource_http(t *testing.T) {
var checkID string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "test-http-check-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "Web Health",
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com/health",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "Web Health",
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com/health",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceHTTP(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "Web Health"),
resource.TestCheckResourceAttr("towerops_check.test", "check_type", "http"),
resource.TestCheckResourceAttr("towerops_check.test", "url", "https://example.com/health"),
resource.TestCheckResourceAttr("towerops_check.test", "expected_status", "200"),
resource.TestCheckResourceAttr("towerops_check.test", "enabled", "true"),
resource.TestCheckResourceAttrSet("towerops_check.test", "id"),
resource.TestCheckResourceAttrSet("towerops_check.test", "inserted_at"),
),
},
},
})
}
func TestAccCheckResource_tcp(t *testing.T) {
var checkID string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
deviceID := "device-123"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "test-tcp-check-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "MySQL Port",
CheckType: "tcp",
DeviceID: &deviceID,
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"host": "10.0.0.5",
"port": 3306,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "MySQL Port",
CheckType: "tcp",
DeviceID: &deviceID,
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"host": "10.0.0.5",
"port": 3306,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceTCP(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "MySQL Port"),
resource.TestCheckResourceAttr("towerops_check.test", "check_type", "tcp"),
resource.TestCheckResourceAttr("towerops_check.test", "host", "10.0.0.5"),
resource.TestCheckResourceAttr("towerops_check.test", "port", "3306"),
resource.TestCheckResourceAttr("towerops_check.test", "device_id", "device-123"),
),
},
},
})
}
func TestAccCheckResource_dns(t *testing.T) {
var checkID string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "test-dns-check-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "DNS Resolution",
CheckType: "dns",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"hostname": "google.com",
"server": "8.8.8.8",
"record_type": "A",
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "DNS Resolution",
CheckType: "dns",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"hostname": "google.com",
"server": "8.8.8.8",
"record_type": "A",
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceDNS(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "DNS Resolution"),
resource.TestCheckResourceAttr("towerops_check.test", "check_type", "dns"),
resource.TestCheckResourceAttr("towerops_check.test", "hostname", "google.com"),
resource.TestCheckResourceAttr("towerops_check.test", "dns_server", "8.8.8.8"),
resource.TestCheckResourceAttr("towerops_check.test", "record_type", "A"),
),
},
},
})
}
func TestAccCheckResource_ping(t *testing.T) {
var checkID string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
deviceID := "device-gw"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "test-ping-check-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "Gateway Ping",
CheckType: "ping",
DeviceID: &deviceID,
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"host": "10.0.0.1",
"count": 3,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: "Gateway Ping",
CheckType: "ping",
DeviceID: &deviceID,
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"host": "10.0.0.1",
"count": 3,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourcePing(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "Gateway Ping"),
resource.TestCheckResourceAttr("towerops_check.test", "check_type", "ping"),
resource.TestCheckResourceAttr("towerops_check.test", "host", "10.0.0.1"),
resource.TestCheckResourceAttr("towerops_check.test", "ping_count", "3"),
resource.TestCheckResourceAttr("towerops_check.test", "device_id", "device-gw"),
),
},
},
})
}
func TestAccCheckResource_update(t *testing.T) {
var checkID string
var currentName string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "test-update-check-id"
currentName = "Original"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: currentName,
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: currentName,
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/checks/"+checkID:
var body map[string]Check
json.NewDecoder(r.Body).Decode(&body)
currentName = body["check"].Name
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(Check{
ID: checkID,
Name: currentName,
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
})
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceHTTPNamed(server.URL, "Original"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "Original"),
),
},
{
Config: testAccCheckResourceHTTPNamed(server.URL, "Updated"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "Updated"),
),
},
},
})
}
func TestAccCheckResource_recreateOn404(t *testing.T) {
var checkID string
var checkDeleted bool
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
checkResp := Check{
ID: "new-check-id",
Name: "Web Health",
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com/health",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
}
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "new-check-id"
checkDeleted = false
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(checkResp)
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/"+checkID:
if checkDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "check not found"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(checkResp)
case r.Method == http.MethodPatch && r.URL.Path == "/api/v1/checks/"+checkID:
if checkDeleted {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{"error": "check not found"}`))
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(checkResp)
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/"+checkID:
checkDeleted = true
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceHTTP(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("towerops_check.test", "name", "Web Health"),
),
},
{
PreConfig: func() {
mu.Lock()
checkDeleted = true
mu.Unlock()
},
Config: testAccCheckResourceHTTP(server.URL),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttrSet("towerops_check.test", "id"),
),
},
},
})
}
func TestAccCheckResource_importState(t *testing.T) {
var checkID string
var mu sync.Mutex
enabled := true
interval := 60
timeout := 5000
retry := 30
maxAttempts := 3
currentState := 3
stateType := "soft"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
checkResp := Check{
ID: "imported-check-id",
Name: "Web Health",
CheckType: "http",
Enabled: &enabled,
IntervalSeconds: &interval,
TimeoutMs: &timeout,
RetryIntervalSeconds: &retry,
MaxCheckAttempts: &maxAttempts,
CurrentState: &currentState,
CurrentStateType: &stateType,
Config: map[string]any{
"url": "https://example.com/health",
"method": "GET",
"expected_status": 200,
"verify_ssl": true,
"follow_redirects": true,
},
InsertedAt: "2024-01-01T00:00:00Z",
}
switch {
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks":
checkID = "imported-check-id"
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(checkResp)
case r.Method == http.MethodGet && r.URL.Path == "/api/v1/checks/imported-check-id":
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(checkResp)
case r.Method == http.MethodDelete && r.URL.Path == "/api/v1/checks/imported-check-id":
w.WriteHeader(http.StatusNoContent)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
_ = checkID
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceHTTP(server.URL),
},
{
ResourceName: "towerops_check.test",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ping_count", "record_type"},
},
},
})
}
func TestAccCheckResource_createError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && r.URL.Path == "/api/v1/checks" {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(`{"error": "name is required"}`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(server.URL),
Steps: []resource.TestStep{
{
Config: testAccCheckResourceHTTP(server.URL),
ExpectError: regexp.MustCompile(`Failed to create check`),
},
},
})
}
// HCL config helpers
func testAccCheckResourceHTTP(apiURL string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_check" "test" {
name = "Web Health"
check_type = "http"
url = "https://example.com/health"
expected_status = 200
}
`, apiURL)
}
func testAccCheckResourceHTTPNamed(apiURL, name string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_check" "test" {
name = %q
check_type = "http"
url = "https://example.com"
expected_status = 200
}
`, apiURL, name)
}
func testAccCheckResourceTCP(apiURL string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_check" "test" {
name = "MySQL Port"
check_type = "tcp"
device_id = "device-123"
host = "10.0.0.5"
port = 3306
}
`, apiURL)
}
func testAccCheckResourceDNS(apiURL string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_check" "test" {
name = "DNS Resolution"
check_type = "dns"
hostname = "google.com"
dns_server = "8.8.8.8"
record_type = "A"
}
`, apiURL)
}
func testAccCheckResourcePing(apiURL string) string {
return fmt.Sprintf(`
provider "towerops" {
token = "test-token"
api_url = %q
}
resource "towerops_check" "test" {
name = "Gateway Ping"
check_type = "ping"
device_id = "device-gw"
host = "10.0.0.1"
ping_count = 3
}
`, apiURL)
}

View file

@ -559,6 +559,79 @@ func (c *Client) DeleteMaintenanceWindow(id string) error {
return err return err
} }
// Check represents a TowerOps service check.
type Check struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
CheckType string `json:"check_type"`
Description *string `json:"description,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
DeviceID *string `json:"device_id,omitempty"`
AgentTokenID *string `json:"agent_token_id,omitempty"`
IntervalSeconds *int `json:"interval_seconds,omitempty"`
TimeoutMs *int `json:"timeout_ms,omitempty"`
RetryIntervalSeconds *int `json:"retry_interval_seconds,omitempty"`
MaxCheckAttempts *int `json:"max_check_attempts,omitempty"`
Config map[string]any `json:"config"`
CurrentState *int `json:"current_state,omitempty"`
CurrentStateType *string `json:"current_state_type,omitempty"`
LastCheckAt *string `json:"last_check_at,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"`
}
// CreateCheck creates a new service check.
func (c *Client) CreateCheck(check Check) (*Check, error) {
body := map[string]Check{"check": check}
respBody, err := c.doRequest(http.MethodPost, "/api/v1/checks", body)
if err != nil {
return nil, err
}
var result Check
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// GetCheck retrieves a check by ID.
func (c *Client) GetCheck(id string) (*Check, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/checks/"+id, nil)
if err != nil {
return nil, err
}
var result Check
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// UpdateCheck updates an existing check.
func (c *Client) UpdateCheck(id string, check Check) (*Check, error) {
body := map[string]Check{"check": check}
respBody, err := c.doRequest(http.MethodPatch, "/api/v1/checks/"+id, body)
if err != nil {
return nil, err
}
var result Check
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// DeleteCheck deletes a check.
func (c *Client) DeleteCheck(id string) error {
_, err := c.doRequest(http.MethodDelete, "/api/v1/checks/"+id, nil)
return err
}
// GetOrganization retrieves the current organization settings. // GetOrganization retrieves the current organization settings.
func (c *Client) GetOrganization() (*Organization, error) { func (c *Client) GetOrganization() (*Organization, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/organization", nil) respBody, err := c.doRequest(http.MethodGet, "/api/v1/organization", nil)

View file

@ -89,6 +89,7 @@ func (p *ToweropsProvider) Resources(ctx context.Context) []func() resource.Reso
NewOrganizationResource, NewOrganizationResource,
NewSiteResource, NewSiteResource,
NewDeviceResource, NewDeviceResource,
NewCheckResource,
NewScheduleResource, NewScheduleResource,
NewEscalationPolicyResource, NewEscalationPolicyResource,
NewAgentResource, NewAgentResource,