Implement DNS record resource and data source #30

Merged
ldraney merged 3 commits from feat/dns-resource-datasource into main 2026-06-14 18:05:09 +00:00
Owner

Summary

  • Implement godaddy_dns_record resource with full CRUD and import support (issue #11)
  • Implement godaddy_dns_records data source with optional type/name filtering (issue #12)
  • Register both in provider.go
  • Add GetAllRecords and GetRecordsByType client methods to support data source filtering

Closes #11, closes #12

Changes

resource_dns_record.go (new)

  • Schema: domain, type, name (required, force-replace), data (required), ttl (optional, default 600), priority/weight/port (optional)
  • Create/Update via client.ReplaceRecords() (PUT-replace semantics)
  • Read via client.GetRecords() -- uses first matching record
  • Delete via client.DeleteRecords()
  • Import via composite ID domain:type:name

data_source_dns_records.go (new)

  • Schema: domain (required), type (optional filter), name (optional filter, requires type)
  • Routes to GetAllRecords, GetRecordsByType, or GetRecords based on which filters are set
  • Output: computed records list with all DNSRecord fields

provider.go (modified)

  • Register NewDNSRecordResource in Resources()
  • Register NewDNSRecordsDataSource in DataSources()

pkg/godaddy/dns.go (modified)

  • Add GetAllRecords() -- GET /v1/domains/{domain}/records
  • Add GetRecordsByType() -- GET /v1/domains/{domain}/records/{type}

Test Plan

  • go build ./... passes
  • go vet ./... passes
  • Manual: tofu plan with A record resource against live API
  • Manual: tofu import godaddy_dns_record.test example.com:A:@
  • Manual: tofu plan with dns_records data source
  • Acceptance tests (Phase 2, future PR)

Review Checklist

  • Compile-time interface checks (var _ resource.Resource = ...)
  • Provider data type assertion with error diagnostic
  • RequiresReplace on domain/type/name (identity fields)
  • TTL default 600 via int64default.StaticInt64
  • Optional fields handled as null when zero from API
  • Import ID validation (3 non-empty parts split on :)
  • Resource uses PUT-replace semantics matching GoDaddy API design
  • Data source added two new GET client methods to cover all filter combinations
  • SRV fields (priority, weight, port) are optional and only populated when non-zero from API
## Summary - Implement `godaddy_dns_record` resource with full CRUD and import support (issue #11) - Implement `godaddy_dns_records` data source with optional type/name filtering (issue #12) - Register both in provider.go - Add `GetAllRecords` and `GetRecordsByType` client methods to support data source filtering Closes #11, closes #12 ## Changes ### `resource_dns_record.go` (new) - Schema: domain, type, name (required, force-replace), data (required), ttl (optional, default 600), priority/weight/port (optional) - Create/Update via `client.ReplaceRecords()` (PUT-replace semantics) - Read via `client.GetRecords()` -- uses first matching record - Delete via `client.DeleteRecords()` - Import via composite ID `domain:type:name` ### `data_source_dns_records.go` (new) - Schema: domain (required), type (optional filter), name (optional filter, requires type) - Routes to `GetAllRecords`, `GetRecordsByType`, or `GetRecords` based on which filters are set - Output: computed `records` list with all DNSRecord fields ### `provider.go` (modified) - Register `NewDNSRecordResource` in `Resources()` - Register `NewDNSRecordsDataSource` in `DataSources()` ### `pkg/godaddy/dns.go` (modified) - Add `GetAllRecords()` -- GET /v1/domains/{domain}/records - Add `GetRecordsByType()` -- GET /v1/domains/{domain}/records/{type} ## Test Plan - [x] `go build ./...` passes - [x] `go vet ./...` passes - [ ] Manual: `tofu plan` with A record resource against live API - [ ] Manual: `tofu import godaddy_dns_record.test example.com:A:@` - [ ] Manual: `tofu plan` with dns_records data source - [ ] Acceptance tests (Phase 2, future PR) ## Review Checklist - [x] Compile-time interface checks (`var _ resource.Resource = ...`) - [x] Provider data type assertion with error diagnostic - [x] RequiresReplace on domain/type/name (identity fields) - [x] TTL default 600 via `int64default.StaticInt64` - [x] Optional fields handled as null when zero from API - [x] Import ID validation (3 non-empty parts split on `:`) ## Related Notes - Resource uses PUT-replace semantics matching GoDaddy API design - Data source added two new GET client methods to cover all filter combinations - SRV fields (priority, weight, port) are optional and only populated when non-zero from API
Closes #11, closes #12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #30 Review

DOMAIN REVIEW

Stack: Go / Terraform Plugin Framework / GoDaddy REST API

This PR implements two core Terraform Plugin Framework components -- godaddy_dns_record (resource) and godaddy_dns_records (data source) -- plus two new client methods (GetAllRecords, GetRecordsByType). 535 additions across 4 files.

Plugin Framework usage -- correctly implemented:

  • Compile-time interface checks (var _ resource.Resource = ..., var _ datasource.DataSource = ...) -- good practice.
  • resource.ResourceWithImportState interface implemented correctly.
  • RequiresReplace plan modifiers on all three identity fields (domain, type, name) -- exactly right for PUT-replace API semantics.
  • TTL default via int64default.StaticInt64(600) with Optional: true, Computed: true -- correct pattern for optional-with-default.
  • Provider data type assertion with diagnostic error in both Configure methods.
  • Metadata correctly appends to ProviderTypeName.

API client methods -- correctly implemented:

  • GetAllRecords and GetRecordsByType follow the same pattern as existing methods: url.PathEscape, http.NewRequestWithContext, doJSON, error wrapping with %w.
  • URL construction matches GoDaddy REST API paths (/v1/domains/{domain}/records and /v1/domains/{domain}/records/{type}).

Import state -- correctly implemented:

  • Composite ID format domain:type:name with SplitN(_, ":", 3).
  • Validates exactly 3 non-empty parts.
  • Reads back from API to populate all fields including computed ones.

Data source filtering logic -- correct:

  • Validates that name requires type (matching GoDaddy API constraint).
  • Routes to the right client method based on which filters are set.
  • Checks both IsNull() and IsUnknown() before using filter values.

BLOCKERS

1. No tests for new client methods -- BLOCKER

GetAllRecords() and GetRecordsByType() in pkg/godaddy/dns.go are new public API methods with zero test coverage. The existing test file (dns_test.go) has integration tests for every other client method (GetRecords, ReplaceRecords, DeleteRecords, ReplaceRecordsByType, AddRecords, ReplaceAllRecords). The two new methods break the pattern. At minimum, add TestGetAllRecords and TestGetRecordsByType integration tests in pkg/godaddy/dns_test.go following the same testClient + retryOn429 pattern.

Note: The PR body Test Plan acknowledges acceptance tests are deferred to Phase 2 (issue #13), which is reasonable for the Terraform resource/data source layer. But the pkg/godaddy/ client methods are a separate concern -- they already have an established test pattern and these two methods should match it.

2. Error wrapping inconsistency in new client methods

GetAllRecords and GetRecordsByType use %s for error wrapping:

return nil, fmt.Errorf("getting all records for %s: %w", domain, err)

Wait -- actually, looking again, these do use %w. I withdraw this as a blocker. The wrapping is correct.

Revised blockers: only item 1 above.

NITS

1. readRecordIntoModel on Read -- should remove state instead of erroring when record not found

In Read(), when the API returns zero records, the current code adds an error diagnostic. Per Terraform Plugin Framework convention, if a resource no longer exists during Read, the provider should call resp.State.RemoveResource(ctx) and return without error. This allows tofu plan to show the resource as needing re-creation rather than failing with an error. Current behavior will cause tofu plan to fail if someone deletes the record outside Terraform.

This is the standard drift-detection pattern. The fix is to check whether we are in a Read context (vs Import) and call RemoveResource when records are empty. One approach: have readRecordIntoModel return a boolean "found" flag, and let the Read caller handle the empty case with RemoveResource.

2. readRecordIntoModel silently picks records[0] when multiple records match

GoDaddy can return multiple records for the same type+name (e.g., multiple A records for round-robin). The code takes records[0] without warning. This is defensible for a v1, but a log warning when len(records) > 1 would help users debug unexpected behavior. Consider tflog.Warn(ctx, "multiple records found, using first", ...).

3. Data source missing id attribute

Some Terraform testing and module composition patterns expect data sources to have an id attribute. While the Plugin Framework does not strictly require it (unlike the older SDK v2), adding a computed id attribute (e.g., set to domain or domain:type:name) improves compatibility with terraform_remote_state and testing frameworks.

4. MX record type validation

GoDaddy requires priority for MX records. The schema marks priority as Optional, which means a user could create an MX record without priority and get a confusing API error. A custom validator on the schema (or at minimum a note in the description) would improve UX. Non-blocking for v1.

5. Data source name filter validation message could be a warning

The validation that name requires type is correct, but using AddError means the plan fails. This is the right behavior -- just noting the UX is good here.

6. Minor: int(model.TTL.ValueInt64()) -- narrowing conversion

ValueInt64() returns int64, and int on 64-bit Go is 64 bits, so this is fine in practice. But the DNSRecord struct uses int for TTL/Priority/Weight/Port. If this ever runs on a 32-bit target, large TTL values could overflow. Extremely unlikely for a Terraform provider, but worth a comment.

SOP COMPLIANCE

  • PR body has: Summary, Changes, Test Plan, Related -- all present and thorough
  • No secrets, .env files, or credentials committed
  • No unnecessary file changes (scope is tight: 2 new files + 2 modified files matching issues #11 and #12)
  • Commit message is descriptive (PR title matches issue scope)
  • Tests exist for new code -- client-layer tests missing for GetAllRecords and GetRecordsByType

PROCESS OBSERVATIONS

  • Combining #11 (resource) and #12 (data source) in a single PR is reasonable given they share the same identity model and the data source was needed to validate the resource during development.
  • Issue #13 (acceptance tests) is correctly deferred -- provider-level acceptance tests are a separate concern. But client-method tests should ship with the methods.
  • The PR is well-structured: clean separation between Terraform framework layer (resource/data source files) and API client layer (dns.go additions).
  • Documentation in CLAUDE.md and PR body is thorough -- the Review Checklist in the PR body shows disciplined self-review.

VERDICT: NOT APPROVED

Reason: New client methods GetAllRecords and GetRecordsByType lack integration tests. The existing test suite covers every other public method in dns.go. These two should match that standard before merge. The nit about Read calling RemoveResource for drift detection is strongly recommended but non-blocking for a first pass.

To unblock: Add TestGetAllRecords and TestGetRecordsByType to pkg/godaddy/dns_test.go following the existing pattern.

## PR #30 Review ### DOMAIN REVIEW **Stack:** Go / Terraform Plugin Framework / GoDaddy REST API This PR implements two core Terraform Plugin Framework components -- `godaddy_dns_record` (resource) and `godaddy_dns_records` (data source) -- plus two new client methods (`GetAllRecords`, `GetRecordsByType`). 535 additions across 4 files. **Plugin Framework usage -- correctly implemented:** - Compile-time interface checks (`var _ resource.Resource = ...`, `var _ datasource.DataSource = ...`) -- good practice. - `resource.ResourceWithImportState` interface implemented correctly. - `RequiresReplace` plan modifiers on all three identity fields (domain, type, name) -- exactly right for PUT-replace API semantics. - TTL default via `int64default.StaticInt64(600)` with `Optional: true, Computed: true` -- correct pattern for optional-with-default. - Provider data type assertion with diagnostic error in both `Configure` methods. - `Metadata` correctly appends to `ProviderTypeName`. **API client methods -- correctly implemented:** - `GetAllRecords` and `GetRecordsByType` follow the same pattern as existing methods: `url.PathEscape`, `http.NewRequestWithContext`, `doJSON`, error wrapping with `%w`. - URL construction matches GoDaddy REST API paths (`/v1/domains/{domain}/records` and `/v1/domains/{domain}/records/{type}`). **Import state -- correctly implemented:** - Composite ID format `domain:type:name` with `SplitN(_, ":", 3)`. - Validates exactly 3 non-empty parts. - Reads back from API to populate all fields including computed ones. **Data source filtering logic -- correct:** - Validates that `name` requires `type` (matching GoDaddy API constraint). - Routes to the right client method based on which filters are set. - Checks both `IsNull()` and `IsUnknown()` before using filter values. ### BLOCKERS **1. No tests for new client methods -- BLOCKER** `GetAllRecords()` and `GetRecordsByType()` in `pkg/godaddy/dns.go` are new public API methods with zero test coverage. The existing test file (`dns_test.go`) has integration tests for every other client method (`GetRecords`, `ReplaceRecords`, `DeleteRecords`, `ReplaceRecordsByType`, `AddRecords`, `ReplaceAllRecords`). The two new methods break the pattern. At minimum, add `TestGetAllRecords` and `TestGetRecordsByType` integration tests in `pkg/godaddy/dns_test.go` following the same `testClient` + `retryOn429` pattern. Note: The PR body Test Plan acknowledges acceptance tests are deferred to Phase 2 (issue #13), which is reasonable for the Terraform resource/data source layer. But the `pkg/godaddy/` client methods are a separate concern -- they already have an established test pattern and these two methods should match it. **2. Error wrapping inconsistency in new client methods** `GetAllRecords` and `GetRecordsByType` use `%s` for error wrapping: ```go return nil, fmt.Errorf("getting all records for %s: %w", domain, err) ``` Wait -- actually, looking again, these do use `%w`. I withdraw this as a blocker. The wrapping is correct. *Revised blockers: only item 1 above.* ### NITS **1. `readRecordIntoModel` on Read -- should remove state instead of erroring when record not found** In `Read()`, when the API returns zero records, the current code adds an error diagnostic. Per Terraform Plugin Framework convention, if a resource no longer exists during `Read`, the provider should call `resp.State.RemoveResource(ctx)` and return without error. This allows `tofu plan` to show the resource as needing re-creation rather than failing with an error. Current behavior will cause `tofu plan` to fail if someone deletes the record outside Terraform. This is the standard drift-detection pattern. The fix is to check whether we are in a Read context (vs Import) and call `RemoveResource` when records are empty. One approach: have `readRecordIntoModel` return a boolean "found" flag, and let the `Read` caller handle the empty case with `RemoveResource`. **2. `readRecordIntoModel` silently picks `records[0]` when multiple records match** GoDaddy can return multiple records for the same type+name (e.g., multiple A records for round-robin). The code takes `records[0]` without warning. This is defensible for a v1, but a log warning when `len(records) > 1` would help users debug unexpected behavior. Consider `tflog.Warn(ctx, "multiple records found, using first", ...)`. **3. Data source missing `id` attribute** Some Terraform testing and module composition patterns expect data sources to have an `id` attribute. While the Plugin Framework does not strictly require it (unlike the older SDK v2), adding a computed `id` attribute (e.g., set to `domain` or `domain:type:name`) improves compatibility with `terraform_remote_state` and testing frameworks. **4. MX record type validation** GoDaddy requires `priority` for MX records. The schema marks `priority` as `Optional`, which means a user could create an MX record without priority and get a confusing API error. A custom validator on the schema (or at minimum a note in the description) would improve UX. Non-blocking for v1. **5. Data source `name` filter validation message could be a warning** The validation that `name` requires `type` is correct, but using `AddError` means the plan fails. This is the right behavior -- just noting the UX is good here. **6. Minor: `int(model.TTL.ValueInt64())` -- narrowing conversion** `ValueInt64()` returns `int64`, and `int` on 64-bit Go is 64 bits, so this is fine in practice. But the `DNSRecord` struct uses `int` for TTL/Priority/Weight/Port. If this ever runs on a 32-bit target, large TTL values could overflow. Extremely unlikely for a Terraform provider, but worth a comment. ### SOP COMPLIANCE - [x] PR body has: Summary, Changes, Test Plan, Related -- all present and thorough - [x] No secrets, .env files, or credentials committed - [x] No unnecessary file changes (scope is tight: 2 new files + 2 modified files matching issues #11 and #12) - [x] Commit message is descriptive (PR title matches issue scope) - [ ] Tests exist for new code -- client-layer tests missing for `GetAllRecords` and `GetRecordsByType` ### PROCESS OBSERVATIONS - Combining #11 (resource) and #12 (data source) in a single PR is reasonable given they share the same identity model and the data source was needed to validate the resource during development. - Issue #13 (acceptance tests) is correctly deferred -- provider-level acceptance tests are a separate concern. But client-method tests should ship with the methods. - The PR is well-structured: clean separation between Terraform framework layer (resource/data source files) and API client layer (dns.go additions). - Documentation in CLAUDE.md and PR body is thorough -- the Review Checklist in the PR body shows disciplined self-review. ### VERDICT: NOT APPROVED **Reason:** New client methods `GetAllRecords` and `GetRecordsByType` lack integration tests. The existing test suite covers every other public method in `dns.go`. These two should match that standard before merge. The nit about `Read` calling `RemoveResource` for drift detection is strongly recommended but non-blocking for a first pass. **To unblock:** Add `TestGetAllRecords` and `TestGetRecordsByType` to `pkg/godaddy/dns_test.go` following the existing pattern.
- Resource Read() removes resource from state when record not found
  (standard Terraform drift-detection pattern)
- readRecordIntoModel returns bool for found/not-found, handles 404
- Add tflog.Warn when multiple records match a type+name query
- Add GetAllRecords and GetRecordsByType integration tests
- Add computed id attribute to dns_records data source

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #30 Review

DOMAIN REVIEW

Stack: Go / Terraform Plugin Framework / GoDaddy REST API

This PR implements the P0 DNS record resource and data source. The Plugin Framework patterns are largely correct and well-structured. Specific domain findings:

Plugin Framework -- correct patterns:

  • Compile-time interface checks (var _ resource.Resource = ..., var _ datasource.DataSource = ...) -- good
  • RequiresReplace on identity fields (domain, type, name) -- correct for PUT-replace semantics
  • TTL default via int64default.StaticInt64(600) -- proper Plugin Framework default handling
  • Configure() type assertion with diagnostic error -- correct
  • No id attribute on the resource (correct for Plugin Framework; the old SDK required it, the new framework does not)
  • resp.State.RemoveResource(ctx) for drift detection -- correct pattern

Plugin Framework -- data source routing logic:

  • The switch on hasType/hasName correctly routes to GetAllRecords, GetRecordsByType, or GetRecords -- clean and correct
  • Validation that name requires type matches the GoDaddy API constraint
  • Composite ID generation from query params is well-designed

PUT-replace semantics:

  • Create and Update both use ReplaceRecords which is correct for GoDaddy's API -- this is a PUT endpoint that replaces the record set for a given domain/type/name
  • Read-after-write pattern (calling readRecordIntoModel after Create/Update) ensures computed fields are populated from the API response

New client methods (GetAllRecords, GetRecordsByType):

  • Follow the same patterns as existing methods
  • URL construction uses url.PathEscape consistently -- good
  • Error wrapping with %w is consistent

BLOCKERS

1. 404 error detection uses direct type assertion instead of errors.As -- will silently break drift detection

In resource_dns_record.go, readRecordIntoModel (line ~261 in diff):

if apiErr, ok := err.(*godaddy.APIError); ok && apiErr.StatusCode == 404 {
    return false
}

This direct type assertion will always fail. The error chain is:

  1. client.do() returns &APIError{StatusCode: 404} (raw)
  2. client.doJSON() returns it unchanged
  3. client.GetRecords() wraps it: fmt.Errorf("getting records for ...: %w", err)

At step 3, the error becomes a *fmt.wrapError containing the *APIError. A direct err.(*godaddy.APIError) assertion will never match the wrapper. The fix is:

var apiErr *godaddy.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == 404 {
    return false
}

Impact: Without this fix, when a DNS record is deleted outside Terraform, Read() will report an error diagnostic instead of removing the resource from state. Drift detection is broken. Import of non-existent records will also error instead of returning "not found". This is the core correctness guarantee of the resource.

NITS

  1. Import ID format assumes no colons in domain/type/name: The import uses strings.SplitN(req.ID, ":", 3) which is fine for DNS use cases (domains, types, and names don't contain colons), but a comment noting this assumption would be helpful for future maintainers.

  2. Multiple-records warning is log-only: When readRecordIntoModel finds multiple records for the same domain/type/name, it logs a warning and silently uses the first one. This is the pragmatic choice, but documenting in the resource description that this resource manages a single record per domain/type/name combination would help users understand the limitation. GoDaddy does allow multiple records with the same type and name (e.g., multiple A records for round-robin).

  3. Optional fields zero-value semantics: The buildDNSRecord function correctly skips null/unknown optional fields, but the zero-value check in readRecordIntoModel means a user cannot explicitly set priority: 0. This is fine for practical DNS use (priority 0 is the default/lowest for MX), but worth a comment.

  4. Data source id could be more deterministic: The computed ID like palinks.app:A:@ is good. Consider whether an empty records list should still set state or produce a warning -- currently it silently returns an empty list, which is correct behavior.

  5. Test coverage is integration-only for new client methods: TestGetAllRecords and TestGetRecordsByType are integration tests hitting the live API (gated by env vars). No unit tests with mocked HTTP responses exist. This is consistent with the existing test strategy, so not blocking, but unit tests would improve CI reliability.

SOP COMPLIANCE

  • PR body follows template (Summary, Changes, Test Plan, Review Checklist, Related Notes)
  • Closes #11, closes #12 present in PR body
  • No secrets, .env files, or credentials committed
  • No scope creep -- all changes are directly related to DNS resource and data source
  • Commit messages are descriptive (title matches issue scope)
  • Branch name feat/dns-resource-datasource does not follow {issue-number}-{description} convention (should be 11-dns-resource-datasource or similar) -- minor process nit
  • Test Plan includes both automated and manual steps
  • Acceptance tests deferred to future PR (noted in Test Plan, acceptable for P0 critical path)

PROCESS OBSERVATIONS

  • This is the critical path PR that makes the provider actually usable. The architecture is sound -- PUT-replace semantics match GoDaddy's API, the Plugin Framework interfaces are correctly implemented, and the import support is well-designed.
  • The 404 bug is a real correctness issue but is a one-line fix (errors.As instead of direct assertion). It does not indicate a design problem.
  • Deferring acceptance tests to Phase 2 is reasonable given the live API dependency, but should be tracked as a follow-up issue.

VERDICT: NOT APPROVED

One blocker: the errors.As bug in readRecordIntoModel breaks drift detection and must be fixed before merge. All other findings are nits.

## PR #30 Review ### DOMAIN REVIEW **Stack:** Go / Terraform Plugin Framework / GoDaddy REST API This PR implements the P0 DNS record resource and data source. The Plugin Framework patterns are largely correct and well-structured. Specific domain findings: **Plugin Framework -- correct patterns:** - Compile-time interface checks (`var _ resource.Resource = ...`, `var _ datasource.DataSource = ...`) -- good - `RequiresReplace` on identity fields (domain, type, name) -- correct for PUT-replace semantics - TTL default via `int64default.StaticInt64(600)` -- proper Plugin Framework default handling - `Configure()` type assertion with diagnostic error -- correct - No `id` attribute on the resource (correct for Plugin Framework; the old SDK required it, the new framework does not) - `resp.State.RemoveResource(ctx)` for drift detection -- correct pattern **Plugin Framework -- data source routing logic:** - The `switch` on `hasType`/`hasName` correctly routes to `GetAllRecords`, `GetRecordsByType`, or `GetRecords` -- clean and correct - Validation that `name` requires `type` matches the GoDaddy API constraint - Composite ID generation from query params is well-designed **PUT-replace semantics:** - Create and Update both use `ReplaceRecords` which is correct for GoDaddy's API -- this is a PUT endpoint that replaces the record set for a given domain/type/name - Read-after-write pattern (calling `readRecordIntoModel` after Create/Update) ensures computed fields are populated from the API response **New client methods (`GetAllRecords`, `GetRecordsByType`):** - Follow the same patterns as existing methods - URL construction uses `url.PathEscape` consistently -- good - Error wrapping with `%w` is consistent ### BLOCKERS **1. 404 error detection uses direct type assertion instead of `errors.As` -- will silently break drift detection** In `resource_dns_record.go`, `readRecordIntoModel` (line ~261 in diff): ```go if apiErr, ok := err.(*godaddy.APIError); ok && apiErr.StatusCode == 404 { return false } ``` This direct type assertion will **always fail**. The error chain is: 1. `client.do()` returns `&APIError{StatusCode: 404}` (raw) 2. `client.doJSON()` returns it unchanged 3. `client.GetRecords()` wraps it: `fmt.Errorf("getting records for ...: %w", err)` At step 3, the error becomes a `*fmt.wrapError` containing the `*APIError`. A direct `err.(*godaddy.APIError)` assertion will never match the wrapper. The fix is: ```go var apiErr *godaddy.APIError if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { return false } ``` **Impact:** Without this fix, when a DNS record is deleted outside Terraform, `Read()` will report an error diagnostic instead of removing the resource from state. Drift detection is broken. Import of non-existent records will also error instead of returning "not found". This is the core correctness guarantee of the resource. ### NITS 1. **Import ID format assumes no colons in domain/type/name:** The import uses `strings.SplitN(req.ID, ":", 3)` which is fine for DNS use cases (domains, types, and names don't contain colons), but a comment noting this assumption would be helpful for future maintainers. 2. **Multiple-records warning is log-only:** When `readRecordIntoModel` finds multiple records for the same domain/type/name, it logs a warning and silently uses the first one. This is the pragmatic choice, but documenting in the resource description that this resource manages a single record per domain/type/name combination would help users understand the limitation. GoDaddy does allow multiple records with the same type and name (e.g., multiple A records for round-robin). 3. **Optional fields zero-value semantics:** The `buildDNSRecord` function correctly skips null/unknown optional fields, but the zero-value check in `readRecordIntoModel` means a user cannot explicitly set `priority: 0`. This is fine for practical DNS use (priority 0 is the default/lowest for MX), but worth a comment. 4. **Data source `id` could be more deterministic:** The computed ID like `palinks.app:A:@` is good. Consider whether an empty records list should still set state or produce a warning -- currently it silently returns an empty list, which is correct behavior. 5. **Test coverage is integration-only for new client methods:** `TestGetAllRecords` and `TestGetRecordsByType` are integration tests hitting the live API (gated by env vars). No unit tests with mocked HTTP responses exist. This is consistent with the existing test strategy, so not blocking, but unit tests would improve CI reliability. ### SOP COMPLIANCE - [x] PR body follows template (Summary, Changes, Test Plan, Review Checklist, Related Notes) - [x] `Closes #11, closes #12` present in PR body - [x] No secrets, `.env` files, or credentials committed - [x] No scope creep -- all changes are directly related to DNS resource and data source - [x] Commit messages are descriptive (title matches issue scope) - [ ] Branch name `feat/dns-resource-datasource` does not follow `{issue-number}-{description}` convention (should be `11-dns-resource-datasource` or similar) -- minor process nit - [x] Test Plan includes both automated and manual steps - [ ] Acceptance tests deferred to future PR (noted in Test Plan, acceptable for P0 critical path) ### PROCESS OBSERVATIONS - This is the critical path PR that makes the provider actually usable. The architecture is sound -- PUT-replace semantics match GoDaddy's API, the Plugin Framework interfaces are correctly implemented, and the import support is well-designed. - The 404 bug is a real correctness issue but is a one-line fix (`errors.As` instead of direct assertion). It does not indicate a design problem. - Deferring acceptance tests to Phase 2 is reasonable given the live API dependency, but should be tracked as a follow-up issue. ### VERDICT: NOT APPROVED One blocker: the `errors.As` bug in `readRecordIntoModel` breaks drift detection and must be fixed before merge. All other findings are nits.
GetRecords wraps errors with fmt.Errorf, so direct type assertion
always fails. Use errors.As to unwrap correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Author
Owner

PR #30 Re-Review

BLOCKER FIX VERIFICATION

Previous blocker: Direct type assertion err.(*godaddy.APIError) instead of errors.As

Status: RESOLVED. The fix is correct on two levels:

  1. Import: "errors" package is properly imported in resource_dns_record.go.
  2. Usage: errors.As(err, &apiErr) is used with the correct pointer-to-pointer pattern (var apiErr *godaddy.APIError then errors.As(err, &apiErr)).
  3. Necessity confirmed: GetRecords in pkg/godaddy/dns.go:37 wraps the *APIError with fmt.Errorf("getting records for %s %s.%s: %w", ...). A direct type assertion would silently fail to match the wrapped error, causing 404s to surface as diagnostic errors instead of being handled as "not found." The errors.As fix correctly unwraps the chain.

Related instances in pkg/godaddy/dns_test.go (lines 93, 101, 181): Three direct type assertions exist in test code using err.(*APIError). These are acceptable -- the tests are in the godaddy package and call client methods directly. Within the same package, the error from doJSON is wrapped only once by the client method, and Go's errors.As would also work, but the direct assertion works here because test code calls the method directly and the comma-ok pattern handles the non-match case safely. Not a blocker.

DOMAIN REVIEW (Go / Terraform Plugin Framework)

Tech stack: Go, Terraform Plugin Framework, GoDaddy REST API.

Resource (resource_dns_record.go, 350 lines)

  • Compile-time interface checks for resource.Resource and resource.ResourceWithImportState -- correct pattern.
  • RequiresReplace on identity fields (domain, type, name) -- correct for PUT-replace semantics.
  • TTL default 600 via int64default.StaticInt64 -- proper use of framework defaults.
  • Import ID parsing with strings.SplitN(req.ID, ":", 3) and validation of 3 non-empty parts -- correct.
  • readRecordIntoModel helper avoids DRY violation across Create, Read, Update, Import -- good.
  • Optional SRV fields (priority, weight, port) handled as null when zero from API -- correct approach for GoDaddy's API which uses 0 as "not set."
  • Read handles drift detection: removes resource from state when record not found externally.
  • Multiple matching records logged as warning, first used -- acceptable for P0, worth documenting as known behavior.

Data source (data_source_dns_records.go, 210 lines)

  • Validation that name requires type is enforced in Read -- matches GoDaddy API constraints.
  • Three-way routing (GetAllRecords/GetRecordsByType/GetRecords) via switch is clean.
  • Computed ID reflects the query shape -- good for state tracking.
  • Schema mirrors record entry model with all 7 fields -- consistent with resource schema.

Client additions (pkg/godaddy/dns.go)

  • GetAllRecords and GetRecordsByType follow the established pattern exactly (NewRequestWithContext, doJSON, error wrapping with %w).
  • url.PathEscape used on all dynamic URL segments -- prevents path traversal and injection.

Tests (pkg/godaddy/dns_test.go, 55 new lines)

  • TestGetAllRecords: Verifies non-empty result and presence of NS records -- reasonable for integration tests.
  • TestGetRecordsByType: Verifies type filtering correctness and non-empty data -- good.
  • Both use retryOn429 helper -- matches existing test patterns for rate-limit handling.

Provider registration (provider.go)

  • NewDNSRecordResource and NewDNSRecordsDataSource registered -- correct.

BLOCKERS

None. The previous blocker (errors.As) is resolved.

NITS

  1. readRecordIntoModel optional field handling: The else if model.Priority.IsNull() || model.Priority.IsUnknown() branches (lines ~308-310 in the diff) preserve the existing model value when API returns 0 and the model already has a non-null value. This means if a user sets priority = 0 explicitly, it would be preserved in state even though the API treats 0 as "not set." This is an edge case worth a code comment, not a bug.

  2. No acceptance tests yet: The PR body notes acceptance tests are Phase 2. This is acceptable for P0 but should be tracked.

  3. dns_test.go direct type assertions: While acceptable in test code as noted above, migrating these to errors.As would make the test code consistent with the resource code pattern. Low priority.

SOP COMPLIANCE

  • PR body has Summary, Changes, Test Plan, Related -- complete and detailed
  • No secrets, .env files, or credentials committed
  • No unnecessary file changes (scope is exactly #11 + #12)
  • Commit messages are descriptive
  • Changes match the stated scope (DNS resource + data source + client methods + tests + provider registration)

PROCESS OBSERVATIONS

  • PR closes two related issues (#11, #12) together -- this aligns with the parallel wave strategy noted in project memory for tightly coupled tickets.
  • Manual test plan items remain unchecked. These should be completed before merge but are not blocking QA approval of the code itself.
  • The provider is now functional for its primary use case (DNS record management). This unblocks the critical path described in the project roadmap.

VERDICT: APPROVED

## PR #30 Re-Review ### BLOCKER FIX VERIFICATION **Previous blocker: Direct type assertion `err.(*godaddy.APIError)` instead of `errors.As`** **Status: RESOLVED.** The fix is correct on two levels: 1. **Import**: `"errors"` package is properly imported in `resource_dns_record.go`. 2. **Usage**: `errors.As(err, &apiErr)` is used with the correct pointer-to-pointer pattern (`var apiErr *godaddy.APIError` then `errors.As(err, &apiErr)`). 3. **Necessity confirmed**: `GetRecords` in `pkg/godaddy/dns.go:37` wraps the `*APIError` with `fmt.Errorf("getting records for %s %s.%s: %w", ...)`. A direct type assertion would silently fail to match the wrapped error, causing 404s to surface as diagnostic errors instead of being handled as "not found." The `errors.As` fix correctly unwraps the chain. **Related instances in `pkg/godaddy/dns_test.go`** (lines 93, 101, 181): Three direct type assertions exist in test code using `err.(*APIError)`. These are acceptable -- the tests are in the `godaddy` package and call client methods directly. Within the same package, the error from `doJSON` is wrapped only once by the client method, and Go's `errors.As` would also work, but the direct assertion works here because test code calls the method directly and the comma-ok pattern handles the non-match case safely. Not a blocker. ### DOMAIN REVIEW (Go / Terraform Plugin Framework) **Tech stack**: Go, Terraform Plugin Framework, GoDaddy REST API. **Resource (`resource_dns_record.go`, 350 lines)** - Compile-time interface checks for `resource.Resource` and `resource.ResourceWithImportState` -- correct pattern. - `RequiresReplace` on identity fields (domain, type, name) -- correct for PUT-replace semantics. - TTL default 600 via `int64default.StaticInt64` -- proper use of framework defaults. - Import ID parsing with `strings.SplitN(req.ID, ":", 3)` and validation of 3 non-empty parts -- correct. - `readRecordIntoModel` helper avoids DRY violation across Create, Read, Update, Import -- good. - Optional SRV fields (priority, weight, port) handled as null when zero from API -- correct approach for GoDaddy's API which uses 0 as "not set." - Read handles drift detection: removes resource from state when record not found externally. - Multiple matching records logged as warning, first used -- acceptable for P0, worth documenting as known behavior. **Data source (`data_source_dns_records.go`, 210 lines)** - Validation that `name` requires `type` is enforced in `Read` -- matches GoDaddy API constraints. - Three-way routing (GetAllRecords/GetRecordsByType/GetRecords) via switch is clean. - Computed ID reflects the query shape -- good for state tracking. - Schema mirrors record entry model with all 7 fields -- consistent with resource schema. **Client additions (`pkg/godaddy/dns.go`)** - `GetAllRecords` and `GetRecordsByType` follow the established pattern exactly (NewRequestWithContext, doJSON, error wrapping with `%w`). - `url.PathEscape` used on all dynamic URL segments -- prevents path traversal and injection. **Tests (`pkg/godaddy/dns_test.go`, 55 new lines)** - `TestGetAllRecords`: Verifies non-empty result and presence of NS records -- reasonable for integration tests. - `TestGetRecordsByType`: Verifies type filtering correctness and non-empty data -- good. - Both use `retryOn429` helper -- matches existing test patterns for rate-limit handling. **Provider registration (`provider.go`)** - `NewDNSRecordResource` and `NewDNSRecordsDataSource` registered -- correct. ### BLOCKERS None. The previous blocker (`errors.As`) is resolved. ### NITS 1. **`readRecordIntoModel` optional field handling**: The `else if model.Priority.IsNull() || model.Priority.IsUnknown()` branches (lines ~308-310 in the diff) preserve the existing model value when API returns 0 and the model already has a non-null value. This means if a user sets `priority = 0` explicitly, it would be preserved in state even though the API treats 0 as "not set." This is an edge case worth a code comment, not a bug. 2. **No acceptance tests yet**: The PR body notes acceptance tests are Phase 2. This is acceptable for P0 but should be tracked. 3. **`dns_test.go` direct type assertions**: While acceptable in test code as noted above, migrating these to `errors.As` would make the test code consistent with the resource code pattern. Low priority. ### SOP COMPLIANCE - [x] PR body has Summary, Changes, Test Plan, Related -- complete and detailed - [x] No secrets, .env files, or credentials committed - [x] No unnecessary file changes (scope is exactly #11 + #12) - [x] Commit messages are descriptive - [x] Changes match the stated scope (DNS resource + data source + client methods + tests + provider registration) ### PROCESS OBSERVATIONS - PR closes two related issues (#11, #12) together -- this aligns with the parallel wave strategy noted in project memory for tightly coupled tickets. - Manual test plan items remain unchecked. These should be completed before merge but are not blocking QA approval of the code itself. - The provider is now functional for its primary use case (DNS record management). This unblocks the critical path described in the project roadmap. ### VERDICT: APPROVED
ldraney force-pushed feat/dns-resource-datasource from eef22767e4 to 33cd612ab4 2026-06-14 18:05:01 +00:00 Compare
ldraney deleted branch feat/dns-resource-datasource 2026-06-14 18:05:09 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ldraney/godaddy-tofu!30
No description provided.