client.go raw
1 package internal
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "net/http"
11 "net/url"
12 "time"
13
14 "github.com/go-acme/lego/v4/providers/dns/internal/errutils"
15 )
16
17 const dnsServiceBaseURL = "https://dns-service.%s.conoha.io"
18
19 // Client is a ConoHa API client.
20 type Client struct {
21 token string
22
23 baseURL *url.URL
24 HTTPClient *http.Client
25 }
26
27 // NewClient returns a client instance logged into the ConoHa service.
28 func NewClient(region, token string) (*Client, error) {
29 baseURL, err := url.Parse(fmt.Sprintf(dnsServiceBaseURL, region))
30 if err != nil {
31 return nil, err
32 }
33
34 return &Client{
35 token: token,
36 baseURL: baseURL,
37 HTTPClient: &http.Client{Timeout: 5 * time.Second},
38 }, nil
39 }
40
41 // GetDomainID returns an ID of specified domain.
42 func (c *Client) GetDomainID(ctx context.Context, domainName string) (string, error) {
43 domainList, err := c.getDomains(ctx)
44 if err != nil {
45 return "", err
46 }
47
48 for _, domain := range domainList.Domains {
49 if domain.Name == domainName {
50 return domain.ID, nil
51 }
52 }
53
54 return "", fmt.Errorf("no such domain: %s", domainName)
55 }
56
57 // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-domains-v2/?btn_id=reference-api-vps2--sidebar_reference-paas-dns-list-domains-v2
58 func (c *Client) getDomains(ctx context.Context) (*DomainListResponse, error) {
59 endpoint := c.baseURL.JoinPath("v1", "domains")
60
61 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
62 if err != nil {
63 return nil, err
64 }
65
66 domainList := &DomainListResponse{}
67
68 err = c.do(req, domainList)
69 if err != nil {
70 return nil, err
71 }
72
73 return domainList, nil
74 }
75
76 // GetRecordID returns an ID of specified record.
77 func (c *Client) GetRecordID(ctx context.Context, domainID, recordName, recordType, data string) (string, error) {
78 recordList, err := c.getRecords(ctx, domainID)
79 if err != nil {
80 return "", err
81 }
82
83 for _, record := range recordList.Records {
84 if record.Name == recordName && record.Type == recordType && record.Data == data {
85 return record.ID, nil
86 }
87 }
88
89 return "", errors.New("no such record")
90 }
91
92 // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-list-records-in-a-domain-v2/?btn_id=reference-paas-dns-list-domains-v2--sidebar_reference-paas-dns-list-records-in-a-domain-v2
93 func (c *Client) getRecords(ctx context.Context, domainID string) (*RecordListResponse, error) {
94 endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records")
95
96 req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil)
97 if err != nil {
98 return nil, err
99 }
100
101 recordList := &RecordListResponse{}
102
103 err = c.do(req, recordList)
104 if err != nil {
105 return nil, err
106 }
107
108 return recordList, nil
109 }
110
111 // CreateRecord adds new record.
112 func (c *Client) CreateRecord(ctx context.Context, domainID string, record Record) error {
113 _, err := c.createRecord(ctx, domainID, record)
114 return err
115 }
116
117 // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-create-record-v2/?btn_id=reference-paas-dns-list-records-in-a-domain-v2--sidebar_reference-paas-dns-create-record-v2
118 func (c *Client) createRecord(ctx context.Context, domainID string, record Record) (*Record, error) {
119 endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records")
120
121 req, err := newJSONRequest(ctx, http.MethodPost, endpoint, record)
122 if err != nil {
123 return nil, err
124 }
125
126 newRecord := &Record{}
127
128 err = c.do(req, newRecord)
129 if err != nil {
130 return nil, err
131 }
132
133 return newRecord, nil
134 }
135
136 // DeleteRecord removes specified record.
137 // https://doc.conoha.jp/reference/api-vps2/api-dns-vps2/paas-dns-delete-a-record-v2/?btn_id=reference-paas-dns-create-record-v2--sidebar_reference-paas-dns-delete-a-record-v2
138 func (c *Client) DeleteRecord(ctx context.Context, domainID, recordID string) error {
139 endpoint := c.baseURL.JoinPath("v1", "domains", domainID, "records", recordID)
140
141 req, err := newJSONRequest(ctx, http.MethodDelete, endpoint, nil)
142 if err != nil {
143 return err
144 }
145
146 return c.do(req, nil)
147 }
148
149 func (c *Client) do(req *http.Request, result any) error {
150 if c.token != "" {
151 req.Header.Set("X-Auth-Token", c.token)
152 }
153
154 resp, err := c.HTTPClient.Do(req)
155 if err != nil {
156 return errutils.NewHTTPDoError(req, err)
157 }
158
159 defer func() { _ = resp.Body.Close() }()
160
161 if resp.StatusCode != http.StatusOK {
162 return errutils.NewUnexpectedResponseStatusCodeError(req, resp)
163 }
164
165 if result == nil {
166 return nil
167 }
168
169 raw, err := io.ReadAll(resp.Body)
170 if err != nil {
171 return errutils.NewReadResponseError(req, resp.StatusCode, err)
172 }
173
174 err = json.Unmarshal(raw, result)
175 if err != nil {
176 return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err)
177 }
178
179 return nil
180 }
181
182 func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) {
183 buf := new(bytes.Buffer)
184
185 if payload != nil {
186 err := json.NewEncoder(buf).Encode(payload)
187 if err != nil {
188 return nil, fmt.Errorf("failed to create request JSON body: %w", err)
189 }
190 }
191
192 req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf)
193 if err != nil {
194 return nil, fmt.Errorf("unable to create request: %w", err)
195 }
196
197 req.Header.Set("Accept", "application/json")
198
199 if payload != nil {
200 req.Header.Set("Content-Type", "application/json")
201 }
202
203 return req, nil
204 }
205