github.com/minamijoyo/terraform@v0.7.8-0.20161029001309-18b3736ba44b/builtin/providers/powerdns/client.go (about) 1 package powerdns 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "net/url" 10 "strconv" 11 "strings" 12 13 "github.com/hashicorp/go-cleanhttp" 14 ) 15 16 type Client struct { 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 21 } 22 23 // NewClient returns a new PowerDNS client 24 func NewClient(serverUrl string, apiKey string) (*Client, error) { 25 client := Client{ 26 ServerUrl: serverUrl, 27 ApiKey: apiKey, 28 Http: cleanhttp.DefaultClient(), 29 } 30 var err error 31 client.ApiVersion, err = client.detectApiVersion() 32 if err != nil { 33 return nil, err 34 } 35 return &client, nil 36 } 37 38 // Creates a new request with necessary headers 39 func (c *Client) newRequest(method string, endpoint string, body []byte) (*http.Request, error) { 40 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) 48 if err != nil { 49 return nil, fmt.Errorf("Error during parsing request URL: %s", err) 50 } 51 52 var bodyReader io.Reader 53 if body != nil { 54 bodyReader = bytes.NewReader(body) 55 } 56 57 req, err := http.NewRequest(method, url.String(), bodyReader) 58 if err != nil { 59 return nil, fmt.Errorf("Error during creation of request: %s", err) 60 } 61 62 req.Header.Add("X-API-Key", c.ApiKey) 63 req.Header.Add("Accept", "application/json") 64 65 if method != "GET" { 66 req.Header.Add("Content-Type", "application/json") 67 } 68 69 return req, nil 70 } 71 72 type ZoneInfo struct { 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"` 81 } 82 83 type Record struct { 84 Name string `json:"name"` 85 Type string `json:"type"` 86 Content string `json:"content"` 87 TTL int `json:"ttl"` // For API v0 88 Disabled bool `json:"disabled"` 89 } 90 91 type ResourceRecordSet struct { 92 Name string `json:"name"` 93 Type string `json:"type"` 94 ChangeType string `json:"changetype"` 95 TTL int `json:"ttl"` // For API v1 96 Records []Record `json:"records,omitempty"` 97 } 98 99 type zonePatchRequest struct { 100 RecordSets []ResourceRecordSet `json:"rrsets"` 101 } 102 103 type errorResponse struct { 104 ErrorMsg string `json:"error"` 105 } 106 107 const idSeparator string = ":::" 108 109 func (record *Record) Id() string { 110 return record.Name + idSeparator + record.Type 111 } 112 113 func (rrSet *ResourceRecordSet) Id() string { 114 return rrSet.Name + idSeparator + rrSet.Type 115 } 116 117 // Returns name and type of record or record set based on it's ID 118 func parseId(recId string) (string, string, error) { 119 s := strings.Split(recId, idSeparator) 120 if len(s) == 2 { 121 return s[0], s[1], nil 122 } else { 123 return "", "", fmt.Errorf("Unknown record ID format") 124 } 125 } 126 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 147 // Returns all Zones of server, without records 148 func (client *Client) ListZones() ([]ZoneInfo, error) { 149 150 req, err := client.newRequest("GET", "/servers/localhost/zones", nil) 151 if err != nil { 152 return nil, err 153 } 154 155 resp, err := client.Http.Do(req) 156 if err != nil { 157 return nil, err 158 } 159 defer resp.Body.Close() 160 161 var zoneInfos []ZoneInfo 162 163 err = json.NewDecoder(resp.Body).Decode(&zoneInfos) 164 if err != nil { 165 return nil, err 166 } 167 168 return zoneInfos, nil 169 } 170 171 // Returns all records in Zone 172 func (client *Client) ListRecords(zone string) ([]Record, error) { 173 req, err := client.newRequest("GET", fmt.Sprintf("/servers/localhost/zones/%s", zone), nil) 174 if err != nil { 175 return nil, err 176 } 177 178 resp, err := client.Http.Do(req) 179 if err != nil { 180 return nil, err 181 } 182 defer resp.Body.Close() 183 184 zoneInfo := new(ZoneInfo) 185 err = json.NewDecoder(resp.Body).Decode(zoneInfo) 186 if err != nil { 187 return nil, err 188 } 189 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 204 } 205 206 // Returns only records of specified name and type 207 func (client *Client) ListRecordsInRRSet(zone string, name string, tpe string) ([]Record, error) { 208 allRecords, err := client.ListRecords(zone) 209 if err != nil { 210 return nil, err 211 } 212 213 records := make([]Record, 0, 10) 214 for _, r := range allRecords { 215 if r.Name == name && r.Type == tpe { 216 records = append(records, r) 217 } 218 } 219 220 return records, nil 221 } 222 223 func (client *Client) ListRecordsByID(zone string, recId string) ([]Record, error) { 224 name, tpe, err := parseId(recId) 225 if err != nil { 226 return nil, err 227 } else { 228 return client.ListRecordsInRRSet(zone, name, tpe) 229 } 230 } 231 232 // Checks if requested record exists in Zone 233 func (client *Client) RecordExists(zone string, name string, tpe string) (bool, error) { 234 allRecords, err := client.ListRecords(zone) 235 if err != nil { 236 return false, err 237 } 238 239 for _, record := range allRecords { 240 if record.Name == name && record.Type == tpe { 241 return true, nil 242 } 243 } 244 return false, nil 245 } 246 247 // Checks if requested record exists in Zone by it's ID 248 func (client *Client) RecordExistsByID(zone string, recId string) (bool, error) { 249 name, tpe, err := parseId(recId) 250 if err != nil { 251 return false, err 252 } else { 253 return client.RecordExists(zone, name, tpe) 254 } 255 } 256 257 // Creates new record with single content entry 258 func (client *Client) CreateRecord(zone string, record Record) (string, error) { 259 reqBody, _ := json.Marshal(zonePatchRequest{ 260 RecordSets: []ResourceRecordSet{ 261 { 262 Name: record.Name, 263 Type: record.Type, 264 ChangeType: "REPLACE", 265 Records: []Record{record}, 266 }, 267 }, 268 }) 269 270 req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) 271 if err != nil { 272 return "", err 273 } 274 275 resp, err := client.Http.Do(req) 276 if err != nil { 277 return "", err 278 } 279 defer resp.Body.Close() 280 281 if resp.StatusCode != 200 && resp.StatusCode != 204 { 282 errorResp := new(errorResponse) 283 if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { 284 return "", fmt.Errorf("Error creating record: %s", record.Id()) 285 } else { 286 return "", fmt.Errorf("Error creating record: %s, reason: %q", record.Id(), errorResp.ErrorMsg) 287 } 288 } else { 289 return record.Id(), nil 290 } 291 } 292 293 // Creates new record set in Zone 294 func (client *Client) ReplaceRecordSet(zone string, rrSet ResourceRecordSet) (string, error) { 295 rrSet.ChangeType = "REPLACE" 296 297 reqBody, _ := json.Marshal(zonePatchRequest{ 298 RecordSets: []ResourceRecordSet{rrSet}, 299 }) 300 301 req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) 302 if err != nil { 303 return "", err 304 } 305 306 resp, err := client.Http.Do(req) 307 if err != nil { 308 return "", err 309 } 310 defer resp.Body.Close() 311 312 if resp.StatusCode != 200 && resp.StatusCode != 204 { 313 errorResp := new(errorResponse) 314 if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { 315 return "", fmt.Errorf("Error creating record set: %s", rrSet.Id()) 316 } else { 317 return "", fmt.Errorf("Error creating record set: %s, reason: %q", rrSet.Id(), errorResp.ErrorMsg) 318 } 319 } else { 320 return rrSet.Id(), nil 321 } 322 } 323 324 // Deletes record set from Zone 325 func (client *Client) DeleteRecordSet(zone string, name string, tpe string) error { 326 reqBody, _ := json.Marshal(zonePatchRequest{ 327 RecordSets: []ResourceRecordSet{ 328 { 329 Name: name, 330 Type: tpe, 331 ChangeType: "DELETE", 332 }, 333 }, 334 }) 335 336 req, err := client.newRequest("PATCH", fmt.Sprintf("/servers/localhost/zones/%s", zone), reqBody) 337 if err != nil { 338 return err 339 } 340 341 resp, err := client.Http.Do(req) 342 if err != nil { 343 return err 344 } 345 defer resp.Body.Close() 346 347 if resp.StatusCode != 200 && resp.StatusCode != 204 { 348 errorResp := new(errorResponse) 349 if err = json.NewDecoder(resp.Body).Decode(errorResp); err != nil { 350 return fmt.Errorf("Error deleting record: %s %s", name, tpe) 351 } else { 352 return fmt.Errorf("Error deleting record: %s %s, reason: %q", name, tpe, errorResp.ErrorMsg) 353 } 354 } else { 355 return nil 356 } 357 } 358 359 // Deletes record from Zone by it's ID 360 func (client *Client) DeleteRecordSetByID(zone string, recId string) error { 361 name, tpe, err := parseId(recId) 362 if err != nil { 363 return err 364 } else { 365 return client.DeleteRecordSet(zone, name, tpe) 366 } 367 }