From 4960adaf372c1c0ab9caf591c636aa825821be0a Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Fri, 13 Mar 2026 17:35:16 -0500 Subject: [PATCH] add address, latitude, longitude to site resource Support geographic coordinates and street address in towerops_site. All three fields are optional; server validates lat/long ranges. --- docs/index.md | 7 +- docs/resources/site.md | 19 +++- internal/provider/client.go | 13 ++- internal/provider/site_resource.go | 150 +++++++++++++++++------------ 4 files changed, 117 insertions(+), 72 deletions(-) diff --git a/docs/index.md b/docs/index.md index b25d008..b8d87dd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,8 +31,11 @@ provider "towerops" { } resource "towerops_site" "example" { - name = "Main Office" - location = "New York, NY" + name = "Main Office" + location = "New York, NY" + address = "350 5th Ave, New York, NY 10118" + latitude = 40.7484 + longitude = -73.9857 } resource "towerops_device" "router" { diff --git a/docs/resources/site.md b/docs/resources/site.md index 9fb0e65..78bffa7 100644 --- a/docs/resources/site.md +++ b/docs/resources/site.md @@ -10,6 +10,8 @@ Manages a TowerOps site. Sites represent physical locations that contain devices ## Example Usage +### Basic + ```terraform resource "towerops_site" "example" { name = "Main Office" @@ -18,6 +20,18 @@ resource "towerops_site" "example" { } ``` +### With Geographic Coordinates + +```terraform +resource "towerops_site" "tower_site" { + name = "Verona Tower" + location = "Verona, TX" + address = "123 Main St, Verona, TX 75482" + latitude = 33.4356 + longitude = -96.0028 +} +``` + ## Schema ### Required @@ -26,7 +40,10 @@ resource "towerops_site" "example" { ### Optional -- `location` (String) - The physical location or address of the site. +- `location` (String) - A short description of the physical location. +- `address` (String) - The street address of the site. Maximum 500 characters. +- `latitude` (Float) - The latitude of the site. Must be between -90 and 90. +- `longitude` (Float) - The longitude of the site. Must be between -180 and 180. - `snmp_community` (String, Sensitive) - The default SNMP community string for devices at this site. ### Read-Only diff --git a/internal/provider/client.go b/internal/provider/client.go index 4949b24..63ec19a 100644 --- a/internal/provider/client.go +++ b/internal/provider/client.go @@ -38,11 +38,14 @@ func NewClient(token, baseURL string) *Client { // Site represents a TowerOps site. type Site struct { - ID string `json:"id,omitempty"` - Name string `json:"name"` - Location *string `json:"location,omitempty"` - SNMPCommunity *string `json:"snmp_community,omitempty"` - InsertedAt string `json:"inserted_at,omitempty"` + ID string `json:"id,omitempty"` + Name string `json:"name"` + Location *string `json:"location,omitempty"` + Address *string `json:"address,omitempty"` + Latitude *float64 `json:"latitude,omitempty"` + Longitude *float64 `json:"longitude,omitempty"` + SNMPCommunity *string `json:"snmp_community,omitempty"` + InsertedAt string `json:"inserted_at,omitempty"` } // Device represents a TowerOps device. diff --git a/internal/provider/site_resource.go b/internal/provider/site_resource.go index ff0cc72..b719595 100644 --- a/internal/provider/site_resource.go +++ b/internal/provider/site_resource.go @@ -23,11 +23,14 @@ type SiteResource struct { // SiteResourceModel describes the resource data model. type SiteResourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Location types.String `tfsdk:"location"` - SNMPCommunity types.String `tfsdk:"snmp_community"` - InsertedAt types.String `tfsdk:"inserted_at"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Location types.String `tfsdk:"location"` + Address types.String `tfsdk:"address"` + Latitude types.Float64 `tfsdk:"latitude"` + Longitude types.Float64 `tfsdk:"longitude"` + SNMPCommunity types.String `tfsdk:"snmp_community"` + InsertedAt types.String `tfsdk:"inserted_at"` } // NewSiteResource creates a new site resource. @@ -55,7 +58,19 @@ func (r *SiteResource) Schema(ctx context.Context, req resource.SchemaRequest, r Required: true, }, "location": schema.StringAttribute{ - Description: "The physical location or address of the site.", + Description: "A short description of the physical location.", + Optional: true, + }, + "address": schema.StringAttribute{ + Description: "The street address of the site.", + Optional: true, + }, + "latitude": schema.Float64Attribute{ + Description: "The latitude of the site (-90 to 90).", + Optional: true, + }, + "longitude": schema.Float64Attribute{ + Description: "The longitude of the site (-180 to 180).", Optional: true, }, "snmp_community": schema.StringAttribute{ @@ -99,19 +114,7 @@ func (r *SiteResource) Create(ctx context.Context, req resource.CreateRequest, r return } - site := Site{ - Name: data.Name.ValueString(), - } - - if !data.Location.IsNull() { - location := data.Location.ValueString() - site.Location = &location - } - - if !data.SNMPCommunity.IsNull() { - community := data.SNMPCommunity.ValueString() - site.SNMPCommunity = &community - } + site := buildSiteFromModel(&data) created, err := r.client.CreateSite(site) if err != nil { @@ -121,13 +124,7 @@ func (r *SiteResource) Create(ctx context.Context, req resource.CreateRequest, r data.ID = types.StringValue(created.ID) data.InsertedAt = types.StringValue(created.InsertedAt) - - if created.Location != nil { - data.Location = types.StringValue(*created.Location) - } - if created.SNMPCommunity != nil { - data.SNMPCommunity = types.StringValue(*created.SNMPCommunity) - } + setSiteOptionalFields(&data, created) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -153,18 +150,7 @@ func (r *SiteResource) Read(ctx context.Context, req resource.ReadRequest, resp data.Name = types.StringValue(site.Name) data.InsertedAt = types.StringValue(site.InsertedAt) - - if site.Location != nil { - data.Location = types.StringValue(*site.Location) - } else { - data.Location = types.StringNull() - } - - if site.SNMPCommunity != nil { - data.SNMPCommunity = types.StringValue(*site.SNMPCommunity) - } else { - data.SNMPCommunity = types.StringNull() - } + setSiteOptionalFields(&data, site) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -177,19 +163,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - site := Site{ - Name: data.Name.ValueString(), - } - - if !data.Location.IsNull() { - location := data.Location.ValueString() - site.Location = &location - } - - if !data.SNMPCommunity.IsNull() { - community := data.SNMPCommunity.ValueString() - site.SNMPCommunity = &community - } + site := buildSiteFromModel(&data) updated, err := r.client.UpdateSite(data.ID.ValueString(), site) if err != nil { @@ -203,12 +177,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r data.ID = types.StringValue(created.ID) data.InsertedAt = types.StringValue(created.InsertedAt) data.Name = types.StringValue(created.Name) - if created.Location != nil { - data.Location = types.StringValue(*created.Location) - } - if created.SNMPCommunity != nil { - data.SNMPCommunity = types.StringValue(*created.SNMPCommunity) - } + setSiteOptionalFields(&data, created) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) return } @@ -217,13 +186,7 @@ func (r *SiteResource) Update(ctx context.Context, req resource.UpdateRequest, r } data.Name = types.StringValue(updated.Name) - - if updated.Location != nil { - data.Location = types.StringValue(*updated.Location) - } - if updated.SNMPCommunity != nil { - data.SNMPCommunity = types.StringValue(*updated.SNMPCommunity) - } + setSiteOptionalFields(&data, updated) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -246,3 +209,62 @@ func (r *SiteResource) Delete(ctx context.Context, req resource.DeleteRequest, r func (r *SiteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } + +// buildSiteFromModel converts the Terraform model to an API Site struct. +func buildSiteFromModel(data *SiteResourceModel) Site { + site := Site{ + Name: data.Name.ValueString(), + } + + if !data.Location.IsNull() { + v := data.Location.ValueString() + site.Location = &v + } + if !data.Address.IsNull() { + v := data.Address.ValueString() + site.Address = &v + } + if !data.Latitude.IsNull() { + v := data.Latitude.ValueFloat64() + site.Latitude = &v + } + if !data.Longitude.IsNull() { + v := data.Longitude.ValueFloat64() + site.Longitude = &v + } + if !data.SNMPCommunity.IsNull() { + v := data.SNMPCommunity.ValueString() + site.SNMPCommunity = &v + } + + return site +} + +// setSiteOptionalFields maps API response optional fields back to the Terraform model. +func setSiteOptionalFields(data *SiteResourceModel, site *Site) { + if site.Location != nil { + data.Location = types.StringValue(*site.Location) + } else { + data.Location = types.StringNull() + } + if site.Address != nil { + data.Address = types.StringValue(*site.Address) + } else { + data.Address = types.StringNull() + } + if site.Latitude != nil { + data.Latitude = types.Float64Value(*site.Latitude) + } else { + data.Latitude = types.Float64Null() + } + if site.Longitude != nil { + data.Longitude = types.Float64Value(*site.Longitude) + } else { + data.Longitude = types.Float64Null() + } + if site.SNMPCommunity != nil { + data.SNMPCommunity = types.StringValue(*site.SNMPCommunity) + } else { + data.SNMPCommunity = types.StringNull() + } +}