terraform-provider-towerops/internal/provider/organization_resource.go
Graham McIntire 70c7a5cfbb
feat: allow setting organization name via Terraform
- Make 'name' field optional and computed in organization resource
- Include name in Create and Update operations when provided
- Name can only be changed by organization owners (enforced by API)
- Matches API capability to update organization name
- Backwards compatible: name remains optional

Example usage:
  resource "towerops_organization" "main" {
    name      = "My Organization"
    use_sites = true
  }
2026-03-10 13:32:56 -05:00

171 lines
5.2 KiB
Go

package provider
import (
"context"
"fmt"
"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 = &OrganizationResource{}
// OrganizationResource manages organization settings.
type OrganizationResource struct {
client *Client
}
// OrganizationResourceModel describes the resource data model.
type OrganizationResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Slug types.String `tfsdk:"slug"`
UseSites types.Bool `tfsdk:"use_sites"`
}
// NewOrganizationResource creates a new organization resource.
func NewOrganizationResource() resource.Resource {
return &OrganizationResource{}
}
func (r *OrganizationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_organization"
}
func (r *OrganizationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Manages organization settings for the organization associated with the API token. There is exactly one organization per token, so this resource manages settings rather than creating or deleting organizations.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "The unique identifier of the organization.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the organization. Can only be set by organization owners.",
Optional: true,
Computed: true,
},
"slug": schema.StringAttribute{
Description: "The URL-friendly slug of the organization.",
Computed: true,
},
"use_sites": schema.BoolAttribute{
Description: "Whether the organization uses sites to group devices. When true, devices are organized under sites. When false, devices belong directly to the organization.",
Required: true,
},
},
}
}
func (r *OrganizationResource) 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 *OrganizationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
org := Organization{
UseSites: data.UseSites.ValueBool(),
}
// Include name if provided
if !data.Name.IsNull() && !data.Name.IsUnknown() {
org.Name = data.Name.ValueString()
}
updated, err := r.client.UpdateOrganization(org)
if err != nil {
resp.Diagnostics.AddError("Failed to update organization", err.Error())
return
}
data.ID = types.StringValue(updated.ID)
data.Name = types.StringValue(updated.Name)
data.Slug = types.StringValue(updated.Slug)
data.UseSites = types.BoolValue(updated.UseSites)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
org, err := r.client.GetOrganization()
if err != nil {
resp.Diagnostics.AddError("Failed to read organization", err.Error())
return
}
data.ID = types.StringValue(org.ID)
data.Name = types.StringValue(org.Name)
data.Slug = types.StringValue(org.Slug)
data.UseSites = types.BoolValue(org.UseSites)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data OrganizationResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
org := Organization{
UseSites: data.UseSites.ValueBool(),
}
// Include name if provided
if !data.Name.IsNull() && !data.Name.IsUnknown() {
org.Name = data.Name.ValueString()
}
updated, err := r.client.UpdateOrganization(org)
if err != nil {
resp.Diagnostics.AddError("Failed to update organization", err.Error())
return
}
data.ID = types.StringValue(updated.ID)
data.Name = types.StringValue(updated.Name)
data.Slug = types.StringValue(updated.Slug)
data.UseSites = types.BoolValue(updated.UseSites)
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}
func (r *OrganizationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
// Organizations cannot be deleted via the API. When this resource is
// removed from Terraform config, we simply remove it from state.
// The organization continues to exist in TowerOps.
}