diff --git a/.github/workflows/pr_integration_tests.yml b/.github/workflows/pr_integration_tests.yml index e3f4dd6b1..1f7ff1d0f 100644 --- a/.github/workflows/pr_integration_tests.yml +++ b/.github/workflows/pr_integration_tests.yml @@ -52,7 +52,7 @@ jobs: Write-Host "Integration test providers: $Providers" echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT env: - PROVIDERS: "['AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HETZNER_V2','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']" + PROVIDERS: "['ALIDNS', 'AXFRDDNS', 'AXFRDDNS_DNSSEC', 'AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','CNR','DIGITALOCEAN','FORTIGATE','GANDI_V5','GCLOUD','HEDNS','HETZNER_V2','HEXONET','HUAWEICLOUD','INWX','JOKER','MYTHICBEASTS', 'NAMEDOTCOM','NS1','POWERDNS','ROUTE53','SAKURACLOUD','TRANSIP']" ENV_CONTEXT: ${{ toJson(env) }} VARS_CONTEXT: ${{ toJson(vars) }} SECRETS_CONTEXT: ${{ toJson(secrets) }} @@ -73,6 +73,7 @@ jobs: # PROVIDER DOMAIN LIST # These providers will be tested if the env variable is set. # Set it to the domain name to use during the test. + ALIDNS_DOMAIN: ${{ vars.ALIDNS_DOMAIN }} AXFRDDNS_DOMAIN: ${{ vars.AXFRDDNS_DOMAIN }} AXFRDDNS_DNSSEC_DOMAIN: ${{ vars.AXFRDDNS_DNSSEC_DOMAIN }} AZURE_DNS_DOMAIN: ${{ vars.AZURE_DNS_DOMAIN }} @@ -103,6 +104,9 @@ jobs: # The above providers have additional env variables they # need for credentials and such. # + ALIDNS_ACCESS_KEY_ID: ${{ secrets.ALIDNS_ACCESS_KEY_ID }} + ALIDNS_ACCESS_KEY_SECRET: ${{ secrets.ALIDNS_ACCESS_KEY_SECRET }} + # AXFRDDNS_MASTER: ${{ secrets.AXFRDDNS_MASTER }} AXFRDDNS_NAMESERVERS: ${{ secrets.AXFRDDNS_NAMESERVERS }} AXFRDDNS_TRANSFER_KEY: ${{ secrets.AXFRDDNS_TRANSFER_KEY }} diff --git a/.goreleaser.yml b/.goreleaser.yml index dad3a4fe4..02dfb97d3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -39,7 +39,7 @@ changelog: regexp: "(?i)^.*(major|new provider|feature)[(\\w)]*:+.*$" order: 1 - title: 'Provider-specific changes:' - regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hetznerv2|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vercel|vultr).*:)+.*" + regexp: "(?i)((adguardhome|akamaiedge|autodns|axfrd|azure|azure_private_dns|alidns|bind|bunnydns|cloudflare|cloudflareapi_old|cloudns|cnr|cscglobal|desec|digitalocean|dnsimple|dnsmadeeasy|doh|domainnameshop|dynadot|easyname|exoscale|fortigate|gandi|gcloud|gcore|hedns|hetzner|hetznerv2|hexonet|hostingde|huaweicloud|inwx|joker|linode|loopia|luadns|mythicbeasts|namecheap|namedotcom|netcup|netlify|ns1|opensrs|oracle|ovh|packetframe|porkbun|powerdns|realtimeregister|route53|rwth|sakuracloud|softlayer|transip|vercel|vultr).*:)+.*" order: 2 - title: 'Documentation:' regexp: "(?i)^.*(docs)[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index b3b6ab09e..3732c3b68 100644 --- a/OWNERS +++ b/OWNERS @@ -1,5 +1,6 @@ providers/adguardhome @ishanjain28 providers/akamaiedgedns @edglynes +providers/alidns @bytemain providers/autodns @arnoschoon providers/axfrddns @hnrgrgr providers/azuredns @vatsalyagoel diff --git a/README.md b/README.md index 6a8985375..09985d8b9 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Currently supported DNS providers: - AdGuard Home - Akamai Edge DNS +- Alibaba Cloud DNS (ALIDNS) - AutoDNS - AWS Route 53 - AXFR+DDNS diff --git a/commands/getZones.go b/commands/getZones.go index 80c6327e7..feb1b95fd 100644 --- a/commands/getZones.go +++ b/commands/getZones.go @@ -167,6 +167,12 @@ func GetZone(args GetZoneArgs) error { return fmt.Errorf("failed GetZone CDP: %w", err) } + // Get the actual provider type name from creds.json or args + providerType := args.ProviderName + if providerType == "" || providerType == "-" { + providerType = providerConfigs[args.CredName][pproviderTypeFieldName] + } + // decide which zones we need to convert zones := args.ZoneNames if len(args.ZoneNames) == 1 && args.ZoneNames[0] == "all" { @@ -253,6 +259,13 @@ func GetZone(args GetZoneArgs) error { if defaultTTL == 0 { defaultTTL = prettyzone.MostCommonTTL(recs) } + // If provider has a registered default TTL and no records exist or MostCommonTTL returns 0, + // use the provider's default TTL + if defaultTTL == 0 || defaultTTL == models.DefaultTTL { + if providerDefaultTTL := providers.GetDefaultTTL(providerType); providerDefaultTTL > 0 { + defaultTTL = providerDefaultTTL + } + } if defaultTTL != models.DefaultTTL && defaultTTL != 0 { o = append(o, fmt.Sprintf("DefaultTTL(%d)", defaultTTL)) } diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index dad8a6a95..2237f6472 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -114,6 +114,7 @@ * [Supported providers](provider/index.md) * [AdGuard Home](provider/adguardhome.md) * [Akamai Edge DNS](provider/akamaiedgedns.md) +* [Alibaba Cloud DNS (ALIDNS)](provider/alidns.md) * [Amazon Route 53](provider/route53.md) * [AutoDNS](provider/autodns.md) * [AXFR+DDNS](provider/axfrddns.md) diff --git a/documentation/provider/alidns.md b/documentation/provider/alidns.md new file mode 100644 index 000000000..c35a66f19 --- /dev/null +++ b/documentation/provider/alidns.md @@ -0,0 +1,126 @@ +## Configuration + +This provider is for [Alibaba Cloud DNS](https://www.alibabacloud.com/product/dns) (also known as ALIDNS). To use this provider, add an entry to `creds.json` with `TYPE` set to `ALIDNS` along with your API credentials. + +Example: + +{% code title="creds.json" %} +```json +{ + "alidns": { + "TYPE": "ALIDNS", + "access_key_id": "YOUR_ACCESS_KEY_ID", + "access_key_secret": "YOUR_ACCESS_KEY_SECRET" + } +} +``` +{% endcode %} + +Optionally, you can specify a `region_id`: + +{% code title="creds.json" %} +```json +{ + "alidns": { + "TYPE": "ALIDNS", + "access_key_id": "YOUR_ACCESS_KEY_ID", + "access_key_secret": "YOUR_ACCESS_KEY_SECRET", + "region_id": "cn-hangzhou" + } +} +``` +{% endcode %} + +Note: The `region_id` defaults to `"cn-hangzhou"`. The region value does not affect DNS management (DNS is global), but Alibaba's SDK requires a region to be provided. + +## Usage + +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_ALIDNS = NewDnsProvider("alidns"); + +D("example.com", REG_NONE, DnsProvider(DSP_ALIDNS), + A("test", "1.2.3.4"), + CNAME("www", "example.com."), + MX("@", 10, "mail.example.com."), +); +``` +{% endcode %} + +## Activation + +DNSControl depends on an Alibaba Cloud [RAM user](https://www.alibabacloud.com/help/en/ram/user-guide/overview-of-ram-users) with permissions to manage DNS records. + +### Creating RAM User and Access Keys + +1. Log in to the [RAM console](https://ram.console.aliyun.com/) +2. Create a new RAM user or use an existing one +3. Generate an AccessKey ID and AccessKey Secret for the user +4. Attach the `AliyunDNSFullAccess` policy to the user + +The minimum required permissions are: + +```json +{ + "Version": "1", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "alidns:DescribeDomains", + "alidns:DescribeDomainRecords", + "alidns:DescribeDomainInfo", + "alidns:AddDomainRecord", + "alidns:UpdateDomainRecord", + "alidns:DeleteDomainRecord" + ], + "Resource": "*" + } + ] +} +``` + +## Important Notes + +### TTL Constraints + +Alibaba Cloud DNS has different TTL constraints depending on your DNS edition: + +- **Enterprise Ultimate Edition**: TTL can be as low as 1 second (1-86400) +- **Personal Edition / Free Edition**: Minimum TTL is 600 seconds (600-86400) + +DNSControl will automatically validate TTL values based on your domain's edition. If you attempt to use a TTL below the minimum for your edition, you will receive an error. + +### Chinese Domain Name Support + +ALIDNS supports Chinese domain names (IDN with Chinese characters). However: + +- **Supported**: ASCII characters and Chinese characters (CJK Unified Ideographs) +- **Not supported**: Other Unicode characters (e.g., German umlauts, Arabic script) + +DNSControl will automatically convert between punycode and unicode as needed. + +### Record Type Support + +The following record types are supported: +- A, AAAA, CNAME, MX, TXT, NS +- CAA (requires quoted values: `0 issue "letsencrypt.org"`) +- SRV + +### TXT Record Constraints + +Alibaba Cloud DNS has specific constraints for TXT records: +- Cannot be empty +- Maximum length: 512 bytes +- Cannot contain unescaped double quotes +- Cannot have trailing spaces +- Cannot have unpaired backslashes (odd number of consecutive backslashes) + +DNSControl will audit and reject records that violate these constraints. + +## New Domains + +If a domain does not exist in your Alibaba Cloud account, you must create it manually through the Alibaba Cloud console. DNSControl does not automatically create new domains for this provider. diff --git a/documentation/provider/index.md b/documentation/provider/index.md index 50c15679f..301d6d7c6 100644 --- a/documentation/provider/index.md +++ b/documentation/provider/index.md @@ -27,6 +27,7 @@ Jump to a table: | ------------- | ---------------- | ------------ | --------- | | [`ADGUARDHOME`](adguardhome.md) | ❌ | ✅ | ❌ | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❌ | ✅ | ❌ | +| [`ALIDNS`](alidns.md) | ❌ | ✅ | ❌ | | [`AUTODNS`](autodns.md) | ❌ | ✅ | ✅ | | [`AXFRDDNS`](axfrddns.md) | ❌ | ✅ | ❌ | | [`AZURE_DNS`](azure_dns.md) | ✅ | ✅ | ❌ | @@ -90,6 +91,7 @@ Jump to a table: | ------------- | -------------------------------------------------------------------- | ---------------------------------------------- | -------------- | --------- | | [`ADGUARDHOME`](adguardhome.md) | ❔ | ❔ | ❌ | ❌ | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❔ | ✅ | ✅ | ✅ | +| [`ALIDNS`](alidns.md) | ✅ | ❌ | ❌ | ✅ | | [`AUTODNS`](autodns.md) | ✅ | ❌ | ❌ | ✅ | | [`AXFRDDNS`](axfrddns.md) | ✅ | ❌ | ❌ | ❌ | | [`AZURE_DNS`](azure_dns.md) | ✅ | ✅ | ✅ | ✅ | @@ -152,6 +154,7 @@ Jump to a table: | ------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | | [`ADGUARDHOME`](adguardhome.md) | ✅ | ❔ | ❔ | ❔ | ❔ | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ✅ | ✅ | ❌ | +| [`ALIDNS`](alidns.md) | ❌ | ❔ | ❔ | ❌ | ❔ | | [`AUTODNS`](autodns.md) | ✅ | ❔ | ❔ | ✅ | ❔ | | [`AXFRDDNS`](axfrddns.md) | ❌ | ✅ | ✅ | ✅ | ❌ | | [`AZURE_DNS`](azure_dns.md) | ❌ | ❔ | ❌ | ✅ | ❔ | @@ -208,6 +211,7 @@ Jump to a table: | Provider name | [`DHCID`](../language-reference/domain-modifiers/DHCID.md) | [`NAPTR`](../language-reference/domain-modifiers/NAPTR.md) | [`SRV`](../language-reference/domain-modifiers/SRV.md) | [`SVCB`](../language-reference/domain-modifiers/SVCB.md) | | ------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------- | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ❔ | ✅ | ✅ | ❔ | +| [`ALIDNS`](alidns.md) | ❔ | ❌ | ✅ | ❔ | | [`AUTODNS`](autodns.md) | ❔ | ❔ | ✅ | ❔ | | [`AXFRDDNS`](axfrddns.md) | ✅ | ✅ | ✅ | ✅ | | [`AZURE_DNS`](azure_dns.md) | ❔ | ❌ | ✅ | ❔ | @@ -263,6 +267,7 @@ Jump to a table: | Provider name | [`CAA`](../language-reference/domain-modifiers/CAA.md) | [`HTTPS`](../language-reference/domain-modifiers/HTTPS.md) | [`SMIMEA`](../language-reference/domain-modifiers/SMIMEA.md) | [`SSHFP`](../language-reference/domain-modifiers/SSHFP.md) | [`TLSA`](../language-reference/domain-modifiers/TLSA.md) | | ------------- | ------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | -------------------------------------------------------- | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ❔ | ✅ | ✅ | +| [`ALIDNS`](alidns.md) | ✅ | ❔ | ❔ | ❌ | ❌ | | [`AUTODNS`](autodns.md) | ✅ | ❔ | ❔ | ❌ | ❌ | | [`AXFRDDNS`](axfrddns.md) | ✅ | ✅ | ❔ | ✅ | ✅ | | [`AZURE_DNS`](azure_dns.md) | ✅ | ❔ | ❔ | ❌ | ❌ | @@ -316,6 +321,7 @@ Jump to a table: | Provider name | [`AUTODNSSEC`](../language-reference/domain-modifiers/AUTODNSSEC_ON.md) | [`DNSKEY`](../language-reference/domain-modifiers/DNSKEY.md) | [`DS`](../language-reference/domain-modifiers/DS.md) | | ------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------- | | [`AKAMAIEDGEDNS`](akamaiedgedns.md) | ✅ | ❔ | ❌ | +| [`ALIDNS`](alidns.md) | ❌ | ❔ | ❔ | | [`AUTODNS`](autodns.md) | ❔ | ❔ | ❌ | | [`AXFRDDNS`](axfrddns.md) | ✅ | ❌ | ✅ | | [`BIND`](bind.md) | ✅ | ✅ | ✅ | @@ -391,6 +397,7 @@ Providers in this category and their maintainers are: |Name|Maintainer| |---|---| |[`ADGUARDHOME`](adguardhome.md)|@ishanjain28| +|[`ALIDNS`](alidns.md)|@bytemain| |[`AZURE_PRIVATE_DNS`](azure_private_dns.md)|@matthewmgamble| |[`AKAMAIEDGEDNS`](akamaiedgedns.md)|@edglynes| |[`AXFRDDNS`](axfrddns.md)|@hnrgrgr| @@ -445,7 +452,6 @@ code to support this provider, we'd be glad to help in any way. *(The list below is sorted alphabetically.)* * [1984 Hosting](https://github.com/StackExchange/dnscontrol/issues/1251) (#1251) -* [Alibaba Cloud DNS](https://github.com/StackExchange/dnscontrol/issues/420)(#420) * [BookMyName](https://github.com/StackExchange/dnscontrol/issues/3451) (#3451) * [Constellix (DNSMadeEasy)](https://github.com/StackExchange/dnscontrol/issues/842) (#842) * [CoreDNS](https://github.com/StackExchange/dnscontrol/issues/1284) (#1284) diff --git a/go.mod b/go.mod index 8a23d46d2..ba74e71eb 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 github.com/G-Core/gcore-dns-sdk-go v0.3.3 + github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 github.com/centralnicgroup-opensource/rtldev-middleware-go-sdk/v5 v5.0.18 github.com/failsafe-go/failsafe-go v0.9.2 @@ -127,6 +128,7 @@ require ( github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -134,6 +136,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/peterhellberg/link v1.2.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index 7c9625425..8fea881c0 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -30,18 +31,23 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7 h1:AJKJCKcb/psppPl/9CUiQQnTG+Bce0/cIweD5w5Q7aQ= github.com/DisposaBoy/JsonConfigReader v0.0.0-20201129172854-99cf318d67e7/go.mod h1:GCzqZQHydohgVLSIqRKZeTt8IGb1Y4NaFfim3H40uUI= github.com/G-Core/gcore-dns-sdk-go v0.3.3 h1:McILJSbJ5nOcT0MI0aBYhEuufCF329YbqKwFIN0RjCI= github.com/G-Core/gcore-dns-sdk-go v0.3.3/go.mod h1:35t795gOfzfVanhzkFyUXEzaBuMXwETmJldPpP28MN4= +github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= github.com/TomOnTime/utfutil v1.0.0 h1:/0Ivgo2OjXJxo8i7zgvs7ewSFZMLwCRGm3P5Umowb90= github.com/TomOnTime/utfutil v1.0.0/go.mod h1:l9lZmOniizVSuIliSkEf87qivMRlSNzbdBFKjuLRg1c= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 h1:F1j7z+/DKEsYqZNoxC6wvfmaiDneLsQOFQmuq9NADSY= github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2/go.mod h1:QlXr/TrICfQ/ANa76sLeQyhAJyNR9sEcfNuZBkY9jgY= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107 h1:qagvUyrgOnBIlVRQWOyCZGVKUIYbMBdGdJ104vBpRFU= +github.com/aliyun/alibaba-cloud-sdk-go v1.63.107/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= @@ -137,6 +143,7 @@ github.com/fbiville/markdown-table-formatter v0.3.0 h1:PIm1UNgJrFs8q1htGTw+wnnNY github.com/fbiville/markdown-table-formatter v0.3.0/go.mod h1:q89TDtSEVDdTaufgSbfHpNVdPU/bmfvqNkrC5HagmLY= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/getkin/kin-openapi v0.87.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= @@ -144,6 +151,7 @@ github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjX github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-gandi/go-gandi v0.7.0 h1:gsP33dUspsN1M+ZW9HEgHchK9HiaSkYnltO73RHhSZA= github.com/go-gandi/go-gandi v0.7.0/go.mod h1:9NoYyfWCjFosClPiWjkbbRK5UViaZ4ctpT8/pKSSFlw= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -176,9 +184,11 @@ github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -201,6 +211,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -248,12 +259,17 @@ github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2 github.com/jarcoal/httpmock v1.4.1/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU= github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -324,6 +340,7 @@ github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/nicholas-fedor/shoutrrr v0.12.1 h1:8NjY+I3K7cGHy89ncnaPGUA0ex44XbYK3SAFJX9YMI8= github.com/nicholas-fedor/shoutrrr v0.12.1/go.mod h1:64qWuPpvTUv9ZppEoR6OdroiFmgf9w11YSaR0h9KZGg= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= github.com/nrdcg/goinwx v0.12.0 h1:ujdUqDBnaRSFwzVnImvPHYw3w3m9XgmGImNUw1GyMb4= @@ -332,6 +349,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= +github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/oracle/oci-go-sdk/v65 v65.105.0 h1:VN3IkW4kwyOOIrjrg7Lh1QGG/sou54c8dqTZB2THeTE= github.com/oracle/oci-go-sdk/v65 v65.105.0/go.mod h1:oB8jFGVc/7/zJ+DbleE8MzGHjhs2ioCz5stRTdZdIcY= github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= @@ -413,6 +432,10 @@ github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA= github.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= +github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= +github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg= +github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= @@ -456,6 +479,8 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= @@ -463,6 +488,7 @@ go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -477,12 +503,22 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -531,6 +567,7 @@ golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -590,12 +627,15 @@ golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -610,8 +650,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI= google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -644,6 +688,7 @@ google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -662,12 +707,15 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 3eca97b54..37c02027f 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -604,6 +604,7 @@ func makeTests() []*TestGroup { // SOFTLAYER: fails at direct internationalization, punycode works, of course. tc("Internationalized name", a("ööö", "1.2.3.4")), tc("Change IDN", a("ööö", "2.2.2.2")), + tc("Chinese label", a("中文", "1.2.3.4")), tc("Internationalized CNAME Target", cname("a", "ööö.com.")), ), testgroup("IDNAs in CNAME targets", diff --git a/integrationTest/profiles.json b/integrationTest/profiles.json index 481f6f947..ec69ab81d 100644 --- a/integrationTest/profiles.json +++ b/integrationTest/profiles.json @@ -16,6 +16,12 @@ "group_id": "$AED_GROUP_ID", "host": "$AED_HOST" }, + "ALIDNS": { + "TYPE": "ALIDNS", + "access_key_id": "$ALIDNS_ACCESS_KEY_ID", + "access_key_secret": "$ALIDNS_ACCESS_KEY_SECRET", + "domain": "$ALIDNS_DOMAIN" + }, "AUTODNS": { "TYPE": "AUTODNS", "context": "$AUTODNS_CONTEXT", diff --git a/pkg/rejectif/audit.go b/pkg/rejectif/audit.go index a92eaee17..1efdbc0bb 100644 --- a/pkg/rejectif/audit.go +++ b/pkg/rejectif/audit.go @@ -32,12 +32,20 @@ func (aud *Auditor) Audit(records models.Records) (errs []error) { // For each record, call the checks for that type, gather errors. for _, rc := range records { + // First, run type-specific checks for _, f := range aud.checksFor[rc.Type] { e := f(rc) if e != nil { errs = append(errs, e) } } + // Then, run wildcard checks that apply to all record types + for _, f := range aud.checksFor["*"] { + e := f(rc) + if e != nil { + errs = append(errs, e) + } + } } return errs diff --git a/pkg/rejectif/txt.go b/pkg/rejectif/txt.go index 7f3f2ef31..bab0a6677 100644 --- a/pkg/rejectif/txt.go +++ b/pkg/rejectif/txt.go @@ -18,6 +18,29 @@ func TxtHasBackslash(rc *models.RecordConfig) error { return nil } +// TxtHasUnpairedBackslash audits TXT records for strings that contain an odd number of consecutive backslashes. +// Some providers strip single backslashes or convert odd consecutive backslashes to even. +// e.g., "1back\slash" -> "1backslash", "3back\\\slash" -> "3back\\slash" +func TxtHasUnpairedBackslash(rc *models.RecordConfig) error { + txt := rc.GetTargetTXTJoined() + i := 0 + for i < len(txt) { + if txt[i] == '\\' { + count := 0 + for i < len(txt) && txt[i] == '\\' { + count++ + i++ + } + if count%2 == 1 { + return errors.New("txtstring contains unpaired backslash (odd count)") + } + } else { + i++ + } + } + return nil +} + // TxtHasBackticks audits TXT records for strings that contain backticks. func TxtHasBackticks(rc *models.RecordConfig) error { if strings.Contains(rc.GetTargetTXTJoined(), "`") { diff --git a/providers/_all/all.go b/providers/_all/all.go index 5e904795e..adb6a0b62 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -5,6 +5,7 @@ import ( // Define all known providers here. They should each register themselves with the providers package via init function. _ "github.com/StackExchange/dnscontrol/v4/providers/adguardhome" _ "github.com/StackExchange/dnscontrol/v4/providers/akamaiedgedns" + _ "github.com/StackExchange/dnscontrol/v4/providers/alidns" _ "github.com/StackExchange/dnscontrol/v4/providers/autodns" _ "github.com/StackExchange/dnscontrol/v4/providers/axfrddns" _ "github.com/StackExchange/dnscontrol/v4/providers/azuredns" diff --git a/providers/alidns/aliDnsProvider.go b/providers/alidns/aliDnsProvider.go new file mode 100644 index 000000000..a702639fa --- /dev/null +++ b/providers/alidns/aliDnsProvider.go @@ -0,0 +1,249 @@ +package alidns + +import ( + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" +) + +var features = providers.DocumentationNotes{ + providers.CanUseAlias: providers.Cannot(), + providers.CanGetZones: providers.Can(), + providers.CanUseCAA: providers.Can(), + providers.CanUsePTR: providers.Cannot(), + providers.CanUseNAPTR: providers.Cannot(), + providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Cannot(), + providers.CanUseTLSA: providers.Cannot(), + providers.CanAutoDNSSEC: providers.Cannot(), + providers.CanConcur: providers.Can(), + providers.DocOfficiallySupported: providers.Cannot(), + providers.DocDualHost: providers.Can("Alibaba Cloud DNS allows full management of apex NS records"), + providers.DocCreateDomains: providers.Cannot(), + providers.CanUseRoute53Alias: providers.Cannot(), +} + +func init() { + const providerName = "ALIDNS" + const providerMaintainer = "@bytemain" + fns := providers.DspFuncs{ + Initializer: newAliDNSDsp, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType(providerName, fns, features) + providers.RegisterMaintainer(providerName, providerMaintainer) + // Register default TTL of 600 seconds (10 minutes) for Alibaba Cloud DNS + // This is the minimum TTL for free/personal edition domains + providers.RegisterDefaultTTL(providerName, 600) + +} + +type aliDNSDsp struct { + client *alidns.Client + domainVersionCache map[string]*domainVersionInfo + cacheMu sync.Mutex +} + +type domainVersionInfo struct { + versionCode string + minTTL uint32 + maxTTL uint32 +} + +func newAliDNSDsp(config map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + accessKeyID := config["access_key_id"] + if accessKeyID == "" { + return nil, fmt.Errorf("creds.json: access_key_id must not be empty") + } + + accessKeySecret := config["access_key_secret"] + if accessKeySecret == "" { + return nil, fmt.Errorf("creds.json: access_key_secret must not be empty") + } + + // Region ID defaults to "cn-hangzhou". The region value does not affect + // DNS management (DNS is global) but Alibaba's SDK/examples require a + // region to be provided — their docs/examples use Hangzhou: + // https://www.alibabacloud.com/help/en/dns/quick-start-1 + region := config["region_id"] + if region == "" { + region = "cn-hangzhou" + } + + client, err := alidns.NewClientWithAccessKey( + region, + accessKeyID, + accessKeySecret, + ) + if err != nil { + return nil, err + } + return &aliDNSDsp{ + client: client, + domainVersionCache: make(map[string]*domainVersionInfo), + }, nil +} + +func (a *aliDNSDsp) GetNameservers(domain string) ([]*models.Nameserver, error) { + nsStrings, err := a.getNameservers(domain) + if err != nil { + return nil, err + } + return models.ToNameserversStripTD(nsStrings) +} + +// GetZoneRecords returns an array of RecordConfig structs for a zone. +func (a *aliDNSDsp) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { + // Fetch all pages of domain records. + records, err := a.describeDomainRecordsAll(domain) + if err != nil { + return nil, err + } + + out := models.Records{} + for _, r := range records { + if r.Status != "ENABLE" { + continue + } + + rc, err := nativeToRecord(r, domain) + if err != nil { + return nil, err + } + + out = append(out, rc) + } + + // Alibaba Cloud's DescribeDomainRecords API doesn't return NS records at the apex. + // We need to fetch them separately using getNameservers and add them to the records. + nameservers, err := a.getNameservers(domain) + if err != nil { + return nil, err + } + + for _, ns := range nameservers { + rc := nativeToRecordNS(ns, domain) + out = append(out, rc) + } + + return out, nil +} + +func (a *aliDNSDsp) ListZones() ([]string, error) { + return a.describeDomainsAll() +} + +func removeTrailingDot(record string) string { + return strings.TrimSuffix(record, ".") +} + +func deduplicateNameServerTargets(newRecs models.Records) models.Records { + dedupedMap := make(map[string]bool) + var deduped models.Records + for _, rec := range newRecs { + if !dedupedMap[rec.GetTargetField()] { + dedupedMap[rec.GetTargetField()] = true + deduped = append(deduped, rec) + } + } + return deduped +} + +// PrepDesiredRecords munges any records to best suit this provider. +func (a *aliDNSDsp) PrepDesiredRecords(dc *models.DomainConfig) { + versionInfo, err := a.getDomainVersionInfo(dc.Name) + if err != nil { + return + } + + recordsToKeep := make([]*models.RecordConfig, 0, len(dc.Records)) + + for _, rec := range dc.Records { + // If TTL is 0 (not set), use the minimum TTL as default + if rec.TTL == 0 { + rec.TTL = versionInfo.minTTL + } + + if rec.TTL < versionInfo.minTTL { + if rec.Type != "NS" { + printer.Warnf("record %s has TTL %d which is below the minimum %d for this domain version (%s)\n", + rec.GetLabelFQDN(), rec.TTL, versionInfo.minTTL, versionInfo.versionCode) + } + rec.TTL = versionInfo.minTTL + } + if rec.TTL > versionInfo.maxTTL { + printer.Warnf("record %s has TTL %d which exceeds the maximum %d\n", + rec.GetLabelFQDN(), rec.TTL, versionInfo.maxTTL) + rec.TTL = versionInfo.maxTTL + } + recordsToKeep = append(recordsToKeep, rec) + } + + dc.Records = recordsToKeep +} + +func (a *aliDNSDsp) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) { + // Prepare desired records first to normalize TTLs and avoid warnings + a.PrepDesiredRecords(dc) + + var corrections []*models.Correction + + // Alibaba Cloud DNS is a "ByRecord" API. + changes, actualChangeCount, err := diff2.ByRecord(existingRecords, dc, nil) + if err != nil { + return nil, 0, err + } + + for _, change := range changes { + // Copy all param values to local variables to avoid overwrites + msgs := change.MsgsJoined + dcn := dc.Name + chaKey := change.Key + + if change.Type == diff2.CHANGE || change.Type == diff2.CREATE { + if chaKey.Type == "NS" && dcn == removeTrailingDot(change.Key.NameFQDN) { + change.New = deduplicateNameServerTargets(change.New) + } + } + + switch change.Type { + case diff2.REPORT: + corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined}) + case diff2.CREATE: + changeNew := change.New + corrections = append(corrections, &models.Correction{ + Msg: msgs, + F: func() error { + return a.createRecordset(changeNew, dcn) + }, + }) + case diff2.CHANGE: + changeNew := change.New + changeExisting := change.Old + corrections = append(corrections, &models.Correction{ + Msg: msgs, + F: func() error { + return a.updateRecordset(changeExisting, changeNew, dcn) + }, + }) + case diff2.DELETE: + corrections = append(corrections, &models.Correction{ + Msg: msgs, + F: func() error { + return a.deleteRecordset(change.Old, dcn) + }, + }) + default: + panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) + } + } + + return corrections, actualChangeCount, nil +} diff --git a/providers/alidns/api.go b/providers/alidns/api.go new file mode 100644 index 000000000..74e0bed69 --- /dev/null +++ b/providers/alidns/api.go @@ -0,0 +1,187 @@ +package alidns + +import ( + "fmt" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" +) + +func (a *aliDNSDsp) getDomainVersionInfo(domain string) (*domainVersionInfo, error) { + // Check cache first + a.cacheMu.Lock() + info, ok := a.domainVersionCache[domain] + a.cacheMu.Unlock() + if ok { + return info, nil + } + + req := alidns.CreateDescribeDomainInfoRequest() + req.DomainName = domain + + resp, err := a.client.DescribeDomainInfo(req) + if err != nil { + return nil, err + } + + // Determine minTTL based on VersionCode + var minTTL uint32 + switch resp.VersionCode { + case "version_enterprise_advanced": + minTTL = 1 // Enterprise Ultimate Edition + case "version_personal", "mianfei": + minTTL = 600 // Personal Edition and Free Edition + default: + // Use MinTtl from API if available, otherwise default to 600 + if resp.MinTtl > 0 { + minTTL = uint32(resp.MinTtl) + } else { + minTTL = 600 + } + } + + info = &domainVersionInfo{ + versionCode: resp.VersionCode, + minTTL: minTTL, + maxTTL: 86400, + } + a.cacheMu.Lock() + a.domainVersionCache[domain] = info + a.cacheMu.Unlock() + return info, nil +} + +// GetNameservers returns the nameservers for a domain. +func (a *aliDNSDsp) getNameservers(domain string) ([]string, error) { + req := alidns.CreateDescribeDomainInfoRequest() + req.DomainName = domain + + resp, err := a.client.DescribeDomainInfo(req) + if err != nil { + return nil, err + } + + // Add trailing dot to each nameserver to make them FQDNs + nameservers := make([]string, len(resp.DnsServers.DnsServer)) + for i, ns := range resp.DnsServers.DnsServer { + if ns != "" && ns[len(ns)-1] != '.' { + nameservers[i] = ns + "." + } else { + nameservers[i] = ns + } + } + + return nameservers, nil +} + +func (a *aliDNSDsp) deleteRecordset(records []*models.RecordConfig, domainName string) error { + for _, r := range records { + req := alidns.CreateDeleteDomainRecordRequest() + original, ok := r.Original.(*alidns.Record) + if !ok { + return fmt.Errorf("deleteRecordset: record original is not of type *alidns.Record") + } + req.RecordId = original.RecordId + + _, err := a.client.DeleteDomainRecord(req) + if err != nil { + return err + } + } + return nil +} + +func (a *aliDNSDsp) createRecordset(records []*models.RecordConfig, domainName string) error { + for _, r := range records { + req := alidns.CreateAddDomainRecordRequest() + req.DomainName = domainName + req.RR = r.Name + req.Type = r.Type + req.TTL = requests.Integer(fmt.Sprintf("%d", r.TTL)) + req.Value = recordToNativeContent(r) + + // Set priority for MX and SRV records + if r.Type == "MX" || r.Type == "SRV" { + req.Priority = requests.Integer(fmt.Sprintf("%d", recordToNativePriority(r))) + } + + _, err := a.client.AddDomainRecord(req) + if err != nil { + return err + } + } + return nil +} + +func (a *aliDNSDsp) updateRecordset(existing, desired []*models.RecordConfig, domainName string) error { + // Strategy: Delete all existing records, then create all desired records. + // This is the simplest and most reliable approach because: + // 1. The number of records in a recordset may change + // 2. There's no guaranteed 1:1 mapping between existing and desired records + // 3. Alibaba Cloud API requires RecordId for updates, which desired records don't have + + // Delete all existing records first + if err := a.deleteRecordset(existing, domainName); err != nil { + return err + } + + // Then create all desired records + return a.createRecordset(desired, domainName) +} + +// describeDomainRecordsAll fetches all domain records for 'domain', handling +// pagination transparently. It returns the slice of *alidns.Record or an error. +func (a *aliDNSDsp) describeDomainRecordsAll(domain string) ([]*alidns.Record, error) { + // The SDK returns a slice of value Records (not pointers). We fetch pages + // as values and then convert to pointers before returning. + fetch := func(pageNumber, pageSize int) ([]alidns.Record, int, error) { + req := alidns.CreateDescribeDomainRecordsRequest() + req.Status = "Enable" + req.DomainName = domain + req.PageNumber = requests.NewInteger(pageNumber) + req.PageSize = requests.NewInteger(pageSize) + + resp, err := a.client.DescribeDomainRecords(req) + if err != nil { + return nil, 0, err + } + + total := int(resp.TotalCount) + return resp.DomainRecords.Record, total, nil + } + + vals, err := paginateAll(fetch, 500) + if err != nil { + return nil, err + } + out := make([]*alidns.Record, 0, len(vals)) + for i := range vals { + out = append(out, &vals[i]) + } + return out, nil +} + +func (a *aliDNSDsp) describeDomainsAll() ([]string, error) { + // describeDomainsAll fetches all domains in the account, handling pagination. + fetch := func(pageNumber, pageSize int) ([]string, int, error) { + req := alidns.CreateDescribeDomainsRequest() + req.PageNumber = requests.NewInteger(pageNumber) + req.PageSize = requests.NewInteger(pageSize) + + resp, err := a.client.DescribeDomains(req) + if err != nil { + return nil, 0, err + } + + domains := make([]string, 0, len(resp.Domains.Domain)) + for _, d := range resp.Domains.Domain { + domains = append(domains, d.DomainName) + } + + total := int(resp.TotalCount) + return domains, total, nil + } + + return paginateAll(fetch, 100) +} diff --git a/providers/alidns/auditrecords.go b/providers/alidns/auditrecords.go new file mode 100644 index 000000000..cd669a7b9 --- /dev/null +++ b/providers/alidns/auditrecords.go @@ -0,0 +1,64 @@ +package alidns + +import ( + "errors" + "unicode" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" +) + +// isValidAliDNSString checks if a string contains only ASCII or Chinese characters. +// Alibaba Cloud DNS allows: a-z, A-Z, 0-9, -, _, ., *, @, and Chinese characters (汉字). +func isValidAliDNSString(s string) bool { + for _, r := range s { + if r > unicode.MaxASCII { + // Allow CJK Unified Ideographs (Chinese characters): U+4E00 to U+9FFF + // and CJK Extension A: U+3400 to U+4DBF + if (r >= 0x4E00 && r <= 0x9FFF) || (r >= 0x3400 && r <= 0x4DBF) { + continue + } + return false + } + } + return true +} + +// labelConstraint detects labels that contain non-ASCII characters except Chinese characters. +func labelConstraint(rc *models.RecordConfig) error { + if !isValidAliDNSString(rc.GetLabel()) { + return errors.New("label contains non-ASCII characters (only Chinese is allowed)") + } + return nil +} + +// targetConstraint detects target values that contain non-ASCII characters except Chinese characters. +// This applies to CNAME, MX, NS, SRV targets. +func targetConstraint(rc *models.RecordConfig) error { + if !isValidAliDNSString(rc.GetTargetField()) { + return errors.New("target contains non-ASCII characters (only Chinese is allowed)") + } + return nil +} + +// 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 { + // Note: We can't get domain version info here because AuditRecords + // is called without provider context. TTL validation will be done + // at the provider level in GetZoneRecordsCorrections. + a := rejectif.Auditor{} + + a.Add("MX", rejectif.MxNull) // Last verified at 2025-12-03 + a.Add("TXT", rejectif.TxtIsEmpty) // Last verified at 2025-12-03 + a.Add("TXT", rejectif.TxtLongerThan(512)) // Last verified at 2025-12-03: 511 bytes OK, 764 bytes failed + a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified at 2025-12-03: Alibaba strips quotes + a.Add("TXT", rejectif.TxtHasTrailingSpace) // Last verified at 2025-12-03: Alibaba strips trailing spaces + a.Add("TXT", rejectif.TxtHasUnpairedBackslash) // Last verified at 2025-12-03: Alibaba mishandles odd backslashes + a.Add("*", labelConstraint) // Last verified at 2025-12-03: Alibaba only allows ASCII + Chinese, rejects other Unicode + a.Add("CNAME", targetConstraint) // Last verified at 2025-12-03: CNAME target must be ASCII or Chinese + a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified at 2025-12-03: SRV target must not be null + a.Add("SRV", rejectif.SrvHasEmptyTarget) // Last verified at 2025-12-03: SRV target must not be empty + return a.Audit(records) +} diff --git a/providers/alidns/convert.go b/providers/alidns/convert.go new file mode 100644 index 000000000..706efc659 --- /dev/null +++ b/providers/alidns/convert.go @@ -0,0 +1,110 @@ +package alidns + +import ( + "fmt" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" + "golang.org/x/net/idna" +) + +// nativeToRecord converts an Alibaba Cloud DNS record to a RecordConfig. +func nativeToRecord(r *alidns.Record, domain string) (*models.RecordConfig, error) { + rc := &models.RecordConfig{ + TTL: uint32(r.TTL), + Original: r, + } + + label, err := idna.ToASCII(r.RR) + if err != nil { + return nil, fmt.Errorf("failed to convert label to ASCII: %w", err) + } + rc.SetLabel(label, domain) + + // Normalize CNAME, MX, NS records with trailing dot to be consistent with FQDN format. + value := r.Value + if r.Type == "CNAME" || r.Type == "MX" || r.Type == "NS" || r.Type == "SRV" { + if value != "" && value != "." && !strings.HasSuffix(value, ".") { + value = value + "." + } + } + + switch r.Type { + case "MX": + if err := rc.SetTargetMX(uint16(r.Priority), value); err != nil { + return nil, fmt.Errorf("unparsable MX record received from ALIDNS: %w", err) + } + case "SRV": + // SRV records in Alibaba Cloud: Value contains "priority weight port target" + // e.g., "1 1 5060 www.cloud-example.com." + // Parse the parts and normalize the target + parts := strings.Fields(r.Value) + if len(parts) != 4 { + return nil, fmt.Errorf("invalid SRV format from ALIDNS: %s", r.Value) + } + target := parts[3] + // Ensure target has trailing dot for FQDN + if target != "" && target != "." && !strings.HasSuffix(target, ".") { + target = target + "." + } + // Reconstruct with normalized target and let PopulateFromString handle it + srvValue := fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], target) + if err := rc.PopulateFromString(r.Type, srvValue, domain); err != nil { + return nil, fmt.Errorf("unparsable SRV record received from ALIDNS: %w", err) + } + case "CAA": + // Alibaba Cloud CAA format: "0 issue \"letsencrypt.org\"" + if err := rc.SetTargetCAAString(r.Value); err != nil { + return nil, fmt.Errorf("unparsable CAA record received from ALIDNS: %w", err) + } + case "TXT": + if err := rc.SetTargetTXT(r.Value); err != nil { + return nil, fmt.Errorf("unparsable TXT record received from ALIDNS: %w", err) + } + default: + rc.Type = r.Type + if err := rc.SetTarget(value); err != nil { + return nil, fmt.Errorf("unparsable record received from ALIDNS: %w", err) + } + } + + return rc, nil +} + +// recordToNativeContent converts a RecordConfig to the Value format expected by Alibaba Cloud DNS API. +func recordToNativeContent(r *models.RecordConfig) string { + switch r.Type { + case "SRV": + return fmt.Sprintf("%d %d %d %s", r.SrvPriority, r.SrvWeight, r.SrvPort, r.GetTargetField()) + case "CAA": + return fmt.Sprintf("%d %s \"%s\"", r.CaaFlag, r.CaaTag, r.GetTargetField()) + case "TXT": + return r.GetTargetTXTJoined() + default: + return r.GetTargetField() + } +} + +// recordToNativePriority returns the priority value for MX and SRV records. +func recordToNativePriority(r *models.RecordConfig) int64 { + switch r.Type { + case "MX": + return int64(r.MxPreference) + case "SRV": + return int64(r.SrvPriority) + default: + return 0 + } +} + +// nativeToRecordNS takes a NS record from DNS and returns a native RecordConfig struct. +func nativeToRecordNS(ns string, origin string) *models.RecordConfig { + rc := &models.RecordConfig{ + Type: "NS", + TTL: 600, + } + rc.SetLabel("@", origin) + rc.MustSetTarget(ns) + return rc +} diff --git a/providers/alidns/pagination.go b/providers/alidns/pagination.go new file mode 100644 index 000000000..6eb46e162 --- /dev/null +++ b/providers/alidns/pagination.go @@ -0,0 +1,27 @@ +package alidns + +// paginateAll is a small generic paginator helper. The caller provides a +// fetch function that requests a single page (pageNumber,pageSize) and +// returns the items for that page, the total number of items available, +// and an error if any. paginateAll will iterate pages until it has +// collected all items or an error occurs. +func paginateAll[T any](fetch func(pageNumber, pageSize int) ([]T, int, error), maxPageSize int) ([]T, error) { + page := 1 + pageSize := maxPageSize + var out []T + + for { + items, total, err := fetch(page, pageSize) + if err != nil { + return nil, err + } + out = append(out, items...) + + // If we've collected all items, or the page returned nothing, stop. + if len(out) >= total || len(items) == 0 { + break + } + page++ + } + return out, nil +} diff --git a/providers/providers.go b/providers/providers.go index 762e91bb8..b8b396b4f 100644 --- a/providers/providers.go +++ b/providers/providers.go @@ -83,6 +83,20 @@ func RegisterMaintainer( ProviderMaintainers[providerName] = gitHubUsername } +// ProviderDefaultTTLs stores the default TTL for each provider. +var ProviderDefaultTTLs = map[string]uint32{} + +// RegisterDefaultTTL registers a default TTL for a provider. +// This is used by get-zones to determine the DefaultTTL when generating output. +func RegisterDefaultTTL(providerName string, defaultTTL uint32) { + ProviderDefaultTTLs[providerName] = defaultTTL +} + +// GetDefaultTTL returns the default TTL for a provider, or 0 if not registered. +func GetDefaultTTL(providerName string) uint32 { + return ProviderDefaultTTLs[providerName] +} + // CreateRegistrar initializes a registrar instance from given credentials. func CreateRegistrar(rType string, config map[string]string) (Registrar, error) { var err error