1package internal 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "net/http" 9) 10 11const ( 12 defaultEndpoint = "https://api.scaleway.com/domain/v2alpha2" 13 uriUpdateRecords = "/dns-zones/%s/records" 14 operationSet = "set" 15 operationDelete = "delete" 16 operationAdd = "add" 17) 18 19// APIError represents an error response from the API. 20type APIError struct { 21 Message string `json:"message"` 22} 23 24func (a APIError) Error() string { 25 return a.Message 26} 27 28// Record represents a DNS record. 29type Record struct { 30 Data string `json:"data,omitempty"` 31 Name string `json:"name,omitempty"` 32 Priority uint32 `json:"priority,omitempty"` 33 TTL uint32 `json:"ttl,omitempty"` 34 Type string `json:"type,omitempty"` 35 Comment string `json:"comment,omitempty"` 36} 37 38// RecordChangeAdd represents a list of add operations. 39type RecordChangeAdd struct { 40 Records []*Record `json:"records,omitempty"` 41} 42 43// RecordChangeSet represents a list of set operations. 44type RecordChangeSet struct { 45 Data string `json:"data,omitempty"` 46 Name string `json:"name,omitempty"` 47 TTL uint32 `json:"ttl,omitempty"` 48 Type string `json:"type,omitempty"` 49 Records []*Record `json:"records,omitempty"` 50} 51 52// RecordChangeDelete represents a list of delete operations. 53type RecordChangeDelete struct { 54 Data string `json:"data,omitempty"` 55 Name string `json:"name,omitempty"` 56 Type string `json:"type,omitempty"` 57} 58 59// UpdateDNSZoneRecordsRequest represents a request to update DNS records on the API. 60type UpdateDNSZoneRecordsRequest struct { 61 DNSZone string `json:"dns_zone,omitempty"` 62 Changes []interface{} `json:"changes,omitempty"` 63 ReturnAllRecords bool `json:"return_all_records,omitempty"` 64} 65 66// ClientOpts represents options to init client. 67type ClientOpts struct { 68 BaseURL string 69 Token string 70} 71 72// Client represents DNS client. 73type Client struct { 74 baseURL string 75 token string 76 httpClient *http.Client 77} 78 79// NewClient returns a client instance. 80func NewClient(opts ClientOpts, httpClient *http.Client) *Client { 81 baseURL := defaultEndpoint 82 if opts.BaseURL != "" { 83 baseURL = opts.BaseURL 84 } 85 86 if httpClient == nil { 87 httpClient = &http.Client{} 88 } 89 90 return &Client{ 91 token: opts.Token, 92 baseURL: baseURL, 93 httpClient: httpClient, 94 } 95} 96 97// AddRecord adds Record for given zone. 98func (c *Client) AddRecord(zone string, record Record) error { 99 changes := map[string]RecordChangeAdd{ 100 operationAdd: { 101 Records: []*Record{&record}, 102 }, 103 } 104 105 request := UpdateDNSZoneRecordsRequest{ 106 DNSZone: zone, 107 Changes: []interface{}{changes}, 108 ReturnAllRecords: false, 109 } 110 111 uri := fmt.Sprintf(uriUpdateRecords, zone) 112 req, err := c.newRequest(http.MethodPatch, uri, request) 113 if err != nil { 114 return err 115 } 116 117 return c.do(req) 118} 119 120// SetRecord sets a unique Record for given zone. 121func (c *Client) SetRecord(zone string, record Record) error { 122 changes := map[string]RecordChangeSet{ 123 operationSet: { 124 Name: record.Name, 125 Type: record.Type, 126 Records: []*Record{&record}, 127 }, 128 } 129 130 request := UpdateDNSZoneRecordsRequest{ 131 DNSZone: zone, 132 Changes: []interface{}{changes}, 133 ReturnAllRecords: false, 134 } 135 136 uri := fmt.Sprintf(uriUpdateRecords, zone) 137 req, err := c.newRequest(http.MethodPatch, uri, request) 138 if err != nil { 139 return err 140 } 141 142 return c.do(req) 143} 144 145// DeleteRecord deletes a Record for given zone. 146func (c *Client) DeleteRecord(zone string, record Record) error { 147 delRecord := map[string]RecordChangeDelete{ 148 operationDelete: { 149 Name: record.Name, 150 Type: record.Type, 151 Data: record.Data, 152 }, 153 } 154 155 request := UpdateDNSZoneRecordsRequest{ 156 DNSZone: zone, 157 Changes: []interface{}{delRecord}, 158 ReturnAllRecords: false, 159 } 160 161 uri := fmt.Sprintf(uriUpdateRecords, zone) 162 req, err := c.newRequest(http.MethodPatch, uri, request) 163 if err != nil { 164 return err 165 } 166 167 return c.do(req) 168} 169 170func (c *Client) newRequest(method, uri string, body interface{}) (*http.Request, error) { 171 buf := new(bytes.Buffer) 172 173 if body != nil { 174 err := json.NewEncoder(buf).Encode(body) 175 if err != nil { 176 return nil, fmt.Errorf("failed to encode request body with error: %w", err) 177 } 178 } 179 180 req, err := http.NewRequest(method, c.baseURL+uri, buf) 181 if err != nil { 182 return nil, fmt.Errorf("failed to create new http request with error: %w", err) 183 } 184 185 req.Header.Add("X-auth-token", c.token) 186 req.Header.Add("Content-Type", "application/json") 187 req.Header.Add("Accept", "application/json") 188 189 return req, nil 190} 191 192func (c *Client) do(req *http.Request) error { 193 resp, err := c.httpClient.Do(req) 194 if err != nil { 195 return fmt.Errorf("request failed with error: %w", err) 196 } 197 198 err = checkResponse(resp) 199 if err != nil { 200 return err 201 } 202 203 return checkResponse(resp) 204} 205 206func checkResponse(resp *http.Response) error { 207 if resp.StatusCode >= http.StatusBadRequest || resp.StatusCode < http.StatusOK { 208 if resp.Body == nil { 209 return fmt.Errorf("request failed with status code %d and empty body", resp.StatusCode) 210 } 211 212 body, err := ioutil.ReadAll(resp.Body) 213 if err != nil { 214 return err 215 } 216 defer resp.Body.Close() 217 218 apiError := APIError{} 219 err = json.Unmarshal(body, &apiError) 220 if err != nil { 221 return fmt.Errorf("request failed with status code %d, response body: %s", resp.StatusCode, string(body)) 222 } 223 224 return fmt.Errorf("request failed with status code %d: %w", resp.StatusCode, apiError) 225 } 226 227 return nil 228} 229