mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 06:46:19 +08:00
NEW PROVIDER: Sakura Cloud (SAKURACLOUD) (#3086)
Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
parent
e8eca6a31e
commit
864d45290f
|
@ -39,7 +39,7 @@ changelog:
|
|||
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
|
||||
order: 1
|
||||
- title: 'Provider-specific changes:'
|
||||
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|huaweicloud|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|softlayer|transip|vultr).*:)+.*"
|
||||
regexp: "(?i)((akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|gandi|gcloud|gcore|hedns|hetzner|hexonet|hostingde|huaweicloud|inwx|linode|loopia|luadns|msdns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vultr).*:)+.*"
|
||||
order: 2
|
||||
- title: 'Documentation:'
|
||||
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
||||
|
|
1
OWNERS
1
OWNERS
|
@ -46,6 +46,7 @@ providers/powerdns @jpbede
|
|||
providers/realtimeregister @PJEilers
|
||||
providers/route53 @tresni
|
||||
providers/rwth @mistererwin
|
||||
providers/sakuracloud @ttkzw
|
||||
# providers/softlayer NEEDS VOLUNTEER
|
||||
providers/transip @blackshadev
|
||||
providers/vultr @pgaskin
|
||||
|
|
|
@ -57,6 +57,7 @@ Currently supported DNS providers:
|
|||
- PowerDNS
|
||||
- Realtime Register
|
||||
- RWTH DNS-Admin
|
||||
- Sakura Cloud
|
||||
- SoftLayer
|
||||
- TransIP
|
||||
- Vultr
|
||||
|
|
|
@ -150,6 +150,7 @@
|
|||
* [PowerDNS](provider/powerdns.md)
|
||||
* [Realtime Register](provider/realtimeregister.md)
|
||||
* [RWTH DNS-Admin](provider/rwth.md)
|
||||
* [Sakura Cloud](provider/sakuracloud.md)
|
||||
* [SoftLayer DNS](provider/softlayer.md)
|
||||
* [TransIP](provider/transip.md)
|
||||
* [Vultr](provider/vultr.md)
|
||||
|
|
95
documentation/provider/sakuracloud.md
Normal file
95
documentation/provider/sakuracloud.md
Normal file
|
@ -0,0 +1,95 @@
|
|||
This is the provider for [Sakura Cloud](https://cloud.sakura.ad.jp/).
|
||||
|
||||
## Configuration
|
||||
To use this provider, add an entry to `creds.json` with `TYPE` set to `SAKURACLOUD`
|
||||
along with API credentials.
|
||||
|
||||
Example:
|
||||
|
||||
{% code title="creds.json" %}
|
||||
```json
|
||||
{
|
||||
"sakuracloud": {
|
||||
"TYPE": "SAKURACLOUD",
|
||||
"access_token": "your-access-token",
|
||||
"access_token_secret": "your-access-token-secret"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
The `endpoint` is optional. If omitted, the default endpoint is assumed.
|
||||
|
||||
Endpoints are as follows:
|
||||
|
||||
* `https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1` (Ishikari first Zone)
|
||||
* `https://secure.sakura.ad.jp/cloud/zone/is1b/api/cloud/1.1` (Ishikari second Zone)
|
||||
* `https://secure.sakura.ad.jp/cloud/zone/tk1a/api/cloud/1.1` (Tokyo first Zone)
|
||||
* `https://secure.sakura.ad.jp/cloud/zone/tk1b/api/cloud/1.1` (Tokyo second Zone)
|
||||
|
||||
DNS service is independent of zones, so you can use any of these endpoints.
|
||||
The default is the Ishikari first Zone.
|
||||
|
||||
Alternatively you can also use environment variables.
|
||||
|
||||
```shell
|
||||
export SAKURACLOUD_ACCESS_TOKEN="your-access-token"
|
||||
export SAKURACLOUD_ACCESS_TOKEN_SECRET="your-access-token-secret"
|
||||
```
|
||||
|
||||
{% code title="creds.json" %}
|
||||
```json
|
||||
{
|
||||
"sakuracloud": {
|
||||
"TYPE": "SAKURACLOUD",
|
||||
"access_token": "$SAKURACLOUD_ACCESS_TOKEN",
|
||||
"access_token_secret": "$SAKURACLOUD_ACCESS_TOKEN_SECRET"
|
||||
}
|
||||
}
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
## Metadata
|
||||
This provider does not recognize any special metadata fields unique to
|
||||
Sakura Cloud.
|
||||
|
||||
## Usage
|
||||
An example configuration:
|
||||
|
||||
{% code title="dnsconfig.js" %}
|
||||
```javascript
|
||||
var REG_NONE = NewRegistrar("none");
|
||||
var DSP_SAKURACLOUD = NewDnsProvider("sakuracloud");
|
||||
|
||||
D("example.com", REG_NONE, DnsProvider(DSP_SAKURACLOUD),
|
||||
A("test", "192.0.2.1"),
|
||||
END);
|
||||
```
|
||||
{% endcode %}
|
||||
|
||||
`NAMESERVER` does not need to be set as the name servers for the
|
||||
Sakura Cloud provider cannot be changed.
|
||||
|
||||
`SOA` cannot be set as SOA record of Sakura Cloud provider cannot be changed.
|
||||
|
||||
## Activation
|
||||
Sakura Cloud depends on an [API Key](https://manual.sakura.ad.jp/cloud/api/apikey.html).
|
||||
|
||||
When creating an API key, select "can modify settings" as "Access level".
|
||||
if you plan to create zones, select "can create and delete resources" as
|
||||
"Access level".
|
||||
None of the options in the "Allow access to other services" field need
|
||||
to be checked.
|
||||
|
||||
## Caveats
|
||||
The limitations of the Sakura Cloud DNS service are described in [the DNS manual](https://manual.sakura.ad.jp/cloud/appliance/dns/index.html), which is written in Japanese.
|
||||
|
||||
The limitations not described in that manual are:
|
||||
|
||||
* "Null MX", RFC 7505, is not supported.
|
||||
* SRV records with a Target of "." are not supported.
|
||||
* SRV records with Port "0" are not supported.
|
||||
* CAA records with a property value longer than 64 bytes are not allowed.
|
||||
* Owner names and RDATA targets containing the following labels are not allowed:
|
||||
* example
|
||||
* exampleN, where N is a numerical character
|
|
@ -62,6 +62,7 @@ If a feature is definitively not supported for whatever reason, we would also li
|
|||
| [`REALTIMEREGISTER`](provider/realtimeregister.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
||||
| [`ROUTE53`](provider/route53.md) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
|
||||
| [`RWTH`](provider/rwth.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ |
|
||||
| [`SAKURACLOUD`](provider/sakuracloud.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||
| [`SOFTLAYER`](provider/softlayer.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ |
|
||||
| [`TRANSIP`](provider/transip.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| [`VULTR`](provider/vultr.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ❌ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ |
|
||||
|
@ -149,6 +150,7 @@ Providers in this category and their maintainers are:
|
|||
|[`REALTIMEREGISTER`](provider/realtimeregister.md)|@PJEilers|
|
||||
|[`ROUTE53`](provider/route53.md)|@tresni|
|
||||
|[`RWTH`](provider/rwth.md)|@MisterErwin|
|
||||
|[`SAKURACLOUD`](provider/sakuracloud.md)|@ttkzw|
|
||||
|[`SOFTLAYER`](provider/softlayer.md)|@jamielennox|
|
||||
|[`TRANSIP`](provider/transip.md)|@blackshadev|
|
||||
|[`VULTR`](provider/vultr.md)|@pgaskin|
|
||||
|
|
|
@ -279,6 +279,12 @@
|
|||
"TYPE": "ROUTE53",
|
||||
"domain": "$ROUTE53_DOMAIN"
|
||||
},
|
||||
"SAKURACLOUD": {
|
||||
"TYPE": "SAKURACLOUD",
|
||||
"access_token": "$SAKURACLOUD_ACCESS_TOKEN",
|
||||
"access_token_secret": "$SAKURACLOUD_ACCESS_TOKEN_SECRET",
|
||||
"domain": "$SAKURACLOUD_DOMAIN"
|
||||
},
|
||||
"SOFTLAYER": {
|
||||
"TYPE": "SOFTLAYER",
|
||||
"api_key": "$SL_API_KEY",
|
||||
|
|
|
@ -51,6 +51,7 @@ import (
|
|||
_ "github.com/StackExchange/dnscontrol/v4/providers/realtimeregister"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/route53"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/rwth"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/sakuracloud"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/softlayer"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/transip"
|
||||
_ "github.com/StackExchange/dnscontrol/v4/providers/vultr"
|
||||
|
|
486
providers/sakuracloud/api.go
Normal file
486
providers/sakuracloud/api.go
Normal file
|
@ -0,0 +1,486 @@
|
|||
// NOTE: As the API documentation of Sakura Cloud is written in Japanese
|
||||
// and lacks further explanation, we have described the API data structures
|
||||
// in English in the structure comments.
|
||||
//
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/index.html
|
||||
|
||||
package sakuracloud
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// requestCommonServiceItem is the body structure of the request to create a zone or update zone data.
|
||||
//
|
||||
// Zone creation:
|
||||
//
|
||||
// POST /commonserviceitem
|
||||
//
|
||||
// {
|
||||
// "CommonServiceItem": {
|
||||
// "Name": "example.com",
|
||||
// "Status": {
|
||||
// "Zone": "example.com"
|
||||
// },
|
||||
// "Settings": {
|
||||
// "DNS": {
|
||||
// "ResourceRecordSets": []
|
||||
// }
|
||||
// },
|
||||
// "Provider": {
|
||||
// "Class": "dns"
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Zone update:
|
||||
//
|
||||
// PUT /commonserviceitem/:commonserviceitemid
|
||||
//
|
||||
// {
|
||||
// "CommonServiceItem": {
|
||||
// "Settings": {
|
||||
// "DNS": {
|
||||
// "ResourceRecordSets": [
|
||||
// {
|
||||
// "Name": "a",
|
||||
// "Type": "A",
|
||||
// "RData": "192.0.2.1",
|
||||
// "TTL": 600
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Reference:
|
||||
//
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#post_commonserviceitem
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#put_commonserviceitem_commonserviceitemid
|
||||
type requestCommonServiceItem struct {
|
||||
CommonServiceItem commonServiceItem `json:"CommonServiceItem"`
|
||||
}
|
||||
|
||||
// responseCommonServiceItems is the body structure of the success response to get a list of zones.
|
||||
//
|
||||
// Request:
|
||||
//
|
||||
// GET /commonserviceitem
|
||||
//
|
||||
// Response body structure:
|
||||
//
|
||||
// {
|
||||
// "From": 0,
|
||||
// "Count": 1,
|
||||
// "Total": 1,
|
||||
// "CommonServiceItems": [
|
||||
// {
|
||||
// "Index": 0,
|
||||
// "ID": "999999999999",
|
||||
// "Name": "example.com",
|
||||
// "Description": "",
|
||||
// "Settings": {
|
||||
// "DNS": {
|
||||
// "ResourceRecordSets": [
|
||||
// {
|
||||
// "Name": "a",
|
||||
// "Type": "A",
|
||||
// "RData": "192.0.2.1",
|
||||
// "TTL": 600
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
// },
|
||||
// "SettingsHash": "ffffffffffffffffffffffffffffffff",
|
||||
// "Status": {
|
||||
// "Zone": "example.com",
|
||||
// "NS": [
|
||||
// "ns1.gslbN.sakura.ne.jp",
|
||||
// "ns2.gslbN.sakura.ne.jp"
|
||||
// ]
|
||||
// },
|
||||
// "ServiceClass": "cloud/dns",
|
||||
// "Availability": "available",
|
||||
// "CreatedAt": "2006-01-02T15:04:05+07:00",
|
||||
// "ModifiedAt": "2006-01-02T15:04:05+07:00",
|
||||
// "Provider": {
|
||||
// "ID": 9999999,
|
||||
// "Class": "dns",
|
||||
// "Name": "gslbN.sakura.ne.jp",
|
||||
// "ServiceClass": "cloud/dns"
|
||||
// },
|
||||
// "Icon": null,
|
||||
// "Tags": []
|
||||
// }
|
||||
// ],
|
||||
// "is_ok": true
|
||||
// }
|
||||
//
|
||||
// References:
|
||||
//
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#get_commonserviceitem
|
||||
type responseCommonServiceItems struct {
|
||||
From int `json:"From"`
|
||||
Count int `json:"Count"`
|
||||
Total int `json:"Total"`
|
||||
CommonServiceItems []commonServiceItem `json:"CommonServiceItems"`
|
||||
IsOk bool `json:"is_ok"`
|
||||
}
|
||||
|
||||
// responseCommonServiceItem is the body structure of the success response to get a zone or update zone data.
|
||||
//
|
||||
// Request:
|
||||
//
|
||||
// GET /commonserviceitem/:commonserviceitemid
|
||||
// PUT /commonserviceitem/:commonserviceitemid
|
||||
//
|
||||
// Response body structure:
|
||||
//
|
||||
// {
|
||||
// "CommonServiceItem": {
|
||||
// "ID": "999999999999",
|
||||
// "Name": "example.com",
|
||||
// "Description": "",
|
||||
// "Settings": {
|
||||
// "DNS": {
|
||||
// "ResourceRecordSets": [
|
||||
// {
|
||||
// "Name": "a",
|
||||
// "Type": "A",
|
||||
// "RData": "192.0.2.1",
|
||||
// "TTL": 600
|
||||
// },
|
||||
// ...
|
||||
// ]
|
||||
// }
|
||||
// },
|
||||
// "SettingsHash": "ffffffffffffffffffffffffffffffff",
|
||||
// "Status": {
|
||||
// "Zone": "example.com",
|
||||
// "NS": [
|
||||
// "ns1.gslbN.sakura.ne.jp",
|
||||
// "ns2.gslbN.sakura.ne.jp"
|
||||
// ]
|
||||
// },
|
||||
// "ServiceClass": "cloud/dns",
|
||||
// "Availability": "available",
|
||||
// "CreatedAt": "2006-01-02T15:04:05+07:00",
|
||||
// "ModifiedAt": "2006-01-02T15:04:05+07:00",
|
||||
// "Provider": {
|
||||
// "ID": 9999999,
|
||||
// "Class": "dns",
|
||||
// "Name": "gslbN.sakura.ne.jp",
|
||||
// "ServiceClass": "cloud/dns"
|
||||
// },
|
||||
// "Icon": null,
|
||||
// "Tags": []
|
||||
// },
|
||||
// "Success": true,
|
||||
// "is_ok": true
|
||||
// }
|
||||
//
|
||||
// References:
|
||||
//
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#get_commonserviceitem_commonserviceitemid
|
||||
// - https://manual.sakura.ad.jp/cloud-api/1.1/appliance/#put_commonserviceitem_commonserviceitemid
|
||||
type responseCommonServiceItem struct {
|
||||
CommonServiceItem commonServiceItem `json:"CommonServiceItem"`
|
||||
Success bool `json:"Success"`
|
||||
IsOk bool `json:"is_ok"`
|
||||
}
|
||||
|
||||
// errorResponse is the body structure of an error response.
|
||||
//
|
||||
// Response body structure:
|
||||
//
|
||||
// {
|
||||
// "is_fatal": true,
|
||||
// "serial": "ffffffffffffffffffffffffffffffff",
|
||||
// "status": "401 Unauthorized",
|
||||
// "error_code": "unauthorized",
|
||||
// "error_msg": "error-unauthorized"
|
||||
// }
|
||||
type errorResponse struct {
|
||||
IsFatal bool `json:"is_fatal"`
|
||||
Serial string `json:"serial"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
ErrorMsg string `json:"error_msg"`
|
||||
}
|
||||
|
||||
// commonServiceItem is a resource structure.
|
||||
type commonServiceItem struct {
|
||||
ID string `json:"ID,omitempty"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
Settings settings `json:"Settings"`
|
||||
Status status `json:"Status,omitempty"`
|
||||
ServiceClass string `json:"ServiceClass,omitempty"`
|
||||
Provider provider `json:"Provider,omitempty"`
|
||||
}
|
||||
|
||||
// settings is a resource setting.
|
||||
type settings struct {
|
||||
DNS dNS `json:"DNS"`
|
||||
}
|
||||
|
||||
// dNS is a set of dNS resources.
|
||||
type dNS struct {
|
||||
ResourceRecordSets []domainRecord `json:"ResourceRecordSets"`
|
||||
}
|
||||
|
||||
// domainRecord is a resource record.
|
||||
type domainRecord struct {
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
RData string `json:"RData"`
|
||||
TTL uint32 `json:"TTL,omitempty"`
|
||||
}
|
||||
|
||||
// status is the metadata of a zone.
|
||||
type status struct {
|
||||
Zone string `json:"Zone,omitempty"`
|
||||
NS []string `json:"NS,omitempty"`
|
||||
}
|
||||
|
||||
// provider is the metadata of a service.
|
||||
type provider struct {
|
||||
ID int `json:"ID,omitempty"`
|
||||
Class string `json:"Class"`
|
||||
Name string `json:"Name,omitempty"`
|
||||
ServiceClass string `json:"ServiceClass,omitempty"`
|
||||
}
|
||||
|
||||
// sakuracloudAPI has information about the API of the Sakura Cloud.
|
||||
type sakuracloudAPI struct {
|
||||
accessToken string
|
||||
accessTokenSecret string
|
||||
baseURL url.URL
|
||||
httpClient *http.Client
|
||||
commonServiceItemMap map[string]*commonServiceItem
|
||||
}
|
||||
|
||||
func NewSakuracloudAPI(accessToken, accessTokenSecret, endpoint string) (*sakuracloudAPI, error) {
|
||||
baseURL, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("endpoint_url parse error: %w", err)
|
||||
}
|
||||
|
||||
return &sakuracloudAPI{
|
||||
accessToken: accessToken,
|
||||
accessTokenSecret: accessTokenSecret,
|
||||
baseURL: *baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Minute,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (api *sakuracloudAPI) request(method, path string, data []byte) ([]byte, error) {
|
||||
req, err := http.NewRequest(method, path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.SetBasicAuth(api.accessToken, api.accessTokenSecret)
|
||||
req.Header.Add("Content-Type", "applicaiton/json; charset=UTF-8")
|
||||
resp, err := api.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
var errResp errorResponse
|
||||
err := json.Unmarshal(respBody, &errResp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Since an error_msg uses HTML entities, unescape it.
|
||||
return nil, fmt.Errorf("request failed: status: %s, serial: %s, error_code: %s, error_msg: %s", errResp.Status, errResp.Serial, errResp.ErrorCode, html.UnescapeString(errResp.ErrorMsg))
|
||||
}
|
||||
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
// getCommonServiceItems return all the zones in the account
|
||||
func (api *sakuracloudAPI) getCommonServiceItems() ([]*commonServiceItem, error) {
|
||||
var items []*commonServiceItem
|
||||
|
||||
nextFrom := 0
|
||||
count := 100
|
||||
for {
|
||||
u := api.baseURL.JoinPath("/commonserviceitem")
|
||||
|
||||
if nextFrom > 0 {
|
||||
// The query string is similar to the flow-style YAML.
|
||||
// {From: 0, Count: 10}
|
||||
query := fmt.Sprintf("{From: %d, Count: %d}", nextFrom, count)
|
||||
u.RawQuery = url.QueryEscape(query)
|
||||
}
|
||||
|
||||
respBody, err := api.request(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respData responseCommonServiceItems
|
||||
err = json.Unmarshal(respBody, &respData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if items == nil {
|
||||
items = make([]*commonServiceItem, 0, respData.Total)
|
||||
}
|
||||
|
||||
for _, item := range respData.CommonServiceItems {
|
||||
items = append(items, &item)
|
||||
}
|
||||
|
||||
count = respData.Count
|
||||
nextFrom = respData.From + respData.Count
|
||||
if nextFrom == respData.Total {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetCommonServiceItemMap return all the zones in the account
|
||||
func (api *sakuracloudAPI) GetCommonServiceItemMap() (map[string]*commonServiceItem, error) {
|
||||
if api.commonServiceItemMap != nil {
|
||||
return api.commonServiceItemMap, nil
|
||||
}
|
||||
|
||||
items, err := api.getCommonServiceItems()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.commonServiceItemMap = make(map[string]*commonServiceItem, len(items))
|
||||
for _, item := range items {
|
||||
if item.ServiceClass != "cloud/dns" {
|
||||
continue
|
||||
}
|
||||
api.commonServiceItemMap[item.Status.Zone] = item
|
||||
}
|
||||
|
||||
return api.commonServiceItemMap, nil
|
||||
}
|
||||
|
||||
// postCommonServiceItem submits a CommonServiceItem to the API and create the zone.
|
||||
func (api *sakuracloudAPI) postCommonServiceItem(reqItem requestCommonServiceItem) (*commonServiceItem, error) {
|
||||
reqBody, err := json.Marshal(reqItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := api.baseURL.JoinPath("/commonserviceitem")
|
||||
respBody, err := api.request(http.MethodPost, u.String(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respData responseCommonServiceItem
|
||||
err = json.Unmarshal(respBody, &respData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &respData.CommonServiceItem, nil
|
||||
}
|
||||
|
||||
// CreateZone submits a CommonServiceItem to the API and create the zone.
|
||||
func (api *sakuracloudAPI) CreateZone(domain string) error {
|
||||
reqItem := requestCommonServiceItem{
|
||||
CommonServiceItem: commonServiceItem{
|
||||
Name: domain,
|
||||
Status: status{
|
||||
Zone: domain,
|
||||
},
|
||||
Settings: settings{
|
||||
DNS: dNS{
|
||||
ResourceRecordSets: []domainRecord{},
|
||||
},
|
||||
},
|
||||
Provider: provider{
|
||||
Class: "dns",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
item, err := api.postCommonServiceItem(reqItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.commonServiceItemMap[domain] = item
|
||||
return nil
|
||||
}
|
||||
|
||||
// putCommonServiceItem submits a CommonServiceItem to the API and updates the zone data.
|
||||
func (api *sakuracloudAPI) putCommonServiceItem(id string, reqItem requestCommonServiceItem) (*commonServiceItem, error) {
|
||||
reqBody, err := json.Marshal(reqItem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u := api.baseURL.JoinPath("/commonserviceitem/").JoinPath(id)
|
||||
respBody, err := api.request(http.MethodPut, u.String(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var respData responseCommonServiceItem
|
||||
err = json.Unmarshal(respBody, &respData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &respData.CommonServiceItem, nil
|
||||
}
|
||||
|
||||
// UpdateZone submits a CommonServiceItem to the API and updates the zone data.
|
||||
func (api *sakuracloudAPI) UpdateZone(domain string, domainRecords []domainRecord) error {
|
||||
drs := make([]domainRecord, 0, len(domainRecords)-2) // Removes 2 NS records.
|
||||
for _, r := range domainRecords {
|
||||
if r.Type == "NS" && r.Name == "@" {
|
||||
continue
|
||||
}
|
||||
drs = append(drs, r)
|
||||
}
|
||||
|
||||
reqItem := requestCommonServiceItem{
|
||||
CommonServiceItem: commonServiceItem{
|
||||
Settings: settings{
|
||||
DNS: dNS{
|
||||
ResourceRecordSets: drs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
item, err := api.putCommonServiceItem(api.commonServiceItemMap[domain].ID, reqItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
api.commonServiceItemMap[domain] = item
|
||||
return nil
|
||||
}
|
93
providers/sakuracloud/auditrecords.go
Normal file
93
providers/sakuracloud/auditrecords.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package sakuracloud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// AuditRecords returns a list of errors corresponding to the records
|
||||
// that aren't supported by this provider. If all records are
|
||||
// supported, an empty list is returned.
|
||||
func AuditRecords(records []*models.RecordConfig) []error {
|
||||
a := rejectif.Auditor{}
|
||||
|
||||
a.Add("MX", rejectif.MxNull) // Last verified 2024-08-09
|
||||
|
||||
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2024-08-09
|
||||
|
||||
a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-08-09
|
||||
|
||||
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-08-09
|
||||
|
||||
a.Add("TXT", rejectif.TxtHasUnpairedDoubleQuotes) // Last verified 2024-08-09
|
||||
|
||||
a.Add("TXT", rejectif.TxtLongerThan(500)) // Last verified 2024-08-09
|
||||
|
||||
a.Add("CAA", rejectifCaaLongerThan(64)) // Last verified 2024-08-09
|
||||
|
||||
a.Add("NS", rejectifNsPointsToOrigin) // Last verified 2024-08-09
|
||||
|
||||
for _, t := range []string{"ALIAS", "CNAME", "HTTPS", "MX", "NS", "PTR", "SRV", "SVCB"} {
|
||||
a.Add(t, rejectifTargetHasExample) // Last verified 2024-08-09
|
||||
}
|
||||
|
||||
for _, t := range []string{"A", "AAAA", "ALIAS", "CAA", "CNAME", "HTTPS", "MX", "NS", "PTR", "SRV", "SVCB", "TXT"} {
|
||||
a.Add(t, rejectifLabelHasExample) // Last verified 2024-08-09
|
||||
}
|
||||
return a.Audit(records)
|
||||
}
|
||||
|
||||
// rejectifCaaLongerThan returns a function that audits CAA records
|
||||
// where the length of the property value is greater than maxLength.
|
||||
func rejectifCaaLongerThan(maxLength int) func(rc *models.RecordConfig) error {
|
||||
return func(rc *models.RecordConfig) error {
|
||||
m := maxLength
|
||||
if len(rc.GetTargetField()) > m {
|
||||
return fmt.Errorf("CAA record longer than %d octets (chars)", m)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// rejectifNsPointsToOrigin audits NS records that point to the origin.
|
||||
func rejectifNsPointsToOrigin(rc *models.RecordConfig) error {
|
||||
originFQDN := strings.TrimPrefix(rc.GetLabelFQDN(), rc.GetLabel()+".") + "."
|
||||
if originFQDN == rc.GetTargetField() {
|
||||
return fmt.Errorf("NS record points to the origin: %s", rc.GetTargetField())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var labelExampleRe = regexp.MustCompile(`^example[0-9]?$`)
|
||||
|
||||
func hasLabelExample(domain string) error {
|
||||
for _, l := range dns.SplitDomainName(domain) {
|
||||
if labelExampleRe.MatchString(l) {
|
||||
return fmt.Errorf("label contains `example`: %s", domain)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// rejectifTargetHasExample returns a function that audits RDATA targets
|
||||
// containing the following labels:
|
||||
//
|
||||
// - example
|
||||
// - exampleN, where N is a numerical character
|
||||
func rejectifTargetHasExample(rc *models.RecordConfig) error {
|
||||
return hasLabelExample(rc.GetTargetField())
|
||||
}
|
||||
|
||||
// rejectifLabelHasExample returns a function that audits owner names
|
||||
// containing the following labels:
|
||||
//
|
||||
// - example
|
||||
// - exampleN, where N is a numerical character
|
||||
func rejectifLabelHasExample(rc *models.RecordConfig) error {
|
||||
return hasLabelExample(rc.GetLabel())
|
||||
}
|
47
providers/sakuracloud/convert.go
Normal file
47
providers/sakuracloud/convert.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package sakuracloud
|
||||
|
||||
import (
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
)
|
||||
|
||||
const defaultTTL = uint32(3600)
|
||||
|
||||
func toRc(domain string, r domainRecord) *models.RecordConfig {
|
||||
rc := &models.RecordConfig{
|
||||
Type: r.Type,
|
||||
TTL: r.TTL,
|
||||
Original: r,
|
||||
}
|
||||
if r.TTL == 0 {
|
||||
rc.TTL = defaultTTL
|
||||
}
|
||||
|
||||
rc.SetLabel(r.Name, domain)
|
||||
|
||||
switch r.Type {
|
||||
case "TXT":
|
||||
// TXT records are stored verbatim; no quoting/escaping to parse.
|
||||
rc.SetTargetTXT(r.RData)
|
||||
default:
|
||||
rc.PopulateFromString(r.Type, r.RData, domain)
|
||||
}
|
||||
|
||||
return rc
|
||||
}
|
||||
|
||||
func toNative(rc *models.RecordConfig) domainRecord {
|
||||
rr := domainRecord{
|
||||
Name: rc.GetLabel(),
|
||||
Type: rc.Type,
|
||||
RData: rc.String(),
|
||||
}
|
||||
if rc.TTL != defaultTTL {
|
||||
rr.TTL = rc.TTL
|
||||
}
|
||||
|
||||
switch rc.Type {
|
||||
case "TXT":
|
||||
rr.RData = rc.GetTargetTXTJoined()
|
||||
}
|
||||
return rr
|
||||
}
|
32
providers/sakuracloud/listzones.go
Normal file
32
providers/sakuracloud/listzones.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package sakuracloud
|
||||
|
||||
import "github.com/StackExchange/dnscontrol/v4/pkg/printer"
|
||||
|
||||
// ListZones return all the zones in the account
|
||||
func (s *sakuracloudProvider) ListZones() ([]string, error) {
|
||||
itemMap, err := s.api.GetCommonServiceItemMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var zones []string
|
||||
for _, item := range itemMap {
|
||||
zones = append(zones, item.Status.Zone)
|
||||
}
|
||||
return zones, nil
|
||||
}
|
||||
|
||||
// EnsureZoneExists creates a zone if it does not exist
|
||||
func (s *sakuracloudProvider) EnsureZoneExists(domain string) error {
|
||||
itemMap, err := s.api.GetCommonServiceItemMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := itemMap[domain]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
printer.Printf("Adding zone for %s to Sakura Cloud account\n", domain)
|
||||
return s.api.CreateZone(domain)
|
||||
}
|
91
providers/sakuracloud/records.go
Normal file
91
providers/sakuracloud/records.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package sakuracloud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||||
)
|
||||
|
||||
// GetZoneRecords gets the records of a zone and returns them in RecordConfig format.
|
||||
func (s *sakuracloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
||||
itemMap, err := s.api.GetCommonServiceItemMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item, ok := itemMap[domain]
|
||||
if !ok {
|
||||
return nil, errNoExist{domain}
|
||||
}
|
||||
|
||||
existingRecords := make([]*models.RecordConfig, 0, len(item.Status.NS)+len(item.Settings.DNS.ResourceRecordSets))
|
||||
|
||||
for _, ns := range item.Status.NS {
|
||||
// CommonServiceItem.Status.NS fields do not end with a dot.
|
||||
// Therefore, a dot is added at the end to make it an absolute domain name.
|
||||
//
|
||||
// "Status": {
|
||||
// "Zone": "example.com",
|
||||
// "NS": [
|
||||
// "ns1.gslbN.sakura.ne.jp",
|
||||
// "ns2.gslbN.sakura.ne.jp"
|
||||
// ]
|
||||
// },
|
||||
rc := &models.RecordConfig{
|
||||
Type: "NS",
|
||||
TTL: defaultTTL,
|
||||
Original: ns,
|
||||
}
|
||||
rc.SetLabel("@", domain)
|
||||
if err := rc.PopulateFromString("NS", ns+".", domain); err != nil {
|
||||
return nil, fmt.Errorf("unparsable record received: %w", err)
|
||||
}
|
||||
existingRecords = append(existingRecords, rc)
|
||||
}
|
||||
|
||||
for _, dr := range item.Settings.DNS.ResourceRecordSets {
|
||||
rc := toRc(domain, dr)
|
||||
existingRecords = append(existingRecords, rc)
|
||||
}
|
||||
return existingRecords, nil
|
||||
}
|
||||
|
||||
// GetZoneRecordsCorrections gets the records of a zone and returns them in RecordConfig format.
|
||||
func (s *sakuracloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) {
|
||||
var corrections []*models.Correction
|
||||
|
||||
// The name servers for the Sakura cloud provider cannot be changed.
|
||||
// These default TTL is 3600 and the default TTL of DNSControl is 300, so NS corrections can be found.
|
||||
// To prevent this, match TTL of DNSControl to one of Sakura Cloud provider.
|
||||
for _, rc := range dc.Records {
|
||||
if rc.Type == "NS" && rc.Name == "@" {
|
||||
rc.TTL = defaultTTL
|
||||
}
|
||||
}
|
||||
|
||||
msgs, changes, err := diff2.ByZone(existing, dc, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !changes {
|
||||
return nil, nil
|
||||
}
|
||||
msg := strings.Join(msgs, "\n")
|
||||
|
||||
corrections = append(corrections,
|
||||
&models.Correction{
|
||||
Msg: msg,
|
||||
F: func() error {
|
||||
drs := make([]domainRecord, 0, len(dc.Records))
|
||||
for _, rc := range dc.Records {
|
||||
drs = append(drs, toNative(rc))
|
||||
}
|
||||
return s.api.UpdateZone(dc.Name, drs)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return corrections, nil
|
||||
}
|
108
providers/sakuracloud/sakuracloudProvider.go
Normal file
108
providers/sakuracloud/sakuracloudProvider.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package sakuracloud
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/StackExchange/dnscontrol/v4/models"
|
||||
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||
)
|
||||
|
||||
const defaultEndpoint = "https://secure.sakura.ad.jp/cloud/zone/is1a/api/cloud/1.1"
|
||||
|
||||
var features = providers.DocumentationNotes{
|
||||
// The default for unlisted capabilities is 'Cannot'.
|
||||
// See providers/capabilities.go for the entire list of capabilities.
|
||||
providers.CanAutoDNSSEC: providers.Cannot(),
|
||||
providers.CanConcur: providers.Cannot(),
|
||||
providers.CanGetZones: providers.Can(),
|
||||
providers.CanUseAlias: providers.Can(),
|
||||
providers.CanUseCAA: providers.Can(),
|
||||
providers.CanUseDHCID: providers.Cannot(),
|
||||
providers.CanUseDNAME: providers.Cannot(),
|
||||
providers.CanUseDS: providers.Cannot(),
|
||||
providers.CanUseDSForChildren: providers.Cannot(),
|
||||
providers.CanUseHTTPS: providers.Can(),
|
||||
providers.CanUseLOC: providers.Cannot(),
|
||||
providers.CanUseNAPTR: providers.Cannot(),
|
||||
providers.CanUsePTR: providers.Can(),
|
||||
providers.CanUseSOA: providers.Cannot(),
|
||||
providers.CanUseSRV: providers.Can(),
|
||||
providers.CanUseSSHFP: providers.Cannot(),
|
||||
providers.CanUseSVCB: providers.Can(),
|
||||
providers.CanUseTLSA: providers.Cannot(),
|
||||
providers.CanUseDNSKEY: providers.Cannot(),
|
||||
providers.DocCreateDomains: providers.Can(),
|
||||
providers.DocDualHost: providers.Cannot(),
|
||||
providers.DocOfficiallySupported: providers.Cannot(),
|
||||
}
|
||||
|
||||
func init() {
|
||||
const providerName = "SAKURACLOUD"
|
||||
const providerMaintainer = "@ttkzw"
|
||||
fns := providers.DspFuncs{
|
||||
Initializer: newSakuracloudDsp,
|
||||
RecordAuditor: AuditRecords,
|
||||
}
|
||||
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
||||
providers.RegisterMaintainer(providerName, providerMaintainer)
|
||||
}
|
||||
|
||||
type sakuracloudProvider struct {
|
||||
api *sakuracloudAPI
|
||||
}
|
||||
|
||||
func newSakuracloudDsp(conf map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||
return newSakuracloud(conf, metadata)
|
||||
}
|
||||
|
||||
// newDSP initializes a Sakura Cloud DNSServiceProvider.
|
||||
func newSakuracloud(config map[string]string, _ json.RawMessage) (*sakuracloudProvider, error) {
|
||||
// config -- the key/values from creds.json
|
||||
accessToken := config["access_token"]
|
||||
if accessToken == "" {
|
||||
return nil, fmt.Errorf("access_token is required")
|
||||
}
|
||||
|
||||
accessTokenSecret := config["access_token_secret"]
|
||||
if accessTokenSecret == "" {
|
||||
return nil, fmt.Errorf("access_token_secret is required")
|
||||
}
|
||||
|
||||
endpoint := config["endpoint"]
|
||||
if endpoint == "" {
|
||||
endpoint = defaultEndpoint
|
||||
}
|
||||
|
||||
api, err := NewSakuracloudAPI(accessToken, accessTokenSecret, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsp := &sakuracloudProvider{
|
||||
api: api,
|
||||
}
|
||||
return dsp, nil
|
||||
}
|
||||
|
||||
type errNoExist struct {
|
||||
domain string
|
||||
}
|
||||
|
||||
func (e errNoExist) Error() string {
|
||||
return fmt.Sprintf("Zone %s not found in your Sakura Cloud account", e.domain)
|
||||
}
|
||||
|
||||
// GetNameservers returns the current nameservers for a domain.
|
||||
func (s *sakuracloudProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||
itemMap, err := s.api.GetCommonServiceItemMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item, ok := itemMap[domain]
|
||||
if !ok {
|
||||
return nil, errNoExist{domain}
|
||||
}
|
||||
|
||||
return models.ToNameservers(item.Status.NS)
|
||||
}
|
Loading…
Reference in a new issue