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