Skip to content

Commit bbd9b2c

Browse files
smuth4stack72
authored andcommitted
provider/powerdns: Add support for PowerDNS 4 API (hashicorp#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 PowerDNS/pdns@f0e76ce for the change in the pdns repository. * Add a note about a possible pitfall when defining some records
1 parent 14f19af commit bbd9b2c

4 files changed

Lines changed: 82 additions & 20 deletions

File tree

builtin/providers/powerdns/client.go

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@ import (
77
"io"
88
"net/http"
99
"net/url"
10+
"strconv"
1011
"strings"
1112

1213
"github.com/hashicorp/go-cleanhttp"
1314
)
1415

1516
type Client struct {
16-
// Location of PowerDNS server to use
17-
ServerUrl string
18-
// REST API Static authentication key
19-
ApiKey string
20-
Http *http.Client
17+
ServerUrl string // Location of PowerDNS server to use
18+
ApiKey string // REST API Static authentication key
19+
ApiVersion int // API version to use
20+
Http *http.Client
2121
}
2222

2323
// NewClient returns a new PowerDNS client
@@ -27,15 +27,26 @@ func NewClient(serverUrl string, apiKey string) (*Client, error) {
2727
ApiKey: apiKey,
2828
Http: cleanhttp.DefaultClient(),
2929
}
30+
var err error
31+
client.ApiVersion, err = client.detectApiVersion()
32+
if err != nil {
33+
return nil, err
34+
}
3035
return &client, nil
3136
}
3237

3338
// Creates a new request with necessary headers
3439
func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) {
3540

36-
url, err := url.Parse(c.ServerUrl + endpoint)
41+
var urlStr string
42+
if c.ApiVersion > 0 {
43+
urlStr = c.ServerUrl + "/api/v" + strconv.Itoa(c.ApiVersion) + endpoint
44+
} else {
45+
urlStr = c.ServerUrl + endpoint
46+
}
47+
url, err := url.Parse(urlStr)
3748
if err != nil {
38-
return nil, fmt.Errorf("Error during parting request URL: %s", err)
49+
return nil, fmt.Errorf("Error during parsing request URL: %s", err)
3950
}
4051

4152
var bodyReader io.Reader
@@ -59,27 +70,29 @@ func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.
5970
}
6071

6172
type ZoneInfo struct {
62-
Id string `json:"id"`
63-
Name string `json:"name"`
64-
URL string `json:"url"`
65-
Kind string `json:"kind"`
66-
DnsSec bool `json:"dnsssec"`
67-
Serial int64 `json:"serial"`
68-
Records []Record `json:"records,omitempty"`
73+
Id string `json:"id"`
74+
Name string `json:"name"`
75+
URL string `json:"url"`
76+
Kind string `json:"kind"`
77+
DnsSec bool `json:"dnsssec"`
78+
Serial int64 `json:"serial"`
79+
Records []Record `json:"records,omitempty"`
80+
ResourceRecordSets []ResourceRecordSet `json:"rrsets,omitempty"`
6981
}
7082

7183
type Record struct {
7284
Name string `json:"name"`
7385
Type string `json:"type"`
7486
Content string `json:"content"`
75-
TTL int `json:"ttl"`
87+
TTL int `json:"ttl"` // For API v0
7688
Disabled bool `json:"disabled"`
7789
}
7890

7991
type ResourceRecordSet struct {
8092
Name string `json:"name"`
8193
Type string `json:"type"`
8294
ChangeType string `json:"changetype"`
95+
TTL int `json:"ttl"` // For API v1
8396
Records []Record `json:"records,omitempty"`
8497
}
8598

@@ -111,6 +124,26 @@ func parseId(recId string) (string, string, error) {
111124
}
112125
}
113126

127+
// Detects the API version in use on the server
128+
// Uses int to represent the API version: 0 is the legacy AKA version 3.4 API
129+
// Any other integer correlates with the same API version
130+
func (client *Client) detectApiVersion() (int, error) {
131+
req, err := client.newRequest("GET", "/api/v1/servers", nil)
132+
if err != nil {
133+
return -1, err
134+
}
135+
resp, err := client.Http.Do(req)
136+
if err != nil {
137+
return -1, err
138+
}
139+
defer resp.Body.Close()
140+
if resp.StatusCode == 200 {
141+
return 1, nil
142+
} else {
143+
return 0, nil
144+
}
145+
}
146+
114147
// Returns all Zones of server, without records
115148
func (client *Client) ListZones() ([]ZoneInfo, error) {
116149

@@ -154,7 +187,20 @@ func (client *Client) ListRecords(zone string) ([]Record, error) {
154187
return nil, err
155188
}
156189

157-
return zoneInfo.Records, nil
190+
records := zoneInfo.Records
191+
// Convert the API v1 response to v0 record structure
192+
for _, rrs := range zoneInfo.ResourceRecordSets {
193+
for _, record := range rrs.Records {
194+
records = append(records, Record{
195+
Name: rrs.Name,
196+
Type: rrs.Type,
197+
Content: record.Content,
198+
TTL: rrs.TTL,
199+
})
200+
}
201+
}
202+
203+
return records, nil
158204
}
159205

160206
// Returns only records of specified name and type
@@ -232,7 +278,7 @@ func (client *Client) CreateRecord(zone string, record Record) (string, error) {
232278
}
233279
defer resp.Body.Close()
234280

235-
if resp.StatusCode != 200 {
281+
if resp.StatusCode != 200 && resp.StatusCode != 204 {
236282
errorResp := new(errorResponse)
237283
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
238284
return "", fmt.Errorf("Error creating record: %s", record.Id())
@@ -263,7 +309,7 @@ func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (st
263309
}
264310
defer resp.Body.Close()
265311

266-
if resp.StatusCode != 200 {
312+
if resp.StatusCode != 200 && resp.StatusCode != 204 {
267313
errorResp := new(errorResponse)
268314
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
269315
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
298344
}
299345
defer resp.Body.Close()
300346

301-
if resp.StatusCode != 200 {
347+
if resp.StatusCode != 200 && resp.StatusCode != 204 {
302348
errorResp := new(errorResponse)
303349
if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil {
304350
return fmt.Errorf("Error deleting record: %s %s", name, tpe)

builtin/providers/powerdns/resource_powerdns_record.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func resourcePDNSRecordCreate(d *schema.ResourceData, meta interface{}) error {
5757
rrSet := ResourceRecordSet{
5858
Name: d.Get("name").(string),
5959
Type: d.Get("type").(string),
60+
TTL: d.Get("ttl").(int),
6061
}
6162

6263
zone := d.Get("zone").(string)

website/source/docs/providers/powerdns/index.html.markdown

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ description: |-
99
# PowerDNS Provider
1010

1111
The PowerDNS provider is used manipulate DNS records supported by PowerDNS server. The provider needs to be configured
12-
with the proper credentials before it can be used.
12+
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.
1313

1414
Use the navigation to the left to read about the available resources.
1515

website/source/docs/providers/powerdns/r/record.html.markdown

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ Provides a PowerDNS record resource.
1212

1313
## Example Usage
1414

15+
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.
16+
17+
For the v1 API (PowerDNS version 4):
18+
```
19+
# Add a record to the zone
20+
resource "powerdns_record" "foobar" {
21+
zone = "example.com."
22+
name = "www.example.com"
23+
type = "A"
24+
ttl = 300
25+
records = ["192.168.0.11"]
26+
}
27+
```
28+
29+
For the legacy API (PowerDNS version 3.4):
1530
```
1631
# Add a record to the zone
1732
resource "powerdns_record" "foobar" {

0 commit comments

Comments
 (0)