This repository has been archived on 2026-08-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
beatzaplentyandClaude Sonnet 4.6 70bb75ad46
ci / test (push) Failing after 5m55s
bump to v0.2.0: complete provider implementation with live-tested DNS record types
- DYNU_API_KEY env var fallback in provider config
- dynu_domain resource: add 9 computed attributes (unicode_name, ipv4/ipv6 flags,
  wildcard aliases, allow_zone_transfer, dnssec, created_on, updated_on)
- dynu_dns_records data source: add 6 type-specific fields (priority, weight,
  port, flags, tag, value)
- Fix normalizeRecordContentForState for MX/SRV/NS/PTR (read host field) and CAA
  (read value field) to avoid plan/state inconsistency
- Allow underscore-prefixed DNS labels in hostname validator (RFC 2782 SRV)
- Preserve plan/state hostname across create/update/read to prevent inconsistent
  result errors when API returns full FQDN for node_name-style records
- Update all version references from 0.1.0 to 0.2.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LFZyrmtKsezBwFWGTWsb2y
2026-08-03 23:40:27 +10:00

179 lines
8.4 KiB
Go

package provider
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"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"
"github.com/beatz174-bit/terraform-provider-dynu/internal/dynuclient"
)
var (
_ resource.Resource = &domainResource{}
_ resource.ResourceWithConfigure = &domainResource{}
_ resource.ResourceWithImportState = &domainResource{}
)
type domainResource struct{ clientProvider *providerData }
type domainResourceModel struct {
ID types.Int64 `tfsdk:"id"`
Name types.String `tfsdk:"name"`
IPv4Address types.String `tfsdk:"ipv4_address"`
IPv6Address types.String `tfsdk:"ipv6_address"`
TTL types.Int64 `tfsdk:"ttl"`
Group types.String `tfsdk:"group"`
State types.String `tfsdk:"state"`
Token types.String `tfsdk:"token"`
UnicodeName types.String `tfsdk:"unicode_name"`
IPv4 types.Bool `tfsdk:"ipv4"`
IPv6 types.Bool `tfsdk:"ipv6"`
IPv4WildcardAlias types.Bool `tfsdk:"ipv4_wildcard_alias"`
IPv6WildcardAlias types.Bool `tfsdk:"ipv6_wildcard_alias"`
AllowZoneTransfer types.Bool `tfsdk:"allow_zone_transfer"`
DNSSEC types.Bool `tfsdk:"dnssec"`
CreatedOn types.String `tfsdk:"created_on"`
UpdatedOn types.String `tfsdk:"updated_on"`
}
func NewDomainResource() resource.Resource { return &domainResource{} }
func (r *domainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_domain"
}
func (r *domainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{Description: "Manages a Dynu DNS domain.", Attributes: map[string]schema.Attribute{
"id": schema.Int64Attribute{Computed: true},
"name": schema.StringAttribute{Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}},
"ipv4_address": schema.StringAttribute{Optional: true, Computed: true},
"ipv6_address": schema.StringAttribute{Optional: true, Computed: true},
"ttl": schema.Int64Attribute{Optional: true, Computed: true},
"group": schema.StringAttribute{Optional: true, Computed: true},
"state": schema.StringAttribute{Computed: true},
"token": schema.StringAttribute{Computed: true, Sensitive: true},
"unicode_name": schema.StringAttribute{Computed: true, Description: "Unicode representation of the domain name."},
"ipv4": schema.BoolAttribute{Computed: true, Description: "Whether IPv4 support is enabled for this domain."},
"ipv6": schema.BoolAttribute{Computed: true, Description: "Whether IPv6 support is enabled for this domain."},
"ipv4_wildcard_alias": schema.BoolAttribute{Computed: true, Description: "Whether IPv4 wildcard alias is enabled."},
"ipv6_wildcard_alias": schema.BoolAttribute{Computed: true, Description: "Whether IPv6 wildcard alias is enabled."},
"allow_zone_transfer": schema.BoolAttribute{Computed: true, Description: "Whether zone transfer is allowed for this domain."},
"dnssec": schema.BoolAttribute{Computed: true, Description: "Whether DNSSEC is enabled for this domain."},
"created_on": schema.StringAttribute{Computed: true, Description: "Creation timestamp as returned by Dynu."},
"updated_on": schema.StringAttribute{Computed: true, Description: "Last update timestamp as returned by Dynu."},
}}
}
func (r *domainResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(*providerData)
if !ok {
resp.Diagnostics.AddError("Unexpected resource configure type", fmt.Sprintf("Expected *providerData, got %T", req.ProviderData))
return
}
r.clientProvider = providerData
}
func (r *domainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan domainResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
domain, err := r.clientProvider.client.CreateDomain(ctx, dynuclient.CreateDomainRequest{Name: plan.Name.ValueString(), IPv4Address: stringFromOptional(plan.IPv4Address), IPv6Address: stringFromOptional(plan.IPv6Address), TTL: int64FromOptional(plan.TTL), Group: stringFromOptional(plan.Group)})
if err != nil {
resp.Diagnostics.AddError(diagnosticSummary("Unable to create Dynu domain", err), err.Error())
return
}
state := mapDomainResource(*domain)
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func (r *domainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state domainResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
domain, err := r.clientProvider.client.GetDomainByID(ctx, state.ID.ValueInt64())
if err != nil {
var apiErr *dynuclient.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError(diagnosticSummary("Unable to read Dynu domain", err), err.Error())
return
}
next := mapDomainResource(*domain)
resp.Diagnostics.Append(resp.State.Set(ctx, &next)...)
}
func (r *domainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan domainResourceModel
var state domainResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
domain, err := r.clientProvider.client.UpdateDomain(ctx, state.ID.ValueInt64(), dynuclient.UpdateDomainRequest{Name: plan.Name.ValueString(), IPv4Address: stringFromOptional(plan.IPv4Address), IPv6Address: stringFromOptional(plan.IPv6Address), TTL: int64FromOptional(plan.TTL), Group: stringFromOptional(plan.Group)})
if err != nil {
resp.Diagnostics.AddError(diagnosticSummary("Unable to update Dynu domain", err), err.Error())
return
}
next := mapDomainResource(*domain)
resp.Diagnostics.Append(resp.State.Set(ctx, &next)...)
}
func (r *domainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state domainResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
err := r.clientProvider.client.DeleteDomain(ctx, state.ID.ValueInt64())
if err == nil {
return
}
var apiErr *dynuclient.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
return
}
resp.Diagnostics.AddError(diagnosticSummary("Unable to delete Dynu domain", err), err.Error())
}
func (r *domainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
id, err := strconv.ParseInt(strings.TrimSpace(req.ID), 10, 64)
if err != nil {
resp.Diagnostics.AddError("Invalid domain import ID", "Use the Dynu numeric domain ID.")
return
}
state := domainResourceModel{ID: types.Int64Value(id)}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}
func mapDomainResource(domain dynuclient.Domain) domainResourceModel {
return domainResourceModel{
ID: types.Int64Value(domain.ID),
Name: types.StringValue(strings.TrimSpace(domain.Name)),
IPv4Address: mapString(domain.IPv4Address),
IPv6Address: mapString(domain.IPv6Address),
TTL: types.Int64Value(domain.TTL),
Group: mapString(domain.Group),
State: mapString(domain.State),
Token: mapString(domain.Token),
UnicodeName: mapString(domain.UnicodeName),
IPv4: types.BoolValue(domain.IPv4),
IPv6: types.BoolValue(domain.IPv6),
IPv4WildcardAlias: types.BoolValue(domain.IPv4WildcardAlias),
IPv6WildcardAlias: types.BoolValue(domain.IPv6WildcardAlias),
AllowZoneTransfer: types.BoolValue(domain.AllowZoneTransfer),
DNSSEC: types.BoolValue(domain.DNSSEC),
CreatedOn: mapString(domain.CreatedOn),
UpdatedOn: mapString(domain.UpdatedOn),
}
}