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