add agent, integration, and maintenance window resources

New Terraform resources: towerops_agent, towerops_integration,
towerops_maintenance_window with full CRUD, docs, and examples.
This commit is contained in:
Graham McIntire 2026-03-11 14:33:52 -05:00
parent e7634da384
commit bbe1babdf7
No known key found for this signature in database
13 changed files with 1172 additions and 0 deletions

View file

@ -117,6 +117,39 @@ resource "towerops_escalation_policy" "critical" {
}
```
### Agent Token
```terraform
resource "towerops_agent" "office" {
name = "Office Poller"
}
output "agent_token" {
value = towerops_agent.office.token
sensitive = true
}
```
### Integration
```terraform
resource "towerops_integration" "pagerduty" {
provider = "pagerduty"
enabled = true
}
```
### Maintenance Window
```terraform
resource "towerops_maintenance_window" "network_upgrade" {
name = "Network Upgrade"
reason = "Upgrading core switches to new firmware"
starts_at = "2024-03-15T02:00:00Z"
ends_at = "2024-03-15T06:00:00Z"
}
```
## Schema
### Required

54
docs/resources/agent.md Normal file
View file

@ -0,0 +1,54 @@
---
page_title: "towerops_agent Resource - TowerOps"
description: |-
Manages a TowerOps agent token.
---
# towerops_agent (Resource)
Manages a TowerOps agent token. Agents are deployed on customer networks to poll devices via SNMP, ping, and SSH. The agent token is returned only on creation and cannot be retrieved again.
~> **Note:** The `token` attribute is only available after creation. If the state is lost, the agent must be deleted and recreated to obtain a new token.
## Example Usage
### Basic Agent
```terraform
resource "towerops_agent" "office" {
name = "Office Poller"
}
```
### Using the Token
```terraform
resource "towerops_agent" "remote" {
name = "Remote Site Poller"
}
output "agent_token" {
value = towerops_agent.remote.token
sensitive = true
}
```
## Schema
### Required
- `name` (String) - The name of the agent. Changing this forces a new resource to be created.
### Read-Only
- `id` (String) - The unique identifier of the agent.
- `token` (String, Sensitive) - The bearer token for this agent. Only available after creation and cannot be retrieved again.
- `inserted_at` (String) - The timestamp when the agent was created.
## Import
Agents can be imported using their UUID. Note that the token will not be available after import.
```shell
terraform import towerops_agent.example 550e8400-e29b-41d4-a716-446655440000
```

View file

@ -0,0 +1,63 @@
---
page_title: "towerops_integration Resource - TowerOps"
description: |-
Manages a TowerOps integration with third-party services.
---
# towerops_integration (Resource)
Manages a TowerOps integration with third-party services such as PagerDuty, Slack, or webhooks.
## Example Usage
### PagerDuty Integration
```terraform
resource "towerops_integration" "pagerduty" {
provider = "pagerduty"
enabled = true
}
```
### Webhook Integration with Sync Interval
```terraform
resource "towerops_integration" "webhook" {
provider = "webhook"
enabled = true
sync_interval_minutes = 15
}
```
### Disabled Integration
```terraform
resource "towerops_integration" "slack" {
provider = "slack"
enabled = false
}
```
## Schema
### Required
- `provider` (String) - The integration provider type (e.g. `pagerduty`, `slack`, `webhook`).
### Optional
- `enabled` (Boolean) - Whether the integration is enabled. Defaults to `true`.
- `sync_interval_minutes` (Number) - How often the integration syncs, in minutes.
### Read-Only
- `id` (String) - The unique identifier of the integration.
- `inserted_at` (String) - The timestamp when the integration was created.
## Import
Integrations can be imported using their UUID:
```shell
terraform import towerops_integration.example 550e8400-e29b-41d4-a716-446655440000
```

View file

@ -0,0 +1,74 @@
---
page_title: "towerops_maintenance_window Resource - TowerOps"
description: |-
Manages a TowerOps maintenance window.
---
# towerops_maintenance_window (Resource)
Manages a TowerOps maintenance window. Maintenance windows suppress alerts during planned work periods and can be scoped to specific sites or devices.
## Example Usage
### Organization-Wide Maintenance Window
```terraform
resource "towerops_maintenance_window" "network_upgrade" {
name = "Network Upgrade"
reason = "Upgrading core switches to new firmware"
starts_at = "2024-03-15T02:00:00Z"
ends_at = "2024-03-15T06:00:00Z"
}
```
### Site-Scoped Maintenance Window
```terraform
resource "towerops_maintenance_window" "office_maintenance" {
name = "Office Network Maintenance"
reason = "Replacing office UPS"
starts_at = "2024-03-20T22:00:00Z"
ends_at = "2024-03-21T02:00:00Z"
site_id = towerops_site.main_office.id
}
```
### Device-Scoped Maintenance Window
```terraform
resource "towerops_maintenance_window" "router_update" {
name = "Router Firmware Update"
starts_at = "2024-03-18T03:00:00Z"
ends_at = "2024-03-18T04:00:00Z"
device_id = towerops_device.core_router.id
suppress_alerts = true
}
```
## Schema
### Required
- `name` (String) - The name of the maintenance window.
- `starts_at` (String) - The start time in ISO 8601 format (e.g. `2024-01-15T02:00:00Z`).
- `ends_at` (String) - The end time in ISO 8601 format (e.g. `2024-01-15T06:00:00Z`).
### Optional
- `reason` (String) - The reason for the maintenance window.
- `suppress_alerts` (Boolean) - Whether to suppress alerts during the window. Defaults to `true`.
- `site_id` (String) - The site to apply the maintenance window to. If omitted, applies to all sites.
- `device_id` (String) - The device to apply the maintenance window to. If omitted, applies to all devices.
### Read-Only
- `id` (String) - The unique identifier of the maintenance window.
- `inserted_at` (String) - The timestamp when the maintenance window was created.
## Import
Maintenance windows can be imported using their UUID:
```shell
terraform import towerops_maintenance_window.example 550e8400-e29b-41d4-a716-446655440000
```

View file

@ -64,6 +64,25 @@ resource "towerops_escalation_policy" "default" {
repeat_count = 3
}
# Create an agent token
resource "towerops_agent" "remote" {
name = "Remote Site Poller"
}
# Create an integration
resource "towerops_integration" "pagerduty" {
provider = "pagerduty"
enabled = true
}
# Create a maintenance window
resource "towerops_maintenance_window" "network_upgrade" {
name = "Network Upgrade"
reason = "Upgrading core switches"
starts_at = "2024-03-15T02:00:00Z"
ends_at = "2024-03-15T06:00:00Z"
}
# Output the site ID
output "site_id" {
value = towerops_site.main_office.id
@ -80,3 +99,8 @@ output "schedule_id" {
output "escalation_policy_id" {
value = towerops_escalation_policy.default.id
}
output "agent_token" {
value = towerops_agent.remote.token
sensitive = true
}

View file

@ -0,0 +1,8 @@
resource "towerops_agent" "office" {
name = "Office Poller"
}
output "agent_token" {
value = towerops_agent.office.token
sensitive = true
}

View file

@ -0,0 +1,5 @@
resource "towerops_integration" "pagerduty" {
provider = "pagerduty"
enabled = true
sync_interval_minutes = 5
}

View file

@ -0,0 +1,6 @@
resource "towerops_maintenance_window" "network_upgrade" {
name = "Network Upgrade"
reason = "Upgrading core switches to new firmware"
starts_at = "2024-03-15T02:00:00Z"
ends_at = "2024-03-15T06:00:00Z"
}

View file

@ -0,0 +1,173 @@
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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &AgentResource{}
var _ resource.ResourceWithImportState = &AgentResource{}
// AgentResource defines the resource implementation.
type AgentResource struct {
client *Client
}
// AgentResourceModel describes the resource data model.
type AgentResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Token types.String `tfsdk:"token"`
InsertedAt types.String `tfsdk:"inserted_at"`
}
// NewAgentResource creates a new agent resource.
func NewAgentResource() resource.Resource {
return &AgentResource{}
}
func (r *AgentResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_agent"
}
func (r *AgentResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a TowerOps agent token. Agents are deployed on customer networks to poll devices via SNMP, ping, and SSH.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier of the agent.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the agent. Changing this forces a new resource to be created.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"token": schema.StringAttribute{
Description: "The bearer token for this agent. Only available after creation and cannot be retrieved again.",
Computed: true,
Sensitive: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"inserted_at": schema.StringAttribute{
Description: "The timestamp when the agent was created.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *AgentResource) 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 *AgentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data AgentResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
agent := Agent{
Name: data.Name.ValueString(),
}
created, err := r.client.CreateAgent(agent)
if err != nil {
resp.Diagnostics.AddError("Failed to create agent", err.Error())
return
}
data.ID = types.StringValue(created.ID)
data.Token = types.StringValue(created.Token)
data.InsertedAt = types.StringValue(created.InsertedAt)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *AgentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data AgentResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
agent, err := r.client.GetAgent(data.ID.ValueString())
if err != nil {
if errors.Is(err, ErrNotFound) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read agent", err.Error())
return
}
data.Name = types.StringValue(agent.Name)
data.InsertedAt = types.StringValue(agent.InsertedAt)
// Token is not returned by GET, preserve existing state value
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *AgentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Agents only support name changes via the API, but the REST API doesn't have
// an update endpoint. For now, if name changes, we must destroy and recreate.
// This is handled by Terraform's ForceNew-like behavior since all mutable
// attributes are required.
resp.Diagnostics.AddError(
"Agent Update Not Supported",
"Agent tokens cannot be updated. Delete and recreate the agent to change its name.",
)
}
func (r *AgentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data AgentResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteAgent(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to delete agent", err.Error())
return
}
}
func (r *AgentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View file

@ -367,6 +367,195 @@ func (c *Client) DeleteEscalationPolicy(id string) error {
return err
}
// Agent represents a TowerOps agent token.
type Agent struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Enabled *bool `json:"enabled,omitempty"`
LastSeenAt *string `json:"last_seen_at,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"`
Token string `json:"token,omitempty"`
}
// agentCreateResponse wraps the create response which includes token.
type agentCreateResponse struct {
Agent
}
// CreateAgent creates a new agent token.
func (c *Client) CreateAgent(agent Agent) (*Agent, error) {
body := map[string]Agent{"agent": agent}
respBody, err := c.doRequest(http.MethodPost, "/api/v1/agents", body)
if err != nil {
return nil, err
}
var result Agent
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// GetAgent retrieves an agent by ID.
func (c *Client) GetAgent(id string) (*Agent, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/agents/"+id, nil)
if err != nil {
return nil, err
}
var result Agent
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// DeleteAgent deletes an agent.
func (c *Client) DeleteAgent(id string) error {
_, err := c.doRequest(http.MethodDelete, "/api/v1/agents/"+id, nil)
return err
}
// Integration represents a TowerOps integration.
type Integration struct {
ID string `json:"id,omitempty"`
Provider string `json:"provider"`
Enabled *bool `json:"enabled,omitempty"`
SyncIntervalMinutes *int `json:"sync_interval_minutes,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
// integrationWithCredentials is used for create/update requests that include credentials.
type integrationWithCredentials struct {
Provider string `json:"provider"`
Enabled *bool `json:"enabled,omitempty"`
Credentials map[string]interface{} `json:"credentials,omitempty"`
SyncIntervalMinutes *int `json:"sync_interval_minutes,omitempty"`
}
// CreateIntegration creates a new integration.
func (c *Client) CreateIntegration(integration integrationWithCredentials) (*Integration, error) {
body := map[string]integrationWithCredentials{"integration": integration}
respBody, err := c.doRequest(http.MethodPost, "/api/v1/integrations", body)
if err != nil {
return nil, err
}
var result Integration
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// GetIntegration retrieves an integration by ID.
func (c *Client) GetIntegration(id string) (*Integration, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/integrations/"+id, nil)
if err != nil {
return nil, err
}
var result Integration
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// UpdateIntegration updates an existing integration.
func (c *Client) UpdateIntegration(id string, integration integrationWithCredentials) (*Integration, error) {
body := map[string]integrationWithCredentials{"integration": integration}
respBody, err := c.doRequest(http.MethodPatch, "/api/v1/integrations/"+id, body)
if err != nil {
return nil, err
}
var result Integration
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// DeleteIntegration deletes an integration.
func (c *Client) DeleteIntegration(id string) error {
_, err := c.doRequest(http.MethodDelete, "/api/v1/integrations/"+id, nil)
return err
}
// MaintenanceWindowAPI represents a TowerOps maintenance window.
type MaintenanceWindowAPI struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Reason *string `json:"reason,omitempty"`
StartsAt string `json:"starts_at"`
EndsAt string `json:"ends_at"`
SuppressAlerts *bool `json:"suppress_alerts,omitempty"`
SiteID *string `json:"site_id,omitempty"`
DeviceID *string `json:"device_id,omitempty"`
InsertedAt string `json:"inserted_at,omitempty"`
}
// CreateMaintenanceWindow creates a new maintenance window.
func (c *Client) CreateMaintenanceWindow(window MaintenanceWindowAPI) (*MaintenanceWindowAPI, error) {
body := map[string]MaintenanceWindowAPI{"maintenance_window": window}
respBody, err := c.doRequest(http.MethodPost, "/api/v1/maintenance_windows", body)
if err != nil {
return nil, err
}
var result MaintenanceWindowAPI
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// GetMaintenanceWindow retrieves a maintenance window by ID.
func (c *Client) GetMaintenanceWindow(id string) (*MaintenanceWindowAPI, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/maintenance_windows/"+id, nil)
if err != nil {
return nil, err
}
var result MaintenanceWindowAPI
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// UpdateMaintenanceWindow updates an existing maintenance window.
func (c *Client) UpdateMaintenanceWindow(id string, window MaintenanceWindowAPI) (*MaintenanceWindowAPI, error) {
body := map[string]MaintenanceWindowAPI{"maintenance_window": window}
respBody, err := c.doRequest(http.MethodPatch, "/api/v1/maintenance_windows/"+id, body)
if err != nil {
return nil, err
}
var result MaintenanceWindowAPI
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
return &result, nil
}
// DeleteMaintenanceWindow deletes a maintenance window.
func (c *Client) DeleteMaintenanceWindow(id string) error {
_, err := c.doRequest(http.MethodDelete, "/api/v1/maintenance_windows/"+id, nil)
return err
}
// GetOrganization retrieves the current organization settings.
func (c *Client) GetOrganization() (*Organization, error) {
respBody, err := c.doRequest(http.MethodGet, "/api/v1/organization", nil)

View file

@ -0,0 +1,244 @@
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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &IntegrationResource{}
var _ resource.ResourceWithImportState = &IntegrationResource{}
// IntegrationResource defines the resource implementation.
type IntegrationResource struct {
client *Client
}
// IntegrationResourceModel describes the resource data model.
type IntegrationResourceModel struct {
ID types.String `tfsdk:"id"`
Provider types.String `tfsdk:"provider"`
Enabled types.Bool `tfsdk:"enabled"`
SyncIntervalMinutes types.Int64 `tfsdk:"sync_interval_minutes"`
InsertedAt types.String `tfsdk:"inserted_at"`
}
// NewIntegrationResource creates a new integration resource.
func NewIntegrationResource() resource.Resource {
return &IntegrationResource{}
}
func (r *IntegrationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_integration"
}
func (r *IntegrationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a TowerOps integration with third-party services.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier of the integration.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"provider": schema.StringAttribute{
Description: "The integration provider type (e.g. pagerduty, slack, webhook).",
Required: true,
},
"enabled": schema.BoolAttribute{
Description: "Whether the integration is enabled. Defaults to true.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"sync_interval_minutes": schema.Int64Attribute{
Description: "How often the integration syncs, in minutes.",
Optional: true,
},
"inserted_at": schema.StringAttribute{
Description: "The timestamp when the integration was created.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *IntegrationResource) 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 *IntegrationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data IntegrationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
integration := integrationWithCredentials{
Provider: data.Provider.ValueString(),
}
if !data.Enabled.IsNull() {
enabled := data.Enabled.ValueBool()
integration.Enabled = &enabled
}
if !data.SyncIntervalMinutes.IsNull() {
interval := int(data.SyncIntervalMinutes.ValueInt64())
integration.SyncIntervalMinutes = &interval
}
created, err := r.client.CreateIntegration(integration)
if err != nil {
resp.Diagnostics.AddError("Failed to create integration", err.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
if created.Enabled != nil {
data.Enabled = types.BoolValue(*created.Enabled)
}
if created.SyncIntervalMinutes != nil {
data.SyncIntervalMinutes = types.Int64Value(int64(*created.SyncIntervalMinutes))
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *IntegrationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data IntegrationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
integration, err := r.client.GetIntegration(data.ID.ValueString())
if err != nil {
if errors.Is(err, ErrNotFound) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read integration", err.Error())
return
}
data.Provider = types.StringValue(integration.Provider)
data.InsertedAt = types.StringValue(integration.InsertedAt)
if integration.Enabled != nil {
data.Enabled = types.BoolValue(*integration.Enabled)
}
if integration.SyncIntervalMinutes != nil {
data.SyncIntervalMinutes = types.Int64Value(int64(*integration.SyncIntervalMinutes))
} else {
data.SyncIntervalMinutes = types.Int64Null()
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *IntegrationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data IntegrationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
integration := integrationWithCredentials{
Provider: data.Provider.ValueString(),
}
if !data.Enabled.IsNull() {
enabled := data.Enabled.ValueBool()
integration.Enabled = &enabled
}
if !data.SyncIntervalMinutes.IsNull() {
interval := int(data.SyncIntervalMinutes.ValueInt64())
integration.SyncIntervalMinutes = &interval
}
updated, err := r.client.UpdateIntegration(data.ID.ValueString(), integration)
if err != nil {
if errors.Is(err, ErrNotFound) {
created, createErr := r.client.CreateIntegration(integration)
if createErr != nil {
resp.Diagnostics.AddError("Failed to create integration (after 404 on update)", createErr.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
if created.Enabled != nil {
data.Enabled = types.BoolValue(*created.Enabled)
}
if created.SyncIntervalMinutes != nil {
data.SyncIntervalMinutes = types.Int64Value(int64(*created.SyncIntervalMinutes))
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
resp.Diagnostics.AddError("Failed to update integration", err.Error())
return
}
data.Provider = types.StringValue(updated.Provider)
if updated.Enabled != nil {
data.Enabled = types.BoolValue(*updated.Enabled)
}
if updated.SyncIntervalMinutes != nil {
data.SyncIntervalMinutes = types.Int64Value(int64(*updated.SyncIntervalMinutes))
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *IntegrationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data IntegrationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteIntegration(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to delete integration", err.Error())
return
}
}
func (r *IntegrationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View file

@ -0,0 +1,296 @@
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/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)
var _ resource.Resource = &MaintenanceWindowResource{}
var _ resource.ResourceWithImportState = &MaintenanceWindowResource{}
// MaintenanceWindowResource defines the resource implementation.
type MaintenanceWindowResource struct {
client *Client
}
// MaintenanceWindowResourceModel describes the resource data model.
type MaintenanceWindowResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Reason types.String `tfsdk:"reason"`
StartsAt types.String `tfsdk:"starts_at"`
EndsAt types.String `tfsdk:"ends_at"`
SuppressAlerts types.Bool `tfsdk:"suppress_alerts"`
SiteID types.String `tfsdk:"site_id"`
DeviceID types.String `tfsdk:"device_id"`
InsertedAt types.String `tfsdk:"inserted_at"`
}
// NewMaintenanceWindowResource creates a new maintenance window resource.
func NewMaintenanceWindowResource() resource.Resource {
return &MaintenanceWindowResource{}
}
func (r *MaintenanceWindowResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_maintenance_window"
}
func (r *MaintenanceWindowResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages a TowerOps maintenance window. Maintenance windows suppress alerts during planned work periods.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier of the maintenance window.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the maintenance window.",
Required: true,
},
"reason": schema.StringAttribute{
Description: "The reason for the maintenance window.",
Optional: true,
},
"starts_at": schema.StringAttribute{
Description: "The start time of the maintenance window in ISO 8601 format (e.g. 2024-01-15T02:00:00Z).",
Required: true,
},
"ends_at": schema.StringAttribute{
Description: "The end time of the maintenance window in ISO 8601 format (e.g. 2024-01-15T06:00:00Z).",
Required: true,
},
"suppress_alerts": schema.BoolAttribute{
Description: "Whether to suppress alerts during the maintenance window. Defaults to true.",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(true),
},
"site_id": schema.StringAttribute{
Description: "The site to apply the maintenance window to. If omitted, applies to all sites.",
Optional: true,
},
"device_id": schema.StringAttribute{
Description: "The device to apply the maintenance window to. If omitted, applies to all devices.",
Optional: true,
},
"inserted_at": schema.StringAttribute{
Description: "The timestamp when the maintenance window was created.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
},
}
}
func (r *MaintenanceWindowResource) 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 *MaintenanceWindowResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data MaintenanceWindowResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
window := MaintenanceWindowAPI{
Name: data.Name.ValueString(),
StartsAt: data.StartsAt.ValueString(),
EndsAt: data.EndsAt.ValueString(),
}
if !data.Reason.IsNull() {
reason := data.Reason.ValueString()
window.Reason = &reason
}
if !data.SuppressAlerts.IsNull() {
suppress := data.SuppressAlerts.ValueBool()
window.SuppressAlerts = &suppress
}
if !data.SiteID.IsNull() {
siteID := data.SiteID.ValueString()
window.SiteID = &siteID
}
if !data.DeviceID.IsNull() {
deviceID := data.DeviceID.ValueString()
window.DeviceID = &deviceID
}
created, err := r.client.CreateMaintenanceWindow(window)
if err != nil {
resp.Diagnostics.AddError("Failed to create maintenance window", err.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
if created.SuppressAlerts != nil {
data.SuppressAlerts = types.BoolValue(*created.SuppressAlerts)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *MaintenanceWindowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data MaintenanceWindowResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
window, err := r.client.GetMaintenanceWindow(data.ID.ValueString())
if err != nil {
if errors.Is(err, ErrNotFound) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Failed to read maintenance window", err.Error())
return
}
data.Name = types.StringValue(window.Name)
data.StartsAt = types.StringValue(window.StartsAt)
data.EndsAt = types.StringValue(window.EndsAt)
data.InsertedAt = types.StringValue(window.InsertedAt)
if window.Reason != nil {
data.Reason = types.StringValue(*window.Reason)
} else {
data.Reason = types.StringNull()
}
if window.SuppressAlerts != nil {
data.SuppressAlerts = types.BoolValue(*window.SuppressAlerts)
}
if window.SiteID != nil {
data.SiteID = types.StringValue(*window.SiteID)
} else {
data.SiteID = types.StringNull()
}
if window.DeviceID != nil {
data.DeviceID = types.StringValue(*window.DeviceID)
} else {
data.DeviceID = types.StringNull()
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *MaintenanceWindowResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data MaintenanceWindowResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
window := MaintenanceWindowAPI{
Name: data.Name.ValueString(),
StartsAt: data.StartsAt.ValueString(),
EndsAt: data.EndsAt.ValueString(),
}
if !data.Reason.IsNull() {
reason := data.Reason.ValueString()
window.Reason = &reason
}
if !data.SuppressAlerts.IsNull() {
suppress := data.SuppressAlerts.ValueBool()
window.SuppressAlerts = &suppress
}
if !data.SiteID.IsNull() {
siteID := data.SiteID.ValueString()
window.SiteID = &siteID
}
if !data.DeviceID.IsNull() {
deviceID := data.DeviceID.ValueString()
window.DeviceID = &deviceID
}
updated, err := r.client.UpdateMaintenanceWindow(data.ID.ValueString(), window)
if err != nil {
if errors.Is(err, ErrNotFound) {
created, createErr := r.client.CreateMaintenanceWindow(window)
if createErr != nil {
resp.Diagnostics.AddError("Failed to create maintenance window (after 404 on update)", createErr.Error())
return
}
data.ID = types.StringValue(created.ID)
data.InsertedAt = types.StringValue(created.InsertedAt)
if created.SuppressAlerts != nil {
data.SuppressAlerts = types.BoolValue(*created.SuppressAlerts)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
return
}
resp.Diagnostics.AddError("Failed to update maintenance window", err.Error())
return
}
data.Name = types.StringValue(updated.Name)
data.StartsAt = types.StringValue(updated.StartsAt)
data.EndsAt = types.StringValue(updated.EndsAt)
if updated.SuppressAlerts != nil {
data.SuppressAlerts = types.BoolValue(*updated.SuppressAlerts)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *MaintenanceWindowResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var data MaintenanceWindowResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
err := r.client.DeleteMaintenanceWindow(data.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Failed to delete maintenance window", err.Error())
return
}
}
func (r *MaintenanceWindowResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View file

@ -91,6 +91,9 @@ func (p *ToweropsProvider) Resources(ctx context.Context) []func() resource.Reso
NewDeviceResource,
NewScheduleResource,
NewEscalationPolicyResource,
NewAgentResource,
NewIntegrationResource,
NewMaintenanceWindowResource,
}
}