1package godo
2
3import (
4	"context"
5	"fmt"
6	"net/http"
7)
8
9const domainsBasePath = "v2/domains"
10
11// DomainsService is an interface for managing DNS with the DigitalOcean API.
12// See: https://developers.digitalocean.com/documentation/v2#domains and
13// https://developers.digitalocean.com/documentation/v2#domain-records
14type DomainsService interface {
15	List(context.Context, *ListOptions) ([]Domain, *Response, error)
16	Get(context.Context, string) (*Domain, *Response, error)
17	Create(context.Context, *DomainCreateRequest) (*Domain, *Response, error)
18	Delete(context.Context, string) (*Response, error)
19
20	Records(context.Context, string, *ListOptions) ([]DomainRecord, *Response, error)
21	Record(context.Context, string, int) (*DomainRecord, *Response, error)
22	DeleteRecord(context.Context, string, int) (*Response, error)
23	EditRecord(context.Context, string, int, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
24	CreateRecord(context.Context, string, *DomainRecordEditRequest) (*DomainRecord, *Response, error)
25}
26
27// DomainsServiceOp handles communication with the domain related methods of the
28// DigitalOcean API.
29type DomainsServiceOp struct {
30	client *Client
31}
32
33var _ DomainsService = &DomainsServiceOp{}
34
35// Domain represents a DigitalOcean domain
36type Domain struct {
37	Name     string `json:"name"`
38	TTL      int    `json:"ttl"`
39	ZoneFile string `json:"zone_file"`
40}
41
42// domainRoot represents a response from the DigitalOcean API
43type domainRoot struct {
44	Domain *Domain `json:"domain"`
45}
46
47type domainsRoot struct {
48	Domains []Domain `json:"domains"`
49	Links   *Links   `json:"links"`
50}
51
52// DomainCreateRequest respresents a request to create a domain.
53type DomainCreateRequest struct {
54	Name      string `json:"name"`
55	IPAddress string `json:"ip_address,omitempty"`
56}
57
58// DomainRecordRoot is the root of an individual Domain Record response
59type domainRecordRoot struct {
60	DomainRecord *DomainRecord `json:"domain_record"`
61}
62
63// DomainRecordsRoot is the root of a group of Domain Record responses
64type domainRecordsRoot struct {
65	DomainRecords []DomainRecord `json:"domain_records"`
66	Links         *Links         `json:"links"`
67}
68
69// DomainRecord represents a DigitalOcean DomainRecord
70type DomainRecord struct {
71	ID       int    `json:"id,float64,omitempty"`
72	Type     string `json:"type,omitempty"`
73	Name     string `json:"name,omitempty"`
74	Data     string `json:"data,omitempty"`
75	Priority int    `json:"priority"`
76	Port     int    `json:"port,omitempty"`
77	TTL      int    `json:"ttl,omitempty"`
78	Weight   int    `json:"weight"`
79	Flags    int    `json:"flags"`
80	Tag      string `json:"tag,omitempty"`
81}
82
83// DomainRecordEditRequest represents a request to update a domain record.
84type DomainRecordEditRequest struct {
85	Type     string `json:"type,omitempty"`
86	Name     string `json:"name,omitempty"`
87	Data     string `json:"data,omitempty"`
88	Priority int    `json:"priority"`
89	Port     int    `json:"port,omitempty"`
90	TTL      int    `json:"ttl,omitempty"`
91	Weight   int    `json:"weight"`
92	Flags    int    `json:"flags"`
93	Tag      string `json:"tag,omitempty"`
94}
95
96func (d Domain) String() string {
97	return Stringify(d)
98}
99
100func (d Domain) URN() string {
101	return ToURN("Domain", d.Name)
102}
103
104// List all domains.
105func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
106	path := domainsBasePath
107	path, err := addOptions(path, opt)
108	if err != nil {
109		return nil, nil, err
110	}
111
112	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
113	if err != nil {
114		return nil, nil, err
115	}
116
117	root := new(domainsRoot)
118	resp, err := s.client.Do(ctx, req, root)
119	if err != nil {
120		return nil, resp, err
121	}
122	if l := root.Links; l != nil {
123		resp.Links = l
124	}
125
126	return root.Domains, resp, err
127}
128
129// Get individual domain. It requires a non-empty domain name.
130func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Response, error) {
131	if len(name) < 1 {
132		return nil, nil, NewArgError("name", "cannot be an empty string")
133	}
134
135	path := fmt.Sprintf("%s/%s", domainsBasePath, name)
136
137	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
138	if err != nil {
139		return nil, nil, err
140	}
141
142	root := new(domainRoot)
143	resp, err := s.client.Do(ctx, req, root)
144	if err != nil {
145		return nil, resp, err
146	}
147
148	return root.Domain, resp, err
149}
150
151// Create a new domain
152func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCreateRequest) (*Domain, *Response, error) {
153	if createRequest == nil {
154		return nil, nil, NewArgError("createRequest", "cannot be nil")
155	}
156
157	path := domainsBasePath
158
159	req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
160	if err != nil {
161		return nil, nil, err
162	}
163
164	root := new(domainRoot)
165	resp, err := s.client.Do(ctx, req, root)
166	if err != nil {
167		return nil, resp, err
168	}
169	return root.Domain, resp, err
170}
171
172// Delete domain
173func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response, error) {
174	if len(name) < 1 {
175		return nil, NewArgError("name", "cannot be an empty string")
176	}
177
178	path := fmt.Sprintf("%s/%s", domainsBasePath, name)
179
180	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
181	if err != nil {
182		return nil, err
183	}
184
185	resp, err := s.client.Do(ctx, req, nil)
186
187	return resp, err
188}
189
190// Converts a DomainRecord to a string.
191func (d DomainRecord) String() string {
192	return Stringify(d)
193}
194
195// Converts a DomainRecordEditRequest to a string.
196func (d DomainRecordEditRequest) String() string {
197	return Stringify(d)
198}
199
200// Records returns a slice of DomainRecords for a domain
201func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *ListOptions) ([]DomainRecord, *Response, error) {
202	if len(domain) < 1 {
203		return nil, nil, NewArgError("domain", "cannot be an empty string")
204	}
205
206	path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
207	path, err := addOptions(path, opt)
208	if err != nil {
209		return nil, nil, err
210	}
211
212	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
213	if err != nil {
214		return nil, nil, err
215	}
216
217	root := new(domainRecordsRoot)
218	resp, err := s.client.Do(ctx, req, root)
219	if err != nil {
220		return nil, resp, err
221	}
222	if l := root.Links; l != nil {
223		resp.Links = l
224	}
225
226	return root.DomainRecords, resp, err
227}
228
229// Record returns the record id from a domain
230func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*DomainRecord, *Response, error) {
231	if len(domain) < 1 {
232		return nil, nil, NewArgError("domain", "cannot be an empty string")
233	}
234
235	if id < 1 {
236		return nil, nil, NewArgError("id", "cannot be less than 1")
237	}
238
239	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
240
241	req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
242	if err != nil {
243		return nil, nil, err
244	}
245
246	record := new(domainRecordRoot)
247	resp, err := s.client.Do(ctx, req, record)
248	if err != nil {
249		return nil, resp, err
250	}
251
252	return record.DomainRecord, resp, err
253}
254
255// DeleteRecord deletes a record from a domain identified by id
256func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id int) (*Response, error) {
257	if len(domain) < 1 {
258		return nil, NewArgError("domain", "cannot be an empty string")
259	}
260
261	if id < 1 {
262		return nil, NewArgError("id", "cannot be less than 1")
263	}
264
265	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
266
267	req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
268	if err != nil {
269		return nil, err
270	}
271
272	resp, err := s.client.Do(ctx, req, nil)
273
274	return resp, err
275}
276
277// EditRecord edits a record using a DomainRecordEditRequest
278func (s *DomainsServiceOp) EditRecord(ctx context.Context,
279	domain string,
280	id int,
281	editRequest *DomainRecordEditRequest,
282) (*DomainRecord, *Response, error) {
283	if len(domain) < 1 {
284		return nil, nil, NewArgError("domain", "cannot be an empty string")
285	}
286
287	if id < 1 {
288		return nil, nil, NewArgError("id", "cannot be less than 1")
289	}
290
291	if editRequest == nil {
292		return nil, nil, NewArgError("editRequest", "cannot be nil")
293	}
294
295	path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
296
297	req, err := s.client.NewRequest(ctx, http.MethodPut, path, editRequest)
298	if err != nil {
299		return nil, nil, err
300	}
301
302	root := new(domainRecordRoot)
303	resp, err := s.client.Do(ctx, req, root)
304	if err != nil {
305		return nil, resp, err
306	}
307
308	return root.DomainRecord, resp, err
309}
310
311// CreateRecord creates a record using a DomainRecordEditRequest
312func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
313	domain string,
314	createRequest *DomainRecordEditRequest) (*DomainRecord, *Response, error) {
315	if len(domain) < 1 {
316		return nil, nil, NewArgError("domain", "cannot be empty string")
317	}
318
319	if createRequest == nil {
320		return nil, nil, NewArgError("createRequest", "cannot be nil")
321	}
322
323	path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
324	req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
325
326	if err != nil {
327		return nil, nil, err
328	}
329
330	d := new(domainRecordRoot)
331	resp, err := s.client.Do(ctx, req, d)
332	if err != nil {
333		return nil, resp, err
334	}
335
336	return d.DomainRecord, resp, err
337}
338