Skip to content

Commit e8cbc03

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 e8cbc03

File tree

5 files changed

+296
-32
lines changed

5 files changed

+296
-32
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: 188 additions & 29 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,9 +62,88 @@ func resourcePDNSRecord() *schema.Resource {
6162
}
6263
}
6364

64-
func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
65-
client := meta.(*Client)
65+
func resourcePDNSRecordSOA() *schema.Resource {
66+
return &schema.Resource{
67+
Create: resourcePDNSRecordCreateSOA,
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+
}
66145

146+
func resourcePDNSRecordCreatePrepare(d *schema.ResourceData, meta interface{}) (ResourceRecordSet, string, int) {
67147
rrSet := ResourceRecordSet{
68148
Name: d.Get("name").(string),
69149
Type: d.Get("type").(string),
@@ -72,53 +152,96 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
72152

73153
zone := d.Get("zone").(string)
74154
ttl := d.Get("ttl").(int)
155+
156+
return rrSet, zone, ttl
157+
}
158+
159+
func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
160+
rrSet, zone, ttl := resourcePDNSRecordCreatePrepare(d, meta)
75161
recs := d.Get("records").(*schema.Set).List()
76162
setPtr := false
77163

78-
if v, ok := d.GetOk("set_ptr"); ok {
79-
setPtr = v.(bool)
80-
}
81-
82164
// begin: ValidateFunc
83165
// https://www.terraform.io/docs/extend/schemas/schema-behaviors.html
84166
// "ValidateFunc is not yet supported on lists or sets"
85167
// when terraform will support ValidateFunc for non-primitives
86168
// we can move this block there
169+
if len(recs) == 0 {
170+
return fmt.Errorf("'records' must not be empty")
171+
}
172+
87173
for _, recs := range recs {
88174
if len(strings.Trim(recs.(string), " ")) == 0 {
89175
log.Printf("[WARN] One or more values in 'records' contain empty '' value(s)")
90176
}
91177
}
92-
if !(len(recs) > 0) {
93-
return fmt.Errorf("'records' must not be empty")
94-
}
95178
// end: ValidateFunc
96179

97-
if len(recs) > 0 {
98-
records := make([]Record, 0, len(recs))
99-
for _, recContent := range recs {
100-
records = append(records,
101-
Record{Name: rrSet.Name,
102-
Type: rrSet.Type,
103-
TTL: ttl,
104-
Content: recContent.(string),
105-
SetPtr: setPtr})
106-
}
180+
if v, ok := d.GetOk("set_ptr"); ok {
181+
setPtr = v.(bool)
182+
}
183+
184+
records := make([]Record, 0, len(recs))
185+
for _, recContent := range recs {
186+
records = append(records,
187+
Record{Name: rrSet.Name,
188+
Type: rrSet.Type,
189+
TTL: ttl,
190+
Content: recContent.(string),
191+
SetPtr: setPtr})
192+
}
107193

108-
rrSet.Records = records
194+
rrSet.Records = records
109195

110-
log.Printf("[DEBUG] Creating PowerDNS Record: %#v", rrSet)
196+
return (resourcePDNSRecordCreateFinish(d, meta, zone, rrSet))
197+
}
111198

112-
recID, err := client.ReplaceRecordSet(zone, rrSet)
199+
func resourcePDNSRecordCreateSOA(d *schema.ResourceData, meta interface{}) error {
200+
rrSet, zone, ttl := resourcePDNSRecordCreatePrepare(d, meta)
201+
client := meta.(*Client)
202+
203+
log.Printf("[DEBUG] Searching existing SOA record at %s => %s", zone, d.Get("name").(string))
204+
soa_records, err := client.ListRecordsInRRSet(zone, d.Get("name").(string), "SOA")
205+
if err != nil {
206+
return fmt.Errorf("Failed to fetch old SOA record: %s", err)
207+
}
208+
var serial int
209+
if len(soa_records) > 0 {
210+
serial, err = strconv.Atoi(strings.Fields(soa_records[0].Content)[2])
113211
if err != nil {
114-
return fmt.Errorf("Failed to create PowerDNS Record: %s", err)
212+
return fmt.Errorf("Failed to parse old serial value in SOA record: %s", err)
115213
}
214+
} else {
215+
serial = d.Get("serial").(int)
216+
}
217+
log.Printf("[DEBUG] Set serial number to %d", serial)
218+
219+
records := make([]Record, 0, 1)
220+
records = append(records,
221+
Record{Name: rrSet.Name,
222+
Type: rrSet.Type,
223+
TTL: ttl,
224+
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")),
225+
SetPtr: false})
226+
227+
rrSet.Records = records
228+
229+
return (resourcePDNSRecordCreateFinish(d, meta, zone, rrSet))
230+
}
231+
232+
func resourcePDNSRecordCreateFinish(d *schema.ResourceData, meta interface{}, zone string, rrSet ResourceRecordSet) error {
233+
client := meta.(*Client)
116234

117-
d.SetId(recID)
118-
log.Printf("[INFO] Created PowerDNS Record with ID: %s", d.Id())
235+
log.Printf("[DEBUG] Creating PowerDNS Record: %#v", rrSet)
119236

237+
recID, err := client.ReplaceRecordSet(zone, rrSet)
238+
if err != nil {
239+
return fmt.Errorf("Failed to create PowerDNS Record: %s", err)
120240
}
121241

242+
d.SetId(recID)
243+
log.Printf("[INFO] Created PowerDNS Record with ID: %s", d.Id())
244+
122245
return resourcePDNSRecordRead(d, meta)
123246
}
124247

@@ -132,12 +255,48 @@ func resourcePDNSRecordRead(d *schema.ResourceData, meta interface{}) error {
132255
}
133256

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

140-
if len(records) > 0 {
299+
if len(records) > 0 || d.Get("Type") == "SOA" {
141300
d.Set("ttl", records[0].TTL)
142301
}
143302

powerdns/resource_powerdns_record_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,9 +444,34 @@ 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: true,
466+
ImportStateVerify: true,
467+
},
468+
},
469+
})
470+
}
471+
447472
func testAccCheckPDNSRecordDestroy(s *terraform.State) error {
448473
for _, rs := range s.RootModule().Resources {
449-
if rs.Type != "powerdns_record" {
474+
if rs.Type != "powerdns_record" && rs.Type != "powerdns_record_soa" {
450475
continue
451476
}
452477

@@ -654,3 +679,24 @@ 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+
697+
lifecycle {
698+
ignore_changes = [
699+
serial,
700+
]
701+
}
702+
}`

0 commit comments

Comments
 (0)