Archived
Improve local dev override docs and add read-only example
This commit is contained in:
@@ -2,212 +2,188 @@
|
|||||||
|
|
||||||
A standalone Terraform provider for Dynu DNS.
|
A standalone Terraform provider for Dynu DNS.
|
||||||
|
|
||||||
> Status: **read-only milestone**. This provider currently implements provider configuration and data sources only.
|
> Status: **read-only milestone**. This provider currently implements provider configuration plus read-only data sources.
|
||||||
|
|
||||||
## Feature scope
|
## Quick start (local dev with `dev_overrides`)
|
||||||
|
|
||||||
Implemented:
|
This provider is **not published** to the Terraform Registry yet. For local development, use Terraform CLI `dev_overrides` and your local provider binary.
|
||||||
- Provider authentication using `api_key` or `DYNU_API_KEY`
|
|
||||||
- Optional provider `base_url` override for local test/dev setups
|
|
||||||
- Data sources:
|
|
||||||
- `dynu_domains`
|
|
||||||
- `dynu_domain`
|
|
||||||
- `dynu_dns_records`
|
|
||||||
|
|
||||||
Not implemented yet:
|
1. Build provider binary in repo root:
|
||||||
- Terraform resources (no create/update/delete)
|
|
||||||
- Any write API operations
|
|
||||||
|
|
||||||
## Provider source and module path
|
```bash
|
||||||
|
go build -o terraform-provider-dynu
|
||||||
|
```
|
||||||
|
|
||||||
- Terraform provider source address: `dynu/dynu`
|
2. Configure `~/.terraformrc`:
|
||||||
- Go module path: `github.com/dynu/terraform-provider-dynu`
|
|
||||||
|
|
||||||
The repository can be hosted elsewhere during development, but module and provider source naming are kept aligned with planned public registry publishing.
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
- Terraform `>= 1.5`
|
|
||||||
- Go `>= 1.23`
|
|
||||||
- Dynu API key for live API usage
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Option 1: Terraform configuration.
|
|
||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
|
provider_installation {
|
||||||
|
dev_overrides {
|
||||||
|
"dynu/dynu" = "/path/to/terraform-dynu-provider"
|
||||||
|
}
|
||||||
|
|
||||||
|
direct {}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the runnable read-only example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd examples/read_only
|
||||||
|
cp terraform.tfvars.example terraform.tfvars
|
||||||
|
terraform validate
|
||||||
|
terraform plan
|
||||||
|
```
|
||||||
|
|
||||||
|
> With `dev_overrides`, Terraform uses your local binary for `dynu/dynu`. `terraform init` is not the primary local-dev loop here and may still try registry/network operations.
|
||||||
|
|
||||||
|
## Copy/paste starter configuration
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
dynu = {
|
||||||
|
source = "dynu/dynu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
provider "dynu" {
|
provider "dynu" {
|
||||||
api_key = var.dynu_api_key
|
api_key = var.dynu_api_key # optional if DYNU_API_KEY is set
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "dynu_api_key" {
|
variable "dynu_api_key" {
|
||||||
type = string
|
type = string
|
||||||
|
default = null
|
||||||
sensitive = true
|
sensitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data "dynu_domains" "all" {}
|
||||||
|
|
||||||
|
# Use a real hostname from your Dynu account.
|
||||||
|
data "dynu_domain" "selected" {
|
||||||
|
hostname = "www.example.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
data "dynu_dns_records" "selected" {
|
||||||
|
hostname = "www.example.com"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Option 2: Environment variable.
|
## Provider schema reference
|
||||||
|
|
||||||
```bash
|
### Provider: `dynu`
|
||||||
export DYNU_API_KEY="your-dynu-api-key"
|
|
||||||
|
Optional arguments:
|
||||||
|
- `api_key` (String, Sensitive)
|
||||||
|
- Falls back to `DYNU_API_KEY` environment variable when omitted.
|
||||||
|
- `base_url` (String)
|
||||||
|
- Test/dev override for Dynu API base URL.
|
||||||
|
|
||||||
|
No provider resources are implemented yet.
|
||||||
|
|
||||||
|
## Data source schema reference
|
||||||
|
|
||||||
|
### `dynu_domains`
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- none
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
- `domains` (List(Object)):
|
||||||
|
- `id`, `name`, `unicode_name`, `token` (sensitive), `state`, `group`
|
||||||
|
- `ipv4_address`, `ipv6_address`, `ttl`
|
||||||
|
- `ipv4`, `ipv6`, `ipv4_wildcard_alias`, `ipv6_wildcard_alias`
|
||||||
|
- `allow_zone_transfer`, `dnssec`, `created_on`, `updated_on`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "dynu_domains" "all" {}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data source examples
|
### `dynu_domain`
|
||||||
|
|
||||||
See the `examples/` directory:
|
Arguments:
|
||||||
- `examples/provider/provider.tf`
|
- `hostname` (String, required)
|
||||||
- `examples/data-sources/dynu_domains/data-source.tf`
|
|
||||||
- `examples/data-sources/dynu_domain/data-source.tf`
|
Attributes:
|
||||||
- `examples/data-sources/dynu_dns_records/data-source.tf`
|
- `domain` (Object) with the same fields as `dynu_domains.domains[*]`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "dynu_domain" "selected" {
|
||||||
|
hostname = "www.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `dynu_dns_records`
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- `hostname` (String, required)
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
- `domain_id` (Number)
|
||||||
|
- `domain_name` (String)
|
||||||
|
- `records` (List(Object)) with:
|
||||||
|
- `id`, `domain_id`, `domain_name`, `node_name`, `hostname`, `record_type`
|
||||||
|
- `ttl`, `state`, `content`, `updated_on`, `group`, `host`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```hcl
|
||||||
|
data "dynu_dns_records" "selected" {
|
||||||
|
hostname = "www.example.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
- Runnable local workflow: `examples/read_only/`
|
||||||
|
- Provider block example: `examples/provider/provider.tf`
|
||||||
|
- Individual data source snippets:
|
||||||
|
- `examples/data-sources/dynu_domains/data-source.tf`
|
||||||
|
- `examples/data-sources/dynu_domain/data-source.tf`
|
||||||
|
- `examples/data-sources/dynu_dns_records/data-source.tf`
|
||||||
|
|
||||||
|
## Troubleshooting local dev
|
||||||
|
|
||||||
|
- **Unsupported provider arguments**
|
||||||
|
- Symptom: errors such as `Unsupported argument` (for example `username`).
|
||||||
|
- Fix: use only `api_key` and/or `base_url` in `provider "dynu"`.
|
||||||
|
|
||||||
|
- **Bad API credentials**
|
||||||
|
- Symptom: diagnostics mention authentication failures.
|
||||||
|
- Fix: verify `api_key` or `DYNU_API_KEY` and re-run `terraform plan`.
|
||||||
|
|
||||||
|
- **Unknown data source arguments**
|
||||||
|
- Symptom: unsupported argument errors in data blocks.
|
||||||
|
- Fix: `dynu_domain` and `dynu_dns_records` require only `hostname`; `dynu_domains` takes no arguments.
|
||||||
|
|
||||||
|
- **Stale provider binary after code changes**
|
||||||
|
- Symptom: Terraform behavior doesn't reflect latest code.
|
||||||
|
- Fix: rebuild binary (`go build -o terraform-provider-dynu`) and run `terraform plan` again.
|
||||||
|
|
||||||
## Developer workflow
|
## Developer workflow
|
||||||
|
|
||||||
- `./scripts/setup-dev.sh` - validate local Go/Terraform toolchains and environment health (no changes)
|
- `./scripts/setup-dev.sh` - validate local toolchain requirements
|
||||||
- `./scripts/setup-dev.sh --fix` - attempt safe remediation with installed version managers (`mise`, `asdf`, `tfenv`, `tenv`)
|
- `./scripts/check.sh` - formatting, vet, and unit tests
|
||||||
- `./scripts/setup-dev.sh --strict` - require Terraform to be installed
|
- `./scripts/test-integration.sh` - local mock-backed provider integration tests
|
||||||
- `./scripts/check.sh` - formatting, vet, and unit tests (Tier A)
|
- `./scripts/testacc.sh` - acceptance/integration test wrapper (live tests opt-in)
|
||||||
- `./scripts/test-integration.sh` - local mock-backed provider integration tests (Tier B)
|
|
||||||
- `./scripts/testacc.sh` - default: Tier B; live mode available with `--live` (Tier C)
|
|
||||||
|
|
||||||
### setup-dev behavior
|
Live acceptance tests are read-only and require:
|
||||||
|
|
||||||
`scripts/setup-dev.sh` now performs robust validation and troubleshooting checks:
|
|
||||||
|
|
||||||
- Always reports resolved paths for `bash`, `git`, `go`, and `terraform` (Terraform is warning-only unless `--strict` is used).
|
|
||||||
- Enforces minimum versions:
|
|
||||||
- Go `>= 1.23` (error if missing or too old)
|
|
||||||
- Terraform `>= 1.5` (warning if missing by default, error if present but too old)
|
|
||||||
- Detects common broken Go setups:
|
|
||||||
- manual `GOROOT` conflicting with `go env GOROOT`
|
|
||||||
- stale stdlib tree mismatches (for example missing `slices`, `maps`, `math/rand/v2`)
|
|
||||||
- Detects malformed `GOPROXY` and prints the recommended non-destructive fix:
|
|
||||||
- `go env -w GOPROXY=https://proxy.golang.org,direct`
|
|
||||||
- Supports optional safe auto-fix mode:
|
|
||||||
- only uses already-installed user-space managers
|
|
||||||
- does **not** run distro package managers, `sudo`, or shell-profile edits
|
|
||||||
- re-validates tools after attempted remediation
|
|
||||||
|
|
||||||
The script is safe for both normal shells and VS Code integrated terminals, and includes guidance when terminal/session restart may be needed after changing versions.
|
|
||||||
|
|
||||||
### Standalone repository guarantee
|
|
||||||
|
|
||||||
This repository is intentionally self-contained:
|
|
||||||
- no dependency on sibling repositories
|
|
||||||
- no dependency on external helper scripts (for example `services-up.sh`)
|
|
||||||
- no hardcoded local paths (for example `/workspace/...` or `/home/...`)
|
|
||||||
|
|
||||||
### setup-dev troubleshooting quick reference
|
|
||||||
|
|
||||||
- **Malformed GOPROXY**
|
|
||||||
- Symptom: warning that GOPROXY has no valid entries.
|
|
||||||
- Fix: `go env -w GOPROXY=https://proxy.golang.org,direct`
|
|
||||||
|
|
||||||
- **Broken GOROOT**
|
|
||||||
- Symptom: `GOROOT` environment value differs from `go env GOROOT`, or stdlib package checks fail.
|
|
||||||
- Fix: usually remove manual override with `unset GOROOT`, then ensure the intended `go` binary is first in `PATH`.
|
|
||||||
|
|
||||||
- **Go version / stdlib path mismatch**
|
|
||||||
- Symptom: `go version` looks modern but build errors mention missing stdlib packages (for example `slices`, `maps`, `math/rand/v2`).
|
|
||||||
- Cause: stale or mismatched Go installation path.
|
|
||||||
- Fix: re-select/install Go via your version manager and re-run `./scripts/setup-dev.sh`.
|
|
||||||
|
|
||||||
- **VS Code integrated terminal stale environment**
|
|
||||||
- Symptom: command paths or versions do not match your expected shell setup.
|
|
||||||
- Fix: restart the integrated terminal (or reload the VS Code window) and run `./scripts/setup-dev.sh` again.
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go build ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing model
|
|
||||||
|
|
||||||
The provider now has three explicit test tiers:
|
|
||||||
|
|
||||||
### Tier A: unit tests (fast, no network)
|
|
||||||
|
|
||||||
Covers focused package behavior (client parsing, mappers, provider helper logic).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/check.sh
|
|
||||||
go test ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
### Troubleshooting Go dependency downloads
|
|
||||||
|
|
||||||
If you see an error like:
|
|
||||||
|
|
||||||
```text
|
|
||||||
GOPROXY list is not the empty string, but contains no entries
|
|
||||||
```
|
|
||||||
|
|
||||||
your local Go environment is misconfigured. A common fix is:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go env -w GOPROXY=https://proxy.golang.org,direct
|
|
||||||
```
|
|
||||||
|
|
||||||
Helpful diagnostics:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go env GOPROXY
|
|
||||||
echo "$GOPROXY"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Acceptance tests
|
|
||||||
### Tier B: local integration tests (mock Dynu API, no real credentials)
|
|
||||||
|
|
||||||
These tests use an `httptest` fake Dynu API server and run the Terraform provider end-to-end against deterministic fixtures.
|
|
||||||
|
|
||||||
- No Dynu account required
|
|
||||||
- Dummy API key is used in test provider configuration
|
|
||||||
- Exercises provider wiring, schema/state mapping, hostname resolution flow, and diagnostic behavior
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/test-integration.sh
|
|
||||||
./scripts/testacc.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tier C: live acceptance tests (opt-in)
|
|
||||||
|
|
||||||
These tests call the real Dynu API and are read-only.
|
|
||||||
|
|
||||||
Required environment variables:
|
|
||||||
- `TF_ACC=1`
|
- `TF_ACC=1`
|
||||||
- `DYNU_API_KEY`
|
- `DYNU_API_KEY`
|
||||||
|
- optional `DYNU_DOMAIN` for domain-specific coverage
|
||||||
|
|
||||||
Optional:
|
## Feature scope
|
||||||
- `DYNU_DOMAIN` (required for domain-specific acceptance tests such as `dynu_domain` and `dynu_dns_records`)
|
|
||||||
|
|
||||||
```bash
|
Implemented:
|
||||||
TF_ACC=1 DYNU_API_KEY="your-dynu-api-key" DYNU_DOMAIN="www.example.com" ./scripts/testacc.sh --live
|
- Provider authentication via `api_key` or `DYNU_API_KEY`
|
||||||
# or
|
- Optional provider `base_url` override
|
||||||
LIVE=1 TF_ACC=1 DYNU_API_KEY="your-dynu-api-key" ./scripts/testacc.sh
|
- Data sources: `dynu_domains`, `dynu_domain`, `dynu_dns_records`
|
||||||
```
|
|
||||||
|
|
||||||
If `DYNU_DOMAIN` is omitted, domain-specific live tests skip cleanly.
|
Not implemented yet:
|
||||||
|
- Terraform resources (create/update/delete)
|
||||||
## CI
|
- Any write API operations
|
||||||
|
|
||||||
GitHub Actions CI runs on push and pull requests and executes:
|
|
||||||
- gofmt verification
|
|
||||||
- `go vet ./...`
|
|
||||||
- `go test ./...`
|
|
||||||
|
|
||||||
Live acceptance tests are intentionally excluded from default CI.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Registry-style markdown docs are stored in `docs/`.
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- Dynu timestamps are currently exposed as strings exactly as returned by Dynu API.
|
|
||||||
- Data returned from Dynu is sorted in provider state for Terraform stability.
|
|
||||||
- Read-only operations only.
|
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
Next planned milestone after this testing foundation:
|
|
||||||
- first writable resource (`dynu_dns_record`) with strict schema validation, import support, mock-first integration tests, and then live acceptance coverage.
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Lists all Dynu domains available to the configured API key.
|
||||||
|
data "dynu_domains" "all" {}
|
||||||
|
|
||||||
|
# Resolves the Dynu root domain for a specific hostname.
|
||||||
|
data "dynu_domain" "selected" {
|
||||||
|
hostname = var.hostname
|
||||||
|
}
|
||||||
|
|
||||||
|
# Lists DNS records for the root domain resolved from the same hostname.
|
||||||
|
data "dynu_dns_records" "selected" {
|
||||||
|
hostname = var.hostname
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
output "domains" {
|
||||||
|
description = "All visible Dynu domains."
|
||||||
|
value = data.dynu_domains.all.domains
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resolved_domain" {
|
||||||
|
description = "Resolved Dynu root domain details for var.hostname."
|
||||||
|
value = data.dynu_domain.selected.domain
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resolved_dns_records" {
|
||||||
|
description = "DNS records for the resolved root domain."
|
||||||
|
value = data.dynu_dns_records.selected.records
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resolved_domain_id" {
|
||||||
|
description = "Domain ID returned by dynu_dns_records."
|
||||||
|
value = data.dynu_dns_records.selected.domain_id
|
||||||
|
}
|
||||||
|
|
||||||
|
output "resolved_domain_name" {
|
||||||
|
description = "Root domain name returned by dynu_dns_records."
|
||||||
|
value = data.dynu_dns_records.selected.domain_name
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
dynu = {
|
||||||
|
source = "dynu/dynu"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Local development note:
|
||||||
|
# This source address stays "dynu/dynu" while using ~/.terraformrc dev_overrides.
|
||||||
|
# Terraform will load your local terraform-provider-dynu binary instead of the public registry.
|
||||||
|
provider "dynu" {
|
||||||
|
# Use DYNU_API_KEY environment variable by default.
|
||||||
|
# Set var.dynu_api_key in terraform.tfvars to override.
|
||||||
|
api_key = var.dynu_api_key
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Copy to terraform.tfvars and adjust values for your environment.
|
||||||
|
# dynu_api_key can be omitted when DYNU_API_KEY is exported in your shell.
|
||||||
|
# dynu_api_key = "replace-with-dynu-api-key"
|
||||||
|
|
||||||
|
# Use a hostname that exists in your Dynu account.
|
||||||
|
hostname = "www.example.com"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
variable "dynu_api_key" {
|
||||||
|
description = "Dynu API key. Leave null to use DYNU_API_KEY from environment."
|
||||||
|
type = string
|
||||||
|
default = null
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "hostname" {
|
||||||
|
description = "Fully-qualified hostname used by dynu_domain and dynu_dns_records data sources."
|
||||||
|
type = string
|
||||||
|
default = "www.example.com"
|
||||||
|
}
|
||||||
@@ -56,6 +56,16 @@ type apiException struct {
|
|||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type APIError struct {
|
||||||
|
StatusCode int
|
||||||
|
Type string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *APIError) Error() string {
|
||||||
|
return fmt.Sprintf("dynu API error %d (%s): %s", e.StatusCode, e.Type, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
type apiResponse struct {
|
type apiResponse struct {
|
||||||
StatusCode int `json:"statusCode"`
|
StatusCode int `json:"statusCode"`
|
||||||
Exception *apiException `json:"exception"`
|
Exception *apiException `json:"exception"`
|
||||||
@@ -202,5 +212,9 @@ func parseAPIException(payload []byte) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("dynu API error %d (%s): %s", apiResult.Exception.StatusCode, apiResult.Exception.Type, apiResult.Exception.Message)
|
return &APIError{
|
||||||
|
StatusCode: apiResult.Exception.StatusCode,
|
||||||
|
Type: apiResult.Exception.Type,
|
||||||
|
Message: apiResult.Exception.Message,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,13 +115,13 @@ func (d *dnsRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequ
|
|||||||
|
|
||||||
domainID, domainName, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString())
|
domainID, domainName, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error())
|
resp.Diagnostics.AddError(diagnosticSummary("Unable to resolve Dynu domain from hostname", err), err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
records, err := d.clientProvider.client.ListDNSRecords(ctx, domainID)
|
records, err := d.clientProvider.client.ListDNSRecords(ctx, domainID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Unable to list Dynu DNS records", err.Error())
|
resp.Diagnostics.AddError(diagnosticSummary("Unable to list Dynu DNS records", err), err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,13 +88,13 @@ func (d *domainDataSource) Read(ctx context.Context, req datasource.ReadRequest,
|
|||||||
|
|
||||||
domainID, _, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString())
|
domainID, _, err := d.clientProvider.client.GetRootDomain(ctx, config.Hostname.ValueString())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error())
|
resp.Diagnostics.AddError(diagnosticSummary("Unable to resolve Dynu domain from hostname", err), err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domain, err := d.clientProvider.client.GetDomainByID(ctx, domainID)
|
domain, err := d.clientProvider.client.GetDomainByID(ctx, domainID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Unable to get Dynu domain", err.Error())
|
resp.Diagnostics.AddError(diagnosticSummary("Unable to get Dynu domain", err), err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ func (d *domainsDataSource) Configure(_ context.Context, req datasource.Configur
|
|||||||
func (d *domainsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
|
func (d *domainsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||||
domains, err := d.clientProvider.client.ListDomains(ctx)
|
domains, err := d.clientProvider.client.ListDomains(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.Diagnostics.AddError("Unable to list Dynu domains", err.Error())
|
resp.Diagnostics.AddError(diagnosticSummary("Unable to list Dynu domains", err), err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||||
|
)
|
||||||
|
|
||||||
|
func diagnosticSummary(defaultSummary string, err error) string {
|
||||||
|
var apiErr *dynuclient.APIError
|
||||||
|
if !errors.As(err, &apiErr) {
|
||||||
|
return defaultSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
switch apiErr.StatusCode {
|
||||||
|
case 401, 403:
|
||||||
|
return defaultSummary + " (authentication failed)"
|
||||||
|
case 404:
|
||||||
|
return defaultSummary + " (not found)"
|
||||||
|
default:
|
||||||
|
return defaultSummary
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -157,6 +157,59 @@ func TestIntegrationDataSourceDiagnosticsFromAPIError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIntegrationDataSourceDiagnosticsFromAuthError(t *testing.T) {
|
||||||
|
fake := fakedynu.NewServer()
|
||||||
|
defer fake.Close()
|
||||||
|
fake.SetAPIError("/dns/getroot/www.a.example.com", fakedynu.APIError{HTTPStatus: 401, StatusCode: 401, Type: "Unauthorized", Message: "invalid api key"})
|
||||||
|
|
||||||
|
ds := NewDomainDataSource().(*domainDataSource)
|
||||||
|
configureDataSource(t, ds, fake.BaseURL())
|
||||||
|
|
||||||
|
var schemaResp datasource.SchemaResponse
|
||||||
|
ds.Schema(context.Background(), datasource.SchemaRequest{}, &schemaResp)
|
||||||
|
|
||||||
|
req := datasource.ReadRequest{Config: tfsdk.Config{
|
||||||
|
Schema: schemaResp.Schema,
|
||||||
|
Raw: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{"hostname": tftypes.String}}, map[string]tftypes.Value{
|
||||||
|
"hostname": tftypes.NewValue(tftypes.String, "www.a.example.com"),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
|
||||||
|
ds.Read(context.Background(), req, &resp)
|
||||||
|
if !resp.Diagnostics.HasError() {
|
||||||
|
t.Fatal("expected diagnostics error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(resp.Diagnostics[0].Summary(), "authentication failed") {
|
||||||
|
t.Fatalf("unexpected diagnostics summary: %s", resp.Diagnostics[0].Summary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntegrationDataSourceDiagnosticsFromNotFoundError(t *testing.T) {
|
||||||
|
fake := fakedynu.NewServer()
|
||||||
|
defer fake.Close()
|
||||||
|
|
||||||
|
ds := NewDomainDataSource().(*domainDataSource)
|
||||||
|
configureDataSource(t, ds, fake.BaseURL())
|
||||||
|
|
||||||
|
var schemaResp datasource.SchemaResponse
|
||||||
|
ds.Schema(context.Background(), datasource.SchemaRequest{}, &schemaResp)
|
||||||
|
|
||||||
|
req := datasource.ReadRequest{Config: tfsdk.Config{
|
||||||
|
Schema: schemaResp.Schema,
|
||||||
|
Raw: tftypes.NewValue(tftypes.Object{AttributeTypes: map[string]tftypes.Type{"hostname": tftypes.String}}, map[string]tftypes.Value{
|
||||||
|
"hostname": tftypes.NewValue(tftypes.String, "missing.a.example.com"),
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
resp := datasource.ReadResponse{State: tfsdk.State{Schema: schemaResp.Schema}}
|
||||||
|
ds.Read(context.Background(), req, &resp)
|
||||||
|
if !resp.Diagnostics.HasError() {
|
||||||
|
t.Fatal("expected diagnostics error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(resp.Diagnostics[0].Summary(), "not found") {
|
||||||
|
t.Fatalf("unexpected diagnostics summary: %s", resp.Diagnostics[0].Summary())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func configureDataSource(t *testing.T, ds datasource.DataSourceWithConfigure, baseURL string) {
|
func configureDataSource(t *testing.T, ds datasource.DataSourceWithConfigure, baseURL string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
resp := datasource.ConfigureResponse{}
|
resp := datasource.ConfigureResponse{}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||||
|
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,3 +34,61 @@ func TestResolveAPIKey(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderSchema(t *testing.T) {
|
||||||
|
p := New("test")()
|
||||||
|
var resp provider.SchemaResponse
|
||||||
|
p.Schema(context.Background(), provider.SchemaRequest{}, &resp)
|
||||||
|
|
||||||
|
apiKeyAttr, ok := resp.Schema.Attributes["api_key"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected api_key in provider schema")
|
||||||
|
}
|
||||||
|
if !apiKeyAttr.IsOptional() {
|
||||||
|
t.Fatal("expected api_key to be optional")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURLAttr, ok := resp.Schema.Attributes["base_url"]
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected base_url in provider schema")
|
||||||
|
}
|
||||||
|
if !baseURLAttr.IsOptional() {
|
||||||
|
t.Fatal("expected base_url to be optional")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDiagnosticSummary(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
summary string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "non api error", err: os.ErrNotExist, summary: "Unable to list Dynu domains", want: "Unable to list Dynu domains"},
|
||||||
|
{name: "auth error", err: &dynuclient.APIError{StatusCode: 401, Type: "Unauthorized", Message: "invalid"}, summary: "Unable to list Dynu domains", want: "Unable to list Dynu domains (authentication failed)"},
|
||||||
|
{name: "not found", err: &dynuclient.APIError{StatusCode: 404, Type: "Not Found", Message: "missing"}, summary: "Unable to resolve Dynu domain from hostname", want: "Unable to resolve Dynu domain from hostname (not found)"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := diagnosticSummary(tc.summary, tc.err); got != tc.want {
|
||||||
|
t.Fatalf("diagnosticSummary() = %q, want %q", got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadOnlyExampleUsesValidProviderArguments(t *testing.T) {
|
||||||
|
contents, err := os.ReadFile(filepath.Join("..", "..", "examples", "read_only", "providers.tf"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read example providers.tf: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := string(contents)
|
||||||
|
if strings.Contains(config, "username") {
|
||||||
|
t.Fatal("example providers.tf must not use unsupported provider argument 'username'")
|
||||||
|
}
|
||||||
|
if !strings.Contains(config, "api_key") {
|
||||||
|
t.Fatal("example providers.tf must include api_key argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user