diff --git a/.gitignore b/.gitignore index fa8bc85..eb807cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -terraform-provider-dns +terraform-provider-powerdns *.dll *.exe diff --git a/examples/zones/example_com.tf b/examples/zones/example_com.tf new file mode 100644 index 0000000..6d8a125 --- /dev/null +++ b/examples/zones/example_com.tf @@ -0,0 +1,46 @@ +module "example_com" { + source = "../../terraform-provider-powerdns" + zones = [ + "example.com.", + ] + + soa_edit_api = "INCREASE" + + # preferably declare NS records instead of this + # https://github.com/pan-net/terraform-provider-powerdns/issues/63 + nameservers = [ + "ns1.example.com.", + "ns2.example.com.", + ] + + records = [ + { + type = "SOA" + ttl = 43200 + rname = "admin.opensuse.org." + refresh = 7200 + retry = 600 + expire = 1209600 + minimum = 6400 + # this can be used to set an initial serial number for new zones + # serial number changes to existing zones will be ignored, the user is expected to use SOA-EDIT-API + serial = 1 + }, + { + type = "SOA", + ttl = 300, + records = [ + "ns1.example.com. hostmaster.example.com. 0 10800 3600 604800 3600" + ] + }, + { + name = "www", + type = "AAAA", + ttl = 300, + records = [ + "::1", + ] + } + ] +} + diff --git a/examples/zones/main.tf b/examples/zones/main.tf new file mode 100644 index 0000000..d576688 --- /dev/null +++ b/examples/zones/main.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + powerdns = { + source = "pan-net/powerdns" + #version = "1.5.0" + } + } +} + +provider "powerdns" { + api_key = var.pdns_api_key + server_url = var.pdns_server_url +} diff --git a/examples/zones/variables.tf b/examples/zones/variables.tf new file mode 100644 index 0000000..eceeb6e --- /dev/null +++ b/examples/zones/variables.tf @@ -0,0 +1,6 @@ +variable "pdns_api_key" { + type = string +} +variable "pdns_server_url" { + type = string +} diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..6a3a22f --- /dev/null +++ b/main.tf @@ -0,0 +1,120 @@ +locals { + zones = var.zones + nameservers = var.nameservers + nameservers_records_data = flatten([ for r in var.records : [ for rd in r.records : rd ] if r.type == "NS" ]) + non_soa_records = [ for r in var.records : r if r.type != "SOA" ] + soa_records = [ for r in var.records : r if r.type == "SOA" ] +} + +resource "powerdns_zone" "zone" { + for_each = toset(local.zones) + name = each.value + kind = "Native" + nameservers = length(var.nameservers) == 0 ? local.nameservers_records_data : var.nameservers + soa_edit_api = var.soa_edit_api + + lifecycle { + ignore_changes = [ + # https://github.com/pan-net/terraform-provider-powerdns/issues/63 + # users of the module are expected to use NS records for tracking nameservers + nameservers, + ] + } +} + +locals { + records_expanded = { + for i, record in local.non_soa_records : join("-", compact([ + lower(record.type), + try(lower(record.name), ""), + ])) => { + type = record.type + name = try(record.name, "") + ttl = try(record.ttl, null) + idx = i + } + } + + records_expanded_soa = { + for i, record in local.soa_records : join("-", compact([ + lower(record.type), + try(lower(record.name), ""), + ])) => { + type = record.type + name = try(record.name, "") + ttl = try(record.ttl, null) + mname = try(record.mname, element(local.nameservers_records_data, 0)), + rname = record.rname, + serial = try(record.serial, 0), + refresh = record.refresh, + retry = record.retry, + expire = record.expire, + minimum = record.minimum, + idx = i + } + } + + records_by_name = { + for product in setproduct(local.zones, keys(local.records_expanded)) : "${product[1]}-${product[0]}" => { + zone = powerdns_zone.zone[product[0]].name + type = local.records_expanded[product[1]].type + name = local.records_expanded[product[1]].name + ttl = local.records_expanded[product[1]].ttl + idx = local.records_expanded[product[1]].idx + } + } + + records_by_name_soa = { + for product in setproduct(local.zones, keys(local.records_expanded_soa)) : "${product[1]}-${product[0]}" => { + zone = powerdns_zone.zone[product[0]].name + type = local.records_expanded_soa[product[1]].type + name = local.records_expanded_soa[product[1]].name + ttl = local.records_expanded_soa[product[1]].ttl + mname = local.records_expanded_soa[product[1]].mname, + rname = local.records_expanded_soa[product[1]].rname, + serial = local.records_expanded_soa[product[1]].serial, + refresh = local.records_expanded_soa[product[1]].refresh, + retry = local.records_expanded_soa[product[1]].retry, + expire = local.records_expanded_soa[product[1]].expire, + minimum = local.records_expanded_soa[product[1]].minimum, + idx = local.records_expanded_soa[product[1]].idx + } + } + + records = local.records_by_name + records_soa = local.records_by_name_soa +} + +resource "powerdns_record_soa" "record_soa" { + for_each = local.records_soa + name = each.value.name == "" ? each.value.zone : join(".", [each.value.name, each.value.zone]) + zone = each.value.zone + type = each.value.type + ttl = each.value.ttl + mname = each.value.mname + rname = each.value.rname + serial = each.value.serial + refresh = each.value.refresh + retry = each.value.retry + expire = each.value.expire + minimum = each.value.minimum + + lifecycle { + ignore_changes = [ + serial, + ] + } + +} + +resource "powerdns_record" "record" { + for_each = local.records + name = each.value.name == "" ? each.value.zone : join(".", [each.value.name, each.value.zone]) + zone = each.value.zone + type = each.value.type + ttl = each.value.ttl + records = can(local.non_soa_records[each.value.idx].records) ? [for r in local.non_soa_records[each.value.idx].records : + each.value.type == "TXT" && length(regexall("(\\\"\\\")", r)) == 0 ? + format("\"%s\"", r) : r + ] : null +} diff --git a/powerdns/provider.go b/powerdns/provider.go index d8d2040..d135372 100644 --- a/powerdns/provider.go +++ b/powerdns/provider.go @@ -56,6 +56,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "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 d2be002..0029737 100644 --- a/powerdns/resource_powerdns_record.go +++ b/powerdns/resource_powerdns_record.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strings" + "strconv" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" ) @@ -61,6 +62,87 @@ func resourcePDNSRecord() *schema.Resource { } } +func resourcePDNSRecordSOA() *schema.Resource { + return &schema.Resource{ + Create: resourcePDNSRecordCreate, + 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: 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 resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Client) @@ -72,11 +154,17 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { zone := d.Get("zone").(string) ttl := d.Get("ttl").(int) - recs := d.Get("records").(*schema.Set).List() + var recs []interface{} + var recslen int setPtr := false - - if v, ok := d.GetOk("set_ptr"); ok { - setPtr = v.(bool) + if d.Get("type") == "SOA" { + recslen = 1 + } else { + recs = d.Get("records").(*schema.Set).List() + recslen = len(recs) + if v, ok := d.GetOk("set_ptr"); ok { + setPtr = v.(bool) + } } // begin: ValidateFunc @@ -89,20 +177,46 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error { log.Printf("[WARN] One or more values in 'records' contain empty '' value(s)") } } - if !(len(recs) > 0) { + if recslen == 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 { + if recslen > 0 { + records := make([]Record, 0, recslen) + + if d.Get("type") == "SOA" { + log.Printf("[DEBUG] Searching existing SOA record at %s => %s", d.Get("zone").(string), d.Get("name").(string)) + soa_records, err := client.ListRecordsInRRSet(d.Get("zone").(string), d.Get("name").(string), "SOA") + 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 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 = append(records, Record{Name: rrSet.Name, Type: rrSet.Type, TTL: ttl, - Content: recContent.(string), + 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: setPtr}) + } else { + for _, recContent := range recs { + records = append(records, + Record{Name: rrSet.Name, + Type: rrSet.Type, + TTL: ttl, + Content: recContent.(string), + SetPtr: setPtr}) + } } rrSet.Records = records @@ -132,12 +246,48 @@ 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) + d.Set("mname", rsplit[0]) + d.Set("rname", rsplit[1]) + + 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) + } 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/variables.tf b/variables.tf new file mode 100644 index 0000000..ec146a0 --- /dev/null +++ b/variables.tf @@ -0,0 +1,23 @@ +variable "zones" { + description = "List of zones to configure." + type = list + default = [] +} + +variable "nameservers" { + description = "List of nameservers to configure in the given zones (automatically populated from NS records if not specified)." + type = list + default = [] +} + +variable "records" { + description = "List of records to configure in the given zones." + type = any + default = [] +} + +variable "soa_edit_api" { + description = "SOA-EDIT-API metadata to configure in the given zones." + type = string + default = "INCREMENT" +}