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:
parent
14f19aff1b
commit
bbd9b2c944
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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" {
|
||||||
|
|
Loading…
Reference in New Issue