github.com/teknogeek/dnscontrol/v2@v2.10.1-0.20200227202244-ae299b55ba42/providers/linode/api.go (about) 1 package linode 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 ) 11 12 const ( 13 mediaType = "application/json" 14 defaultBaseURL = "https://api.linode.com/v4/" 15 domainsPath = "domains" 16 ) 17 18 func (c *LinodeApi) fetchDomainList() error { 19 c.domainIndex = map[string]int{} 20 page := 1 21 for { 22 dr := &domainResponse{} 23 endpoint := fmt.Sprintf("%s?page=%d", domainsPath, page) 24 if err := c.get(endpoint, dr); err != nil { 25 return fmt.Errorf("Error fetching domain list from Linode: %s", err) 26 } 27 for _, domain := range dr.Data { 28 c.domainIndex[domain.Domain] = domain.ID 29 } 30 if len(dr.Data) == 0 || dr.Page >= dr.Pages { 31 break 32 } 33 page++ 34 } 35 return nil 36 } 37 38 func (c *LinodeApi) getRecords(id int) ([]domainRecord, error) { 39 records := []domainRecord{} 40 page := 1 41 for { 42 dr := &recordResponse{} 43 endpoint := fmt.Sprintf("%s/%d/records?page=%d", domainsPath, id, page) 44 if err := c.get(endpoint, dr); err != nil { 45 return nil, fmt.Errorf("Error fetching record list from Linode: %s", err) 46 } 47 48 records = append(records, dr.Data...) 49 50 if len(dr.Data) == 0 || dr.Page >= dr.Pages { 51 break 52 } 53 page++ 54 } 55 56 return records, nil 57 } 58 59 func (c *LinodeApi) createRecord(domainID int, rec *recordEditRequest) (*domainRecord, error) { 60 endpoint := fmt.Sprintf("%s/%d/records", domainsPath, domainID) 61 62 req, err := c.newRequest(http.MethodPost, endpoint, rec) 63 if err != nil { 64 return nil, err 65 } 66 67 resp, err := c.client.Do(req) 68 if err != nil { 69 return nil, err 70 } 71 72 if resp.StatusCode != http.StatusOK { 73 return nil, c.handleErrors(resp) 74 } 75 76 record := &domainRecord{} 77 78 defer resp.Body.Close() 79 decoder := json.NewDecoder(resp.Body) 80 if err := decoder.Decode(record); err != nil { 81 return nil, err 82 } 83 84 return record, nil 85 } 86 87 func (c *LinodeApi) modifyRecord(domainID, recordID int, rec *recordEditRequest) error { 88 endpoint := fmt.Sprintf("%s/%d/records/%d", domainsPath, domainID, recordID) 89 90 req, err := c.newRequest(http.MethodPut, endpoint, rec) 91 if err != nil { 92 return err 93 } 94 95 resp, err := c.client.Do(req) 96 if err != nil { 97 return err 98 } 99 100 if resp.StatusCode != http.StatusOK { 101 return c.handleErrors(resp) 102 } 103 104 return nil 105 } 106 107 func (c *LinodeApi) deleteRecord(domainID, recordID int) error { 108 endpoint := fmt.Sprintf("%s/%d/records/%d", domainsPath, domainID, recordID) 109 req, err := c.newRequest(http.MethodDelete, endpoint, nil) 110 if err != nil { 111 return err 112 } 113 114 resp, err := c.client.Do(req) 115 if err != nil { 116 return err 117 } 118 119 if resp.StatusCode != http.StatusOK { 120 return c.handleErrors(resp) 121 } 122 123 return nil 124 } 125 126 func (c *LinodeApi) newRequest(method, endpoint string, body interface{}) (*http.Request, error) { 127 rel, err := url.Parse(endpoint) 128 if err != nil { 129 return nil, err 130 } 131 132 u := c.baseURL.ResolveReference(rel) 133 134 buf := new(bytes.Buffer) 135 if body != nil { 136 err = json.NewEncoder(buf).Encode(body) 137 if err != nil { 138 return nil, err 139 } 140 } 141 142 req, err := http.NewRequest(method, u.String(), buf) 143 if err != nil { 144 return nil, err 145 } 146 147 req.Header.Add("Content-Type", mediaType) 148 req.Header.Add("Accept", mediaType) 149 return req, nil 150 } 151 152 func (c *LinodeApi) get(endpoint string, target interface{}) error { 153 req, err := c.newRequest(http.MethodGet, endpoint, nil) 154 if err != nil { 155 return err 156 } 157 resp, err := c.client.Do(req) 158 if err != nil { 159 return err 160 } 161 if resp.StatusCode != http.StatusOK { 162 return c.handleErrors(resp) 163 } 164 defer resp.Body.Close() 165 decoder := json.NewDecoder(resp.Body) 166 return decoder.Decode(target) 167 } 168 169 func (c *LinodeApi) handleErrors(resp *http.Response) error { 170 defer resp.Body.Close() 171 decoder := json.NewDecoder(resp.Body) 172 173 errs := &errorResponse{} 174 175 if err := decoder.Decode(errs); err != nil { 176 return fmt.Errorf("bad status code from Linode: %d not 200. Failed to decode response", resp.StatusCode) 177 } 178 179 buf := bytes.NewBufferString(fmt.Sprintf("bad status code from Linode: %d not 200", resp.StatusCode)) 180 181 for _, err := range errs.Errors { 182 buf.WriteString("\n- ") 183 184 if err.Field != "" { 185 buf.WriteString(err.Field) 186 buf.WriteString(": ") 187 } 188 189 buf.WriteString(err.Reason) 190 } 191 192 return errors.New(buf.String()) 193 } 194 195 type basicResponse struct { 196 Results int `json:"results"` 197 Pages int `json:"pages"` 198 Page int `json:"page"` 199 } 200 201 type domainResponse struct { 202 basicResponse 203 Data []struct { 204 ID int `json:"id"` 205 Domain string `json:"domain"` 206 } `json:"data"` 207 } 208 209 type recordResponse struct { 210 basicResponse 211 Data []domainRecord `json:"data"` 212 } 213 214 type domainRecord struct { 215 ID int `json:"id"` 216 Type string `json:"type"` 217 Name string `json:"name"` 218 Target string `json:"target"` 219 Priority uint16 `json:"priority"` 220 Weight uint16 `json:"weight"` 221 Port uint16 `json:"port"` 222 Service string `json:"service"` 223 Protocol string `json:"protocol"` 224 TTLSec uint32 `json:"ttl_sec"` 225 } 226 227 type recordEditRequest struct { 228 Type string `json:"type,omitempty"` 229 Name string `json:"name,omitempty"` 230 Target string `json:"target,omitempty"` 231 Priority int `json:"priority,omitempty"` 232 Weight int `json:"weight,omitempty"` 233 Port int `json:"port,omitempty"` 234 Service string `json:"service,omitempty"` 235 Protocol string `json:"protocol,omitempty"` 236 // Documented as field `ttl` in the documentation, but in reality `ttl_sec` should be used 237 TTL int `json:"ttl_sec,omitempty"` 238 } 239 240 type errorResponse struct { 241 Errors []struct { 242 Field string `json:"field"` 243 Reason string `json:"reason"` 244 } `json:"errors"` 245 }