Archived
Implement read-only Dynu Terraform provider skeleton
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# terraform-provider-dynu
|
||||
|
||||
Terraform provider for Dynu DNS using the Terraform Plugin Framework.
|
||||
|
||||
> Current phase is **read-only**: this provider implements provider configuration and data sources only.
|
||||
|
||||
## Features
|
||||
|
||||
- Provider configuration with API key authentication.
|
||||
- Environment variable support (`DYNU_API_KEY`).
|
||||
- Read-only data sources:
|
||||
- `dynu_domains`
|
||||
- `dynu_domain`
|
||||
- `dynu_dns_records`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Terraform >= 1.5
|
||||
- Go >= 1.22 (for building)
|
||||
- A Dynu API key
|
||||
|
||||
## Provider configuration
|
||||
|
||||
```hcl
|
||||
provider "dynu" {
|
||||
api_key = var.dynu_api_key
|
||||
}
|
||||
```
|
||||
|
||||
You can also omit `api_key` and set `DYNU_API_KEY` in your environment.
|
||||
|
||||
## Data sources
|
||||
|
||||
### dynu_domains
|
||||
|
||||
Returns all DNS domains associated with the account.
|
||||
|
||||
```hcl
|
||||
data "dynu_domains" "all" {}
|
||||
```
|
||||
|
||||
### dynu_domain
|
||||
|
||||
Resolves the root domain from a hostname, then returns full domain details.
|
||||
|
||||
```hcl
|
||||
data "dynu_domain" "selected" {
|
||||
hostname = "www.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
### dynu_dns_records
|
||||
|
||||
Resolves the root domain from a hostname, then returns DNS records for that domain.
|
||||
|
||||
```hcl
|
||||
data "dynu_dns_records" "records" {
|
||||
hostname = "www.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
./codex/setup.sh
|
||||
./codex/maintain.sh
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
Acceptance tests (requires live Dynu credentials):
|
||||
|
||||
```bash
|
||||
TF_ACC=1 DYNU_API_KEY=... go test ./internal/provider -run TestAcc
|
||||
```
|
||||
|
||||
## Limitations
|
||||
|
||||
- No Terraform resources are implemented in this phase.
|
||||
- No write operations are supported by provider code in this phase.
|
||||
@@ -0,0 +1,7 @@
|
||||
data "dynu_dns_records" "records" {
|
||||
hostname = "www.example.com"
|
||||
}
|
||||
|
||||
output "records" {
|
||||
value = data.dynu_dns_records.records.records
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
data "dynu_domain" "selected" {
|
||||
hostname = "www.example.com"
|
||||
}
|
||||
|
||||
output "domain" {
|
||||
value = data.dynu_domain.selected.domain
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
data "dynu_domains" "all" {}
|
||||
|
||||
output "domains" {
|
||||
value = data.dynu_domains.all.domains
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
provider "dynu" {
|
||||
# api_key can be omitted when DYNU_API_KEY is set
|
||||
api_key = var.dynu_api_key
|
||||
}
|
||||
|
||||
variable "dynu_api_key" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
module github.com/dynu/terraform-provider-dynu
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.3
|
||||
|
||||
require (
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.17.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/go-hclog v1.6.3 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.3 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0 // indirect
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5 // indirect
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
golang.org/x/net v0.41.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
|
||||
google.golang.org/grpc v1.72.2 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg=
|
||||
github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1 h1:jaT1yvU/kEKEsxnbrn4ZHlgcxyIfjvZ41BLdlLk52fY=
|
||||
github.com/hashicorp/terraform-plugin-framework v1.14.1/go.mod h1:xNUKmvTs6ldbwTuId5euAtg37dTxuyj3LHS3uj7BHQ4=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0 h1:0uYQcqqgW3BMyyve07WJgpKorXST3zkpzvrOnf3mpbg=
|
||||
github.com/hashicorp/terraform-plugin-framework-validators v0.17.0/go.mod h1:VwdfgE/5Zxm43flraNa0VjcvKQOGVrcO4X8peIri0T0=
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0 h1:ujykws/fWIdsi6oTUT5Or4ukvEan4aN9lY+LOxVP8EE=
|
||||
github.com/hashicorp/terraform-plugin-go v0.27.0/go.mod h1:FDa2Bb3uumkTGSkTFpWSOwWJDwA7bf3vdP3ltLDTH6o=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
|
||||
github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5 h1:2GTftHqmUhVOeuu9CW3kwDkRe4pcBDq0uuK5VJngU1M=
|
||||
github.com/hashicorp/terraform-registry-address v0.2.5/go.mod h1:PpzXWINwB5kuVS5CA7m1+eO2f1jKb5ZDIxrOPfpnGkg=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
|
||||
github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
|
||||
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
|
||||
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
|
||||
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
|
||||
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
|
||||
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
|
||||
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
|
||||
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,193 @@
|
||||
package dynuclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultBaseURL = "https://api.dynu.com/v2"
|
||||
|
||||
type Client struct {
|
||||
apiKey string
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type Option func(*Client)
|
||||
|
||||
func WithHTTPClient(httpClient *http.Client) Option {
|
||||
return func(c *Client) {
|
||||
c.httpClient = httpClient
|
||||
}
|
||||
}
|
||||
|
||||
func WithBaseURL(baseURL string) Option {
|
||||
return func(c *Client) {
|
||||
c.baseURL = strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
}
|
||||
|
||||
func New(apiKey string, opts ...Option) *Client {
|
||||
c := &Client{
|
||||
apiKey: apiKey,
|
||||
baseURL: defaultBaseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
type apiException struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
StatusCode int `json:"statusCode"`
|
||||
Exception *apiException `json:"exception"`
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UnicodeName string `json:"unicodeName"`
|
||||
Token string `json:"token"`
|
||||
State string `json:"state"`
|
||||
Group string `json:"group"`
|
||||
IPv4Address string `json:"ipv4Address"`
|
||||
IPv6Address string `json:"ipv6Address"`
|
||||
TTL int64 `json:"ttl"`
|
||||
IPv4 bool `json:"ipv4"`
|
||||
IPv6 bool `json:"ipv6"`
|
||||
IPv4WildcardAlias bool `json:"ipv4WildcardAlias"`
|
||||
IPv6WildcardAlias bool `json:"ipv6WildcardAlias"`
|
||||
AllowZoneTransfer bool `json:"allowZoneTransfer"`
|
||||
DNSSEC bool `json:"dnssec"`
|
||||
CreatedOn string `json:"createdOn"`
|
||||
UpdatedOn string `json:"updatedOn"`
|
||||
}
|
||||
|
||||
type DNSRecord struct {
|
||||
ID int64 `json:"id"`
|
||||
DomainID int64 `json:"domainId"`
|
||||
DomainName string `json:"domainName"`
|
||||
NodeName string `json:"nodeName"`
|
||||
Hostname string `json:"hostname"`
|
||||
RecordType string `json:"recordType"`
|
||||
State bool `json:"state"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Content string `json:"content"`
|
||||
UpdatedOn string `json:"updatedOn"`
|
||||
Group string `json:"group"`
|
||||
Host string `json:"host"`
|
||||
}
|
||||
|
||||
type listDomainsResponse struct {
|
||||
apiResponse
|
||||
Domains []Domain `json:"domains"`
|
||||
}
|
||||
|
||||
type getDomainResponse struct {
|
||||
apiResponse
|
||||
Domain
|
||||
}
|
||||
|
||||
type listDNSRecordsResponse struct {
|
||||
apiResponse
|
||||
DNSRecords []DNSRecord `json:"dnsRecords"`
|
||||
}
|
||||
|
||||
type getRootResponse struct {
|
||||
apiResponse
|
||||
ID int64 `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
DomainName string `json:"domainName"`
|
||||
Node string `json:"node"`
|
||||
}
|
||||
|
||||
func (c *Client) ListDomains(ctx context.Context) ([]Domain, error) {
|
||||
var resp listDomainsResponse
|
||||
if err := c.doGET(ctx, "/dns", &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Domains, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetDomainByID(ctx context.Context, domainID int64) (*Domain, error) {
|
||||
var resp getDomainResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/%d", domainID), &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp.Domain, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetRootDomain(ctx context.Context, hostname string) (int64, string, error) {
|
||||
var resp getRootResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/getroot/%s", hostname), &resp); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
if resp.ID == 0 || resp.DomainName == "" {
|
||||
return 0, "", errors.New("dynu API returned an incomplete root domain response")
|
||||
}
|
||||
|
||||
return resp.ID, resp.DomainName, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListDNSRecords(ctx context.Context, domainID int64) ([]DNSRecord, error) {
|
||||
var resp listDNSRecordsResponse
|
||||
if err := c.doGET(ctx, fmt.Sprintf("/dns/%d/record", domainID), &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.DNSRecords, nil
|
||||
}
|
||||
|
||||
func (c *Client) doGET(ctx context.Context, path string, target any) error {
|
||||
url := c.baseURL + path
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("API-Key", c.apiKey)
|
||||
|
||||
res, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
payload, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||
return fmt.Errorf("dynu API returned status %d: %s", res.StatusCode, strings.TrimSpace(string(payload)))
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(payload, target); err != nil {
|
||||
return fmt.Errorf("failed to decode dynu API response: %w", err)
|
||||
}
|
||||
|
||||
apiResult := apiResponse{}
|
||||
if err := json.Unmarshal(payload, &apiResult); err == nil && apiResult.Exception != nil {
|
||||
return fmt.Errorf("dynu API error %d (%s): %s", apiResult.Exception.StatusCode, apiResult.Exception.Type, apiResult.Exception.Message)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package dynuclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestListDomains(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/dns" {
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("API-Key"); got != "test-key" {
|
||||
t.Fatalf("unexpected api key header: %s", got)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"statusCode":200,"domains":[{"id":1,"name":"example.com","state":"Complete"}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
domains, err := client.ListDomains(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ListDomains() error = %v", err)
|
||||
}
|
||||
if len(domains) != 1 {
|
||||
t.Fatalf("expected 1 domain, got %d", len(domains))
|
||||
}
|
||||
if domains[0].Name != "example.com" {
|
||||
t.Fatalf("unexpected domain name %q", domains[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoGetErrorResponse(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"statusCode":401,"exception":{"statusCode":401,"type":"Authentication Exception","message":"invalid"}}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := New("test-key", WithBaseURL(ts.URL), WithHTTPClient(ts.Client()))
|
||||
_, err := client.ListDomains(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.DataSource = &dnsRecordsDataSource{}
|
||||
_ datasource.DataSourceWithConfigure = &dnsRecordsDataSource{}
|
||||
)
|
||||
|
||||
type dnsRecordsDataSource struct {
|
||||
clientProvider *providerData
|
||||
}
|
||||
|
||||
type dnsRecordsDataSourceModel struct {
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
DomainID types.Int64 `tfsdk:"domain_id"`
|
||||
DomainName types.String `tfsdk:"domain_name"`
|
||||
Records []dnsRecordStateItem `tfsdk:"records"`
|
||||
}
|
||||
|
||||
type dnsRecordStateItem struct {
|
||||
ID types.Int64 `tfsdk:"id"`
|
||||
DomainID types.Int64 `tfsdk:"domain_id"`
|
||||
DomainName types.String `tfsdk:"domain_name"`
|
||||
NodeName types.String `tfsdk:"node_name"`
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
RecordType types.String `tfsdk:"record_type"`
|
||||
TTL types.Int64 `tfsdk:"ttl"`
|
||||
State types.Bool `tfsdk:"state"`
|
||||
Content types.String `tfsdk:"content"`
|
||||
UpdatedOn types.String `tfsdk:"updated_on"`
|
||||
Group types.String `tfsdk:"group"`
|
||||
Host types.String `tfsdk:"host"`
|
||||
}
|
||||
|
||||
func NewDNSRecordsDataSource() datasource.DataSource {
|
||||
return &dnsRecordsDataSource{}
|
||||
}
|
||||
|
||||
func (d *dnsRecordsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_dns_records"
|
||||
}
|
||||
|
||||
func (d *dnsRecordsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Get DNS records from Dynu for the domain resolved from a hostname.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"hostname": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "Any hostname under the target root domain.",
|
||||
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
|
||||
},
|
||||
"domain_id": schema.Int64Attribute{Computed: true},
|
||||
"domain_name": schema.StringAttribute{Computed: true},
|
||||
"records": schema.ListNestedAttribute{
|
||||
Computed: true,
|
||||
NestedObject: schema.NestedAttributeObject{Attributes: map[string]schema.Attribute{
|
||||
"id": schema.Int64Attribute{Computed: true},
|
||||
"domain_id": schema.Int64Attribute{Computed: true},
|
||||
"domain_name": schema.StringAttribute{Computed: true},
|
||||
"node_name": schema.StringAttribute{Computed: true},
|
||||
"hostname": schema.StringAttribute{Computed: true},
|
||||
"record_type": schema.StringAttribute{Computed: true},
|
||||
"ttl": schema.Int64Attribute{Computed: true},
|
||||
"state": schema.BoolAttribute{Computed: true},
|
||||
"content": schema.StringAttribute{Computed: true},
|
||||
"updated_on": schema.StringAttribute{Computed: true},
|
||||
"group": schema.StringAttribute{Computed: true},
|
||||
"host": schema.StringAttribute{Computed: true},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dnsRecordsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
providerData, ok := req.ProviderData.(*providerData)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("Unexpected data source configure type", fmt.Sprintf("Expected *providerData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
d.clientProvider = providerData
|
||||
}
|
||||
|
||||
func (d *dnsRecordsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var state dnsRecordsDataSourceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
domainID, domainName, err := d.clientProvider.client.GetRootDomain(ctx, state.Hostname.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
records, err := d.clientProvider.client.ListDNSRecords(ctx, domainID)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to list Dynu DNS records", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
state.DomainID = types.Int64Value(domainID)
|
||||
state.DomainName = types.StringValue(domainName)
|
||||
state.Records = make([]dnsRecordStateItem, 0, len(records))
|
||||
for _, record := range records {
|
||||
state.Records = append(state.Records, dnsRecordStateItem{
|
||||
ID: types.Int64Value(record.ID),
|
||||
DomainID: types.Int64Value(record.DomainID),
|
||||
DomainName: mapString(record.DomainName),
|
||||
NodeName: mapString(record.NodeName),
|
||||
Hostname: mapString(record.Hostname),
|
||||
RecordType: mapString(record.RecordType),
|
||||
TTL: types.Int64Value(record.TTL),
|
||||
State: types.BoolValue(record.State),
|
||||
Content: mapString(record.Content),
|
||||
UpdatedOn: mapString(record.UpdatedOn),
|
||||
Group: mapString(record.Group),
|
||||
Host: mapString(record.Host),
|
||||
})
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.DataSource = &domainDataSource{}
|
||||
_ datasource.DataSourceWithConfigure = &domainDataSource{}
|
||||
)
|
||||
|
||||
type domainDataSource struct {
|
||||
clientProvider *providerData
|
||||
}
|
||||
|
||||
type domainDataSourceModel struct {
|
||||
Hostname types.String `tfsdk:"hostname"`
|
||||
Domain types.Object `tfsdk:"domain"`
|
||||
}
|
||||
|
||||
func NewDomainDataSource() datasource.DataSource {
|
||||
return &domainDataSource{}
|
||||
}
|
||||
|
||||
func (d *domainDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_domain"
|
||||
}
|
||||
|
||||
func (d *domainDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Get a Dynu DNS domain by hostname.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"hostname": schema.StringAttribute{
|
||||
Required: true,
|
||||
Description: "Hostname to resolve to a root domain.",
|
||||
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
|
||||
},
|
||||
"domain": schema.SingleNestedAttribute{
|
||||
Computed: true,
|
||||
Description: "Resolved Dynu DNS domain details.",
|
||||
Attributes: domainAttributes(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *domainDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(*providerData)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("Unexpected data source configure type", fmt.Sprintf("Expected *providerData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
d.clientProvider = providerData
|
||||
}
|
||||
|
||||
func (d *domainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var state domainDataSourceModel
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if state.Hostname.IsUnknown() || state.Hostname.IsNull() {
|
||||
resp.Diagnostics.AddAttributeError(path.Root("hostname"), "Invalid hostname", "hostname must be known and non-null.")
|
||||
return
|
||||
}
|
||||
|
||||
domainID, _, err := d.clientProvider.client.GetRootDomain(ctx, state.Hostname.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to resolve Dynu domain from hostname", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := d.clientProvider.client.GetDomainByID(ctx, domainID)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to get Dynu domain", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
domainValue := mapDomain(*domain)
|
||||
domainObject, diags := types.ObjectValue(
|
||||
map[string]attr.Type{
|
||||
"id": types.Int64Type,
|
||||
"name": types.StringType,
|
||||
"unicode_name": types.StringType,
|
||||
"token": types.StringType,
|
||||
"state": types.StringType,
|
||||
"group": types.StringType,
|
||||
"ipv4_address": types.StringType,
|
||||
"ipv6_address": types.StringType,
|
||||
"ttl": types.Int64Type,
|
||||
"ipv4": types.BoolType,
|
||||
"ipv6": types.BoolType,
|
||||
"ipv4_wildcard_alias": types.BoolType,
|
||||
"ipv6_wildcard_alias": types.BoolType,
|
||||
"allow_zone_transfer": types.BoolType,
|
||||
"dnssec": types.BoolType,
|
||||
"created_on": types.StringType,
|
||||
"updated_on": types.StringType,
|
||||
},
|
||||
map[string]attr.Value{
|
||||
"id": domainValue.ID,
|
||||
"name": domainValue.Name,
|
||||
"unicode_name": domainValue.UnicodeName,
|
||||
"token": domainValue.Token,
|
||||
"state": domainValue.State,
|
||||
"group": domainValue.Group,
|
||||
"ipv4_address": domainValue.IPv4Address,
|
||||
"ipv6_address": domainValue.IPv6Address,
|
||||
"ttl": domainValue.TTL,
|
||||
"ipv4": domainValue.IPv4,
|
||||
"ipv6": domainValue.IPv6,
|
||||
"ipv4_wildcard_alias": domainValue.IPv4WildcardAlias,
|
||||
"ipv6_wildcard_alias": domainValue.IPv6WildcardAlias,
|
||||
"allow_zone_transfer": domainValue.AllowZoneTransfer,
|
||||
"dnssec": domainValue.DNSSEC,
|
||||
"created_on": domainValue.CreatedOn,
|
||||
"updated_on": domainValue.UpdatedOn,
|
||||
},
|
||||
)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
state.Domain = domainObject
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
var (
|
||||
_ datasource.DataSource = &domainsDataSource{}
|
||||
_ datasource.DataSourceWithConfigure = &domainsDataSource{}
|
||||
)
|
||||
|
||||
type domainsDataSource struct {
|
||||
clientProvider *providerData
|
||||
}
|
||||
|
||||
type domainsDataSourceModel struct {
|
||||
Domains []domainModel `tfsdk:"domains"`
|
||||
}
|
||||
|
||||
type domainModel struct {
|
||||
ID types.Int64 `tfsdk:"id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
UnicodeName types.String `tfsdk:"unicode_name"`
|
||||
Token types.String `tfsdk:"token"`
|
||||
State types.String `tfsdk:"state"`
|
||||
Group types.String `tfsdk:"group"`
|
||||
IPv4Address types.String `tfsdk:"ipv4_address"`
|
||||
IPv6Address types.String `tfsdk:"ipv6_address"`
|
||||
TTL types.Int64 `tfsdk:"ttl"`
|
||||
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 NewDomainsDataSource() datasource.DataSource {
|
||||
return &domainsDataSource{}
|
||||
}
|
||||
|
||||
func (d *domainsDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_domains"
|
||||
}
|
||||
|
||||
func (d *domainsDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "List DNS domains from Dynu.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"domains": schema.ListNestedAttribute{
|
||||
Computed: true,
|
||||
Description: "List of Dynu DNS domains.",
|
||||
NestedObject: schema.NestedAttributeObject{Attributes: domainAttributes()},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *domainsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(*providerData)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError("Unexpected data source configure type", fmt.Sprintf("Expected *providerData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
d.clientProvider = providerData
|
||||
}
|
||||
|
||||
func (d *domainsDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
domains, err := d.clientProvider.client.ListDomains(ctx)
|
||||
if err != nil {
|
||||
resp.Diagnostics.AddError("Unable to list Dynu domains", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
state := domainsDataSourceModel{Domains: make([]domainModel, 0, len(domains))}
|
||||
for _, domain := range domains {
|
||||
state.Domains = append(state.Domains, mapDomain(domain))
|
||||
}
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
|
||||
}
|
||||
|
||||
func domainAttributes() map[string]schema.Attribute {
|
||||
return map[string]schema.Attribute{
|
||||
"id": schema.Int64Attribute{Computed: true},
|
||||
"name": schema.StringAttribute{Computed: true},
|
||||
"unicode_name": schema.StringAttribute{Computed: true},
|
||||
"token": schema.StringAttribute{Computed: true, Sensitive: true},
|
||||
"state": schema.StringAttribute{Computed: true},
|
||||
"group": schema.StringAttribute{Computed: true},
|
||||
"ipv4_address": schema.StringAttribute{Computed: true},
|
||||
"ipv6_address": schema.StringAttribute{Computed: true},
|
||||
"ttl": schema.Int64Attribute{Computed: true},
|
||||
"ipv4": schema.BoolAttribute{Computed: true},
|
||||
"ipv6": schema.BoolAttribute{Computed: true},
|
||||
"ipv4_wildcard_alias": schema.BoolAttribute{Computed: true},
|
||||
"ipv6_wildcard_alias": schema.BoolAttribute{Computed: true},
|
||||
"allow_zone_transfer": schema.BoolAttribute{Computed: true},
|
||||
"dnssec": schema.BoolAttribute{Computed: true},
|
||||
"created_on": schema.StringAttribute{Computed: true},
|
||||
"updated_on": schema.StringAttribute{Computed: true},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
)
|
||||
|
||||
func mapDomain(domain dynuclient.Domain) domainModel {
|
||||
return domainModel{
|
||||
ID: types.Int64Value(domain.ID),
|
||||
Name: types.StringValue(domain.Name),
|
||||
UnicodeName: types.StringValue(domain.UnicodeName),
|
||||
Token: types.StringValue(domain.Token),
|
||||
State: types.StringValue(domain.State),
|
||||
Group: types.StringValue(domain.Group),
|
||||
IPv4Address: types.StringValue(domain.IPv4Address),
|
||||
IPv6Address: types.StringValue(domain.IPv6Address),
|
||||
TTL: types.Int64Value(domain.TTL),
|
||||
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: types.StringValue(domain.CreatedOn),
|
||||
UpdatedOn: types.StringValue(domain.UpdatedOn),
|
||||
}
|
||||
}
|
||||
|
||||
func mapString(in string) types.String {
|
||||
if in == "" {
|
||||
return types.StringNull()
|
||||
}
|
||||
return types.StringValue(in)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider"
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/dynuclient"
|
||||
)
|
||||
|
||||
var _ provider.Provider = &dynuProvider{}
|
||||
|
||||
type dynuProvider struct {
|
||||
version string
|
||||
}
|
||||
|
||||
type dynuProviderModel struct {
|
||||
APIKey types.String `tfsdk:"api_key"`
|
||||
}
|
||||
|
||||
type providerData struct {
|
||||
client *dynuclient.Client
|
||||
}
|
||||
|
||||
func New(version string) func() provider.Provider {
|
||||
return func() provider.Provider {
|
||||
return &dynuProvider{version: version}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dynuProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
|
||||
resp.TypeName = "dynu"
|
||||
resp.Version = p.version
|
||||
}
|
||||
|
||||
func (p *dynuProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Terraform provider for Dynu DNS read-only operations.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"api_key": schema.StringAttribute{
|
||||
Optional: true,
|
||||
Sensitive: true,
|
||||
Description: "Dynu API key. Can also be provided using DYNU_API_KEY.",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dynuProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
|
||||
var data dynuProviderModel
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := os.Getenv("DYNU_API_KEY")
|
||||
if !data.APIKey.IsNull() {
|
||||
apiKey = data.APIKey.ValueString()
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("api_key"),
|
||||
"Missing Dynu API key",
|
||||
"Set api_key in the provider configuration or DYNU_API_KEY in the environment.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
providerData := &providerData{client: dynuclient.New(apiKey)}
|
||||
resp.DataSourceData = providerData
|
||||
resp.ResourceData = nil
|
||||
}
|
||||
|
||||
func (p *dynuProvider) DataSources(_ context.Context) []func() datasource.DataSource {
|
||||
return []func() datasource.DataSource{
|
||||
NewDomainsDataSource,
|
||||
NewDomainDataSource,
|
||||
NewDNSRecordsDataSource,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *dynuProvider) Resources(_ context.Context) []func() resource.Resource {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAccScaffold(t *testing.T) {
|
||||
if os.Getenv("TF_ACC") == "" || os.Getenv("DYNU_API_KEY") == "" {
|
||||
t.Skip("set TF_ACC=1 and DYNU_API_KEY to enable acceptance tests")
|
||||
}
|
||||
|
||||
// Acceptance tests for read-only data sources are intentionally scaffolded in phase 1.
|
||||
// Add terraform-plugin-testing based test cases in a follow-up with stable fixtures.
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/providerserver"
|
||||
|
||||
"github.com/dynu/terraform-provider-dynu/internal/provider"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := providerserver.Serve(context.Background(), provider.New("dev"), providerserver.ServeOpts{
|
||||
Address: "registry.terraform.io/dynu/dynu",
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user