package powerdns import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "strings" "time" ) // Client contains an API client for a PowerDNS server type Client struct { apiKey string serverURL string runDomain string httpClient *http.Client } // NewClient creates a new bind API client func NewClient(serverURL, apiKey, runDomain string) *Client { httpClient := &http.Client{ Timeout: time.Minute, } return &Client{apiKey, serverURL, runDomain, httpClient} } // RecordData represents the data required to create or delete an A/CNAME record // for the nameserver type RecordData struct { RRSets []RR `json:"rrsets"` } type RR struct { Name string `json:"name"` Type string `json:"type"` ChangeType string `json:"changetype"` TTL uint `json:"ttl"` Records []Record `json:"records"` } type Record struct { Content string `json:"content"` Disabled bool `json:"disabled"` Name string `json:"name"` Type string `json:"type"` Priority uint `json:"priority"` } // CreateCNAMERecord creates a new CNAME record for the nameserver func (c *Client) CreateCNAMERecord(value, hostname string) error { valueC := canonicalize(value) hostnameC := canonicalize(hostname) return c.sendRequest("PATCH", &RecordData{ RRSets: []RR{{ Name: hostnameC, Type: "CNAME", ChangeType: "REPLACE", TTL: 300, Records: []Record{{ Content: valueC, Disabled: false, Name: hostnameC, Type: "CNAME", Priority: 0, }}, }}, }) } // CreateARecord creates a new A record for the nameserver func (c *Client) CreateARecord(value, hostname string) error { hostnameC := canonicalize(hostname) return c.sendRequest("PATCH", &RecordData{ RRSets: []RR{{ Name: hostnameC, Type: "A", ChangeType: "REPLACE", TTL: 300, Records: []Record{{ Content: value, Disabled: false, Name: hostnameC, Type: "A", Priority: 0, }}, }}, }) } func canonicalize(value string) string { // if the string ends in a period, return if value[len(value)-1:] == "." { return value } return fmt.Sprintf("%s.", value) } func (c *Client) sendRequest(method string, data *RecordData) error { reqURL, err := url.Parse(c.serverURL) if err != nil { return nil } reqURL.Path = fmt.Sprintf("/api/v1/servers/localhost/zones/%s", c.runDomain) strData, err := json.Marshal(data) if err != nil { return err } req, err := http.NewRequest( method, reqURL.String(), strings.NewReader(string(strData)), ) if err != nil { return err } req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Accept", "application/json; charset=utf-8") req.Header.Set("X-Api-Key", c.apiKey) res, err := c.httpClient.Do(req) if err != nil { return err } defer res.Body.Close() if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { resBytes, err := ioutil.ReadAll(res.Body) if err != nil { return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error()) } return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes)) } return nil }