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:
parent
e7634da384
commit
bbe1babdf7
13 changed files with 1172 additions and 0 deletions
|
|
@ -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
|
## Schema
|
||||||
|
|
||||||
### Required
|
### Required
|
||||||
|
|
|
||||||
54
docs/resources/agent.md
Normal file
54
docs/resources/agent.md
Normal 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
|
||||||
|
```
|
||||||
63
docs/resources/integration.md
Normal file
63
docs/resources/integration.md
Normal 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
|
||||||
|
```
|
||||||
74
docs/resources/maintenance_window.md
Normal file
74
docs/resources/maintenance_window.md
Normal 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
|
||||||
|
```
|
||||||
|
|
@ -64,6 +64,25 @@ resource "towerops_escalation_policy" "default" {
|
||||||
repeat_count = 3
|
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 the site ID
|
||||||
output "site_id" {
|
output "site_id" {
|
||||||
value = towerops_site.main_office.id
|
value = towerops_site.main_office.id
|
||||||
|
|
@ -80,3 +99,8 @@ output "schedule_id" {
|
||||||
output "escalation_policy_id" {
|
output "escalation_policy_id" {
|
||||||
value = towerops_escalation_policy.default.id
|
value = towerops_escalation_policy.default.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "agent_token" {
|
||||||
|
value = towerops_agent.remote.token
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
|
||||||
8
examples/resources/towerops_agent/resource.tf
Normal file
8
examples/resources/towerops_agent/resource.tf
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
resource "towerops_agent" "office" {
|
||||||
|
name = "Office Poller"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "agent_token" {
|
||||||
|
value = towerops_agent.office.token
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
5
examples/resources/towerops_integration/resource.tf
Normal file
5
examples/resources/towerops_integration/resource.tf
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
resource "towerops_integration" "pagerduty" {
|
||||||
|
provider = "pagerduty"
|
||||||
|
enabled = true
|
||||||
|
sync_interval_minutes = 5
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
173
internal/provider/agent_resource.go
Normal file
173
internal/provider/agent_resource.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -367,6 +367,195 @@ func (c *Client) DeleteEscalationPolicy(id string) error {
|
||||||
return err
|
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.
|
// 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)
|
||||||
|
|
|
||||||
244
internal/provider/integration_resource.go
Normal file
244
internal/provider/integration_resource.go
Normal 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)
|
||||||
|
}
|
||||||
296
internal/provider/maintenance_window_resource.go
Normal file
296
internal/provider/maintenance_window_resource.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -91,6 +91,9 @@ func (p *ToweropsProvider) Resources(ctx context.Context) []func() resource.Reso
|
||||||
NewDeviceResource,
|
NewDeviceResource,
|
||||||
NewScheduleResource,
|
NewScheduleResource,
|
||||||
NewEscalationPolicyResource,
|
NewEscalationPolicyResource,
|
||||||
|
NewAgentResource,
|
||||||
|
NewIntegrationResource,
|
||||||
|
NewMaintenanceWindowResource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue