Skip to content

Commit 793f8cc

Browse files
committed
Implement powerdns_record_soa resource
Allow management of the SOA record using a friendly data structure and avoid serial number conflicts if SOA-EDIT-API is used by reading the latest serial number from the server before applying changes. Signed-off-by: Georg Pfuetzenreuter <[email protected]>
1 parent 7432364 commit 793f8cc

File tree

5 files changed

+264
-15
lines changed

5 files changed

+264
-15
lines changed

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ services:
5151
curl -X POST http://pdns:8081/api/v1/servers/localhost/zones \
5252
-d '{"name": "test-soa-sysa.xyz.", "kind": "Native", "soa_edit_api": "", "nameservers": ["ns1.sysa.xyz."]}' \
5353
-H "X-API-Key: secret"
54+
curl -X POST http://pdns:8081/api/v1/servers/localhost/zones \
55+
-d '{"name": "test-soa2-sysa.xyz.", "kind": "Native", "soa_edit_api": "EPOCH", "nameservers": ["ns1.sysa.xyz."]}' \
56+
-H "X-API-Key: secret"
5457
curl -s -X POST http://pdns:8081/api/v1/servers/localhost/zones \
5558
-d '{"name": "in-addr.arpa.", "kind": "Native", "nameservers": ["ns1.sysa.xyz."]}' \
5659
-H "X-API-Key: secret"

powerdns/provider.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,9 @@ func Provider() terraform.ResourceProvider {
5454
},
5555

5656
ResourcesMap: map[string]*schema.Resource{
57-
"powerdns_zone": resourcePDNSZone(),
58-
"powerdns_record": resourcePDNSRecord(),
57+
"powerdns_zone": resourcePDNSZone(),
58+
"powerdns_record": resourcePDNSRecord(),
59+
"powerdns_record_soa": resourcePDNSRecordSOA(),
5960
},
6061

6162
ConfigureFunc: providerConfigure,

powerdns/resource_powerdns_record.go

Lines changed: 163 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"log"
7+
"strconv"
78
"strings"
89

910
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
@@ -61,6 +62,87 @@ func resourcePDNSRecord() *schema.Resource {
6162
}
6263
}
6364

65+
func resourcePDNSRecordSOA() *schema.Resource {
66+
return &schema.Resource{
67+
Create: resourcePDNSRecordCreate,
68+
Read: resourcePDNSRecordRead,
69+
Delete: resourcePDNSRecordDelete,
70+
Exists: resourcePDNSRecordExists,
71+
Importer: &schema.ResourceImporter{
72+
State: resourcePDNSRecordImport,
73+
},
74+
75+
Schema: map[string]*schema.Schema{
76+
"zone": {
77+
Type: schema.TypeString,
78+
Required: true,
79+
ForceNew: true,
80+
},
81+
82+
"name": {
83+
Type: schema.TypeString,
84+
Required: true,
85+
ForceNew: true,
86+
},
87+
88+
"type": {
89+
Type: schema.TypeString,
90+
Required: true,
91+
ForceNew: true,
92+
},
93+
94+
"ttl": {
95+
Type: schema.TypeInt,
96+
Required: true,
97+
ForceNew: true,
98+
},
99+
100+
"mname": {
101+
Type: schema.TypeString,
102+
Optional: false,
103+
Required: true,
104+
ForceNew: true,
105+
},
106+
"rname": {
107+
Type: schema.TypeString,
108+
Optional: false,
109+
Required: true,
110+
ForceNew: true,
111+
},
112+
"serial": {
113+
Type: schema.TypeInt,
114+
Optional: false,
115+
Required: true,
116+
ForceNew: true,
117+
},
118+
"refresh": {
119+
Type: schema.TypeInt,
120+
Optional: false,
121+
Required: true,
122+
ForceNew: true,
123+
},
124+
"retry": {
125+
Type: schema.TypeInt,
126+
Optional: false,
127+
Required: true,
128+
ForceNew: true,
129+
},
130+
"expire": {
131+
Type: schema.TypeInt,
132+
Optional: false,
133+
Required: true,
134+
ForceNew: true,
135+
},
136+
"minimum": {
137+
Type: schema.TypeInt,
138+
Optional: false,
139+
Required: true,
140+
ForceNew: true,
141+
},
142+
},
143+
}
144+
}
145+
64146
func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
65147
client := meta.(*Client)
66148

@@ -72,11 +154,17 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
72154

73155
zone := d.Get("zone").(string)
74156
ttl := d.Get("ttl").(int)
75-
recs := d.Get("records").(*schema.Set).List()
157+
var recs []interface{}
158+
var recslen int
76159
setPtr := false
77-
78-
if v, ok := d.GetOk("set_ptr"); ok {
79-
setPtr = v.(bool)
160+
if d.Get("type") == "SOA" {
161+
recslen = 1
162+
} else {
163+
recs = d.Get("records").(*schema.Set).List()
164+
recslen = len(recs)
165+
if v, ok := d.GetOk("set_ptr"); ok {
166+
setPtr = v.(bool)
167+
}
80168
}
81169

82170
// begin: ValidateFunc
@@ -89,20 +177,46 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
89177
log.Printf("[WARN] One or more values in 'records' contain empty '' value(s)")
90178
}
91179
}
92-
if !(len(recs) > 0) {
180+
if recslen == 0 {
93181
return fmt.Errorf("'records' must not be empty")
94182
}
95183
// end: ValidateFunc
96184

97-
if len(recs) > 0 {
98-
records := make([]Record, 0, len(recs))
99-
for _, recContent := range recs {
185+
if recslen > 0 {
186+
records := make([]Record, 0, recslen)
187+
188+
if d.Get("type") == "SOA" {
189+
log.Printf("[DEBUG] Searching existing SOA record at %s => %s", d.Get("zone").(string), d.Get("name").(string))
190+
soa_records, err := client.ListRecordsInRRSet(d.Get("zone").(string), d.Get("name").(string), "SOA")
191+
if err != nil {
192+
return fmt.Errorf("Failed to fetch old SOA record: %s", err)
193+
}
194+
var serial int
195+
if len(soa_records) > 0 {
196+
serial, err = strconv.Atoi(strings.Fields(soa_records[0].Content)[2])
197+
if err != nil {
198+
return fmt.Errorf("Failed to parse old serial value in SOA record: %s", err)
199+
}
200+
} else {
201+
serial = d.Get("serial").(int)
202+
}
203+
log.Printf("[DEBUG] Set serial number to %d", serial)
204+
100205
records = append(records,
101206
Record{Name: rrSet.Name,
102207
Type: rrSet.Type,
103208
TTL: ttl,
104-
Content: recContent.(string),
209+
Content: fmt.Sprintf("%s %s %d %d %d %d %d", d.Get("mname"), d.Get("rname"), serial, d.Get("refresh"), d.Get("retry"), d.Get("expire"), d.Get("minimum")),
105210
SetPtr: setPtr})
211+
} else {
212+
for _, recContent := range recs {
213+
records = append(records,
214+
Record{Name: rrSet.Name,
215+
Type: rrSet.Type,
216+
TTL: ttl,
217+
Content: recContent.(string),
218+
SetPtr: setPtr})
219+
}
106220
}
107221

108222
rrSet.Records = records
@@ -132,12 +246,48 @@ func resourcePDNSRecordRead(d *schema.ResourceData, meta interface{}) error {
132246
}
133247

134248
recs := make([]string, 0, len(records))
135-
for _, r := range records {
136-
recs = append(recs, r.Content)
249+
if d.Get("type") == "SOA" {
250+
rsplit := strings.Fields(records[0].Content)
251+
d.Set("mname", rsplit[0])
252+
d.Set("rname", rsplit[1])
253+
254+
serial, err := strconv.Atoi(rsplit[2])
255+
if err != nil {
256+
return fmt.Errorf("Failed to parse serial value in SOA record: %s", err)
257+
}
258+
d.Set("serial", serial)
259+
260+
refresh, err := strconv.Atoi(rsplit[3])
261+
if err != nil {
262+
return fmt.Errorf("Failed to parse refresh value in SOA record: %s", err)
263+
}
264+
d.Set("refresh", refresh)
265+
266+
retry, err := strconv.Atoi(rsplit[4])
267+
if err != nil {
268+
return fmt.Errorf("Failed to parse retry value in SOA record: %s", err)
269+
}
270+
d.Set("retry", retry)
271+
272+
expire, err := strconv.Atoi(rsplit[5])
273+
if err != nil {
274+
return fmt.Errorf("Failed to parse expire value in SOA record: %s", err)
275+
}
276+
d.Set("expire", expire)
277+
278+
minimum, err := strconv.Atoi(rsplit[6])
279+
if err != nil {
280+
return fmt.Errorf("Failed to parse minimum value in SOA record: %s", err)
281+
}
282+
d.Set("minimum", minimum)
283+
} else {
284+
for _, r := range records {
285+
recs = append(recs, r.Content)
286+
}
287+
d.Set("records", recs)
137288
}
138-
d.Set("records", recs)
139289

140-
if len(records) > 0 {
290+
if len(records) > 0 || d.Get("Type") == "SOA" {
141291
d.Set("ttl", records[0].TTL)
142292
}
143293

powerdns/resource_powerdns_record_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,31 @@ func TestAccPDNSRecord_SOA(t *testing.T) {
444444
})
445445
}
446446

447+
func TestAccPDNSRecordSOA_SOA(t *testing.T) {
448+
resourceName := "powerdns_record_soa.test-soa2"
449+
resourceID := `{"zone":"test-soa2-sysa.xyz.","id":"test-soa2-sysa.xyz.:::SOA"}`
450+
451+
resource.ParallelTest(t, resource.TestCase{
452+
PreCheck: func() { testAccPreCheck(t) },
453+
Providers: testAccProviders,
454+
CheckDestroy: testAccCheckPDNSRecordDestroy,
455+
Steps: []resource.TestStep{
456+
{
457+
Config: testPDNSRecordConfigSOA2,
458+
Check: resource.ComposeTestCheckFunc(
459+
testAccCheckPDNSRecordExists(resourceName),
460+
),
461+
},
462+
{
463+
ResourceName: resourceName,
464+
ImportStateId: resourceID,
465+
ImportState: false,
466+
ImportStateVerify: false,
467+
},
468+
},
469+
})
470+
}
471+
447472
func testAccCheckPDNSRecordDestroy(s *terraform.State) error {
448473
for _, rs := range s.RootModule().Resources {
449474
if rs.Type != "powerdns_record" {
@@ -654,3 +679,18 @@ resource "powerdns_record" "test-soa" {
654679
ttl = 3600
655680
records = [ "something.something. hostmaster.sysa.xyz. 2019090301 10800 3600 604800 3600" ]
656681
}`
682+
683+
const testPDNSRecordConfigSOA2 = `
684+
resource "powerdns_record_soa" "test-soa2" {
685+
zone = "test-soa2-sysa.xyz."
686+
name = "test-soa2-sysa.xyz."
687+
type = "SOA"
688+
ttl = 3600
689+
mname = "ns1.sysa.xyz."
690+
rname = "hostmaster.sysa.xyz."
691+
serial = 0
692+
refresh = 7200
693+
retry = 600
694+
expire = 1209600
695+
minimum = 6400
696+
}`
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
layout: "powerdns"
3+
page_title: "PowerDNS: powerdns_record_soa"
4+
sidebar_current: "docs-powerdns-resource-record-soa"
5+
description: |-
6+
Provides a PowerDNS SOA record resource.
7+
---
8+
9+
# powerdns\_record
10+
11+
Provides a PowerDNS SOA record resource.
12+
This is offers an alternative to managing the SOA record of a zone through the generic `powerdns_record`.
13+
14+
## Example Usage
15+
16+
### A record example
17+
18+
```hcl
19+
resource "powerdns_record_soa" "soa-example_com" {
20+
zone = "example.com."
21+
name = "example.com."
22+
type = "SOA"
23+
ttl = 8600
24+
mnme = "ns1.example.com."
25+
rname = "hostmaster.example.com."
26+
serial = 0
27+
refresh = 7200
28+
retry = 600
29+
expire = 1209600
30+
minimum = 6400
31+
32+
}
33+
```
34+
35+
## Argument Reference
36+
37+
The following arguments are supported:
38+
39+
* `zone` - (Required) The name of zone to contain this record.
40+
* `name` - (Required) The name of the record, typically the same as `zone`.
41+
* `type` - (Required) The record type, must be `SOA`.
42+
* `ttl` - (Required) The TTL of the record.
43+
* `mname` - (Required) SOA MNAME.
44+
* `rname` - (Required) SOA RNAME.
45+
* `serial` - (Required) SOA SERIAL - it will only be used for creation of the zone, subsequent changes are expected to use SOA-EDIT-API if a serial number update is desired. Set to `0` if no specific starting serial number is desired, or if the zone to manage already exists.
46+
* `refresh` - (Required) SOA REFRESH.
47+
* `retry` - (Required) SOA RETRY.
48+
* `expire` - (Required) SOA EXPIRE.
49+
* `minimum` - (Required) SOA MINIMUM.
50+
51+
### Attribute Reference
52+
53+
The id of the resource is a composite of the record name and record type, joined by a separator - `:::`.
54+
55+
For example, record `example.com.` of type `SOA` will be represented with the following `id`: `example.com.:::SOA`

0 commit comments

Comments
 (0)