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,17 +7,17 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/hashicorp/go-cleanhttp"
)
type Client struct {
// Location of PowerDNS server to use
ServerUrl string
// REST API Static authentication key
ApiKey string
Http *http.Client
ServerUrl string // Location of PowerDNS server to use
ApiKey string // REST API Static authentication key
ApiVersion int // API version to use
Http *http.Client
}
// NewClient returns a new PowerDNS client
@ -27,15 +27,26 @@ func NewClient(serverUrl string, apiKey string) (*Client, error) {
ApiKey: apiKey,
Http: cleanhttp.DefaultClient(),
}
var err error
client.ApiVersion, err = client.detectApiVersion()
if err != nil {
return nil, err
}
return &client, nil
}
// Creates a new request with necessary headers
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 {
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
@ -59,20 +70,21 @@ func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.
}
type ZoneInfo struct {
Id string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Kind string `json:"kind"`
DnsSec bool `json:"dnsssec"`
Serial int64 `json:"serial"`
Records []Record `json:"records,omitempty"`
Id string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Kind string `json:"kind"`
DnsSec bool `json:"dnsssec"`
Serial int64 `json:"serial"`
Records []Record `json:"records,omitempty"`
ResourceRecordSets []ResourceRecordSet `json:"rrsets,omitempty"`
}
type Record struct {
Name string `json:"name"`
Type string `json:"type"`
Content string `json:"content"`
TTL int `json:"ttl"`
TTL int `json:"ttl"` // For API v0
Disabled bool `json:"disabled"`
}
@ -80,6 +92,7 @@ type ResourceRecordSet struct {
Name string `json:"name"`
Type string `json:"type"`
ChangeType string `json:"changetype"`
TTL int `json:"ttl"` // For API v1
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
func (client *Client) ListZones() ([]ZoneInfo, error) {
@ -154,7 +187,20 @@ func (client *Client) ListRecords(zone string) ([]Record, error) {
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
@ -232,7 +278,7 @@ func (client *Client) CreateRecord(zone string, record Record) (string, error) {
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
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()
if resp.StatusCode != 200 {
if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
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()
if resp.StatusCode != 200 {
if resp.StatusCode != 200 && resp.StatusCode != 204 {
errorResp := new(errorResponse)
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
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{
Name: d.Get("name").(string),
Type: d.Get("type").(string),
TTL: d.Get("ttl").(int),
}
zone := d.Get("zone").(string)

View File

@ -9,7 +9,7 @@ description: |-
# PowerDNS Provider
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.

View File

@ -12,6 +12,21 @@ Provides a PowerDNS record resource.
## 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
resource "powerdns_record" "foobar" {