powerdns.go 3.5 KB

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