diff --git a/docker-compose.yml b/docker-compose.yml index 7040d96b..33ec3f93 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -51,6 +51,9 @@ services: curl -X POST http://pdns:8081/api/v1/servers/localhost/zones \ -d '{"name": "test-soa-sysa.xyz.", "kind": "Native", "soa_edit_api": "", "nameservers": ["ns1.sysa.xyz."]}' \ -H "X-API-Key: secret" + curl -X POST http://pdns:8081/api/v1/servers/localhost/zones \ + -d '{"name": "test-soa2-sysa.xyz.", "kind": "Native", "soa_edit_api": "EPOCH", "nameservers": ["ns1.sysa.xyz."]}' \ + -H "X-API-Key: secret" curl -s -X POST http://pdns:8081/api/v1/servers/localhost/zones \ -d '{"name": "in-addr.arpa.", "kind": "Native", "nameservers": ["ns1.sysa.xyz."]}' \ -H "X-API-Key: secret" diff --git a/powerdns/provider.go b/powerdns/provider.go index d8d20402..2d89357b 100644 --- a/powerdns/provider.go +++ b/powerdns/provider.go @@ -54,8 +54,9 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "powerdns_zone": resourcePDNSZone(), - "powerdns_record": resourcePDNSRecord(), + "powerdns_zone": resourcePDNSZone(), + "powerdns_record": resourcePDNSRecord(), + "powerdns_record_soa": resourcePDNSRecordSOA(), }, ConfigureFunc: providerConfigure, diff --git a/powerdns/resource_powerdns_record.go b/powerdns/resource_powerdns_record.go index d2be002c..43b7446c 100644 --- a/powerdns/resource_powerdns_record.go +++ b/powerdns/resource_powerdns_record.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "strconv" "strings" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" @@ -61,9 +62,88 @@ func resourcePDNSRecord() *schema.Resource { } } -func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { - client := meta.(*Client) +func resourcePDNSRecordSOA() *schema.Resource { + return &schema.Resource{ + Create: resourcePDNSRecordCreateSOA, + Read: resourcePDNSRecordRead, + Delete: resourcePDNSRecordDelete, + Exists: resourcePDNSRecordExists, + Importer: &schema.ResourceImporter{ + State: resourcePDNSRecordImport, + }, + + Schema: map[string]*schema.Schema{ + "zone": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "ttl": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + + "mname": { + Type: schema.TypeString, + Optional: false, + Required: true, + ForceNew: true, + }, + "rname": { + Type: schema.TypeString, + Optional: false, + Required: true, + ForceNew: true, + }, + "serial": { + Type: schema.TypeInt, + Optional: false, + Required: true, + ForceNew: true, + }, + "refresh": { + Type: schema.TypeInt, + Optional: false, + Required: true, + ForceNew: true, + }, + "retry": { + Type: schema.TypeInt, + Optional: false, + Required: true, + ForceNew: true, + }, + "expire": { + Type: schema.TypeInt, + Optional: false, + Required: true, + ForceNew: true, + }, + "minimum": { + Type: schema.TypeInt, + Optional: false, + Required: true, + ForceNew: true, + }, + }, + } +} +func resourcePDNSRecordCreatePrepare(d *schema.ResourceData, meta interface{}) (ResourceRecordSet, string, int) { rrSet := ResourceRecordSet{ Name: d.Get("name").(string), Type: d.Get("type").(string), @@ -72,53 +152,97 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { zone := d.Get("zone").(string) ttl := d.Get("ttl").(int) + + return rrSet, zone, ttl +} + +func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { + rrSet, zone, ttl := resourcePDNSRecordCreatePrepare(d, meta) recs := d.Get("records").(*schema.Set).List() setPtr := false - if v, ok := d.GetOk("set_ptr"); ok { - setPtr = v.(bool) - } - // begin: ValidateFunc // https://www.terraform.io/docs/extend/schemas/schema-behaviors.html // "ValidateFunc is not yet supported on lists or sets" // when terraform will support ValidateFunc for non-primitives // we can move this block there + if len(recs) == 0 { + return fmt.Errorf("'records' must not be empty") + } + for _, recs := range recs { if len(strings.Trim(recs.(string), " ")) == 0 { log.Printf("[WARN] One or more values in 'records' contain empty '' value(s)") } } - if !(len(recs) > 0) { - return fmt.Errorf("'records' must not be empty") - } // end: ValidateFunc - if len(recs) > 0 { - records := make([]Record, 0, len(recs)) - for _, recContent := range recs { - records = append(records, - Record{Name: rrSet.Name, - Type: rrSet.Type, - TTL: ttl, - Content: recContent.(string), - SetPtr: setPtr}) - } + if v, ok := d.GetOk("set_ptr"); ok { + setPtr = v.(bool) + } + + records := make([]Record, 0, len(recs)) + for _, recContent := range recs { + records = append(records, + Record{Name: rrSet.Name, + Type: rrSet.Type, + TTL: ttl, + Content: recContent.(string), + SetPtr: setPtr}) + } - rrSet.Records = records + rrSet.Records = records - log.Printf("[DEBUG] Creating PowerDNS Record: %#v", rrSet) + return (resourcePDNSRecordCreateFinish(d, meta, zone, rrSet)) +} - recID, err := client.ReplaceRecordSet(zone, rrSet) +func resourcePDNSRecordCreateSOA(d *schema.ResourceData, meta interface{}) error { + rrSet, zone, ttl := resourcePDNSRecordCreatePrepare(d, meta) + client := meta.(*Client) + + log.Printf("[DEBUG] Searching existing SOA record at %s => %s", zone, d.Get("name").(string)) + soa_records, err := client.ListRecordsInRRSet(zone, d.Get("name").(string), "SOA") + log.Printf("[DEBUG] Found existing SOA records %v", soa_records) + if err != nil { + return fmt.Errorf("Failed to fetch old SOA record: %s", err) + } + var serial int + if len(soa_records) > 0 { + serial, err = strconv.Atoi(strings.Fields(soa_records[0].Content)[2]) if err != nil { - return fmt.Errorf("Failed to create PowerDNS Record: %s", err) + return fmt.Errorf("Failed to parse old serial value in SOA record: %s", err) } + } else { + serial = d.Get("serial").(int) + } + log.Printf("[DEBUG] Set serial number to %d", serial) + + records := make([]Record, 0, 1) + records = append(records, + Record{Name: rrSet.Name, + Type: rrSet.Type, + TTL: ttl, + 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")), + SetPtr: false}) + + rrSet.Records = records + + return (resourcePDNSRecordCreateFinish(d, meta, zone, rrSet)) +} + +func resourcePDNSRecordCreateFinish(d *schema.ResourceData, meta interface{}, zone string, rrSet ResourceRecordSet) error { + client := meta.(*Client) - d.SetId(recID) - log.Printf("[INFO] Created PowerDNS Record with ID: %s", d.Id()) + log.Printf("[DEBUG] Creating PowerDNS Record: %#v", rrSet) + recID, err := client.ReplaceRecordSet(zone, rrSet) + if err != nil { + return fmt.Errorf("Failed to create PowerDNS Record: %s", err) } + d.SetId(recID) + log.Printf("[INFO] Created PowerDNS Record with ID: %s", d.Id()) + return resourcePDNSRecordRead(d, meta) } @@ -132,12 +256,52 @@ func resourcePDNSRecordRead(d *schema.ResourceData, meta interface{}) error { } recs := make([]string, 0, len(records)) - for _, r := range records { - recs = append(recs, r.Content) + if d.Get("type") == "SOA" { + rsplit := strings.Fields(records[0].Content) + mname := rsplit[0] + d.Set("mname", mname) + rname := rsplit[1] + d.Set("rname", rname) + + serial, err := strconv.Atoi(rsplit[2]) + if err != nil { + return fmt.Errorf("Failed to parse serial value in SOA record: %s", err) + } + d.Set("serial", serial) + + refresh, err := strconv.Atoi(rsplit[3]) + if err != nil { + return fmt.Errorf("Failed to parse refresh value in SOA record: %s", err) + } + d.Set("refresh", refresh) + + retry, err := strconv.Atoi(rsplit[4]) + if err != nil { + return fmt.Errorf("Failed to parse retry value in SOA record: %s", err) + } + d.Set("retry", retry) + + expire, err := strconv.Atoi(rsplit[5]) + if err != nil { + return fmt.Errorf("Failed to parse expire value in SOA record: %s", err) + } + d.Set("expire", expire) + + minimum, err := strconv.Atoi(rsplit[6]) + if err != nil { + return fmt.Errorf("Failed to parse minimum value in SOA record: %s", err) + } + d.Set("minimum", minimum) + + log.Printf("[DEBUG] Parsed PowerDNS SOA Record contents: mname %s rname %s serial %d refresh %d expire %d minimum %d", mname, rname, serial, refresh, expire, minimum) + } else { + for _, r := range records { + recs = append(recs, r.Content) + } + d.Set("records", recs) } - d.Set("records", recs) - if len(records) > 0 { + if len(records) > 0 || d.Get("Type") == "SOA" { d.Set("ttl", records[0].TTL) } diff --git a/powerdns/resource_powerdns_record_test.go b/powerdns/resource_powerdns_record_test.go index 4641387d..5f0db15a 100644 --- a/powerdns/resource_powerdns_record_test.go +++ b/powerdns/resource_powerdns_record_test.go @@ -444,9 +444,34 @@ func TestAccPDNSRecord_SOA(t *testing.T) { }) } +func TestAccPDNSRecordSOA_SOA(t *testing.T) { + resourceName := "powerdns_record_soa.test-soa2" + resourceID := `{"zone":"test-soa2-sysa.xyz.","id":"test-soa2-sysa.xyz.:::SOA"}` + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPDNSRecordDestroy, + Steps: []resource.TestStep{ + { + Config: testPDNSRecordConfigSOA2, + Check: resource.ComposeTestCheckFunc( + testAccCheckPDNSRecordExists(resourceName), + ), + }, + { + ResourceName: resourceName, + ImportStateId: resourceID, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckPDNSRecordDestroy(s *terraform.State) error { for _, rs := range s.RootModule().Resources { - if rs.Type != "powerdns_record" { + if rs.Type != "powerdns_record" && rs.Type != "powerdns_record_soa" { continue } @@ -654,3 +679,24 @@ resource "powerdns_record" "test-soa" { ttl = 3600 records = [ "something.something. hostmaster.sysa.xyz. 2019090301 10800 3600 604800 3600" ] }` + +const testPDNSRecordConfigSOA2 = ` +resource "powerdns_record_soa" "test-soa2" { + zone = "test-soa2-sysa.xyz." + name = "test-soa2-sysa.xyz." + type = "SOA" + ttl = 3600 + mname = "ns1.sysa.xyz." + rname = "hostmaster.sysa.xyz." + serial = 0 + refresh = 7200 + retry = 600 + expire = 1209600 + minimum = 6400 + + lifecycle { + ignore_changes = [ + serial, + ] + } +}` diff --git a/website/docs/r/record_soa.html.markdown b/website/docs/r/record_soa.html.markdown new file mode 100644 index 00000000..f2de4139 --- /dev/null +++ b/website/docs/r/record_soa.html.markdown @@ -0,0 +1,55 @@ +--- +layout: "powerdns" +page_title: "PowerDNS: powerdns_record_soa" +sidebar_current: "docs-powerdns-resource-record-soa" +description: |- + Provides a PowerDNS SOA record resource. +--- + +# powerdns\_record + +Provides a PowerDNS SOA record resource. +This is offers an alternative to managing the SOA record of a zone through the generic `powerdns_record`. + +## Example Usage + +### A record example + +```hcl +resource "powerdns_record_soa" "soa-example_com" { + zone = "example.com." + name = "example.com." + type = "SOA" + ttl = 8600 + mnme = "ns1.example.com." + rname = "hostmaster.example.com." + serial = 0 + refresh = 7200 + retry = 600 + expire = 1209600 + minimum = 6400 + +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone` - (Required) The name of zone to contain this record. +* `name` - (Required) The name of the record, typically the same as `zone`. +* `type` - (Required) The record type, must be `SOA`. +* `ttl` - (Required) The TTL of the record. +* `mname` - (Required) SOA MNAME. +* `rname` - (Required) SOA RNAME. +* `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. +* `refresh` - (Required) SOA REFRESH. +* `retry` - (Required) SOA RETRY. +* `expire` - (Required) SOA EXPIRE. +* `minimum` - (Required) SOA MINIMUM. + +### Attribute Reference + +The id of the resource is a composite of the record name and record type, joined by a separator - `:::`. + +For example, record `example.com.` of type `SOA` will be represented with the following `id`: `example.com.:::SOA`