powerdns.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package powerdns
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "time"
  10. )
  11. // Client contains an API client for a PowerDNS server
  12. type Client struct {
  13. apiKey string
  14. serverURL string
  15. runDomain string
  16. httpClient *http.Client
  17. }
  18. // NewClient creates a new bind API client
  19. func NewClient(serverURL, apiKey, runDomain string) *Client {
  20. httpClient := &http.Client{
  21. Timeout: time.Minute,
  22. }
  23. return &Client{apiKey, serverURL, runDomain, httpClient}
  24. }
  25. // RecordData represents the data required to create or delete an A/CNAME record
  26. // for the nameserver
  27. type RecordData struct {
  28. RRSets []RR `json:"rrsets"`
  29. }
  30. type RR struct {
  31. Name string `json:"name"`
  32. Type string `json:"type"`
  33. ChangeType string `json:"changetype"`
  34. TTL uint `json:"ttl"`
  35. Records []Record `json:"records"`
  36. }
  37. type Record struct {
  38. Content string `json:"content"`
  39. Disabled bool `json:"disabled"`
  40. Name string `json:"name"`
  41. Type string `json:"type"`
  42. Priority uint `json:"priority"`
  43. }
  44. // CreateCNAMERecord creates a new CNAME record for the nameserver
  45. func (c *Client) CreateCNAMERecord(value, hostname string) error {
  46. valueC := canonicalize(value)
  47. hostnameC := canonicalize(hostname)
  48. return c.sendRequest("PATCH", &RecordData{
  49. RRSets: []RR{{
  50. Name: hostnameC,
  51. Type: "CNAME",
  52. ChangeType: "REPLACE",
  53. TTL: 300,
  54. Records: []Record{{
  55. Content: valueC,
  56. Disabled: false,
  57. Name: hostnameC,
  58. Type: "CNAME",
  59. Priority: 0,
  60. }},
  61. }},
  62. })
  63. }
  64. // CreateARecord creates a new A record for the nameserver
  65. func (c *Client) CreateARecord(value, hostname string) error {
  66. hostnameC := canonicalize(hostname)
  67. return c.sendRequest("PATCH", &RecordData{
  68. RRSets: []RR{{
  69. Name: hostnameC,
  70. Type: "A",
  71. ChangeType: "REPLACE",
  72. TTL: 300,
  73. Records: []Record{{
  74. Content: value,
  75. Disabled: false,
  76. Name: hostnameC,
  77. Type: "A",
  78. Priority: 0,
  79. }},
  80. }},
  81. })
  82. }
  83. func canonicalize(value string) string {
  84. // if the string ends in a period, return
  85. if value[len(value)-1:] == "." {
  86. return value
  87. }
  88. return fmt.Sprintf("%s.", value)
  89. }
  90. func (c *Client) sendRequest(method string, data *RecordData) error {
  91. reqURL, err := url.Parse(c.serverURL)
  92. if err != nil {
  93. return nil
  94. }
  95. reqURL.Path = fmt.Sprintf("/api/v1/servers/localhost/zones/%s", c.runDomain)
  96. strData, err := json.Marshal(data)
  97. if err != nil {
  98. return err
  99. }
  100. req, err := http.NewRequest(
  101. method,
  102. reqURL.String(),
  103. strings.NewReader(string(strData)),
  104. )
  105. if err != nil {
  106. return err
  107. }
  108. req.Header.Set("Content-Type", "application/json; charset=utf-8")
  109. req.Header.Set("Accept", "application/json; charset=utf-8")
  110. req.Header.Set("X-Api-Key", c.apiKey)
  111. res, err := c.httpClient.Do(req)
  112. if err != nil {
  113. return err
  114. }
  115. defer res.Body.Close()
  116. if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
  117. resBytes, err := ioutil.ReadAll(res.Body)
  118. if err != nil {
  119. return fmt.Errorf("request failed with status code %d, but could not read body (%s)\n", res.StatusCode, err.Error())
  120. }
  121. return fmt.Errorf("request failed with status code %d: %s\n", res.StatusCode, string(resBytes))
  122. }
  123. return nil
  124. }