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)]*:+.*$"
|
regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$"
|
||||||
order: 1
|
order: 1
|
||||||
- title: 'Provider-specific changes:'
|
- 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
|
order: 2
|
||||||
- title: 'Documentation:'
|
- title: 'Documentation:'
|
||||||
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
regexp: "(?i)^.*(docs)[(\\w)]*:+.*$"
|
||||||
|
|
1
OWNERS
1
OWNERS
|
@ -46,6 +46,7 @@ providers/powerdns @jpbede
|
||||||
providers/realtimeregister @PJEilers
|
providers/realtimeregister @PJEilers
|
||||||
providers/route53 @tresni
|
providers/route53 @tresni
|
||||||
providers/rwth @mistererwin
|
providers/rwth @mistererwin
|
||||||
|
providers/sakuracloud @ttkzw
|
||||||
# providers/softlayer NEEDS VOLUNTEER
|
# providers/softlayer NEEDS VOLUNTEER
|
||||||
providers/transip @blackshadev
|
providers/transip @blackshadev
|
||||||
providers/vultr @pgaskin
|
providers/vultr @pgaskin
|
||||||
|
|
|
@ -57,6 +57,7 @@ Currently supported DNS providers:
|
||||||
- PowerDNS
|
- PowerDNS
|
||||||
- Realtime Register
|
- Realtime Register
|
||||||
- RWTH DNS-Admin
|
- RWTH DNS-Admin
|
||||||
|
- Sakura Cloud
|
||||||
- SoftLayer
|
- SoftLayer
|
||||||
- TransIP
|
- TransIP
|
||||||
- Vultr
|
- Vultr
|
||||||
|
|
|
@ -150,6 +150,7 @@
|
||||||
* [PowerDNS](provider/powerdns.md)
|
* [PowerDNS](provider/powerdns.md)
|
||||||
* [Realtime Register](provider/realtimeregister.md)
|
* [Realtime Register](provider/realtimeregister.md)
|
||||||
* [RWTH DNS-Admin](provider/rwth.md)
|
* [RWTH DNS-Admin](provider/rwth.md)
|
||||||
|
* [Sakura Cloud](provider/sakuracloud.md)
|
||||||
* [SoftLayer DNS](provider/softlayer.md)
|
* [SoftLayer DNS](provider/softlayer.md)
|
||||||
* [TransIP](provider/transip.md)
|
* [TransIP](provider/transip.md)
|
||||||
* [Vultr](provider/vultr.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) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
| [`REALTIMEREGISTER`](provider/realtimeregister.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | ❔ | ✅ | ❌ | ❌ | ❔ | ❔ | ❌ | ✅ | ✅ |
|
||||||
| [`ROUTE53`](provider/route53.md) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
|
| [`ROUTE53`](provider/route53.md) | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ |
|
||||||
| [`RWTH`](provider/rwth.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ |
|
| [`RWTH`](provider/rwth.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❌ | ✅ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ |
|
||||||
|
| [`SAKURACLOUD`](provider/sakuracloud.md) | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
|
||||||
| [`SOFTLAYER`](provider/softlayer.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ |
|
| [`SOFTLAYER`](provider/softlayer.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ |
|
||||||
| [`TRANSIP`](provider/transip.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
| [`TRANSIP`](provider/transip.md) | ❌ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||||
| [`VULTR`](provider/vultr.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ❌ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ |
|
| [`VULTR`](provider/vultr.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❔ | ❔ | ❌ | ❔ | ❌ | ❔ | ✅ | ✅ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ |
|
||||||
|
@ -149,6 +150,7 @@ Providers in this category and their maintainers are:
|
||||||
|[`REALTIMEREGISTER`](provider/realtimeregister.md)|@PJEilers|
|
|[`REALTIMEREGISTER`](provider/realtimeregister.md)|@PJEilers|
|
||||||
|[`ROUTE53`](provider/route53.md)|@tresni|
|
|[`ROUTE53`](provider/route53.md)|@tresni|
|
||||||
|[`RWTH`](provider/rwth.md)|@MisterErwin|
|
|[`RWTH`](provider/rwth.md)|@MisterErwin|
|
||||||
|
|[`SAKURACLOUD`](provider/sakuracloud.md)|@ttkzw|
|
||||||
|[`SOFTLAYER`](provider/softlayer.md)|@jamielennox|
|
|[`SOFTLAYER`](provider/softlayer.md)|@jamielennox|
|
||||||
|[`TRANSIP`](provider/transip.md)|@blackshadev|
|
|[`TRANSIP`](provider/transip.md)|@blackshadev|
|
||||||
|[`VULTR`](provider/vultr.md)|@pgaskin|
|
|[`VULTR`](provider/vultr.md)|@pgaskin|
|
||||||
|
|
|
@ -279,6 +279,12 @@
|
||||||
"TYPE": "ROUTE53",
|
"TYPE": "ROUTE53",
|
||||||
"domain": "$ROUTE53_DOMAIN"
|
"domain": "$ROUTE53_DOMAIN"
|
||||||
},
|
},
|
||||||
|
"SAKURACLOUD": {
|
||||||
|
"TYPE": "SAKURACLOUD",
|
||||||
|
"access_token": "$SAKURACLOUD_ACCESS_TOKEN",
|
||||||
|
"access_token_secret": "$SAKURACLOUD_ACCESS_TOKEN_SECRET",
|
||||||
|
"domain": "$SAKURACLOUD_DOMAIN"
|
||||||
|
},
|
||||||
"SOFTLAYER": {
|
"SOFTLAYER": {
|
||||||
"TYPE": "SOFTLAYER",
|
"TYPE": "SOFTLAYER",
|
||||||
"api_key": "$SL_API_KEY",
|
"api_key": "$SL_API_KEY",
|
||||||
|
|
|
@ -51,6 +51,7 @@ import (
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/realtimeregister"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/realtimeregister"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/route53"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/route53"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/rwth"
|
_ "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/softlayer"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/transip"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/transip"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/vultr"
|
_ "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