provider/powerdns: Add support for PowerDNS 4 API (#7819)

* Auto-detect the API version

and update the endpoint URL accordingly

* Typo fix

* Make client and resource work with the 4.X API

* Update documentation

* Fix typos

* 204 now counts as a "success" response

See
f0e76cee2c
for the change in the pdns repository.

* Add a note about a possible pitfall when defining some records
This commit is contained in:
Stephen Muth 2016-07-28 12:01:06 -04:00 committed by Paul Stack
parent 14f19aff1b
commit bbd9b2c944
4 changed files with 82 additions and 20 deletions

View File

@ -7,16 +7,16 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"strings" "strings"
"github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-cleanhttp"
) )
type Client struct { type Client struct {
// Location of PowerDNS server to use ServerUrl string // Location of PowerDNS server to use
ServerUrl string ApiKey string // REST API Static authentication key
// REST API Static authentication key ApiVersion int // API version to use
ApiKey string
Http *http.Client Http *http.Client
} }
@ -27,15 +27,26 @@ func NewClient(serverUrl string, apiKey string) (*Client, error) {
ApiKey: apiKey, ApiKey: apiKey,
Http: cleanhttp.DefaultClient(), Http: cleanhttp.DefaultClient(),
} }
var err error
client.ApiVersion, err = client.detectApiVersion()
if err != nil {
return nil, err
}
return &client, nil return &client, nil
} }
// Creates a new request with necessary headers // Creates a new request with necessary headers
func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) { func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) {
url, err := url.Parse(c.ServerUrl + endpoint) var urlStr string
if c.ApiVersion > 0 {
urlStr = c.ServerUrl + "/api/v" + strconv.Itoa(c.ApiVersion) + endpoint
} else {
urlStr = c.ServerUrl + endpoint
}
url, err := url.Parse(urlStr)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error during parting request URL: %s", err) return nil, fmt.Errorf("Error during parsing request URL: %s", err)
} }
var bodyReader io.Reader var bodyReader io.Reader
@ -66,13 +77,14 @@ type ZoneInfo struct {
DnsSec bool `json:"dnsssec"` DnsSec bool `json:"dnsssec"`
Serial int64 `json:"serial"` Serial int64 `json:"serial"`
Records []Record `json:"records,omitempty"` Records []Record `json:"records,omitempty"`
ResourceRecordSets []ResourceRecordSet `json:"rrsets,omitempty"`
} }
type Record struct { type Record struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Content string `json:"content"` Content string `json:"content"`
TTL int `json:"ttl"` TTL int `json:"ttl"` // For API v0
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
} }
@ -80,6 +92,7 @@ type ResourceRecordSet struct {
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
ChangeType string `json:"changetype"` ChangeType string `json:"changetype"`
TTL int `json:"ttl"` // For API v1
Records []Record `json:"records,omitempty"` Records []Record `json:"records,omitempty"`
} }
@ -111,6 +124,26 @@ func parseId(recId string) (string, string, error) {
} }
} }
// Detects the API version in use on the server
// Uses int to represent the API version: 0 is the legacy AKA version 3.4 API
// Any other integer correlates with the same API version
func (client *Client) detectApiVersion() (int, error) {
req, err := client.newRequest("GET", "/api/v1/servers", nil)
if err != nil {
return -1, err
}
resp, err := client.Http.Do(req)
if err != nil {
return -1, err
}
defer resp.Body.Close()
if resp.StatusCode == 200 {
return 1, nil
} else {
return 0, nil
}
}
// Returns all Zones of server, without records // Returns all Zones of server, without records
func (client *Client) ListZones() ([]ZoneInfo, error) { func (client *Client) ListZones() ([]ZoneInfo, error) {
@ -154,7 +187,20 @@ func (client *Client) ListRecords(zone string) ([]Record, error) {
return nil, err return nil, err
} }
return zoneInfo.Records, nil records := zoneInfo.Records
// Convert the API v1 response to v0 record structure
for _, rrs := range zoneInfo.ResourceRecordSets {
for _, record := range rrs.Records {
records = append(records, Record{
Name: rrs.Name,
Type: rrs.Type,
Content: record.Content,
TTL: rrs.TTL,
})
}
}
return records, nil
} }
// Returns only records of specified name and type // Returns only records of specified name and type
@ -232,7 +278,7 @@ func (client *Client) CreateRecord(zone string, record Record) (string, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse) errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
return "", fmt.Errorf("Error creating record: %s", record.Id()) return "", fmt.Errorf("Error creating record: %s", record.Id())
@ -263,7 +309,7 @@ func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (st
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse) errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
return "", fmt.Errorf("Error creating record set: %s", rrSet.Id()) return "", fmt.Errorf("Error creating record set: %s", rrSet.Id())
@ -298,7 +344,7 @@ func (client *Client) DeleteRecordSet(zone string, name string, tpe string) erro
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse) errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
return fmt.Errorf("Error deleting record: %s %s", name, tpe) return fmt.Errorf("Error deleting record: %s %s", name, tpe)

View File

@ -57,6 +57,7 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
rrSet := ResourceRecordSet{ rrSet := ResourceRecordSet{
Name: d.Get("name").(string), Name: d.Get("name").(string),
Type: d.Get("type").(string), Type: d.Get("type").(string),
TTL: d.Get("ttl").(int),
} }
zone := d.Get("zone").(string) zone := d.Get("zone").(string)

View File

@ -9,7 +9,7 @@ description: |-
# PowerDNS Provider # PowerDNS Provider
The PowerDNS provider is used manipulate DNS records supported by PowerDNS server. The provider needs to be configured The PowerDNS provider is used manipulate DNS records supported by PowerDNS server. The provider needs to be configured
with the proper credentials before it can be used. with the proper credentials before it can be used. It supports both the [legacy API](https://doc.powerdns.com/3/httpapi/api_spec/) and the new [version 1 API](https://doc.powerdns.com/md/httpapi/api_spec/), however resources may need to be configured differently.
Use the navigation to the left to read about the available resources. Use the navigation to the left to read about the available resources.

View File

@ -12,6 +12,21 @@ Provides a PowerDNS record resource.
## Example Usage ## Example Usage
Note that PowerDNS internally lowercases certain records (e.g. CNAME and AAAA), which can lead to resources being marked for a change in every singe plan.
For the v1 API (PowerDNS version 4):
```
# Add a record to the zone
resource "powerdns_record" "foobar" {
zone = "example.com."
name = "www.example.com"
type = "A"
ttl = 300
records = ["192.168.0.11"]
}
```
For the legacy API (PowerDNS version 3.4):
``` ```
# Add a record to the zone # Add a record to the zone
resource "powerdns_record" "foobar" { resource "powerdns_record" "foobar" {