diff --git a/.github/workflows/pr_test.yml b/.github/workflows/pr_test.yml index 2890114283..5665313c10 100644 --- a/.github/workflows/pr_test.yml +++ b/.github/workflows/pr_test.yml @@ -88,7 +88,7 @@ jobs: Write-Host "Integration test providers: $Providers" echo "integration_test_providers=$(ConvertTo-Json -InputObject $Providers -Compress)" >> $env:GITHUB_OUTPUT env: - PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']" + PROVIDERS: "['AZURE_DNS','BIND','BUNNY_DNS','CLOUDFLAREAPI','CLOUDNS','DIGITALOCEAN','GANDI_V5','GCLOUD','HEDNS','HEXONET','HUAWEICLOUD','INWX','NAMEDOTCOM','NS1','POWERDNS','ROUTE53','TRANSIP']" ENV_CONTEXT: ${{ toJson(env) }} VARS_CONTEXT: ${{ toJson(vars) }} SECRETS_CONTEXT: ${{ toJson(secrets) }} diff --git a/.goreleaser.yml b/.goreleaser.yml index 886a226f65..268ae3d728 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)((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|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|softlayer|transip|vultr).*:)+.*" order: 2 - title: 'Documentation:' regexp: "(?i)^.*(docs)[(\\w)]*:+.*$" diff --git a/OWNERS b/OWNERS index dd6b2c9eaf..3a2c3403a4 100644 --- a/OWNERS +++ b/OWNERS @@ -24,6 +24,7 @@ providers/hedns @rblenkinsopp providers/hetzner @das7pad providers/hexonet @KaiSchwarz-cnic providers/hostingde @juliusrickert +providers/huaweicloud @huihuimoe providers/internetbs @pragmaton providers/inwx @patschi providers/linode @koesie10 diff --git a/README.md b/README.md index f2ea460562..8fd972e2bb 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Currently supported DNS providers: - Hetzner - HEXONET - hosting.de +- Huawei Cloud DNS - Hurricane Electric DNS - INWX - Linode diff --git a/documentation/SUMMARY.md b/documentation/SUMMARY.md index 1dec62ccd3..26a8d11726 100644 --- a/documentation/SUMMARY.md +++ b/documentation/SUMMARY.md @@ -127,6 +127,7 @@ * [Hetzner DNS Console](provider/hetzner.md) * [HEXONET](provider/hexonet.md) * [hosting.de](provider/hostingde.md) +* [Huawei Cloud DNS](provider/huaweicloud.md) * [Hurricane Electric DNS](provider/hedns.md) * [Internet.bs](provider/internetbs.md) * [INWX](provider/inwx.md) diff --git a/documentation/provider/huaweicloud.md b/documentation/provider/huaweicloud.md new file mode 100644 index 0000000000..411040d94b --- /dev/null +++ b/documentation/provider/huaweicloud.md @@ -0,0 +1,77 @@ +## Configuration + + +This provider is for the [Huawei Cloud DNS](https://www.huaweicloud.com/intl/en-us/product/dns.html)(Public DNS). To use this provider, add an entry to `creds.json` with `TYPE` set to `HUAWEICLOUD`. +along with the API credentials. + +Example: + +{% code title="creds.json" %} +```json +{ + "huaweicloud": { + "TYPE": "HUAWEICLOUD", + "KeyId": "YOUR_ACCESS_KEY_ID", + "SecretKey": "YOUR_SECRET_ACCESS_KEY", + "Region": "YOUR_SERVICE_REGION" + } +} +``` +{% endcode %} + +## Metadata +This provider does not recognize any special metadata fields unique to Huawei Cloud DNS. + +## Usage +An example configuration: + +{% code title="dnsconfig.js" %} +```javascript +var REG_NONE = NewRegistrar("none"); +var DSP_HWCLOUD = NewDnsProvider("huaweicloud"); + +D("example.com", REG_NONE, DnsProvider(DSP_HWCLOUD), + A("test", "1.2.3.4"), +END); +``` +{% endcode %} + +## Activation +DNSControl depends on a standard [IAM User](https://support.huaweicloud.com/intl/en-us/usermanual-iam/iam_02_0003.html) with permission to list, create and update hosted zones. + +The `DNS FullAccess` policy will also work, but that provides access to many other areas and violates the "principle of least privilege". + +The minimum permissions required are as follows: + +```json +{ + "Version": "1.1", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dns:recordset:delete", + "dns:recordset:create", + "dns:zone:create", + "dns:recordset:get", + "dns:nameserver:getZoneNameServer", + "dns:zone:list", + "dns:recordset:update", + "dns:recordset:list", + "dns:zone:get" + ] + } + ] +} +``` + +To determine the `Region` parameter, refer to the [endpoint page of huaweicloud](https://developer.huaweicloud.com/intl/en-us/endpoint?DNS). For example, on the international site, the `Region` name `ap-southeast-1` is known to work. + +If that doesn't work, log into Huaweicloud's website and open the [API Explorer](https://console-intl.huaweicloud.com/apiexplorer/#/openapi/DNS/debug?api=ListPublicZones), find the `ListPublicZones` API, select a different Region and click Debug to try and find your Region. + +## New domains +If a domain does not exist in your Huawei Cloud account, DNSControl will automatically add it with the `push` command. + +## GeoDNS +Managing GeoDNS RRSet on Huawei Cloud (also called **Line** in Huawei Cloud DNS) is not supported in DNSControl. +If your Zone needs to use GeoDNS, please create it manually in the console and use [IGNORE](../language-reference/domain-modifiers/IGNORE.md) modifiers in DNSControl to prevent changing it. diff --git a/documentation/providers.md b/documentation/providers.md index 0e06bea373..840a3b8745 100644 --- a/documentation/providers.md +++ b/documentation/providers.md @@ -40,6 +40,7 @@ If a feature is definitively not supported for whatever reason, we would also li | [`HETZNER`](provider/hetzner.md) | ❌ | ✅ | ❌ | ❌ | ❌ | ✅ | ❌ | ❔ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❔ | ✅ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`HEXONET`](provider/hexonet.md) | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ❔ | ✅ | ❔ | ❔ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ❔ | | [`HOSTINGDE`](provider/hostingde.md) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❔ | ❌ | ❌ | ✅ | ✅ | ✅ | ✅ | ❔ | ✅ | ✅ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | +| [`HUAWEICLOUD`](provider/huaweicloud.md) | ❌ | ✅ | ❌ | ❔ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`INTERNETBS`](provider/internetbs.md) | ❌ | ❌ | ✅ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❔ | | [`INWX`](provider/inwx.md) | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | ❔ | ✅ | ❔ | ✅ | ✅ | ❔ | ✅ | ✅ | ✅ | ✅ | ❔ | ❔ | ❔ | ❔ | ✅ | ✅ | ✅ | | [`LINODE`](provider/linode.md) | ❌ | ✅ | ❌ | ❌ | ❔ | ✅ | ❔ | ❔ | ❌ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❌ | ❌ | ✅ | @@ -130,6 +131,7 @@ Providers in this category and their maintainers are: |[`HETZNER`](provider/hetzner.md)|@das7pad| |[`HEXONET`](provider/hexonet.md)|@KaiSchwarz-cnic| |[`HOSTINGDE`](provider/hostingde.md)|@membero| +|[`HUAWEICLOUD`](provider/huaweicloud.md)|@huihuimoe| |[`INTERNETBS`](provider/internetbs.md)|@pragmaton| |[`INWX`](provider/inwx.md)|@patschi| |[`LINODE`](provider/linode.md)|@koesie10| diff --git a/go.mod b/go.mod index 5d42c94e7f..c42a4494f6 100644 --- a/go.mod +++ b/go.mod @@ -65,6 +65,7 @@ require ( github.com/fbiville/markdown-table-formatter v0.3.0 github.com/google/go-cmp v0.6.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.100 github.com/juju/errors v1.0.0 github.com/kylelemons/godebug v1.1.0 github.com/mattn/go-isatty v0.0.20 @@ -125,10 +126,13 @@ require ( github.com/hashicorp/go-sockaddr v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/peterhellberg/link v1.1.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect @@ -141,7 +145,9 @@ require ( github.com/smartystreets/assertions v1.2.0 // indirect github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect github.com/stretchr/objx v0.5.2 // indirect + github.com/tjfoc/gmsm v1.4.1 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + go.mongodb.org/mongo-driver v1.12.0 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go.opentelemetry.io/otel v1.24.0 // indirect diff --git a/go.sum b/go.sum index 906c2098e9..b63fde7652 100644 --- a/go.sum +++ b/go.sum @@ -175,11 +175,13 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -240,6 +242,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.14.0 h1:Ah3CFLixD5jmjusOgm8grfN9M0d+Y8fVR2SW0K6pJLU= github.com/hashicorp/vault/api v1.14.0/go.mod h1:pV9YLxBGSz+cItFDd8Ii4G17waWOQ32zVjMWHe/cOqk= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.100 h1:OXGVtlUZLIKAX8ERmytPeypL9CdeCwo9/FTxvg90IRg= +github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.100/go.mod h1:lhdEO9Bbb3hZ0wG+JeK9/GqMOp/sgc92mFmVk5tNSCk= github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= @@ -249,11 +253,13 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/juju/errors v1.0.0 h1:yiq7kjCLll1BiaRuNY53MGI0+EQ3rF6GB+wvboZDefM= github.com/juju/errors v1.0.0/go.mod h1:B5x9thDqx0wIMH3+aLIMP9HjItInYWObRovoCFM5Qe8= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b h1:udzkj9S/zlT5X367kqJis0QP7YMxobob6zhzq6Yre00= github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -306,9 +312,12 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mittwald/go-powerdns v0.6.2 h1:jNZoW5vPzsQvUQ3BzXEWteHPWoJ1GwcHyF4mSh4YZ7Y= github.com/mittwald/go-powerdns v0.6.2/go.mod h1:adWJ860laOgm14afg+7V0nCa5NQT37oEYe2HRhoS/CA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 h1:o6uBwrhM5C8Ll3MAAxrQxRHEu7FkapwTuI2WmL1rw4g= github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= @@ -381,8 +390,11 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.24.0 h1:QaHgRT3ikpMCXr8Ntojiced/W4izd9ra9PNE/i+7qTE= github.com/transip/gotransip/v6 v6.24.0/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= @@ -397,13 +409,19 @@ github.com/vultr/govultr/v2 v2.17.2 h1:gej/rwr91Puc/tgh+j33p/BLR16UrIPnSr+AIwYWZ github.com/vultr/govultr/v2 v2.17.2/go.mod h1:ZFOKGWmgjytfyjeyAdhQlSWwTjh2ig+X49cAp50dzXI= github.com/xddxdd/ottoext v0.0.0-20221109171055-210517fa4419 h1:PT5KYEimicg1GRkBtBxCLcHWvMcBRGljOLwG/y4+T5c= github.com/xddxdd/ottoext v0.0.0-20221109171055-210517fa4419/go.mod h1:BxZUa1xZ189Ww28wRT0LjHcmHgQmPh27hqfHIwET0ok= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.0 h1:aPx33jmn/rQuJXPQLZQ8NtfPQG8CaqgLThFtqRb0PiE= +go.mongodb.org/mongo-driver v1.12.0/go.mod h1:AZkxhPnFJUoH7kZlFkVKucV20K387miPfm7oimrSmK0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= @@ -417,10 +435,14 @@ go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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= golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -442,13 +464,17 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -487,6 +513,9 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -494,6 +523,9 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -502,6 +534,7 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -540,6 +573,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= diff --git a/integrationTest/providers.json b/integrationTest/providers.json index e931b29f9f..a2e1567eae 100644 --- a/integrationTest/providers.json +++ b/integrationTest/providers.json @@ -288,5 +288,12 @@ "TYPE": "VULTR", "domain": "$VULTR_DOMAIN", "token": "$VULTR_TOKEN" + }, + "HUAWEICLOUD": { + "TYPE": "HUAWEICLOUD", + "domain": "$HUAWEICLOUD_DOMAIN", + "Region": "$HUAWEICLOUD_REGION", + "KeyId": "$HUAWEICLOUD_KEY_ID", + "SecretKey": "$HUAWEICLOUD_KEY" } } diff --git a/providers/_all/all.go b/providers/_all/all.go index c7689b6ae8..9b99d6ed51 100644 --- a/providers/_all/all.go +++ b/providers/_all/all.go @@ -29,6 +29,7 @@ import ( _ "github.com/StackExchange/dnscontrol/v4/providers/hetzner" _ "github.com/StackExchange/dnscontrol/v4/providers/hexonet" _ "github.com/StackExchange/dnscontrol/v4/providers/hostingde" + _ "github.com/StackExchange/dnscontrol/v4/providers/huaweicloud" _ "github.com/StackExchange/dnscontrol/v4/providers/internetbs" _ "github.com/StackExchange/dnscontrol/v4/providers/inwx" _ "github.com/StackExchange/dnscontrol/v4/providers/linode" diff --git a/providers/huaweicloud/auditrecords.go b/providers/huaweicloud/auditrecords.go new file mode 100644 index 0000000000..5b055393ca --- /dev/null +++ b/providers/huaweicloud/auditrecords.go @@ -0,0 +1,18 @@ +package huaweicloud + +import ( + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/rejectif" +) + +// 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-06-14 + a.Add("TXT", rejectif.TxtHasBackslash) // Last verified 2024-06-14 + a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2024-06-14 + + return a.Audit(records) +} diff --git a/providers/huaweicloud/convert.go b/providers/huaweicloud/convert.go new file mode 100644 index 0000000000..8b33bd44bf --- /dev/null +++ b/providers/huaweicloud/convert.go @@ -0,0 +1,88 @@ +package huaweicloud + +import ( + "fmt" + "slices" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" +) + +func getRRSetIDFromRecords(rcs models.Records) []string { + ids := []string{} + for _, r := range rcs { + if r.Original == nil { + continue + } + if r.Original.(*model.ListRecordSets).Id == nil { + printer.Warnf("RecordSet ID is nil for record %+v\n", r) + continue + } + ids = append(ids, *r.Original.(*model.ListRecordSets).Id) + } + return slices.Compact(ids) +} + +func nativeToRecords(n *model.ListRecordSets, zoneName string) (models.Records, error) { + if n.Name == nil || n.Type == nil || n.Records == nil || n.Ttl == nil { + return nil, fmt.Errorf("missing required fields in Huaweicloud's RRset: %+v", n) + } + var rcs models.Records + recName := *n.Name + recType := *n.Type + + // Split into records + for _, value := range *n.Records { + rc := &models.RecordConfig{ + TTL: uint32(*n.Ttl), + Original: n, + } + rc.SetLabelFromFQDN(recName, zoneName) + if err := rc.PopulateFromString(recType, value, zoneName); err != nil { + return nil, fmt.Errorf("unparsable record received from Huaweicloud: %w", err) + } + rcs = append(rcs, rc) + } + + return rcs, nil +} + +func recordsToNative(rcs models.Records, expectedKey models.RecordKey) *model.ListRecordSets { + resultTTL := int32(0) + resultVal := []string{} + name := expectedKey.NameFQDN + "." + result := &model.ListRecordSets{ + Name: &name, + Type: &expectedKey.Type, + Ttl: &resultTTL, + Records: &resultVal, + } + + for _, r := range rcs { + key := r.Key() + if key != expectedKey { + continue + } + val := r.GetTargetCombined() + // special case for empty TXT records + if key.Type == "TXT" && len(val) == 0 { + val = "\"\"" + } + + resultVal = append(resultVal, val) + if resultTTL == 0 { + resultTTL = int32(r.TTL) + } + + // Check if all TTLs are the same + if int32(r.TTL) != resultTTL { + printer.Warnf("All TTLs for a rrset (%v) must be the same. Using smaller of %v and %v.\n", key, r.TTL, resultTTL) + if int32(r.TTL) < resultTTL { + resultTTL = int32(r.TTL) + } + } + } + + return result +} diff --git a/providers/huaweicloud/huaweicloudProvider.go b/providers/huaweicloud/huaweicloudProvider.go new file mode 100644 index 0000000000..7ab3f75b90 --- /dev/null +++ b/providers/huaweicloud/huaweicloudProvider.go @@ -0,0 +1,155 @@ +package huaweicloud + +import ( + "encoding/json" + "strings" + "time" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/StackExchange/dnscontrol/v4/providers" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/region" + dnssdk "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" + dnsRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/region" +) + +// Support for Huawei Cloud DNS. +// API Documentation: https://www.huaweicloud.com/intl/en-us/product/dns.html + +/* +Huaweicloud API DNS provider: +Info required in `creds.json`: + - KeyId + - SecretKey + - Region +*/ + +type huaweicloudProvider struct { + client *dnssdk.DnsClient + domainByZoneID map[string]string + zoneIDByDomain map[string]string + region *region.Region +} + +// newHuaweicloud creates the provider. +func newHuaweicloud(m map[string]string, metadata json.RawMessage) (providers.DNSServiceProvider, error) { + auth, err := basic.NewCredentialsBuilder(). + WithAk(m["KeyId"]). + WithSk(m["SecretKey"]). + SafeBuild() + if err != nil { + return nil, err + } + region, err := dnsRegion.SafeValueOf(m["Region"]) + if err != nil { + return nil, err + } + + client, err := dnssdk.DnsClientBuilder(). + WithRegion(region). + WithCredential(auth). + SafeBuild() + if err != nil { + return nil, err + } + + c := &huaweicloudProvider{ + client: dnssdk.NewDnsClient(client), + region: region, + } + + return c, nil +} + +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.CanGetZones: providers.Can(), + providers.CanUseAlias: providers.Cannot(), + providers.CanUseCAA: providers.Can(), + providers.CanUseDS: providers.Cannot(), + providers.CanUseLOC: providers.Cannot(), + providers.CanUseNAPTR: providers.Cannot(), + providers.CanUsePTR: providers.Cannot(), + providers.CanUseSRV: providers.Can(), + providers.CanUseSSHFP: providers.Cannot(), + providers.CanUseTLSA: providers.Cannot(), + providers.CanUseHTTPS: providers.Cannot(), + providers.CanUseSVCB: providers.Cannot(), + providers.CanUseSOA: providers.Cannot(), + providers.DocCreateDomains: providers.Can(), + providers.DocDualHost: providers.Can(), + providers.DocOfficiallySupported: providers.Cannot(), +} + +var defaultNameServerNames = []string{ + // DNS server for regions in the Chinese mainland + "ns1.huaweicloud-dns.com.", + "ns1.huaweicloud-dns.cn.", + // DNS server for countries or regions outside the Chinese mainland + "ns1.huaweicloud-dns.net.", + "ns1.huaweicloud-dns.org.", +} + +func init() { + fns := providers.DspFuncs{ + Initializer: newHuaweicloud, + RecordAuditor: AuditRecords, + } + providers.RegisterDomainServiceProviderType("HUAWEICLOUD", fns, features) +} + +// huaweicloud has request limiting like above. +// "The throttling threshold has been reached: policy user over ratelimit,limit:100,time:1 minute" +func withRetry(f func() error) { + const maxRetries = 23 + const sleepTime = 5 * time.Second + var currentRetry int + for { + err := f() + if err == nil { + return + } + if strings.Contains(err.Error(), "over ratelimit") { + currentRetry++ + if currentRetry >= maxRetries { + return + } + printer.Printf("Huaweicloud rate limit exceeded. Waiting %s to retry.\n", sleepTime) + time.Sleep(sleepTime) + } else { + return + } + } +} + +// GetNameservers returns the nameservers for a domain. +func (c *huaweicloudProvider) GetNameservers(domain string) ([]*models.Nameserver, error) { + if err := c.getZones(); err != nil { + return nil, err + } + + payload := &model.ShowPublicZoneNameServerRequest{ + ZoneId: c.zoneIDByDomain[domain], + } + res, err := c.client.ShowPublicZoneNameServer(payload) + if err != nil { + return nil, err + } + nameservers := []string{} + if res.Nameservers != nil { + for _, record := range *res.Nameservers { + if record.Hostname != nil { + nameservers = append(nameservers, *record.Hostname) + } + } + } + if len(nameservers) != 0 { + return models.ToNameserversStripTD(nameservers) + } + + return models.ToNameserversStripTD(defaultNameServerNames) +} diff --git a/providers/huaweicloud/listzones.go b/providers/huaweicloud/listzones.go new file mode 100644 index 0000000000..1b7f06905d --- /dev/null +++ b/providers/huaweicloud/listzones.go @@ -0,0 +1,95 @@ +package huaweicloud + +import ( + "strings" + + "github.com/StackExchange/dnscontrol/v4/pkg/printer" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" +) + +// EnsureZoneExists creates a zone if it does not exist +func (c *huaweicloudProvider) EnsureZoneExists(domain string) error { + if err := c.getZones(); err != nil { + return err + } + + if _, ok := c.zoneIDByDomain[domain]; ok { + return nil + } + + printer.Printf("Adding zone for %s to huaweicloud account in region %s\n", domain, c.region.Id) + createPayload := model.CreatePublicZoneRequest{ + Body: &model.CreatePublicZoneReq{ + Name: domain, + }, + } + res, err := c.client.CreatePublicZone(&createPayload) + if err != nil { + return err + } + if res.Id == nil { + // clear cache + c.zoneIDByDomain = nil + c.domainByZoneID = nil + return nil + } + c.zoneIDByDomain[domain] = *res.Id + c.domainByZoneID[*res.Id] = domain + return nil +} + +// ListZones returns all DNS zones managed by this provider. +func (c *huaweicloudProvider) ListZones() ([]string, error) { + if err := c.getZones(); err != nil { + return nil, err + } + var zones []string + for i := range c.zoneIDByDomain { + zones = append(zones, i) + } + return zones, nil +} + +func (c *huaweicloudProvider) getZones() error { + if c.zoneIDByDomain != nil { + return nil + } + + var nextMarker *string + c.zoneIDByDomain = make(map[string]string) + c.domainByZoneID = make(map[string]string) + + for { + listPayload := model.ListPublicZonesRequest{ + Marker: nextMarker, + } + zonesRes, err := c.client.ListPublicZones(&listPayload) + if err != nil { + return err + } + // empty zones + if zonesRes.Zones == nil { + return nil + } + for _, zone := range *zonesRes.Zones { + // just a safety check + if zone.Name == nil || zone.Id == nil { + continue + } + domain := strings.TrimSuffix(*zone.Name, ".") + c.zoneIDByDomain[domain] = *zone.Id + c.domainByZoneID[*zone.Id] = domain + } + + // if has next page, continue to get next page + if zonesRes.Links.Next != nil { + marker, err := parseMarkerFromURL(*zonesRes.Links.Next) + if err != nil { + return err + } + nextMarker = &marker + } else { + return nil + } + } +} diff --git a/providers/huaweicloud/records.go b/providers/huaweicloud/records.go new file mode 100644 index 0000000000..9b0e2e58a8 --- /dev/null +++ b/providers/huaweicloud/records.go @@ -0,0 +1,226 @@ +package huaweicloud + +import ( + "fmt" + "net/url" + "slices" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/StackExchange/dnscontrol/v4/pkg/diff2" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/dns/v2/model" +) + +// GetZoneRecords gets the records of a zone and returns them in RecordConfig format. +func (c *huaweicloudProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) { + if err := c.getZones(); err != nil { + return nil, err + } + zoneID, ok := c.zoneIDByDomain[domain] + if !ok { + return nil, fmt.Errorf("zone %s not found", domain) + } + records, err := c.fetchZoneRecordsFromRemote(zoneID) + if err != nil { + return nil, err + } + + // Convert rrsets to DNSControl's RecordConfig + existingRecords := []*models.RecordConfig{} + for _, rec := range *records { + if *rec.Type == "SOA" { + continue + } + nativeRecords, err := nativeToRecords(&rec, domain) + if err != nil { + return nil, err + } + existingRecords = append(existingRecords, nativeRecords...) + } + + return existingRecords, nil +} + +// GenerateDomainCorrections takes the desired and existing records +// and produces a Correction list. The correction list is simply +// a list of functions to call to actually make the desired +// correction, and a message to output to the user when the change is +// made. +func (c *huaweicloudProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existing models.Records) ([]*models.Correction, error) { + if err := c.getZones(); err != nil { + return nil, err + } + zoneID, ok := c.zoneIDByDomain[dc.Name] + if !ok { + return nil, fmt.Errorf("zone %s not found", dc.Name) + } + + // Make delete happen earlier than creates & updates. + var corrections []*models.Correction + var deletions []*models.Correction + var reports []*models.Correction + + changes, err := diff2.ByRecordSet(existing, dc, nil) + if err != nil { + return nil, err + } + + for _, change := range changes { + switch change.Type { + case diff2.REPORT: + reports = append(reports, &models.Correction{Msg: change.MsgsJoined}) + case diff2.CREATE: + fallthrough + case diff2.CHANGE: + records := recordsToNative(change.New, change.Key) + rrsetsID := getRRSetIDFromRecords(change.Old) + corrections = append(corrections, &models.Correction{ + Msg: change.MsgsJoined, + F: func() error { + if len(rrsetsID) == 1 { + return c.updateRRSet(zoneID, rrsetsID[0], records) + } else { + err := c.deleteRRSets(zoneID, rrsetsID) + if err != nil { + return err + } + return c.createRRSet(zoneID, records) + } + }, + }) + case diff2.DELETE: + rrsetsID := getRRSetIDFromRecords(change.Old) + deletions = append(deletions, &models.Correction{ + Msg: change.MsgsJoined, + F: func() error { + return c.deleteRRSets(zoneID, rrsetsID) + }, + }) + default: + panic(fmt.Sprintf("unhandled change.Type %s", change.Type)) + } + } + + result := append(reports, deletions...) + result = append(result, corrections...) + return result, nil +} + +func (c *huaweicloudProvider) deleteRRSets(zoneID string, rrsets []string) error { + for _, rrset := range rrsets { + deletePayload := &model.DeleteRecordSetRequest{ + ZoneId: zoneID, + RecordsetId: rrset, + } + var err error + withRetry(func() error { + _, err = c.client.DeleteRecordSet(deletePayload) + return err + }) + if err != nil { + return err + } + } + return nil +} + +func (c *huaweicloudProvider) createRRSet(zoneID string, rc *model.ListRecordSets) error { + createPayload := &model.CreateRecordSetRequest{ + ZoneId: zoneID, + Body: &model.CreateRecordSetRequestBody{ + Name: *rc.Name, + Type: *rc.Type, + Ttl: rc.Ttl, + Records: *rc.Records, + }, + } + var err error + withRetry(func() error { + _, err = c.client.CreateRecordSet(createPayload) + return err + }) + if err != nil { + return err + } + return nil +} + +func (c *huaweicloudProvider) updateRRSet(zoneID, rrsetID string, rc *model.ListRecordSets) error { + updatePayload := &model.UpdateRecordSetRequest{ + ZoneId: zoneID, + RecordsetId: rrsetID, + Body: &model.UpdateRecordSetReq{ + Name: *rc.Name, + Type: *rc.Type, + Ttl: rc.Ttl, + Records: rc.Records, + }, + } + var err error + withRetry(func() error { + _, err = c.client.UpdateRecordSet(updatePayload) + return err + }) + if err != nil { + return err + } + return nil +} + +func parseMarkerFromURL(link string) (string, error) { + // Parse the marker params from the URL + // Example: https://dns.myhuaweicloud.com/v2/zones?marker=abcdefg + url, err := url.Parse(link) + if err != nil { + return "", err + } + marker := url.Query().Get("marker") + if marker == "" { + return "", fmt.Errorf("marker not found in URL %s", link) + } + return marker, nil +} + +func (c *huaweicloudProvider) fetchZoneRecordsFromRemote(zoneID string) (*[]model.ListRecordSets, error) { + var nextMarker *string + existingRecords := []model.ListRecordSets{} + availableStatus := []string{"ACTIVE", "PENDING_CREATE", "PENDING_UPDATE"} + + for { + payload := model.ListRecordSetsByZoneRequest{ + ZoneId: zoneID, + Marker: nextMarker, + } + var res *model.ListRecordSetsByZoneResponse + var err error + withRetry(func() error { + res, err = c.client.ListRecordSetsByZone(&payload) + return err + }) + if err != nil { + return nil, err + } + if res.Recordsets == nil { + return &existingRecords, nil + } + for _, record := range *res.Recordsets { + if record.Records == nil { + continue + } + if !slices.Contains(availableStatus, *record.Status) { + continue + } + existingRecords = append(existingRecords, record) + } + + // if has next page, continue to get next page + if res.Links.Next != nil { + marker, err := parseMarkerFromURL(*res.Links.Next) + if err != nil { + return nil, err + } + nextMarker = &marker + } else { + return &existingRecords, nil + } + } +}