diff --git a/README.md b/README.md index 6e0469fac..ab011b9a9 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,8 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz * [Gandi](https://www.gandi.net) * [ANS Group SafeDNS](https://portal.ans.co.uk/safedns/) * [IBM Cloud DNS](https://www.ibm.com/cloud/dns) +* [TencentCloud PrivateDNS](https://cloud.tencent.com/product/privatedns) +* [TencentCloud DNSPod](https://cloud.tencent.com/product/cns) From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API. @@ -115,6 +117,7 @@ The following table clarifies the current status of the providers according to t | Gandi | Alpha | @packi | | SafeDNS | Alpha | @assureddt | | IBMCloud | Alpha | @hughhuangzh | +| TencentCloud | Alpha | @Hyzhou | ## Kubernetes version compatibility @@ -183,6 +186,7 @@ The following tutorials are provided: * [SafeDNS](docs/tutorials/UKFast_SafeDNS.md) * [IBM Cloud](docs/tutorials/ibmcloud.md) * [Nodes as source](docs/tutorials/nodes.md) +* [TencentCloud](docs/tutorials/tencentcloud.md) ### Running Locally diff --git a/docs/tutorials/tencentcloud.md b/docs/tutorials/tencentcloud.md new file mode 100644 index 000000000..76033d410 --- /dev/null +++ b/docs/tutorials/tencentcloud.md @@ -0,0 +1,208 @@ +# Setting up ExternalDNS for Tencent Cloud + +## External Dns Version +* Make sure to use **>=1.7.2** version of ExternalDNS for this tutorial + +## Set up PrivateDns or DNSPod + +Tencent Cloud DNSPod Service is the domain name resolution and management service for public access. +Tencent Cloud PrivateDNS Service is the domain name resolution and management service for VPC internal access. + +* If you want to use internal dns service in Tencent Cloud. +1. Set up the args `--tencent-cloud-zone-type=private` +2. Create a DNS domain in PrivateDNS console. DNS domain which will contain the managed DNS records. + +* If you want to use public dns service in Tencent Cloud. +1. Set up the args `--tencent-cloud-zone-type=public` +2. Create a Domain in DnsPod console. DNS domain which will contain the managed DNS records. + +## Set up CAM for API Key + +In Tencent CAM Console. you may get the secretId and secretKey pair. make sure the key pair has those Policy. +```json +{ + "version": "2.0", + "statement": [ + { + "effect": "allow", + "action": [ + "dnspod:ModifyRecord", + "dnspod:DeleteRecord", + "dnspod:CreateRecord", + "dnspod:DescribeRecordList", + "dnspod:DescribeDomainList" + ], + "resource": [ + "*" + ] + }, + { + "effect": "allow", + "action": [ + "privatedns:DescribePrivateZoneList", + "privatedns:DescribePrivateZoneRecordList", + "privatedns:CreatePrivateZoneRecord", + "privatedns:DeletePrivateZoneRecord", + "privatedns:ModifyPrivateZoneRecord" + ], + "resource": [ + "*" + ] + } + ] +} +``` + +# Deploy ExternalDNS + +## Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services","endpoints","pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions","networking.k8s.io"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: external-dns-viewer +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: external-dns +subjects: +- kind: ServiceAccount + name: external-dns + namespace: default +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: external-dns +data: + tencent-cloud.json: | + { + "regionId": "ap-shanghai", + "secretId": "******", + "secretKey": "******", + "vpcId": "vpc-******" + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + selector: + matchLabels: + app: external-dns + template: + metadata: + labels: + app: external-dns + spec: + containers: + - args: + - --source=service + - --source=ingress + - --domain-filter=external-dns-test.com # will make ExternalDNS see only the hosted zones matching provided domain, omit to process all available hosted zones + - --provider=tencentcloud + - --policy=sync # set `upsert-only` would prevent ExternalDNS from deleting any records + - --tencent-cloud-zone-type=private # only look at private hosted zones. set `public` to use the public dns service. + - --tencent-cloud-config-file=/etc/kubernetes/tencent-cloud.json + image: k8s.gcr.io/external-dns/external-dns:v1.7.2 + imagePullPolicy: Always + name: external-dns + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /etc/kubernetes + name: config-volume + readOnly: true + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: external-dns + serviceAccountName: external-dns + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + items: + - key: tencent-cloud.json + path: tencent-cloud.json + name: external-dns + name: config-volume +``` + +# Example + +## Service + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + annotations: + external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com + external-dns.alpha.kubernetes.io/internal-hostname: nginx-internal.external-dns-test.com + external-dns.alpha.kubernetes.io/ttl: "600" +spec: + type: LoadBalancer + ports: + - port: 80 + name: http + targetPort: 80 + selector: + app: nginx +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx +spec: + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 + name: http +``` + +`nginx.external-dns-test.com` will record to the Loadbalancer VIP. +`nginx-internal.external-dns-test.com` will record to the ClusterIP. +all of the DNS Record ttl will be 600. + +# Attention + +This makes ExternalDNS safe for running in environments where there are other records managed via other means. + diff --git a/go.mod b/go.mod index b2b2bb9a5..d24344ff8 100644 --- a/go.mod +++ b/go.mod @@ -142,6 +142,9 @@ require ( github.com/smartystreets/gunit v1.3.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.4.0 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.344 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.344 github.com/terra-farm/udnssdk v1.3.5 // indirect go.etcd.io/etcd/client/pkg/v3 v3.5.2 // indirect go.mongodb.org/mongo-driver v1.5.1 // indirect diff --git a/go.sum b/go.sum index fae7cbc0e..0b8c9769b 100644 --- a/go.sum +++ b/go.sum @@ -1349,6 +1349,12 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.344 h1:QhPDamT0YL04UaoteA9AEHnE/sklwYr+VSKd/pPQ6r8= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.344/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344 h1:pdwJ6T3iEjP5nB9Mgi4y/OBO8XNtkGN2/+mjGZ8yCbw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344/go.mod h1:CuOaLxOQr477GhMWAQPYQFUJrsZbW+ZqkAgP2uHDZXg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.344 h1:q4r39zJkMyHvrORok48IOJz/nJ235dIkHStA9LZYwgw= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.344/go.mod h1:En+pdagcHkAASorHT1l8R6tUtieRNNxaQ7nfyqWPefk= github.com/terra-farm/udnssdk v1.3.5 h1:MNR3adfuuEK/l04+jzo8WW/0fnorY+nW515qb3vEr6I= github.com/terra-farm/udnssdk v1.3.5/go.mod h1:8RnM56yZTR7mYyUIvrDgXzdRaEyFIzqdEi7+um26Sv8= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= diff --git a/main.go b/main.go index 6d4ab2b63..ca84f7d53 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,7 @@ import ( "sigs.k8s.io/external-dns/provider/rfc2136" "sigs.k8s.io/external-dns/provider/safedns" "sigs.k8s.io/external-dns/provider/scaleway" + "sigs.k8s.io/external-dns/provider/tencentcloud" "sigs.k8s.io/external-dns/provider/transip" "sigs.k8s.io/external-dns/provider/ultradns" "sigs.k8s.io/external-dns/provider/vinyldns" @@ -334,6 +335,8 @@ func main() { p, err = ibmcloud.NewIBMCloudProvider(cfg.IBMCloudConfigFile, domainFilter, zoneIDFilter, endpointsSource, cfg.IBMCloudProxied, cfg.DryRun) case "safedns": p, err = safedns.NewSafeDNSProvider(domainFilter, cfg.DryRun) + case "tencentcloud": + p, err = tencentcloud.NewTencentCloudProvider(domainFilter, zoneIDFilter, cfg.TencentCloudConfigFile, cfg.TencentCloudZoneType, cfg.DryRun) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index e62720227..0ac106d93 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -192,6 +192,8 @@ type Config struct { OCPRouterName string IBMCloudProxied bool IBMCloudConfigFile string + TencentCloudConfigFile string + TencentCloudZoneType string } var defaultConfig = &Config{ @@ -223,6 +225,7 @@ var defaultConfig = &Config{ GoogleBatchChangeInterval: time.Second, GoogleZoneVisibility: "", DomainFilter: []string{}, + ZoneIDFilter: []string{}, ExcludeDomains: []string{}, RegexDomainFilter: regexp.MustCompile(""), RegexDomainExclusion: regexp.MustCompile(""), @@ -326,6 +329,8 @@ var defaultConfig = &Config{ GoDaddyOTE: false, IBMCloudProxied: false, IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", + TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", + TencentCloudZoneType: "", } // NewConfig returns new Config object @@ -415,7 +420,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, bluecat, cloudflare, rcodezero, digitalocean, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, ibmcloud, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns, gandi, safedns, tencentcloud)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "ibmcloud", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy", "bluecat", "gandi", "safedns", "tencentcloud") app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) app.Flag("regex-domain-filter", "Limit possible domains and target zones by a Regex filter; Overrides domain-filter (optional)").Default(defaultConfig.RegexDomainFilter.String()).RegexpVar(&cfg.RegexDomainFilter) @@ -443,6 +448,8 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("azure-resource-group", "When using the Azure provider, override the Azure resource group to use (required when --provider=azure-private-dns)").Default(defaultConfig.AzureResourceGroup).StringVar(&cfg.AzureResourceGroup) app.Flag("azure-subscription-id", "When using the Azure provider, specify the Azure configuration file (required when --provider=azure-private-dns)").Default(defaultConfig.AzureSubscriptionID).StringVar(&cfg.AzureSubscriptionID) app.Flag("azure-user-assigned-identity-client-id", "When using the Azure provider, override the client id of user assigned identity in config file (optional)").Default("").StringVar(&cfg.AzureUserAssignedIdentityClientID) + app.Flag("tencent-cloud-config-file", "When using the Tencent Cloud provider, specify the Tencent Cloud configuration file (required when --provider=tencentcloud").Default(defaultConfig.TencentCloudConfigFile).StringVar(&cfg.TencentCloudConfigFile) + app.Flag("tencent-cloud-zone-type", "When using the Tencent Cloud provider, filter for zones with visibility (optional, options: public, private)").Default(defaultConfig.TencentCloudZoneType).EnumVar(&cfg.TencentCloudZoneType, "", "public", "private") // Flags related to BlueCat provider app.Flag("bluecat-dns-configuration", "When using the Bluecat provider, specify the Bluecat DNS configuration string (optional when --provider=bluecat)").Default("").StringVar(&cfg.BluecatDNSConfiguration) diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 1d6f202e6..6cd42b5b4 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -127,6 +127,8 @@ var ( OCPRouterName: "default", IBMCloudProxied: false, IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", + TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", + TencentCloudZoneType: "", } overriddenConfig = &Config{ @@ -235,6 +237,8 @@ var ( RFC2136BatchChangeSize: 100, IBMCloudProxied: true, IBMCloudConfigFile: "ibmcloud.json", + TencentCloudConfigFile: "tencent-cloud.json", + TencentCloudZoneType: "private", } ) @@ -373,6 +377,8 @@ func TestParseFlags(t *testing.T) { "--rfc2136-batch-change-size=100", "--ibmcloud-proxied", "--ibmcloud-config-file=ibmcloud.json", + "--tencent-cloud-config-file=tencent-cloud.json", + "--tencent-cloud-zone-type=private", }, envVars: map[string]string{}, expected: overriddenConfig, @@ -486,6 +492,8 @@ func TestParseFlags(t *testing.T) { "EXTERNAL_DNS_RFC2136_BATCH_CHANGE_SIZE": "100", "EXTERNAL_DNS_IBMCLOUD_PROXIED": "1", "EXTERNAL_DNS_IBMCLOUD_CONFIG_FILE": "ibmcloud.json", + "EXTERNAL_DNS_TENCENT_CLOUD_CONFIG_FILE": "tencent-cloud.json", + "EXTERNAL_DNS_TENCENT_CLOUD_ZONE_TYPE": "private", }, expected: overriddenConfig, }, diff --git a/provider/tencentcloud/cloudapi/api.go b/provider/tencentcloud/cloudapi/api.go new file mode 100644 index 000000000..0294f6f15 --- /dev/null +++ b/provider/tencentcloud/cloudapi/api.go @@ -0,0 +1,60 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudapi + +import ( + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" +) + +type Action struct { + Service string `json:"service"` + Name string `json:"name"` + ReadOnly bool `json:"readOnly"` +} + +var ( + /* PrivateDNS */ + CreatePrivateZoneRecord = Action{Service: "PrivateDns", Name: "CreatePrivateZoneRecord", ReadOnly: false} + DeletePrivateZoneRecord = Action{Service: "PrivateDns", Name: "DeletePrivateZoneRecord", ReadOnly: false} + ModifyPrivateZoneRecord = Action{Service: "PrivateDns", Name: "ModifyPrivateZoneRecord", ReadOnly: false} + DescribePrivateZoneList = Action{Service: "PrivateDns", Name: "DescribePrivateZoneList", ReadOnly: true} + DescribePrivateZoneRecordList = Action{Service: "PrivateDns", Name: "DescribePrivateZoneRecordList", ReadOnly: true} + + /* DNSPod */ + DescribeDomainList = Action{Service: "DnsPod", Name: "DescribeDomainList", ReadOnly: true} + DescribeRecordList = Action{Service: "DnsPod", Name: "DescribeRecordList", ReadOnly: true} + CreateRecord = Action{Service: "DnsPod", Name: "CreateRecord", ReadOnly: false} + DeleteRecord = Action{Service: "DnsPod", Name: "DeleteRecord", ReadOnly: false} + ModifyRecord = Action{Service: "DnsPod", Name: "ModifyRecord", ReadOnly: false} +) + +type TencentAPIService interface { + // PrivateDNS + CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) + DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) + ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) + DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) + DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) + + // DNSPod + DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) + DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) + CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) + DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) + ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) +} diff --git a/provider/tencentcloud/cloudapi/clientset.go b/provider/tencentcloud/cloudapi/clientset.go new file mode 100644 index 000000000..c7f307875 --- /dev/null +++ b/provider/tencentcloud/cloudapi/clientset.go @@ -0,0 +1,81 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudapi + +import ( + "fmt" + "sync" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" + "go.uber.org/ratelimit" +) + +type TencentClientSetService interface { + PrivateDnsCli(action string) *privatedns.Client + DnsPodCli(action string) *dnspod.Client +} + +func NewTencentClientSetService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *defaultTencentClientSetService { + p := &defaultTencentClientSetService{ + Region: region, + RateLimit: rate, + } + cred := common.NewCredential(secretId, secretKey) + + privatednsProf := profile.NewClientProfile() + if !internetEndpoint { + privatednsProf.HttpProfile.Endpoint = "privatedns.internal.tencentcloudapi.com" + } + p.privateDnsClient, _ = privatedns.NewClient(cred, region, privatednsProf) + + dnsPodProf := profile.NewClientProfile() + if !internetEndpoint { + dnsPodProf.HttpProfile.Endpoint = "dnspod.internal.tencentcloudapi.com" + } + p.dnsPodClient, _ = dnspod.NewClient(cred, region, dnsPodProf) + + return p +} + +type defaultTencentClientSetService struct { + Region string + RateLimit int + RateLimitSyncMap sync.Map + + privateDnsClient *privatedns.Client + dnsPodClient *dnspod.Client +} + +func (p *defaultTencentClientSetService) checkRateLimit(request, method string) { + action := fmt.Sprintf("%s_%s", request, method) + if rl, ok := p.RateLimitSyncMap.LoadOrStore(action, ratelimit.New(p.RateLimit, ratelimit.WithoutSlack)); ok { + rl.(ratelimit.Limiter).Take() + } +} + +func (p *defaultTencentClientSetService) PrivateDnsCli(action string) *privatedns.Client { + p.checkRateLimit("privateDns", action) + return p.privateDnsClient +} + +func (p *defaultTencentClientSetService) DnsPodCli(action string) *dnspod.Client { + p.checkRateLimit("dnsPod", action) + return p.dnsPodClient +} diff --git a/provider/tencentcloud/cloudapi/mockapi.go b/provider/tencentcloud/cloudapi/mockapi.go new file mode 100644 index 000000000..87a43bf68 --- /dev/null +++ b/provider/tencentcloud/cloudapi/mockapi.go @@ -0,0 +1,247 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudapi + +import ( + "math/rand" + "time" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" +) + +type mockAPIService struct { + privateZones []*privatedns.PrivateZone + privateZoneRecords map[string][]*privatedns.PrivateZoneRecord + + dnspodDomains []*dnspod.DomainListItem + dnspodRecords map[string][]*dnspod.RecordListItem +} + +func NewMockService(privateZones []*privatedns.PrivateZone, privateZoneRecords map[string][]*privatedns.PrivateZoneRecord, dnspodDomains []*dnspod.DomainListItem, dnspodRecords map[string][]*dnspod.RecordListItem) *mockAPIService { + rand.Seed(time.Now().Unix()) + return &mockAPIService{ + privateZones: privateZones, + privateZoneRecords: privateZoneRecords, + dnspodDomains: dnspodDomains, + dnspodRecords: dnspodRecords, + } +} + +//////////////////////////////////////////////////////////////// +// PrivateDns API +//////////////////////////////////////////////////////////////// + +func (api *mockAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { + randomRecordId := RandStringRunes(8) + if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { + api.privateZoneRecords[*request.ZoneId] = make([]*privatedns.PrivateZoneRecord, 0) + } + if request.TTL == nil { + request.TTL = common.Int64Ptr(300) + } + api.privateZoneRecords[*request.ZoneId] = append(api.privateZoneRecords[*request.ZoneId], &privatedns.PrivateZoneRecord{ + RecordId: common.StringPtr(randomRecordId), + ZoneId: request.ZoneId, + SubDomain: request.SubDomain, + RecordType: request.RecordType, + RecordValue: request.RecordValue, + TTL: request.TTL, + }) + return response, nil +} + +func (api *mockAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { + result := make([]*privatedns.PrivateZoneRecord, 0) + if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { + return response, nil + } + for _, privateZoneRecord := range api.privateZoneRecords[*request.ZoneId] { + deleteflag := false + if request.RecordIdSet != nil && len(request.RecordIdSet) != 0 { + for _, recordId := range request.RecordIdSet { + if *privateZoneRecord.RecordId == *recordId { + deleteflag = true + break + } + } + } + if request.RecordId != nil && *request.RecordId == *privateZoneRecord.RecordId { + deleteflag = true + } + if !deleteflag { + result = append(result, privateZoneRecord) + } + } + api.privateZoneRecords[*request.ZoneId] = result + return response, nil +} + +func (api *mockAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { + if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { + return response, nil + } + for _, privateZoneRecord := range api.privateZoneRecords[*request.ZoneId] { + if *privateZoneRecord.RecordId != *request.RecordId { + continue + } + privateZoneRecord.ZoneId = request.ZoneId + privateZoneRecord.SubDomain = request.SubDomain + privateZoneRecord.RecordType = request.RecordType + privateZoneRecord.RecordValue = request.RecordValue + privateZoneRecord.TTL = request.TTL + } + return response, nil +} + +func (api *mockAPIService) DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) { + response = privatedns.NewDescribePrivateZoneListResponse() + response.Response = &struct { + TotalCount *int64 `json:"TotalCount,omitempty" name:"TotalCount"` + PrivateZoneSet []*privatedns.PrivateZone `json:"PrivateZoneSet,omitempty" name:"PrivateZoneSet"` + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + }{ + TotalCount: common.Int64Ptr(int64(len(api.privateZones))), + PrivateZoneSet: api.privateZones, + } + return response, nil +} + +func (api *mockAPIService) DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) { + response = privatedns.NewDescribePrivateZoneRecordListResponse() + response.Response = &struct { + TotalCount *int64 `json:"TotalCount,omitempty" name:"TotalCount"` + RecordSet []*privatedns.PrivateZoneRecord `json:"RecordSet,omitempty" name:"RecordSet"` + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + }{} + if _, exist := api.privateZoneRecords[*request.ZoneId]; !exist { + response.Response.TotalCount = common.Int64Ptr(0) + response.Response.RecordSet = make([]*privatedns.PrivateZoneRecord, 0) + return response, nil + } + response.Response.TotalCount = common.Int64Ptr(int64(len(api.privateZoneRecords[*request.ZoneId]))) + response.Response.RecordSet = api.privateZoneRecords[*request.ZoneId] + return response, nil +} + +//////////////////////////////////////////////////////////////// +// DnsPod API +//////////////////////////////////////////////////////////////// + +func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) { + response = dnspod.NewDescribeDomainListResponse() + response.Response = &struct { + DomainCountInfo *dnspod.DomainCountInfo `json:"DomainCountInfo,omitempty" name:"DomainCountInfo"` + DomainList []*dnspod.DomainListItem `json:"DomainList,omitempty" name:"DomainList"` + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + }{} + response.Response.DomainList = api.dnspodDomains + response.Response.DomainCountInfo = &dnspod.DomainCountInfo{ + AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))), + } + return response, nil +} + +func (api *mockAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) { + response = dnspod.NewDescribeRecordListResponse() + response.Response = &struct { + RecordCountInfo *dnspod.RecordCountInfo `json:"RecordCountInfo,omitempty" name:"RecordCountInfo"` + RecordList []*dnspod.RecordListItem `json:"RecordList,omitempty" name:"RecordList"` + RequestId *string `json:"RequestId,omitempty" name:"RequestId"` + }{} + if _, exist := api.dnspodRecords[*request.Domain]; !exist { + response.Response.RecordList = make([]*dnspod.RecordListItem, 0) + response.Response.RecordCountInfo = &dnspod.RecordCountInfo{ + TotalCount: common.Uint64Ptr(uint64(0)), + } + return response, nil + } + response.Response.RecordList = api.dnspodRecords[*request.Domain] + response.Response.RecordCountInfo = &dnspod.RecordCountInfo{ + TotalCount: common.Uint64Ptr(uint64(len(api.dnspodRecords[*request.Domain]))), + } + return response, nil +} + +func (api *mockAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { + randomRecordId := RandUint64() + if _, exist := api.dnspodRecords[*request.Domain]; !exist { + api.dnspodRecords[*request.Domain] = make([]*dnspod.RecordListItem, 0) + } + if request.TTL == nil { + request.TTL = common.Uint64Ptr(300) + } + api.dnspodRecords[*request.Domain] = append(api.dnspodRecords[*request.Domain], &dnspod.RecordListItem{ + RecordId: common.Uint64Ptr(randomRecordId), + Value: request.Value, + TTL: request.TTL, + Name: request.SubDomain, + Line: request.RecordLine, + LineId: request.RecordLineId, + Type: request.RecordType, + }) + return response, nil +} + +func (api *mockAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { + result := make([]*dnspod.RecordListItem, 0) + if _, exist := api.dnspodRecords[*request.Domain]; !exist { + return response, nil + } + for _, zoneRecord := range api.dnspodRecords[*request.Domain] { + deleteflag := false + if request.RecordId != nil && *request.RecordId == *zoneRecord.RecordId { + deleteflag = true + } + if !deleteflag { + result = append(result, zoneRecord) + } + } + api.dnspodRecords[*request.Domain] = result + return response, nil +} + +func (api *mockAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { + if _, exist := api.dnspodRecords[*request.Domain]; !exist { + return response, nil + } + for _, zoneRecord := range api.dnspodRecords[*request.Domain] { + if *zoneRecord.RecordId != *request.RecordId { + continue + } + zoneRecord.Type = request.RecordType + zoneRecord.Name = request.SubDomain + zoneRecord.Value = request.Value + zoneRecord.TTL = request.TTL + } + return response, nil +} + +var letterRunes = []byte("abcdefghijklmnopqrstuvwxyz") + +func RandStringRunes(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} + +func RandUint64() uint64 { + return rand.Uint64() +} diff --git a/provider/tencentcloud/cloudapi/readonlyapi.go b/provider/tencentcloud/cloudapi/readonlyapi.go new file mode 100644 index 000000000..c86662deb --- /dev/null +++ b/provider/tencentcloud/cloudapi/readonlyapi.go @@ -0,0 +1,78 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudapi + +import ( + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" +) + +type readonlyAPIService struct { + defaultTencentAPIService +} + +func NewReadOnlyAPIService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *readonlyAPIService { + apiService := NewTencentAPIService(region, rate, secretId, secretKey, internetEndpoint) + tencentAPIService := &readonlyAPIService{ + *apiService, + } + return tencentAPIService +} + +//////////////////////////////////////////////////////////////// +// PrivateDns API +//////////////////////////////////////////////////////////////// + +func (api *readonlyAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { + apiAction := CreatePrivateZoneRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} + +func (api *readonlyAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { + apiAction := DeletePrivateZoneRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} + +func (api *readonlyAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { + apiAction := ModifyPrivateZoneRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} + +//////////////////////////////////////////////////////////////// +// DnsPod API +//////////////////////////////////////////////////////////////// + +func (api *readonlyAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { + apiAction := CreateRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} + +func (api *readonlyAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { + apiAction := DeleteRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} + +func (api *readonlyAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { + apiAction := ModifyRecord + APIRecord(apiAction, JsonWrapper(request), "dryRun") + return response, nil +} diff --git a/provider/tencentcloud/cloudapi/tencentapi.go b/provider/tencentcloud/cloudapi/tencentapi.go new file mode 100644 index 000000000..6004cf9cd --- /dev/null +++ b/provider/tencentcloud/cloudapi/tencentapi.go @@ -0,0 +1,279 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cloudapi + +import ( + "encoding/json" + "fmt" + "net" + "time" + + log "github.com/sirupsen/logrus" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" +) + +type defaultTencentAPIService struct { + RetryDefault int + TaskCheckInterval time.Duration + ClientSetService TencentClientSetService +} + +func NewTencentAPIService(region string, rate int, secretId string, secretKey string, internetEndpoint bool) *defaultTencentAPIService { + tencentAPIService := &defaultTencentAPIService{ + RetryDefault: 3, + TaskCheckInterval: 3 * time.Second, + ClientSetService: NewTencentClientSetService(region, rate, secretId, secretKey, internetEndpoint), + } + return tencentAPIService +} + +//////////////////////////////////////////////////////////////// +// PrivateDns API +//////////////////////////////////////////////////////////////// + +func (api *defaultTencentAPIService) CreatePrivateZoneRecord(request *privatedns.CreatePrivateZoneRecordRequest) (response *privatedns.CreatePrivateZoneRecordResponse, err error) { + apiAction := CreatePrivateZoneRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.PrivateDnsCli(apiAction.Name) + if response, err = client.CreatePrivateZoneRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) DeletePrivateZoneRecord(request *privatedns.DeletePrivateZoneRecordRequest) (response *privatedns.DeletePrivateZoneRecordResponse, err error) { + apiAction := DeletePrivateZoneRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.PrivateDnsCli(apiAction.Name) + if response, err = client.DeletePrivateZoneRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) ModifyPrivateZoneRecord(request *privatedns.ModifyPrivateZoneRecordRequest) (response *privatedns.ModifyPrivateZoneRecordResponse, err error) { + apiAction := ModifyPrivateZoneRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.PrivateDnsCli(apiAction.Name) + if response, err = client.ModifyPrivateZoneRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) DescribePrivateZoneList(request *privatedns.DescribePrivateZoneListRequest) (response *privatedns.DescribePrivateZoneListResponse, err error) { + apiAction := DescribePrivateZoneList + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.PrivateDnsCli(apiAction.Name) + if response, err = client.DescribePrivateZoneList(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) DescribePrivateZoneRecordList(request *privatedns.DescribePrivateZoneRecordListRequest) (response *privatedns.DescribePrivateZoneRecordListResponse, err error) { + apiAction := DescribePrivateZoneRecordList + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.PrivateDnsCli(apiAction.Name) + if response, err = client.DescribePrivateZoneRecordList(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +//////////////////////////////////////////////////////////////// +// DnsPod API +//////////////////////////////////////////////////////////////// + +func (api *defaultTencentAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) { + apiAction := DescribeDomainList + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.DnsPodCli(apiAction.Name) + if response, err = client.DescribeDomainList(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) { + apiAction := DescribeRecordList + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.DnsPodCli(apiAction.Name) + if response, err = client.DescribeRecordList(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) CreateRecord(request *dnspod.CreateRecordRequest) (response *dnspod.CreateRecordResponse, err error) { + apiAction := CreateRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.DnsPodCli(apiAction.Name) + if response, err = client.CreateRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) DeleteRecord(request *dnspod.DeleteRecordRequest) (response *dnspod.DeleteRecordResponse, err error) { + apiAction := DeleteRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.DnsPodCli(apiAction.Name) + if response, err = client.DeleteRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +func (api *defaultTencentAPIService) ModifyRecord(request *dnspod.ModifyRecordRequest) (response *dnspod.ModifyRecordResponse, err error) { + apiAction := ModifyRecord + for times := 1; times <= api.RetryDefault; times++ { + client := api.ClientSetService.DnsPodCli(apiAction.Name) + if response, err = client.ModifyRecord(request); err != nil { + requestJson := JsonWrapper(request) + if retry := dealWithError(apiAction, requestJson, err); retry == false || times == api.RetryDefault { + APIErrorRecord(apiAction, requestJson, JsonWrapper(response), err) + return nil, err + } + continue + } + break + } + APIRecord(apiAction, JsonWrapper(request), JsonWrapper(response)) + return response, nil +} + +//////////////////////////////////////////////////////////////// +// API Error Report +//////////////////////////////////////////////////////////////// + +func dealWithError(action Action, request string, err error) bool { + log.Errorf("dealWithError %s/%s request: %s, error: %s.", action.Service, action.Name, request, err.Error()) + if sdkError, ok := err.(*errors.TencentCloudSDKError); ok { + if sdkError.Code == "RequestLimitExceeded" { + return true + } else if sdkError.Code == "InternalError" || sdkError.Code == "ClientError.HttpStatusCodeError" { + return false + } else if sdkError.Code == "ClientError.NetworkError" { + return false + } else if sdkError.Code == "AuthFailure.UnauthorizedOperation" || sdkError.Code == "UnauthorizedOperation.CamNoAuth" { + return false + } + return false + } + + if _, ok := err.(net.Error); ok { + return true + } + + return false +} + +func APIErrorRecord(apiAction Action, request string, response string, err error) { + log.Infof(fmt.Sprintf("APIError API: %s/%s Request: %s, Response: %s, Error: %s", apiAction.Service, apiAction.Name, request, response, err.Error())) +} + +func APIRecord(apiAction Action, request string, response string) { + message := fmt.Sprintf("APIRecord API: %s/%s Request: %s, Response: %s", apiAction.Service, apiAction.Name, request, response) + + if apiAction.ReadOnly { + //log.Infof(message) + } else { + log.Infof(message) + } +} + +func JsonWrapper(obj interface{}) string { + if jsonStr, jsonErr := json.Marshal(obj); jsonErr == nil { + return string(jsonStr) + } + return "json_format_error" +} diff --git a/provider/tencentcloud/dnspod.go b/provider/tencentcloud/dnspod.go new file mode 100644 index 000000000..f2e714cf8 --- /dev/null +++ b/provider/tencentcloud/dnspod.go @@ -0,0 +1,281 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tencentcloud + +import ( + "fmt" + "strconv" + "strings" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +// DnsPod For Public Dns + +func (p *TencentCloudProvider) dnsRecords() ([]*endpoint.Endpoint, error) { + recordsList, err := p.recordsForDNS() + if err != nil { + return nil, err + } + + endpoints := make([]*endpoint.Endpoint, 0) + recordMap := groupDomainRecordList(recordsList) + for _, recordList := range recordMap { + name := getDnsDomain(*recordList.RecordList[0].Name, *recordList.Domain.Name) + recordType := *recordList.RecordList[0].Type + ttl := *recordList.RecordList[0].TTL + var targets []string + for _, record := range recordList.RecordList { + targets = append(targets, *record.Value) + } + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)) + } + return endpoints, nil +} + +func (p *TencentCloudProvider) recordsForDNS() (map[uint64]*RecordListGroup, error) { + domainList, err := p.getDomainList() + if err != nil { + return nil, err + } + + recordListGroup := make(map[uint64]*RecordListGroup, 0) + for _, domain := range domainList { + records, err := p.getDomainRecordList(*domain.Name) + if err != nil { + return nil, err + } + for _, record := range records { + if *record.Type == "TXT" && strings.HasPrefix(*record.Value, "heritage=") { + record.Value = common.StringPtr(fmt.Sprintf(`"%s"`, *record.Value)) + } + } + recordListGroup[*domain.DomainId] = &RecordListGroup{ + Domain: domain, + RecordList: records, + } + } + return recordListGroup, nil +} + +func (p *TencentCloudProvider) getDomainList() ([]*dnspod.DomainListItem, error) { + request := dnspod.NewDescribeDomainListRequest() + request.Offset = common.Int64Ptr(0) + request.Limit = common.Int64Ptr(3000) + + domainList := make([]*dnspod.DomainListItem, 0) + totalCount := int64(100) + for *request.Offset < totalCount { + response, err := p.apiService.DescribeDomainList(request) + if err != nil { + return nil, err + } + if response.Response.DomainList != nil && len(response.Response.DomainList) > 0 { + if !p.domainFilter.IsConfigured() { + domainList = append(domainList, response.Response.DomainList...) + } else { + for _, domain := range response.Response.DomainList { + if p.domainFilter.Match(*domain.Name) { + domainList = append(domainList, domain) + } + } + } + } + totalCount = int64(*response.Response.DomainCountInfo.AllTotal) + request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.DomainList))) + } + return domainList, nil +} + +func (p *TencentCloudProvider) getDomainRecordList(domain string) ([]*dnspod.RecordListItem, error) { + request := dnspod.NewDescribeRecordListRequest() + request.Domain = common.StringPtr(domain) + request.Offset = common.Uint64Ptr(0) + request.Limit = common.Uint64Ptr(3000) + + domainList := make([]*dnspod.RecordListItem, 0) + totalCount := uint64(100) + for *request.Offset < totalCount { + response, err := p.apiService.DescribeRecordList(request) + if err != nil { + return nil, err + } + if response.Response.RecordList != nil && len(response.Response.RecordList) > 0 { + for _, record := range response.Response.RecordList { + if *record.Name == "@" && *record.Type == "NS" { // Special Record, Skip it. + continue + } + domainList = append(domainList, record) + } + } + totalCount = *response.Response.RecordCountInfo.TotalCount + request.Offset = common.Uint64Ptr(*request.Offset + uint64(len(response.Response.RecordList))) + } + return domainList, nil +} + +type RecordListGroup struct { + Domain *dnspod.DomainListItem + RecordList []*dnspod.RecordListItem +} + +func (p *TencentCloudProvider) applyChangesForDNS(changes *plan.Changes) error { + recordsGroupMap, err := p.recordsForDNS() + if err != nil { + return err + } + + zoneNameIDMapper := provider.ZoneIDName{} + for _, recordsGroup := range recordsGroupMap { + if recordsGroup.Domain.DomainId != nil { + zoneNameIDMapper.Add(strconv.FormatUint(*recordsGroup.Domain.DomainId, 10), *recordsGroup.Domain.Name) + } + } + + // Apply Change Delete + deleteEndpoints := make(map[string][]uint64) + for _, change := range [][]*endpoint.Endpoint{changes.Delete, changes.UpdateOld} { + for _, deleteChange := range change { + if zoneId, _ := zoneNameIDMapper.FindZone(deleteChange.DNSName); zoneId != "" { + zoneIdString, _ := strconv.ParseUint(zoneId, 10, 64) + recordListGroup := recordsGroupMap[zoneIdString] + for _, domainRecord := range recordListGroup.RecordList { + subDomain := getSubDomain(*recordListGroup.Domain.Name, deleteChange) + if *domainRecord.Name == subDomain && *domainRecord.Type == deleteChange.RecordType { + for _, target := range deleteChange.Targets { + if *domainRecord.Value == target { + if _, exist := deleteEndpoints[*recordListGroup.Domain.Name]; !exist { + deleteEndpoints[*recordListGroup.Domain.Name] = make([]uint64, 0) + } + deleteEndpoints[*recordListGroup.Domain.Name] = append(deleteEndpoints[*recordListGroup.Domain.Name], *domainRecord.RecordId) + } + } + } + } + } + } + } + + if err := p.deleteRecords(deleteEndpoints); err != nil { + return err + } + + // Apply Change Create + createEndpoints := make(map[string][]*endpoint.Endpoint) + for zoneId := range zoneNameIDMapper { + createEndpoints[zoneId] = make([]*endpoint.Endpoint, 0) + } + for _, change := range [][]*endpoint.Endpoint{changes.Create, changes.UpdateNew} { + for _, createChange := range change { + if zoneId, _ := zoneNameIDMapper.FindZone(createChange.DNSName); zoneId != "" { + createEndpoints[zoneId] = append(createEndpoints[zoneId], createChange) + } + } + } + if err := p.createRecord(recordsGroupMap, createEndpoints); err != nil { + return err + } + return nil +} + +func (p *TencentCloudProvider) createRecord(zoneMap map[uint64]*RecordListGroup, endpointsMap map[string][]*endpoint.Endpoint) error { + for zoneId, endpoints := range endpointsMap { + zoneIdString, _ := strconv.ParseUint(zoneId, 10, 64) + domain := zoneMap[zoneIdString] + for _, endpoint := range endpoints { + for _, target := range endpoint.Targets { + if endpoint.RecordType == "TXT" && strings.HasPrefix(target, `"heritage=`) { + target = strings.Trim(target, `"`) + } + if err := p.createRecords(domain.Domain, endpoint, target); err != nil { + return err + } + } + } + } + return nil +} + +func (p *TencentCloudProvider) createRecords(domain *dnspod.DomainListItem, endpoint *endpoint.Endpoint, target string) error { + request := dnspod.NewCreateRecordRequest() + + request.Domain = common.StringPtr(*domain.Name) + request.RecordType = common.StringPtr(endpoint.RecordType) + request.Value = common.StringPtr(target) + request.SubDomain = common.StringPtr(getSubDomain(*domain.Name, endpoint)) + if endpoint.RecordTTL.IsConfigured() { + request.TTL = common.Uint64Ptr(uint64(endpoint.RecordTTL)) + } + request.RecordLine = common.StringPtr("默认") + + if _, err := p.apiService.CreateRecord(request); err != nil { + return err + } + return nil +} + +func (p *TencentCloudProvider) deleteRecords(RecordIdsMap map[string][]uint64) error { + for domain, recordIds := range RecordIdsMap { + if len(recordIds) == 0 { + continue + } + if err := p.deleteRecord(domain, recordIds); err != nil { + return err + } + } + return nil +} + +func (p *TencentCloudProvider) deleteRecord(domain string, recordIds []uint64) error { + request := dnspod.NewDeleteRecordRequest() + request.Domain = common.StringPtr(domain) + + for _, recordId := range recordIds { + request.RecordId = common.Uint64Ptr(recordId) + if _, err := p.apiService.DeleteRecord(request); err != nil { + return err + } + } + return nil +} + +func groupDomainRecordList(recordListGroup map[uint64]*RecordListGroup) (endpointMap map[string]*RecordListGroup) { + endpointMap = make(map[string]*RecordListGroup) + + for _, recordGroup := range recordListGroup { + for _, record := range recordGroup.RecordList { + key := fmt.Sprintf("%s:%s.%s", *record.Type, *record.Name, *recordGroup.Domain.Name) + if *record.Name == TencentCloudEmptyPrefix { + key = fmt.Sprintf("%s:%s", *record.Type, *recordGroup.Domain.Name) + } + if _, exist := endpointMap[key]; !exist { + endpointMap[key] = &RecordListGroup{ + Domain: recordGroup.Domain, + RecordList: make([]*dnspod.RecordListItem, 0), + } + } + endpointMap[key].RecordList = append(endpointMap[key].RecordList, record) + } + } + + return endpointMap +} diff --git a/provider/tencentcloud/privatedns.go b/provider/tencentcloud/privatedns.go new file mode 100644 index 000000000..ad2941833 --- /dev/null +++ b/provider/tencentcloud/privatedns.go @@ -0,0 +1,319 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tencentcloud + +import ( + "fmt" + "strings" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" +) + +// PrivateZone For Internal Dns + +func (p *TencentCloudProvider) privateZoneRecords() ([]*endpoint.Endpoint, error) { + privateZones, err := p.recordForPrivateZone() + if err != nil { + return nil, err + } + + endpoints := make([]*endpoint.Endpoint, 0) + recordMap := groupPrivateZoneRecords(privateZones) + for _, recordList := range recordMap { + name := getDnsDomain(*recordList.RecordList[0].SubDomain, *recordList.Zone.Domain) + recordType := *recordList.RecordList[0].RecordType + ttl := *recordList.RecordList[0].TTL + var targets []string + for _, record := range recordList.RecordList { + targets = append(targets, *record.RecordValue) + } + endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...)) + } + return endpoints, nil +} + +func (p *TencentCloudProvider) recordForPrivateZone() (map[string]*PrivateZoneRecordListGroup, error) { + privateZones, err := p.getPrivateZones() + if err != nil { + return nil, err + } + + recordListGroup := make(map[string]*PrivateZoneRecordListGroup, 0) + for _, zone := range privateZones { + records, err := p.getPrivateZoneRecords(*zone.ZoneId) + if err != nil { + return nil, err + } + + for _, record := range records { + if *record.RecordType == "TXT" && strings.HasPrefix(*record.RecordValue, "heritage=") { + record.RecordValue = common.StringPtr(fmt.Sprintf("\"%s\"", *record.RecordValue)) + } + } + recordListGroup[*zone.ZoneId] = &PrivateZoneRecordListGroup{ + Zone: zone, + RecordList: records, + } + } + + return recordListGroup, nil +} + +func (p *TencentCloudProvider) getPrivateZones() ([]*privatedns.PrivateZone, error) { + filters := make([]*privatedns.Filter, 1) + filters[0] = &privatedns.Filter{ + Name: common.StringPtr("Vpc"), + Values: []*string{ + common.StringPtr(p.vpcID), + }, + } + + if p.zoneIDFilter.IsConfigured() { + zoneIDs := make([]*string, len(p.zoneIDFilter.ZoneIDs)) + for index, zoneId := range p.zoneIDFilter.ZoneIDs { + zoneIDs[index] = common.StringPtr(zoneId) + } + filters = append(filters, &privatedns.Filter{ + Name: common.StringPtr("ZoneId"), + Values: zoneIDs, + }) + } + + request := privatedns.NewDescribePrivateZoneListRequest() + request.Filters = filters + request.Offset = common.Int64Ptr(0) + request.Limit = common.Int64Ptr(100) + + privateZones := make([]*privatedns.PrivateZone, 0) + totalCount := int64(100) + for *request.Offset < totalCount { + response, err := p.apiService.DescribePrivateZoneList(request) + if err != nil { + return nil, err + } + if response.Response.PrivateZoneSet != nil && len(response.Response.PrivateZoneSet) > 0 { + privateZones = append(privateZones, response.Response.PrivateZoneSet...) + } + totalCount = *response.Response.TotalCount + request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.PrivateZoneSet))) + } + + privateZonesFilter := make([]*privatedns.PrivateZone, 0) + for _, privateZone := range privateZones { + if p.domainFilter.IsConfigured() && !p.domainFilter.Match(*privateZone.Domain) { + continue + } + privateZonesFilter = append(privateZonesFilter, privateZone) + } + return privateZonesFilter, nil +} + +func (p *TencentCloudProvider) getPrivateZoneRecords(zoneId string) ([]*privatedns.PrivateZoneRecord, error) { + request := privatedns.NewDescribePrivateZoneRecordListRequest() + request.ZoneId = common.StringPtr(zoneId) + request.Offset = common.Int64Ptr(0) + request.Limit = common.Int64Ptr(100) + + privateZoneRecords := make([]*privatedns.PrivateZoneRecord, 0) + totalCount := int64(100) + for *request.Offset < totalCount { + response, err := p.apiService.DescribePrivateZoneRecordList(request) + if err != nil { + return nil, err + } + if response.Response.RecordSet != nil && len(response.Response.RecordSet) > 0 { + privateZoneRecords = append(privateZoneRecords, response.Response.RecordSet...) + } + totalCount = *response.Response.TotalCount + request.Offset = common.Int64Ptr(*request.Offset + int64(len(response.Response.RecordSet))) + } + return privateZoneRecords, nil +} + +type PrivateZoneRecordListGroup struct { + Zone *privatedns.PrivateZone + RecordList []*privatedns.PrivateZoneRecord +} + +// Returns nil if the operation was successful or an error if the operation failed. +func (p *TencentCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error { + zoneGroups, err := p.recordForPrivateZone() + if err != nil { + return err + } + + // In PrivateDns Service. A Zone has at least one record. The last rule cannot be deleted. + for _, zoneGroup := range zoneGroups { + if !containsBaseRecord(zoneGroup.RecordList) { + err := p.createPrivateZoneRecord(zoneGroup.Zone, &endpoint.Endpoint{ + DNSName: *zoneGroup.Zone.Domain, + RecordType: "TXT", + }, "tencent_provider_record") + if err != nil { + return err + } + } + } + + zoneNameIDMapper := provider.ZoneIDName{} + for _, zoneGroup := range zoneGroups { + if zoneGroup.Zone.ZoneId != nil { + zoneNameIDMapper.Add(*zoneGroup.Zone.ZoneId, *zoneGroup.Zone.Domain) + } + } + + // Apply Change Delete + deleteEndpoints := make(map[string][]string) + for _, change := range [][]*endpoint.Endpoint{changes.Delete, changes.UpdateOld} { + for _, deleteChange := range change { + if zoneId, _ := zoneNameIDMapper.FindZone(deleteChange.DNSName); zoneId != "" { + zoneGroup := zoneGroups[zoneId] + for _, zoneRecord := range zoneGroup.RecordList { + subDomain := getSubDomain(*zoneGroup.Zone.Domain, deleteChange) + if *zoneRecord.SubDomain == subDomain && *zoneRecord.RecordType == deleteChange.RecordType { + for _, target := range deleteChange.Targets { + if *zoneRecord.RecordValue == target { + if _, exist := deleteEndpoints[zoneId]; !exist { + deleteEndpoints[zoneId] = make([]string, 0) + } + deleteEndpoints[zoneId] = append(deleteEndpoints[zoneId], *zoneRecord.RecordId) + } + } + } + } + } + } + } + + if err := p.deletePrivateZoneRecords(deleteEndpoints); err != nil { + return err + } + + // Apply Change Create + createEndpoints := make(map[string][]*endpoint.Endpoint) + for _, change := range [][]*endpoint.Endpoint{changes.Create, changes.UpdateNew} { + for _, createChange := range change { + if zoneId, _ := zoneNameIDMapper.FindZone(createChange.DNSName); zoneId != "" { + if _, exist := createEndpoints[zoneId]; !exist { + createEndpoints[zoneId] = make([]*endpoint.Endpoint, 0) + } + createEndpoints[zoneId] = append(createEndpoints[zoneId], createChange) + } + } + } + if err := p.createPrivateZoneRecords(zoneGroups, createEndpoints); err != nil { + return err + } + return nil +} + +func containsBaseRecord(records []*privatedns.PrivateZoneRecord) bool { + for _, record := range records { + if *record.SubDomain == TencentCloudEmptyPrefix && *record.RecordType == "TXT" && *record.RecordValue == "tencent_provider_record" { + return true + } + } + return false +} + +func (p *TencentCloudProvider) createPrivateZoneRecords(zoneGroups map[string]*PrivateZoneRecordListGroup, endpointsMap map[string][]*endpoint.Endpoint) error { + for zoneId, endpoints := range endpointsMap { + zoneGroup := zoneGroups[zoneId] + for _, endpoint := range endpoints { + for _, target := range endpoint.Targets { + if endpoint.RecordType == "TXT" && strings.HasPrefix(target, "\"heritage=") { + target = strings.Trim(target, "\"") + } + if err := p.createPrivateZoneRecord(zoneGroup.Zone, endpoint, target); err != nil { + return err + } + } + } + } + return nil +} + +func (p *TencentCloudProvider) deletePrivateZoneRecords(zoneRecordIdsMap map[string][]string) error { + for zoneId, zoneRecordIds := range zoneRecordIdsMap { + if len(zoneRecordIds) == 0 { + continue + } + if err := p.deletePrivateZoneRecord(zoneId, zoneRecordIds); err != nil { + return err + } + } + return nil +} + +func (p *TencentCloudProvider) createPrivateZoneRecord(zone *privatedns.PrivateZone, endpoint *endpoint.Endpoint, target string) error { + request := privatedns.NewCreatePrivateZoneRecordRequest() + request.ZoneId = common.StringPtr(*zone.ZoneId) + request.RecordType = common.StringPtr(endpoint.RecordType) + request.RecordValue = common.StringPtr(target) + request.SubDomain = common.StringPtr(getSubDomain(*zone.Domain, endpoint)) + if endpoint.RecordTTL.IsConfigured() { + request.TTL = common.Int64Ptr(int64(endpoint.RecordTTL)) + } + + if _, err := p.apiService.CreatePrivateZoneRecord(request); err != nil { + return err + } + return nil +} + +func (p *TencentCloudProvider) deletePrivateZoneRecord(zoneId string, zoneRecordIds []string) error { + recordIds := make([]*string, len(zoneRecordIds)) + for index, recordId := range zoneRecordIds { + recordIds[index] = common.StringPtr(recordId) + } + + request := privatedns.NewDeletePrivateZoneRecordRequest() + request.ZoneId = common.StringPtr(zoneId) + request.RecordIdSet = recordIds + + if _, err := p.apiService.DeletePrivateZoneRecord(request); err != nil { + return err + } + return nil +} + +func groupPrivateZoneRecords(zoneRecords map[string]*PrivateZoneRecordListGroup) (endpointMap map[string]*PrivateZoneRecordListGroup) { + endpointMap = make(map[string]*PrivateZoneRecordListGroup) + + for _, recordGroup := range zoneRecords { + for _, record := range recordGroup.RecordList { + key := fmt.Sprintf("%s:%s.%s", *record.RecordType, *record.SubDomain, *recordGroup.Zone.Domain) + if *record.SubDomain == TencentCloudEmptyPrefix { + key = fmt.Sprintf("%s:%s", *record.RecordType, *recordGroup.Zone.Domain) + } + if _, exist := endpointMap[key]; !exist { + endpointMap[key] = &PrivateZoneRecordListGroup{ + Zone: recordGroup.Zone, + RecordList: make([]*privatedns.PrivateZoneRecord, 0), + } + } + endpointMap[key].RecordList = append(endpointMap[key].RecordList, record) + } + } + + return endpointMap +} diff --git a/provider/tencentcloud/tencent_cloud.go b/provider/tencentcloud/tencent_cloud.go new file mode 100644 index 000000000..e551af663 --- /dev/null +++ b/provider/tencentcloud/tencent_cloud.go @@ -0,0 +1,122 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tencentcloud + +import ( + "context" + "encoding/json" + "fmt" + "os" + "strings" + + log "github.com/sirupsen/logrus" + + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/provider/tencentcloud/cloudapi" +) + +const ( + TencentCloudEmptyPrefix = "@" + DefaultAPIRate = 9 +) + +func NewTencentCloudProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, configFile string, zoneType string, dryRun bool) (*TencentCloudProvider, error) { + cfg := tencentCloudConfig{} + if configFile != "" { + contents, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("failed to read Tencent Cloud config file '%s': %w", configFile, err) + } + err = json.Unmarshal(contents, &cfg) + if err != nil { + return nil, fmt.Errorf("failed to parse Tencent Cloud config file '%s': %w", configFile, err) + } + } + + var apiService cloudapi.TencentAPIService = cloudapi.NewTencentAPIService(cfg.RegionId, DefaultAPIRate, cfg.SecretId, cfg.SecretKey, cfg.InternetEndpoint) + if dryRun { + apiService = cloudapi.NewReadOnlyAPIService(cfg.RegionId, DefaultAPIRate, cfg.SecretId, cfg.SecretKey, cfg.InternetEndpoint) + } + + tencentCloudProvider := &TencentCloudProvider{ + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + apiService: apiService, + vpcID: cfg.VPCId, + privateZone: zoneType == "private", + } + + return tencentCloudProvider, nil +} + +type TencentCloudProvider struct { + provider.BaseProvider + logger *log.Logger + apiService cloudapi.TencentAPIService + domainFilter endpoint.DomainFilter + zoneIDFilter provider.ZoneIDFilter // Private Zone only + vpcID string // Private Zone only + privateZone bool +} + +type tencentCloudConfig struct { + RegionId string `json:"regionId" yaml:"regionId"` + SecretId string `json:"secretId" yaml:"secretId"` + SecretKey string `json:"secretKey" yaml:"secretKey"` + VPCId string `json:"vpcId" yaml:"vpcId"` + InternetEndpoint bool `json:"internetEndpoint" yaml:"internetEndpoint"` +} + +func (p *TencentCloudProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + if p.privateZone { + return p.privateZoneRecords() + } + return p.dnsRecords() +} + +func (p *TencentCloudProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + if !changes.HasChanges() { + return nil + } + + log.Infof("apply changes. %s", cloudapi.JsonWrapper(changes)) + + if p.privateZone { + return p.applyChangesForPrivateZone(changes) + } + return p.applyChangesForDNS(changes) +} + +func getSubDomain(domain string, endpoint *endpoint.Endpoint) string { + name := endpoint.DNSName + name = name[:len(name)-len(domain)] + name = strings.TrimSuffix(name, ".") + + if name == "" { + return TencentCloudEmptyPrefix + } + return name +} + +func getDnsDomain(subDomain string, domain string) string { + if subDomain == TencentCloudEmptyPrefix { + return domain + } + return subDomain + "." + domain +} diff --git a/provider/tencentcloud/tencent_cloud_test.go b/provider/tencentcloud/tencent_cloud_test.go new file mode 100644 index 000000000..f5b29093b --- /dev/null +++ b/provider/tencentcloud/tencent_cloud_test.go @@ -0,0 +1,404 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tencentcloud + +import ( + "context" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + dnspod "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod/v20210323" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" + "sigs.k8s.io/external-dns/provider" + "sigs.k8s.io/external-dns/provider/tencentcloud/cloudapi" + "testing" + + privatedns "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns/v20201028" +) + +func NewMockTencentCloudProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, zoneType string) *TencentCloudProvider { + cfg := tencentCloudConfig{ + //SecretId: "", + //SecretKey: "", + RegionId: "ap-shanghai", + VPCId: "vpc-abcdefg", + } + + zoneId1 := common.StringPtr(cloudapi.RandStringRunes(8)) + + privateZones := []*privatedns.PrivateZone{ + { + ZoneId: zoneId1, + Domain: common.StringPtr("external-dns-test.com"), + VpcSet: []*privatedns.VpcInfo{ + { + UniqVpcId: common.StringPtr("vpc-abcdefg"), + Region: common.StringPtr("ap-shanghai"), + }, + }, + }, + } + + zoneRecordId1 := common.StringPtr(cloudapi.RandStringRunes(8)) + zoneRecordId2 := common.StringPtr(cloudapi.RandStringRunes(8)) + privateZoneRecords := map[string][]*privatedns.PrivateZoneRecord{ + *zoneId1: { + { + ZoneId: zoneId1, + RecordId: zoneRecordId1, + SubDomain: common.StringPtr("nginx"), + RecordType: common.StringPtr("TXT"), + RecordValue: common.StringPtr("heritage=external-dns,external-dns/owner=default"), + TTL: common.Int64Ptr(300), + }, + { + ZoneId: zoneId1, + RecordId: zoneRecordId2, + SubDomain: common.StringPtr("nginx"), + RecordType: common.StringPtr("A"), + RecordValue: common.StringPtr("10.10.10.10"), + TTL: common.Int64Ptr(300), + }, + }, + } + + dnsDomainId1 := common.Uint64Ptr(cloudapi.RandUint64()) + dnsPodDomains := []*dnspod.DomainListItem{ + { + DomainId: dnsDomainId1, + Name: common.StringPtr("external-dns-test.com"), + }, + } + dnsDomainRecordId1 := common.Uint64Ptr(cloudapi.RandUint64()) + dnsDomainRecordId2 := common.Uint64Ptr(cloudapi.RandUint64()) + dnspodRecords := map[string][]*dnspod.RecordListItem{ + "external-dns-test.com": { + { + RecordId: dnsDomainRecordId1, + Value: common.StringPtr("heritage=external-dns,external-dns/owner=default"), + Name: common.StringPtr("nginx"), + Type: common.StringPtr("TXT"), + TTL: common.Uint64Ptr(300), + }, + { + RecordId: dnsDomainRecordId2, + Name: common.StringPtr("nginx"), + Type: common.StringPtr("A"), + Value: common.StringPtr("10.10.10.10"), + TTL: common.Uint64Ptr(300), + }, + }, + } + + var apiService cloudapi.TencentAPIService = cloudapi.NewMockService(privateZones, privateZoneRecords, dnsPodDomains, dnspodRecords) + + tencentCloudProvider := &TencentCloudProvider{ + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + apiService: apiService, + vpcID: cfg.VPCId, + privateZone: zoneType == "private", + } + + return tencentCloudProvider +} + +func TestTencentPrivateProvider_Records(t *testing.T) { + p := NewMockTencentCloudProvider(endpoint.NewDomainFilter([]string{"external-dns-test.com"}), provider.NewZoneIDFilter([]string{}), "private") + endpoints, err := p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Create、UpdateOld、UpdateNew、Delete + // The base record will be created. + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "redis.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("4.3.2.1"), + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "nginx.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("10.10.10.10"), + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "nginx.external-dns-test.com", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 3 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete one target + changes = &plan.Changes{ + Delete: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("5.6.7.8"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 3 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete another target + changes = &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "redis.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("5.6.7.8"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 3 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete another target + changes = &plan.Changes{ + Delete: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("1.2.3.4"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } +} + +func TestTencentPublicProvider_Records(t *testing.T) { + p := NewMockTencentCloudProvider(endpoint.NewDomainFilter([]string{"external-dns-test.com"}), provider.NewZoneIDFilter([]string{}), "public") + endpoints, err := p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Create、UpdateOld、UpdateNew、Delete + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "redis.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("4.3.2.1"), + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "nginx.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("10.10.10.10"), + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "nginx.external-dns-test.com", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete one target + changes = &plan.Changes{ + Delete: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("5.6.7.8"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete another target + changes = &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "redis.external-dns-test.com", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("5.6.7.8"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 2 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } + + // Test for Delete another target + changes = &plan.Changes{ + Delete: []*endpoint.Endpoint{ + { + DNSName: "tencent.external-dns-test.com", + RecordType: "A", + RecordTTL: 600, + Targets: endpoint.NewTargets("1.2.3.4"), + }, + }, + } + if err := p.ApplyChanges(context.Background(), changes); err != nil { + t.Errorf("Failed to get records: %v", err) + } + endpoints, err = p.Records(context.Background()) + if err != nil { + t.Errorf("Failed to get records: %v", err) + } else { + if len(endpoints) != 1 { + t.Errorf("Incorrect number of records: %d", len(endpoints)) + } + for _, endpoint := range endpoints { + t.Logf("Endpoint for %+v", *endpoint) + } + } +} diff --git a/provider/zone_id_filter.go b/provider/zone_id_filter.go index e15581f27..50a016b2d 100644 --- a/provider/zone_id_filter.go +++ b/provider/zone_id_filter.go @@ -43,3 +43,11 @@ func (f ZoneIDFilter) Match(zoneID string) bool { return false } + +// IsConfigured returns true if DomainFilter is configured, false otherwise +func (f ZoneIDFilter) IsConfigured() bool { + if len(f.ZoneIDs) == 1 { + return f.ZoneIDs[0] != "" + } + return len(f.ZoneIDs) > 0 +}