From 05068e3ad3b2541eb25f87be7b2de67b46c13f1a Mon Sep 17 00:00:00 2001 From: Devatoria Date: Tue, 31 Jul 2018 11:05:19 -0400 Subject: [PATCH 1/9] Add filter by service type feature --- main.go | 1 + pkg/apis/externaldns/types.go | 3 ++ source/service.go | 29 ++++++++++- source/service_test.go | 98 +++++++++++++++++++++++++++++++++-- source/store.go | 3 +- 5 files changed, 127 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 295594560..4f8cbb3da 100644 --- a/main.go +++ b/main.go @@ -75,6 +75,7 @@ func main() { PublishInternal: cfg.PublishInternal, PublishHostIP: cfg.PublishHostIP, ConnectorServer: cfg.ConnectorSourceServer, + ServiceTypeFilter: cfg.ServiceTypeFilter, } // Lookup all the selected sources by names and pass them the desired configuration. diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index de55a2af3..5917f3c82 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -91,6 +91,7 @@ type Config struct { ExoscaleEndpoint string ExoscaleAPIKey string ExoscaleAPISecret string + ServiceTypeFilter []string } var defaultConfig = &Config{ @@ -144,6 +145,7 @@ var defaultConfig = &Config{ ExoscaleEndpoint: "https://api.exoscale.ch/dns", ExoscaleAPIKey: "", ExoscaleAPISecret: "", + ServiceTypeFilter: []string{}, } // NewConfig returns new Config object @@ -197,6 +199,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal) app.Flag("publish-host-ip", "Allow external-dns to publish host-ip for headless services (optional)").BoolVar(&cfg.PublishHostIP) app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer) + app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter) // Flags related to providers app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode") diff --git a/source/service.go b/source/service.go index 83c8ec990..cc6639ab7 100644 --- a/source/service.go +++ b/source/service.go @@ -52,10 +52,11 @@ type serviceSource struct { combineFQDNAnnotation bool publishInternal bool publishHostIP bool + serviceTypeFilter map[string]struct{} } // NewServiceSource creates a new serviceSource with the given config. -func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool) (Source, error) { +func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilter string, fqdnTemplate string, combineFqdnAnnotation bool, compatibility string, publishInternal bool, publishHostIP bool, serviceTypeFilter []string) (Source, error) { var ( tmpl *template.Template err error @@ -69,6 +70,13 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt } } + // Transform the slice into a map so it will + // be way much easier and fast to filter later + serviceTypes := make(map[string]struct{}) + for _, serviceType := range serviceTypeFilter { + serviceTypes[serviceType] = struct{}{} + } + return &serviceSource{ client: kubeClient, namespace: namespace, @@ -78,6 +86,7 @@ func NewServiceSource(kubeClient kubernetes.Interface, namespace, annotationFilt combineFQDNAnnotation: combineFqdnAnnotation, publishInternal: publishInternal, publishHostIP: publishHostIP, + serviceTypeFilter: serviceTypes, }, nil } @@ -92,6 +101,11 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) { return nil, err } + // filter on service types if at least one has been provided + if len(sc.serviceTypeFilter) > 0 { + services.Items = sc.filterByServiceType(services.Items) + } + // get the ip addresses of all the nodes and cache them for this run nodeTargets, err := sc.extractNodeTargets() if err != nil { @@ -254,6 +268,19 @@ func (sc *serviceSource) filterByAnnotations(services []v1.Service) ([]v1.Servic return filteredList, nil } +// filterByServiceType filters services according their types +func (sc *serviceSource) filterByServiceType(services []v1.Service) []v1.Service { + filteredList := []v1.Service{} + for _, service := range services { + // Check if the service is of the given type or not + if _, ok := sc.serviceTypeFilter[string(service.Spec.Type)]; ok { + filteredList = append(filteredList, service) + } + } + + return filteredList +} + func (sc *serviceSource) setResourceLabel(service v1.Service, endpoints []*endpoint.Endpoint) { for _, ep := range endpoints { ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("service/%s/%s", service.Namespace, service.Name) diff --git a/source/service_test.go b/source/service_test.go index dc06d61ec..edc4666ab 100644 --- a/source/service_test.go +++ b/source/service_test.go @@ -50,6 +50,7 @@ func (suite *ServiceSuite) SetupTest() { "", false, false, + []string{}, ) suite.fooWithTargets = &v1.Service{ Spec: v1.ServiceSpec{ @@ -99,10 +100,11 @@ func testServiceSourceImplementsSource(t *testing.T) { // testServiceSourceNewServiceSource tests that NewServiceSource doesn't return an error. func testServiceSourceNewServiceSource(t *testing.T) { for _, ti := range []struct { - title string - annotationFilter string - fqdnTemplate string - expectError bool + title string + annotationFilter string + fqdnTemplate string + serviceTypesFilter []string + expectError bool }{ { title: "invalid template", @@ -123,6 +125,11 @@ func testServiceSourceNewServiceSource(t *testing.T) { expectError: false, annotationFilter: "kubernetes.io/ingress.class=nginx", }, + { + title: "non-empty service types filter", + expectError: false, + serviceTypesFilter: []string{string(v1.ServiceTypeClusterIP)}, + }, } { t.Run(ti.title, func(t *testing.T) { _, err := NewServiceSource( @@ -134,6 +141,7 @@ func testServiceSourceNewServiceSource(t *testing.T) { "", false, false, + ti.serviceTypesFilter, ) if ti.expectError { @@ -161,6 +169,7 @@ func testServiceSourceEndpoints(t *testing.T) { annotations map[string]string clusterIP string lbs []string + serviceTypesFilter []string expected []*endpoint.Endpoint expectError bool }{ @@ -178,6 +187,7 @@ func testServiceSourceEndpoints(t *testing.T) { map[string]string{}, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -197,6 +207,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -218,6 +229,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "1.2.3.4", []string{}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -235,6 +247,7 @@ func testServiceSourceEndpoints(t *testing.T) { map[string]string{}, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.fqdn.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.fqdn.com", Targets: endpoint.Targets{"1.2.3.4"}}, @@ -257,6 +270,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, @@ -281,6 +295,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, @@ -303,6 +318,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, @@ -325,6 +341,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"lb.example.com"}, // Kubernetes omits the trailing dot + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, }, @@ -346,6 +363,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4", "lb.example.com"}, // Kubernetes omits the trailing dot + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"lb.example.com"}}, @@ -369,6 +387,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -391,6 +410,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -410,6 +430,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -431,6 +452,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -450,6 +472,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -472,6 +495,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -494,6 +518,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -514,6 +539,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, true, }, @@ -534,6 +560,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -556,6 +583,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -575,6 +603,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -594,6 +623,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4", "8.8.8.8"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4", "8.8.8.8"}}, }, @@ -615,6 +645,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, false, }, @@ -634,6 +665,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -657,6 +689,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "bar.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, @@ -677,6 +710,7 @@ func testServiceSourceEndpoints(t *testing.T) { map[string]string{}, "", []string{"1.2.3.4", "elb.com"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"elb.com"}}, @@ -699,6 +733,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4", "elb.com"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, {DNSName: "foo.example.org", Targets: endpoint.Targets{"elb.com"}}, @@ -721,6 +756,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "mate.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, }, @@ -740,6 +776,7 @@ func testServiceSourceEndpoints(t *testing.T) { map[string]string{}, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{}, true, }, @@ -759,6 +796,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, @@ -781,6 +819,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, @@ -803,6 +842,7 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(10)}, }, @@ -825,11 +865,54 @@ func testServiceSourceEndpoints(t *testing.T) { }, "", []string{"1.2.3.4"}, + []string{}, []*endpoint.Endpoint{ {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}, RecordTTL: endpoint.TTL(0)}, }, false, }, + { + "filter on service types should include matching services", + "", + "", + "testing", + "foo", + v1.ServiceTypeLoadBalancer, + "", + "", + false, + map[string]string{}, + map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + }, + "", + []string{"1.2.3.4"}, + []string{string(v1.ServiceTypeLoadBalancer)}, + []*endpoint.Endpoint{ + {DNSName: "foo.example.org", Targets: endpoint.Targets{"1.2.3.4"}}, + }, + false, + }, + { + "filter on service types should exclude non-matching services", + "", + "", + "testing", + "foo", + v1.ServiceTypeNodePort, + "", + "", + false, + map[string]string{}, + map[string]string{ + hostnameAnnotationKey: "foo.example.org.", + }, + "", + []string{"1.2.3.4"}, + []string{string(v1.ServiceTypeLoadBalancer)}, + []*endpoint.Endpoint{}, + false, + }, } { t.Run(tc.title, func(t *testing.T) { // Create a Kubernetes testing client @@ -876,6 +959,7 @@ func testServiceSourceEndpoints(t *testing.T) { tc.compatibility, false, false, + tc.serviceTypesFilter, ) require.NoError(t, err) @@ -1010,6 +1094,7 @@ func TestClusterIpServices(t *testing.T) { tc.compatibility, true, false, + []string{}, ) require.NoError(t, err) @@ -1206,6 +1291,7 @@ func TestNodePortServices(t *testing.T) { tc.compatibility, true, false, + []string{}, ) require.NoError(t, err) @@ -1406,6 +1492,7 @@ func TestHeadlessServices(t *testing.T) { tc.compatibility, true, false, + []string{}, ) require.NoError(t, err) @@ -1606,6 +1693,7 @@ func TestHeadlessServicesHostIP(t *testing.T) { tc.compatibility, true, true, + []string{}, ) require.NoError(t, err) @@ -1646,7 +1734,7 @@ func BenchmarkServiceEndpoints(b *testing.B) { _, err := kubernetes.CoreV1().Services(service.Namespace).Create(service) require.NoError(b, err) - client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false) + client, err := NewServiceSource(kubernetes, v1.NamespaceAll, "", "", false, "", false, false, []string{}) require.NoError(b, err) for i := 0; i < b.N; i++ { diff --git a/source/store.go b/source/store.go index d082a5b54..e6a24ce81 100644 --- a/source/store.go +++ b/source/store.go @@ -44,6 +44,7 @@ type Config struct { PublishInternal bool PublishHostIP bool ConnectorServer string + ServiceTypeFilter []string } // ClientGenerator provides clients @@ -92,7 +93,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err if err != nil { return nil, err } - return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP) + return NewServiceSource(client, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.Compatibility, cfg.PublishInternal, cfg.PublishHostIP, cfg.ServiceTypeFilter) case "ingress": client, err := p.KubeClient() if err != nil { From 1db16f35afcf7c47912a845e02bb02912bdb74e3 Mon Sep 17 00:00:00 2001 From: Li Yi Date: Sun, 22 Jul 2018 16:53:36 +0800 Subject: [PATCH 2/9] Initial support for Alibaba Cloud --- docs/tutorials/alibabacloud.md | 360 +++++++++++++ main.go | 2 + pkg/apis/externaldns/types.go | 7 +- provider/alibaba_cloud.go | 928 +++++++++++++++++++++++++++++++++ provider/alibaba_cloud_test.go | 385 ++++++++++++++ 5 files changed, 1681 insertions(+), 1 deletion(-) create mode 100644 docs/tutorials/alibabacloud.md create mode 100644 provider/alibaba_cloud.go create mode 100644 provider/alibaba_cloud_test.go diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md new file mode 100644 index 000000000..fc299e3f7 --- /dev/null +++ b/docs/tutorials/alibabacloud.md @@ -0,0 +1,360 @@ +# Setting up ExternalDNS for Services on Alibaba Cloud + +This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use **>=0.4** version of ExternalDNS for this tutorial + +## RAM Permissions + +```json +{ + "Version": "1", + "Statement": [ + { + "Action": "alidns:AddDomainRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "alidns:DeleteDomainRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "alidns:UpdateDomainRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "alidns:DescribeDomainRecords", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:AddZoneRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:DeleteZoneRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:UpdateZoneRecord", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:DescribeZoneRecords", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:DescribeZones", + "Resource": "*", + "Effect": "Allow" + }, + { + "Action": "pvtz:DescribeZoneInfo", + "Resource": "*", + "Effect": "Allow" + } + ] +} +``` + +When running on Alibaba Cloud, you need to make sure that your nodes (on which External DNS runs) have the RAM instance profile with the above RAM role assigned. + +## Set up a Alibaba Cloud DNS service or Private Zone service + +Alibaba Cloud DNS Service is the domain name resolution and management service for public access. It routes access from end-users to the designated web app. +Alibaba Cloud Private Zone is the domain name resolution and management service for VPC internal access. + +*If you prefer to try-out ExternalDNS in one of the existing domain or zone you can skip this step* + +Create a DNS domain which will contain the managed DNS records. For public DNS service, the domain name should be valid and owned by yourself. + +```console +$ aliyun alidns AddDomain --DomainName "external-dns-test.com" +``` + + +Make a note of the ID of the hosted zone you just created. + +```console +$ aliyun alidns DescribeDomains --KeyWord="external-dns-test.com" | jq -r '.Domains.Domain[0].DomainId' +``` + +## Deploy ExternalDNS + +Connect your `kubectl` client to the cluster you want to test ExternalDNS with. +Then apply one of the following manifests file to deploy ExternalDNS. + +### Manifest (for clusters without RBAC enabled) +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + 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=alibabacloud + - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization + - --alibaba-cloud-zone=public # only look at public hosted zones (valid values are public, private or no value for both) + - --registry=txt + - --txt-owner-id=my-identifier +``` + +### Manifest (for clusters with RBAC enabled) + +```yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: external-dns +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: external-dns +rules: +- apiGroups: [""] + resources: ["services"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["pods"] + verbs: ["get","watch","list"] +- apiGroups: ["extensions"] + resources: ["ingresses"] + verbs: ["get","watch","list"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["list"] +--- +apiVersion: rbac.authorization.k8s.io/v1beta1 +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: extensions/v1beta1 +kind: Deployment +metadata: + name: external-dns +spec: + strategy: + type: Recreate + template: + metadata: + labels: + app: external-dns + spec: + serviceAccountName: external-dns + containers: + - name: external-dns + image: registry.opensource.zalan.do/teapot/external-dns:latest + 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=alibabacloud + - --policy=upsert-only # would prevent ExternalDNS from deleting any records, omit to enable full synchronization + - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) + - --registry=txt + - --txt-owner-id=my-identifier +``` + + + +## Arguments + +This list is not the full list, but a few arguments that where chosen. + +### alibabacloud-zone-type + +`alibabacloud-zone-type` allows filtering for private and public zones + +* If value is `public`, it will sync with records in Alibaba Cloud DNS Service +* If value is `private`, it will sync with records in Alibaba Cloud Private Zone Service + + +## Verify ExternalDNS works (Ingress example) + +Create an ingress resource manifest file. + +> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object. + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: foo + annotations: + kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller. +spec: + rules: + - host: foo.external-dns-test.com + http: + paths: + - backend: + serviceName: foo + servicePort: 80 +``` + +## Verify ExternalDNS works (Service example) + +Create the following sample application to test that ExternalDNS works. + +> For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value. + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + annotations: + external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.com. +spec: + type: LoadBalancer + ports: + - port: 80 + name: http + targetPort: 80 + selector: + app: nginx + +--- + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: nginx +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 + name: http +``` + +After roughly two minutes check that a corresponding DNS record for your service was created. + +```console +$ aliyun aliyun alidns DescribeDomainRecords --DomainName=external-dns-test.com +{ + "PageNumber": 1, + "TotalCount": 1, + "PageSize": 20, + "RequestId": "1DBEF426-F771-46C7-9802-4989E9C94EE8", + "DomainRecords": { + "Record": [ + { + "RR": "nginx", + "Status": "ENABLE", + "Value": "1.2.3.4", + "Weight": 1, + "RecordId": "3994015629411328", + "Type": "A", + "DomainName": "external-dns-test.com", + "Locked": false, + "Line": "default", + "TTL": 600 + }, + { + "RR": "nginx", + "Status": "ENABLE", + "Value": "heritage=external-dns;external-dns/owner=my-identifier", + "Weight": 1, + "RecordId": "3994015629411329", + "Type": "TTL", + "DomainName": "external-dns-test.com", + "Locked": false, + "Line": "default", + "TTL": 600 + } + ] + } +} +``` + +Note created TXT record alongside ALIAS record. TXT record signifies that the corresponding ALIAS record is managed by ExternalDNS. This makes ExternalDNS safe for running in environments where there are other records managed via other means. + +Let's check that we can resolve this DNS name. We'll ask the nameservers assigned to your zone first. + +```console +$ dig nginx.external-dns-test.com. +``` + +If you hooked up your DNS zone with its parent zone correctly you can use `curl` to access your site. + +```console +$ curl nginx.external-dns-test.com. + + + +Welcome to nginx! +... + + +... + + +``` + +## Custom TTL + +The default DNS record TTL (Time-To-Live) is 300 seconds. You can customize this value by setting the annotation `external-dns.alpha.kubernetes.io/ttl`. +e.g., modify the service manifest YAML file above: + +```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/ttl: 60 +spec: + ... +``` + +This will set the DNS record's TTL to 60 seconds. + +## Clean up + +Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly. + +```console +$ kubectl delete service nginx +``` + +Give ExternalDNS some time to clean up the DNS records for you. Then delete the hosted zone if you created one for the testing purpose. + +```console +$ aliyun alidns DeleteDomain --DomainName external-dns-test.com +``` diff --git a/main.go b/main.go index 295594560..235247104 100644 --- a/main.go +++ b/main.go @@ -96,6 +96,8 @@ func main() { var p provider.Provider switch cfg.Provider { + case "alibabacloud": + p, err = provider.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun) case "aws": p, err = provider.NewAWSProvider( provider.AWSConfig{ diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index de55a2af3..252b8d4e7 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -52,6 +52,8 @@ type Config struct { GoogleProject string DomainFilter []string ZoneIDFilter []string + AlibabaCloudConfigFile string + AlibabaCloudZoneType string AWSZoneType string AWSAssumeRole string AWSMaxChangeCount int @@ -109,6 +111,7 @@ var defaultConfig = &Config{ Provider: "", GoogleProject: "", DomainFilter: []string{}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", AWSAssumeRole: "", AWSMaxChangeCount: 4000, @@ -199,10 +202,12 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer) // Flags related to providers - app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode") + app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode") 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("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter) app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject) + app.Flag("alibaba-cloud-config-file", "When using the Alibaba Cloud provider, specify the Alibaba Cloud configuration file (required when --provider=alibabacloud").Default(defaultConfig.AlibabaCloudConfigFile).StringVar(&cfg.AlibabaCloudConfigFile) + app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private") app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private") app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole) app.Flag("aws-max-change-count", "When using the AWS provider, set the maximum number of changes that will be applied.").Default(strconv.Itoa(defaultConfig.AWSMaxChangeCount)).IntVar(&cfg.AWSMaxChangeCount) diff --git a/provider/alibaba_cloud.go b/provider/alibaba_cloud.go new file mode 100644 index 000000000..836d83eec --- /dev/null +++ b/provider/alibaba_cloud.go @@ -0,0 +1,928 @@ +/* +Copyright 2017 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 provider + +import ( + "fmt" + "io/ioutil" + + log "github.com/sirupsen/logrus" + + "gopkg.in/yaml.v2" + + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" + + "github.com/kubernetes-incubator/external-dns/endpoint" + "github.com/kubernetes-incubator/external-dns/plan" + + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" + "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz" + "github.com/denverdino/aliyungo/metadata" + "strings" +) + +const ( + defaultAlibabaCloudRecordTTL = 600 + defaultAlibabaCloudPrivateZoneRecordTTL = 60 + defaultAlibabaCloudPageSize = 50 + nullHostAlibabaCloud = "@" +) + +// AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing. +// See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods. + +type AlibabaCloudDNSAPI interface { + AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) + DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) + UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) + DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) +} + +// AlibabaCloudDNSAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing. +// See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods. +type AlibabaCloudPrivateZoneAPI interface { + AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) + DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) + UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) + DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) + DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) + DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) +} + +// AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. + +type AlibabaCloudProvider struct { + domainFilter DomainFilter + zoneIDFilter ZoneIDFilter // Private Zone only + zoneTypeFilter ZoneTypeFilter + MaxChangeCount int + EvaluateTargetHealth bool + AssumeRole string + vpcID string // Private Zone only + dryRun bool + dnsClient AlibabaCloudDNSAPI + pvtzClient AlibabaCloudPrivateZoneAPI + privateZone bool +} + +type alibabaCloudConfig struct { + RegionID string `json:"regionId" yaml:"regionId"` + AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"` + VPCID string `json:"vpcId" yaml:"vpcId"` + RoleName string `json:"-" yaml:"-"` // For ECS RAM role only +} + +// NewAlibabaCloudProvider creates a new Alibaba Cloud provider. +// +// Returns the provider or an error if a provider could not be created. +func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneIDFileter ZoneIDFilter, zoneType string, dryRun bool) (*AlibabaCloudProvider, error) { + cfg := alibabaCloudConfig{} + if configFile != "" { + contents, err := ioutil.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("Failed to read Alibaba Cloud config file '%s': %v", configFile, err) + } + err = yaml.Unmarshal(contents, &cfg) + if err != nil { + return nil, fmt.Errorf("Failed to parse Alibaba Cloud config file '%s': %v", configFile, err) + } + } else { + // Load config from Metadata Service + m := metadata.NewMetaData(nil) + roleName := "" + var err error + if roleName, err = m.RoleName(); err != nil { + return nil, fmt.Errorf("Failed to get role name from Metadata Service: %v", err) + } + vpcID, err := m.VpcID() + if err != nil { + return nil, fmt.Errorf("Failed to get VPC ID from Metadata Service: %v", err) + } + regionID, err := m.Region() + if err != nil { + return nil, fmt.Errorf("Failed to get Region ID from Metadata Service: %v", err) + } + cfg.RegionID = regionID + cfg.RoleName = roleName + cfg.VPCID = vpcID + } + + // Public DNS service + var dnsClient AlibabaCloudDNSAPI + var err error + + if cfg.RoleName == "" { + dnsClient, err = alidns.NewClientWithAccessKey( + cfg.RegionID, + cfg.AccessKeyID, + cfg.AccessKeySecret, + ) + } else { + dnsClient, err = alidns.NewClientWithEcsRamRole( + cfg.RegionID, + cfg.RoleName, + ) + } + + if err != nil { + return nil, fmt.Errorf("failed to create Alibaba Cloud DNS client: %v", err) + } + + // Private DNS service + var pvtzClient AlibabaCloudPrivateZoneAPI + if cfg.RoleName == "" { + pvtzClient, err = pvtz.NewClientWithAccessKey( + "cn-hangzhou", // The Private Zone location is fixed + cfg.AccessKeyID, + cfg.AccessKeySecret, + ) + } else { + pvtzClient, err = pvtz.NewClientWithEcsRamRole( + "cn-hangzhou", // The Private Zone location is fixed + cfg.RoleName, + ) + } + + provider := &AlibabaCloudProvider{ + domainFilter: domainFilter, + zoneIDFilter: zoneIDFileter, + vpcID: cfg.VPCID, + dryRun: dryRun, + dnsClient: dnsClient, + pvtzClient: pvtzClient, + privateZone: (zoneType == "private"), + } + return provider, nil +} + +// Records gets the current records. +// +// Returns the current records or an error if the operation failed. +func (p *AlibabaCloudProvider) Records() (endpoints []*endpoint.Endpoint, err error) { + if p.privateZone { + endpoints, err = p.privateZoneRecords() + } else { + endpoints, err = p.recordsForDNS() + } + return endpoints, err +} + +// ApplyChanges applies the given changes. +// +// Returns nil if the operation was successful or an error if the operation failed. +func (p *AlibabaCloudProvider) ApplyChanges(changes *plan.Changes) error { + if changes == nil || len(changes.Create)+len(changes.Delete)+len(changes.UpdateNew) == 0 { + // No op + return nil + } + + if p.privateZone { + return p.applyChangesForPrivateZone(changes) + } else { + return p.applyChangesForDNS(changes) + } +} + +func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string { + if rr == nullHostAlibabaCloud { + return domain + } else { + return rr + "." + domain + } +} + +// recordsForDNS gets the current records. +// +// Returns the current records or an error if the operation failed. +func (p *AlibabaCloudProvider) recordsForDNS() (endpoints []*endpoint.Endpoint, _ error) { + + records, err := p.records() + if err != nil { + return nil, err + } + for _, recordList := range p.groupRecords(records) { + name := p.getDNSName(recordList[0].RR, recordList[0].DomainName) + recordType := recordList[0].Type + ttl := recordList[0].TTL + + if ttl == defaultAlibabaCloudRecordTTL { + ttl = 0 + } + + var targets []string + for _, record := range recordList { + target := record.Value + if recordType == "TXT" { + target = p.unescapeTXTRecordValue(target) + } + targets = append(targets, target) + } + ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...) + endpoints = append(endpoints, ep) + } + return endpoints, nil +} + +func getNextPageNumber(pageNumber, pageSize, totalCount int) int { + + if pageNumber*pageSize >= totalCount { + return 0 + } else { + return pageNumber + 1 + } + +} + +func (p *AlibabaCloudProvider) getRecordKey(record alidns.Record) string { + if record.RR == nullHostAlibabaCloud { + return record.Type + ":" + record.DomainName + } else { + return record.Type + ":" + record.RR + "." + record.DomainName + } +} + +func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoint) string { + return endpoint.RecordType + ":" + endpoint.DNSName +} + +func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMap map[string][]alidns.Record) { + + endpointMap = make(map[string][]alidns.Record) + + for _, record := range records { + + key := p.getRecordKey(record) + + recordList := endpointMap[key] + endpointMap[key] = append(recordList, record) + + } + + return endpointMap +} + +func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) { + log.Debug("Retrieving Alibaba Cloud DNS Domain Records") + var results []alidns.Record + + for _, domainName := range p.domainFilter.filters { + request := alidns.CreateDescribeDomainRecordsRequest() + request.DomainName = domainName + request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) + request.PageNumber = "1" + + for { + response, err := p.dnsClient.DescribeDomainRecords(request) + + if err != nil { + log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err) + return nil, err + } + + for _, record := range response.DomainRecords.Record { + + domainName := record.DomainName + recordType := record.Type + + if !p.domainFilter.Match(domainName) { + continue + } + + if !supportedRecordType(recordType) { + continue + } + + //TODO filter Locked record + results = append(results, record) + } + nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalCount) + if nextPage == 0 { + break + } else { + request.PageNumber = requests.NewInteger(nextPage) + } + } + } + log.Infof("Found %d Alibaba Cloud DNS record(s).", len(results)) + return results, nil +} + +func (p *AlibabaCloudProvider) applyChangesForDNS(changes *plan.Changes) error { + log.Debugf("ApplyChanges to Alibaba Cloud DNS: %++v", *changes) + + records, err := p.records() + if err != nil { + return err + } + + recordMap := p.groupRecords(records) + + p.createRecords(changes.Create) + p.deleteRecords(recordMap, changes.Delete) + p.updateRecords(recordMap, changes.UpdateNew) + return nil +} + +func (p *AlibabaCloudProvider) escapeTXTRecordValue(value string) string { + // For unsupported chars + return value +} + +func (p *AlibabaCloudProvider) unescapeTXTRecordValue(value string) string { + if strings.HasPrefix(value, "heritage=") { + return fmt.Sprintf("\"%s\"", strings.Replace(value, ";", ",", -1)) + } + return value +} + +func (p *AlibabaCloudProvider) createRecord(endpoint *endpoint.Endpoint, target string) error { + rr, domain := p.splitDNSName(endpoint) + request := alidns.CreateAddDomainRecordRequest() + request.DomainName = domain + request.Type = endpoint.RecordType + request.RR = rr + + ttl := int(endpoint.RecordTTL) + if ttl != 0 { + request.TTL = requests.NewInteger(ttl) + } + + if endpoint.RecordType == "TXT" { + target = p.escapeTXTRecordValue(target) + } + + request.Value = target + + if p.dryRun { + log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName, target, ttl) + return nil + } + + response, err := p.dnsClient.AddDomainRecord(request) + if err == nil { + log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: Record ID=%s", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) + } else { + log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err) + } + return err +} + +func (p *AlibabaCloudProvider) createRecords(endpoints []*endpoint.Endpoint) error { + for _, endpoint := range endpoints { + for _, target := range endpoint.Targets { + p.createRecord(endpoint, target) + } + } + return nil +} + +func (p *AlibabaCloudProvider) deleteRecord(recordID string) error { + if p.dryRun { + log.Infof("Dry run: Delete record id '%s' in Alibaba Cloud DNS", recordID) + return nil + } + + request := alidns.CreateDeleteDomainRecordRequest() + request.RecordId = recordID + response, err := p.dnsClient.DeleteDomainRecord(request) + if err == nil { + log.Infof("Delete record id '%s' in Alibaba Cloud DNS", response.RecordId) + } else { + log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", err) + } + return err +} + +func (p *AlibabaCloudProvider) updateRecord(record alidns.Record, endpoint *endpoint.Endpoint) error { + request := alidns.CreateUpdateDomainRecordRequest() + request.RecordId = record.RecordId + request.RR = record.RR + request.Type = record.Type + request.Value = record.Value + ttl := int(endpoint.RecordTTL) + if ttl != 0 { + request.TTL = requests.NewInteger(ttl) + } + response, err := p.dnsClient.UpdateDomainRecord(request) + if err == nil { + log.Infof("Update record id '%s' in Alibaba Cloud DNS", response.RecordId) + } else { + log.Errorf("Failed to update record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err) + } + return err +} + +func (p *AlibabaCloudProvider) deleteRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error { + for _, endpoint := range endpoints { + key := p.getRecordKeyByEndpoint(endpoint) + records := recordMap[key] + found := false + for _, record := range records { + value := record.Value + if record.Type == "TXT" { + value = p.unescapeTXTRecordValue(value) + } + + for _, target := range endpoint.Targets { + // Find matched record to delete + if value == target { + p.deleteRecord(record.RecordId) + found = true + break + } + } + } + if !found { + log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud DNS", endpoint.RecordType, endpoint.DNSName) + } + } + return nil +} + +func (p *AlibabaCloudProvider) equals(record alidns.Record, endpoint *endpoint.Endpoint) bool { + ttl1 := record.TTL + if ttl1 == defaultAlibabaCloudRecordTTL { + ttl1 = 0 + } + + ttl2 := int(endpoint.RecordTTL) + if ttl2 == defaultAlibabaCloudRecordTTL { + ttl2 = 0 + } + + return ttl1 == ttl2 +} + +func (p *AlibabaCloudProvider) updateRecords(recordMap map[string][]alidns.Record, endpoints []*endpoint.Endpoint) error { + + for _, endpoint := range endpoints { + key := p.getRecordKeyByEndpoint(endpoint) + records := recordMap[key] + for _, record := range records { + value := record.Value + if record.Type == "TXT" { + value = p.unescapeTXTRecordValue(value) + } + found := false + for _, target := range endpoint.Targets { + // Find matched record to delete + if value == target { + found = true + } + } + if found { + if !p.equals(record, endpoint) { + // Update record + p.updateRecord(record, endpoint) + } + } else { + p.deleteRecord(record.RecordId) + } + } + for _, target := range endpoint.Targets { + if endpoint.RecordType == "TXT" { + target = p.escapeTXTRecordValue(target) + } + found := false + for _, record := range records { + // Find matched record to delete + if record.Value == target { + found = true + } + } + if !found { + p.createRecord(endpoint, target) + } + } + } + return nil +} + +func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr string, domain string) { + + name := strings.TrimSuffix(endpoint.DNSName, ".") + + found := false + + for _, filter := range p.domainFilter.filters { + if strings.HasSuffix(name, "."+filter) { + rr = name[0 : len(name)-len(filter)-1] + domain = filter + found = true + break + } else if name == filter { + domain = filter + rr = "" + found = true + } + } + + if !found { + idx := strings.Index(name, ".") + if idx >= 0 { + rr = name[0:idx] + domain = name[idx+1:] + } else { + rr = name + domain = "" + } + } + if rr == "" { + rr = nullHostAlibabaCloud + } + return rr, domain +} + +func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool { + request := pvtz.CreateDescribeZoneInfoRequest() + request.ZoneId = zoneID + response, err := p.pvtzClient.DescribeZoneInfo(request) + if err != nil { + log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err) + return false + } + foundVPC := false + for _, vpc := range response.BindVpcs.Vpc { + if vpc.VpcId == p.vpcID { + foundVPC = true + break + } + } + return foundVPC +} + +func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { + + var zones []pvtz.Zone + + request := pvtz.CreateDescribeZonesRequest() + request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) + request.PageNumber = "1" + + for { + response, err := p.pvtzClient.DescribeZones(request) + if err != nil { + log.Errorf("Failed to describe zones in Alibaba Cloud DNS: %v", err) + return nil, err + } + for _, zone := range response.Zones.Zone { + log.Debugf("Zone: %++v", zone) + + if !p.zoneIDFilter.Match(zone.ZoneId) { + continue + } + if !p.domainFilter.Match(zone.ZoneName) { + continue + } + if !p.matchVPC(zone.ZoneId) { + continue + } + zones = append(zones, zone) + } + nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalItems) + if nextPage == 0 { + break + } else { + request.PageNumber = requests.NewInteger(nextPage) + } + } + return zones, nil +} + +type alibabaPrivateZone struct { + pvtz.Zone + records []pvtz.Record +} + +func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone, error) { + log.Debug("Retrieving Alibaba Cloud Private Zone records") + + result := make(map[string]*alibabaPrivateZone) + recordsCount := 0 + + zones, err := p.privateZones() + + if err != nil { + return nil, err + } + + for _, zone := range zones { + + request := pvtz.CreateDescribeZoneRecordsRequest() + request.ZoneId = zone.ZoneId + request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) + request.PageNumber = "1" + var records []pvtz.Record + + for { + response, err := p.pvtzClient.DescribeZoneRecords(request) + + if err != nil { + log.Errorf("Failed to describe zone record '%s' in Alibaba Cloud DNS: %v", zone.ZoneId, err) + return nil, err + } + + for _, record := range response.Records.Record { + + recordType := record.Type + + if !supportedRecordType(recordType) { + continue + } + + //TODO filter Locked + records = append(records, record) + } + nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalItems) + if nextPage == 0 { + break + } else { + request.PageNumber = requests.NewInteger(nextPage) + } + } + + privateZone := alibabaPrivateZone{ + Zone: zone, + records: records, + } + recordsCount += len(records) + result[zone.ZoneName] = &privateZone + } + log.Debugf("Found %d Alibaba Cloud Private Zone record(s).", recordsCount) + return result, nil +} + +func (p *AlibabaCloudProvider) groupPrivateZoneRecords(zone *alibabaPrivateZone) (endpointMap map[string][]pvtz.Record) { + + endpointMap = make(map[string][]pvtz.Record) + + for _, record := range zone.records { + key := record.Type + ":" + record.Rr + recordList := endpointMap[key] + endpointMap[key] = append(recordList, record) + } + + return endpointMap +} + +// recordsForPrivateZone gets the current records. +// +// Returns the current records or an error if the operation failed. +func (p *AlibabaCloudProvider) privateZoneRecords() (endpoints []*endpoint.Endpoint, _ error) { + + zones, err := p.getPrivateZones() + if err != nil { + return nil, err + } + + for _, zone := range zones { + recordMap := p.groupPrivateZoneRecords(zone) + for _, recordList := range recordMap { + name := p.getDNSName(recordList[0].Rr, zone.ZoneName) + recordType := recordList[0].Type + ttl := recordList[0].Ttl + if ttl == defaultAlibabaCloudPrivateZoneRecordTTL { + ttl = 0 + } + var targets []string + for _, record := range recordList { + target := record.Value + if recordType == "TXT" { + target = p.unescapeTXTRecordValue(target) + } + targets = append(targets, target) + } + ep := endpoint.NewEndpointWithTTL(name, recordType, endpoint.TTL(ttl), targets...) + endpoints = append(endpoints, ep) + } + } + return endpoints, nil +} + +func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibabaPrivateZone, endpoint *endpoint.Endpoint, target string) error { + rr, domain := p.splitDNSName(endpoint) + zone := zones[domain] + if zone == nil { + err := fmt.Errorf("Failed to find private zone '%s'", domain) + log.Errorf("Failed to create %s record named '%s' to '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, err) + return err + } + + request := pvtz.CreateAddZoneRecordRequest() + request.ZoneId = zone.ZoneId + request.Type = endpoint.RecordType + request.Rr = rr + + ttl := int(endpoint.RecordTTL) + if ttl != 0 { + request.Ttl = requests.NewInteger(ttl) + } + + if endpoint.RecordType == "TXT" { + target = p.escapeTXTRecordValue(target) + } + + request.Value = target + + if p.dryRun { + log.Infof("Dry run: Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName, target, ttl) + return nil + } + + response, err := p.pvtzClient.AddZoneRecord(request) + if err == nil { + log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: Record ID=%d", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) + } else { + log.Errorf("Failed to create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, target, ttl, err) + } + return err +} + +func (p *AlibabaCloudProvider) createPrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { + for _, endpoint := range endpoints { + for _, target := range endpoint.Targets { + p.createPrivateZoneRecord(zones, endpoint, target) + } + } + return nil +} + +func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error { + + if p.dryRun { + log.Infof("Dry run: Delete record id '%d' in Alibaba Cloud Private Zone", recordID) + } + + request := pvtz.CreateDeleteZoneRecordRequest() + request.RecordId = requests.NewInteger(recordID) + + response, err := p.pvtzClient.DeleteZoneRecord(request) + if err == nil { + log.Infof("Delete record id '%d' in Alibaba Cloud Private Zone", response.RecordId) + } else { + log.Errorf("Failed to delete record '%s' in Alibaba Cloud Private Zone: %v", err) + } + return err +} + +func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { + for _, endpoint := range endpoints { + rr, domain := p.splitDNSName(endpoint) + + zone := zones[domain] + if zone == nil { + err := fmt.Errorf("Failed to find private zone '%s'", domain) + log.Errorf("Failed to delete %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) + continue + } + found := false + for _, record := range zone.records { + if rr == record.Rr && endpoint.RecordType == record.Type { + value := record.Value + if record.Type == "TXT" { + value = p.unescapeTXTRecordValue(value) + } + for _, target := range endpoint.Targets { + // Find matched record to delete + if value == target { + p.deletePrivateZoneRecord(record.RecordId) + found = true + break + } + } + } + } + if !found { + log.Errorf("Failed to find %s record named '%s' to delete for Alibaba Cloud Private Zone", endpoint.RecordType, endpoint.DNSName) + } + } + return nil +} + +// ApplyChanges applies the given changes. +// +// Returns nil if the operation was successful or an error if the operation failed. +func (p *AlibabaCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error { + log.Debugf("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes) + + zones, err := p.getPrivateZones() + if err != nil { + return err + } + + for zoneName, zone := range zones { + log.Debugf("%s: %++v", zoneName, zone) + } + + p.createPrivateZoneRecords(zones, changes.Create) + p.deletePrivateZoneRecords(zones, changes.Delete) + p.updatePrivateZoneRecords(zones, changes.UpdateNew) + return nil +} + +func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpoint *endpoint.Endpoint) error { + request := pvtz.CreateUpdateZoneRecordRequest() + request.RecordId = requests.NewInteger(record.RecordId) + request.Rr = record.Rr + request.Type = record.Type + request.Value = record.Value + ttl := int(endpoint.RecordTTL) + if ttl != 0 { + request.Ttl = requests.NewInteger(ttl) + } + response, err := p.pvtzClient.UpdateZoneRecord(request) + if err == nil { + log.Infof("Update record id '%d' in Alibaba Cloud Private Zone", response.RecordId) + } else { + log.Errorf("Failed to update record '%d' in Alibaba Cloud Private Zone: %v", response.RecordId, err) + } + return err +} + +func (p *AlibabaCloudProvider) equalsPrivateZone(record pvtz.Record, endpoint *endpoint.Endpoint) bool { + ttl1 := record.Ttl + if ttl1 == defaultAlibabaCloudPrivateZoneRecordTTL { + ttl1 = 0 + } + + ttl2 := int(endpoint.RecordTTL) + if ttl2 == defaultAlibabaCloudPrivateZoneRecordTTL { + ttl2 = 0 + } + + return ttl1 == ttl2 +} + +func (p *AlibabaCloudProvider) updatePrivateZoneRecords(zones map[string]*alibabaPrivateZone, endpoints []*endpoint.Endpoint) error { + + for _, endpoint := range endpoints { + rr, domain := p.splitDNSName(endpoint) + zone := zones[domain] + if zone == nil { + err := fmt.Errorf("Failed to find private zone '%s'", domain) + log.Errorf("Failed to update %s record named '%s' for Alibaba Cloud Private Zone: %v", endpoint.RecordType, endpoint.DNSName, err) + continue + } + + for _, record := range zone.records { + if record.Rr != rr || record.Type != endpoint.RecordType { + continue + } + value := record.Value + if record.Type == "TXT" { + value = p.unescapeTXTRecordValue(value) + } + found := false + for _, target := range endpoint.Targets { + // Find matched record to delete + if value == target { + found = true + break + } + } + if found { + if !p.equalsPrivateZone(record, endpoint) { + // Update record + p.updatePrivateZoneRecord(record, endpoint) + } + } else { + p.deletePrivateZoneRecord(record.RecordId) + } + } + for _, target := range endpoint.Targets { + if endpoint.RecordType == "TXT" { + target = p.escapeTXTRecordValue(target) + } + found := false + for _, record := range zone.records { + if record.Rr != rr || record.Type != endpoint.RecordType { + continue + } + // Find matched record to delete + if record.Value == target { + found = true + break + } + } + if !found { + p.createPrivateZoneRecord(zones, endpoint, target) + } + } + } + return nil +} diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go new file mode 100644 index 000000000..9311bd96f --- /dev/null +++ b/provider/alibaba_cloud_test.go @@ -0,0 +1,385 @@ +/* +Copyright 2017 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 provider + +import ( + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns" + "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz" + "github.com/kubernetes-incubator/external-dns/endpoint" + "testing" + + "github.com/kubernetes-incubator/external-dns/plan" +) + +type mockAlibabaCloudDNSAPI struct { + records []alidns.Record +} + +func NewMockAlibabaCloudDNSAPI() *mockAlibabaCloudDNSAPI { + api := mockAlibabaCloudDNSAPI{} + api.records = []alidns.Record{ + alidns.Record{ + RecordId: "1", + DomainName: "container-service.top", + Type: "A", + TTL: 300, + RR: "abc", + Value: "1.2.3.4", + }, + alidns.Record{ + RecordId: "2", + DomainName: "container-service.top", + Type: "TXT", + TTL: 300, + RR: "abc", + Value: "heritage=external-dns;external-dns/owner=default", + }, + } + return &api +} + +func (m *mockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) { + ttl, _ := request.TTL.GetValue() + m.records = append(m.records, alidns.Record{ + RecordId: "3", + DomainName: request.DomainName, + Type: request.Type, + TTL: ttl, + RR: request.RR, + Value: request.Value, + }) + response = alidns.CreateAddDomainRecordResponse() + return response, nil +} + +func (m *mockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) { + var result []alidns.Record + for _, record := range m.records { + if record.RecordId != request.RecordId { + result = append(result, record) + } + } + m.records = result + response = alidns.CreateDeleteDomainRecordResponse() + response.RecordId = request.RecordId + return response, nil +} + +func (m *mockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) { + ttl, _ := request.TTL.GetValue() + for i, _ := range m.records { + if m.records[i].RecordId == request.RecordId { + m.records[i].TTL = ttl + } + } + response = alidns.CreateUpdateDomainRecordResponse() + response.RecordId = request.RecordId + return response, nil +} + +func (m *mockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) { + var result []alidns.Record + for _, record := range m.records { + if record.DomainName == request.DomainName { + result = append(result, record) + } + } + response = alidns.CreateDescribeDomainRecordsResponse() + response.DomainRecords.Record = result + return response, nil +} + +type mockAlibabaCloudPrivateZoneAPI struct { + zone pvtz.Zone + records []pvtz.Record +} + +func NewMockAlibabaCloudPrivateZoneAPI() *mockAlibabaCloudPrivateZoneAPI { + api := mockAlibabaCloudPrivateZoneAPI{} + api.zone = pvtz.Zone{ + ZoneId: "test-zone", + ZoneName: "container-service.top", + } + api.records = []pvtz.Record{ + pvtz.Record{ + RecordId: 1, + Type: "A", + Ttl: 300, + Rr: "abc", + Value: "1.2.3.4", + }, + pvtz.Record{ + RecordId: 2, + Type: "TXT", + Ttl: 300, + Rr: "abc", + Value: "heritage=external-dns;external-dns/owner=default", + }, + } + return &api +} + +func (m *mockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) { + ttl, _ := request.Ttl.GetValue() + m.records = append(m.records, pvtz.Record{ + RecordId: 3, + Type: request.Type, + Ttl: ttl, + Rr: request.Rr, + Value: request.Value, + }) + response = pvtz.CreateAddZoneRecordResponse() + return response, nil +} + +func (m *mockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) { + recordId, _ := request.RecordId.GetValue() + + var result []pvtz.Record + for _, record := range m.records { + if record.RecordId != recordId { + result = append(result, record) + } + } + response = pvtz.CreateDeleteZoneRecordResponse() + return response, nil +} + +func (m *mockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) { + recordId, _ := request.RecordId.GetValue() + ttl, _ := request.Ttl.GetValue() + for i, _ := range m.records { + if m.records[i].RecordId == recordId { + m.records[i].Ttl = ttl + } + } + response = pvtz.CreateUpdateZoneRecordResponse() + return response, nil +} + +func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) { + response = pvtz.CreateDescribeZoneRecordsResponse() + response.Records.Record = append(response.Records.Record, m.records...) + return response, nil +} + +func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) { + response = pvtz.CreateDescribeZonesResponse() + response.Zones.Zone = append(response.Zones.Zone, m.zone) + return response, nil +} + +func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) { + response = pvtz.CreateDescribeZoneInfoResponse() + response.ZoneId = m.zone.ZoneId + response.ZoneName = m.zone.ZoneName + return response, nil +} + +func newTestAlibabaCloudProvider(private bool) *AlibabaCloudProvider { + cfg := alibabaCloudConfig{ + RegionID: "cn-beijing", + AccessKeyID: "xxxxxx", + AccessKeySecret: "xxxxxx", + VPCID: "vpc-xxxxxx", + } + // + //dnsClient, _ := alidns.NewClientWithAccessKey( + // cfg.RegionID, + // cfg.AccessKeyID, + // cfg.AccessKeySecret, + //) + // + //pvtzClient, _ := pvtz.NewClientWithAccessKey( + // "cn-hangzhou", + // cfg.AccessKeyID, + // cfg.AccessKeySecret, + //) + domainFilterTest := NewDomainFilter([]string{"container-service.top.", "example.org"}) + + return &AlibabaCloudProvider{ + domainFilter: domainFilterTest, + vpcID: cfg.VPCID, + dryRun: false, + dnsClient: NewMockAlibabaCloudDNSAPI(), + pvtzClient: &mockAlibabaCloudPrivateZoneAPI{}, + privateZone: false, + } +} + +func TestAlibabaCloudProvider_Records(t *testing.T) { + p := newTestAlibabaCloudProvider(false) + endpoints, err := p.Records() + 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 TestAlibabaCloudProvider_ApplyChanges(t *testing.T) { + p := newTestAlibabaCloudProvider(false) + changes := plan.Changes{ + Create: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "xyz.container-service.top", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("4.3.2.1"), + }, + }, + UpdateNew: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "abc.container-service.top", + RecordType: "A", + RecordTTL: 500, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + }, + }, + Delete: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "abc.container-service.top", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), + }, + }, + } + p.ApplyChanges(&changes) + endpoints, err := p.Records() + 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 TestAlibabaCloudProvider_Records_PrivateZone(t *testing.T) { + p := newTestAlibabaCloudProvider(true) + endpoints, err := p.Records() + 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 TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) { + p := newTestAlibabaCloudProvider(true) + changes := plan.Changes{ + Create: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "xyz.container-service.top", + RecordType: "A", + RecordTTL: 300, + Targets: endpoint.NewTargets("4.3.2.1"), + }, + }, + UpdateNew: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "abc.container-service.top", + RecordType: "A", + RecordTTL: 500, + Targets: endpoint.NewTargets("1.2.3.4", "5.6.7.8"), + }, + }, + Delete: []*endpoint.Endpoint{ + &endpoint.Endpoint{ + DNSName: "abc.container-service.top", + RecordType: "TXT", + RecordTTL: 300, + Targets: endpoint.NewTargets("\"heritage=external-dns,external-dns/owner=default\""), + }, + }, + } + p.ApplyChanges(&changes) + endpoints, err := p.Records() + 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 TestAlibabaCloudProvider_splitDNSName(t *testing.T) { + p := newTestAlibabaCloudProvider(false) + endpoint := &endpoint.Endpoint{} + endpoint.DNSName = "www.example.org" + rr, domain := p.splitDNSName(endpoint) + if rr != "www" || domain != "example.org" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = ".example.org" + rr, domain = p.splitDNSName(endpoint) + if rr != "@" || domain != "example.org" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = "www" + rr, domain = p.splitDNSName(endpoint) + if rr != "www" || domain != "" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = "" + rr, domain = p.splitDNSName(endpoint) + if rr != "@" || domain != "" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = "_30000._tcp.container-service.top" + rr, domain = p.splitDNSName(endpoint) + if rr != "_30000._tcp" || domain != "container-service.top" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } + endpoint.DNSName = "container-service.top" + rr, domain = p.splitDNSName(endpoint) + if rr != "@" || domain != "container-service.top" { + t.Errorf("Failed to splitDNSName for %s: rr=%s, domain=%s", endpoint.DNSName, rr, domain) + } +} + +func TestAlibabaCloudProvider_TXTEndpoint(t *testing.T) { + + p := newTestAlibabaCloudProvider(false) + const recordValue = "heritage=external-dns;external-dns/owner=default" + const endpointTarget = "\"heritage=external-dns,external-dns/owner=default\"" + + if p.normalizedTXTRecordValue(endpointTarget) != recordValue { + t.Errorf("Failed to normalizedTXTRecordValue: %s", p.normalizedTXTRecordValue(endpointTarget)) + } + if p.unescapeTXTRecordValue(recordValue) != endpointTarget { + t.Errorf("Failed to unescapeTXTRecordValue: %s", p.unescapeTXTRecordValue(recordValue)) + } +} From 808a5aef3ad8d010a520ac254ab72f4c3ac9ab34 Mon Sep 17 00:00:00 2001 From: Li Yi Date: Wed, 29 Aug 2018 09:40:07 +0800 Subject: [PATCH 3/9] Add dep constraint for Alibaba Cloud --- Gopkg.lock | 135 +++++++++++++++++++++++++++++++++-------------------- Gopkg.toml | 6 ++- 2 files changed, 90 insertions(+), 51 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index b2af46e43..0732a4d47 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,7 +2,7 @@ [[projects]] - digest = "1:ae9d0182a5cf7dbb025a8fc5821234cc1f26ca342fc41d951a99f71b9adc1b87" + digest = "1:e94ea655a0038d2274be202f77a2ea0eb2d3f74dfee674fd5d1f541e81008039" name = "cloud.google.com/go" packages = [ "compute/metadata", @@ -12,7 +12,7 @@ revision = "3b1ae45394a234c385be014e9a488f2bb6eef821" [[projects]] - digest = "1:fd38e3b8c27cab6561a7d2e8557205c3ca5c57cbb6d3a79e10f22e73e84fd776" + digest = "1:b341fb465b057e991b166d073b35a224f5a84228e5ef7e40b4da7a70c152e7ec" name = "github.com/Azure/azure-sdk-for-go" packages = ["arm/dns"] pruneopts = "" @@ -20,7 +20,7 @@ version = "v10.0.4-beta" [[projects]] - digest = "1:f719ef698feb8da2923ebda9a8d553b977320b02182f3797e185202e588a94b1" + digest = "1:767f5f5dd4fa8e4f7f206726361d29aa0f7622b0bb8294b73d071864368c0d6b" name = "github.com/Azure/go-autorest" packages = [ "autorest", @@ -34,7 +34,7 @@ version = "v10.9.0" [[projects]] - digest = "1:7dc69d1597e4773ec5f64e5c078d55f0f011bb05ec0435346d0649ad978a23fd" + digest = "1:283a95024c33e84b23f24b1b47e3157ff2df2517d786a2e17bb0e6e4955e94e4" name = "github.com/alecthomas/kingpin" packages = ["."] pruneopts = "" @@ -43,7 +43,7 @@ [[projects]] branch = "master" - digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8" + digest = "1:1399282ad03ac819f0e8a747c888407c5c98bb497d33821a7047c7bae667ede0" name = "github.com/alecthomas/template" packages = [ ".", @@ -61,7 +61,27 @@ revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a" [[projects]] - digest = "1:1c82dd6a02941a3c4f23a32eca182717ab79691939e97d6b971466b780f06eea" + digest = "1:7b6c017b0290ccf1dd98c47a51e1db8b72b0863b6c7c52ddaa5a0d894aa3c2fc" + name = "github.com/aliyun/alibaba-cloud-sdk-go" + packages = [ + "sdk", + "sdk/auth", + "sdk/auth/credentials", + "sdk/auth/signers", + "sdk/endpoints", + "sdk/errors", + "sdk/requests", + "sdk/responses", + "sdk/utils", + "services/alidns", + "services/pvtz", + ] + pruneopts = "" + revision = "cad214d7d71fba7883fcf3b7e550ba782c15b400" + version = "1.27.7" + +[[projects]] + digest = "1:f04a72eefe1c7adec1dce30e099cec1e5fea8903a66e2db25bbbdfa66915428d" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -101,14 +121,14 @@ [[projects]] branch = "master" - digest = "1:0c5485088ce274fac2e931c1b979f2619345097b39d91af3239977114adf0320" + digest = "1:d20bdb6bf44087574af3139835946875bb098440426785282c741865b7bc66d3" name = "github.com/beorn7/perks" packages = ["quantile"] pruneopts = "" revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" [[projects]] - digest = "1:85fd00554a6ed5b33687684b76635d532c74141508b5bce2843d85e8a3c9dc91" + digest = "1:d9d9c71f9776ef8f15b5c0a20246d5303071294743863ac3f4dde056f8c7b40a" name = "github.com/cloudflare/cloudflare-go" packages = ["."] pruneopts = "" @@ -116,7 +136,7 @@ version = "v0.7.4" [[projects]] - digest = "1:eaeede87b418b97f9dee473f8940fd9b65ca5cdac0503350c7c8f8965ea3cf4d" + digest = "1:31259dbcb4c073aace59b951f5b471b3d5dbc4051b4a9d7e000f4392e143977e" name = "github.com/coreos/etcd" packages = [ "client", @@ -138,7 +158,7 @@ version = "v0.2.0" [[projects]] - digest = "1:56c130d885a4aacae1dd9c7b71cfe39912c7ebc1ff7d2b46083c8812996dc43b" + digest = "1:0a39ec8bf5629610a4bc7873a92039ee509246da3cef1a0ea60f1ed7e5f9cea5" name = "github.com/davecgh/go-spew" packages = ["spew"] pruneopts = "" @@ -146,7 +166,18 @@ version = "v1.1.0" [[projects]] - digest = "1:6098222470fe0172157ce9bbef5d2200df4edde17ee649c5d6e48330e4afa4c6" + branch = "master" + digest = "1:64ee6871ef691c663f910e29bc2f7c10c8c342b06665920f1138b6aa8b11cb5a" + name = "github.com/denverdino/aliyungo" + packages = [ + "metadata", + "util", + ] + pruneopts = "" + revision = "69560d9530f5265ba00ffad2520d7ef01c2cddd4" + +[[projects]] + digest = "1:2426da75f49e5b8507a6ed5d4c49b06b2ff795f4aec401c106b7db8fb2625cd7" name = "github.com/dgrijalva/jwt-go" packages = ["."] pruneopts = "" @@ -154,7 +185,7 @@ version = "v3.2.0" [[projects]] - digest = "1:32d1941b093bb945de75b0276348494be318d34f3df39c4413d61e002c800bc6" + digest = "1:3da5806ef37ea163fee80ed179d40a5e013e671ccbe321a04c47c5aee3d5080a" name = "github.com/digitalocean/godo" packages = [ ".", @@ -165,7 +196,7 @@ version = "v1.1.1" [[projects]] - digest = "1:5ffd39844bdd1259a6227d544f582c6686ce43c8c44399a46052fe3bd2bed93c" + digest = "1:ca3b228bf258217cff2070f4045e53729886c66a27bf9cce30dcbf8a575ea86a" name = "github.com/dnsimple/dnsimple-go" packages = ["dnsimple"] pruneopts = "" @@ -173,7 +204,7 @@ version = "v0.14.0" [[projects]] - digest = "1:e17d18b233f506404061c27ac4a08624dad38dcd0d49f9cfdae67a7772a4fb8c" + digest = "1:bfce2cc5b829073f93962e742275d45913948e22d182fbc5464104da1c5f2f89" name = "github.com/exoscale/egoscale" packages = ["."] pruneopts = "" @@ -182,7 +213,7 @@ [[projects]] branch = "master" - digest = "1:ae7fb2062735e966ab69d14d2a091f3778b0d676dc8d1f01d092bcb0fb8ed45b" + digest = "1:bc12846e4bae094e01a33ef98cad0a1afa35da37090e5126513be6f747e074ab" name = "github.com/ffledgling/pdns-go" packages = ["."] pruneopts = "" @@ -196,7 +227,7 @@ revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee" [[projects]] - digest = "1:a00483fe4106b86fb1187a92b5cf6915c85f294ed4c129ccbe7cb1f1a06abd46" + digest = "1:bbc763f3c703dc3c6a99a22c1318760099b52bc00a47a36dc4462e88eee7846b" name = "github.com/go-ini/ini" packages = ["."] pruneopts = "" @@ -204,7 +235,7 @@ version = "v1.32.0" [[projects]] - digest = "1:8e67153fc0a9fb0d6c9707e36cf80e217a012364307b222eb4ba6828f7e881e6" + digest = "1:cdeb6a9eb9f2356b2987c401d013d41e018b819ee1e8d5a1b32a5b714e53c392" name = "github.com/go-resty/resty" packages = ["."] pruneopts = "" @@ -212,7 +243,7 @@ version = "v1.8.0" [[projects]] - digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918" + digest = "1:673df1d02ca0c6f51458fe94bbb6fae0b05e54084a31db2288f1c4321255c2da" name = "github.com/gogo/protobuf" packages = [ "proto", @@ -230,7 +261,7 @@ revision = "44145f04b68cf362d9c4df2182967c2275eaefed" [[projects]] - digest = "1:617ff1d7c2ba3ba08f0c4cde79ff569324e972c3dbba3d42ed07f9649595c520" + digest = "1:4321edcf693514c7a3811c9b58a19705ef0b4aaec5ee4e5baa2f5c07ad3b96ae" name = "github.com/golang/protobuf" packages = [ "proto", @@ -266,7 +297,7 @@ revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c" [[projects]] - digest = "1:16b2837c8b3cf045fa2cdc82af0cf78b19582701394484ae76b2c3bc3c99ad73" + digest = "1:1962b5d00f5285d08504697049627d45ad876912894528d31cdc1c05cdc853f6" name = "github.com/googleapis/gnostic" packages = [ "OpenAPIv2", @@ -279,7 +310,7 @@ [[projects]] branch = "master" - digest = "1:54a44d48a24a104e078ef5f94d82f025a6be757e7c42b4370c621a3928d6ab7c" + digest = "1:815036d12757902f85888f3cb0440c2e00220dd4177e4c2bb048e03259db077a" name = "github.com/gophercloud/gophercloud" packages = [ ".", @@ -297,7 +328,7 @@ [[projects]] branch = "master" - digest = "1:009a1928b8c096338b68b5822d838a72b4d8520715c1463614476359f3282ec8" + digest = "1:8c4d156acec272201ffc4d1bdb9302de1c48314e0451eb38c70150cf11bdb33a" name = "github.com/gregjones/httpcache" packages = [ ".", @@ -322,14 +353,14 @@ revision = "61dc5f9b0a655ebf43026f0d8a837ad1e28e4b96" [[projects]] - digest = "1:6f49eae0c1e5dab1dafafee34b207aeb7a42303105960944828c2079b92fc88e" + digest = "1:4f767a115bc8e08576f6d38ab73c376fc1b1cd3bb5041171c9e8668cc7739b52" name = "github.com/jmespath/go-jmespath" packages = ["."] pruneopts = "" revision = "0b12d6b5" [[projects]] - digest = "1:53ac4e911e12dde0ab68655e2006449d207a5a681f084974da2b06e5dbeaca72" + digest = "1:890dd7615573f096655600bbe7beb2f532a437f6d8ef237831894301fca31f23" name = "github.com/json-iterator/go" packages = ["."] pruneopts = "" @@ -337,14 +368,14 @@ version = "1.1.4" [[projects]] - digest = "1:1c88ec29544b281964ed7a9a365b2802a523cd06c50cdee87eb3eec89cd864f4" + digest = "1:def40684a573560241c8344da452fa3574dfc2c7da525903992a3790d2262625" name = "github.com/kubernetes/repo-infra" packages = ["verify/boilerplate/test"] pruneopts = "" revision = "c2f9667a4c29e70a39b0e89db2d4f0cab907dbee" [[projects]] - digest = "1:7c23a751ce2f84663fa411acb87eae0da2d09c39a8e99b08bd8f65fae75d8928" + digest = "1:c3aa5f9d5119ca1cfdaa41a5084e3deceef0460eef3e6c71b58fa50e500f01a0" name = "github.com/linki/instrumented_http" packages = ["."] pruneopts = "" @@ -352,7 +383,7 @@ version = "v0.2.0" [[projects]] - digest = "1:1c41354ef11c9dbae2fe1ceee8369fcb2634977ba07e701e19ea53e8742c5420" + digest = "1:93d29291d0c37678592d77ee847031aec2ce1631f3ce4cf975b77216e8bd4a01" name = "github.com/linode/linodego" packages = ["."] pruneopts = "" @@ -361,7 +392,7 @@ [[projects]] branch = "master" - digest = "1:63722a4b1e1717be7b98fc686e0b30d5e7f734b9e93d7dee86293b6deab7ea28" + digest = "1:49a8b01a6cd6558d504b65608214ca40a78000e1b343ed0da5c6a9ccd83d6d30" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] pruneopts = "" @@ -384,7 +415,7 @@ version = "1.0.1" [[projects]] - digest = "1:d8b5d0ecca348c835914a1ed8589f17a6a7f309befab7327b0470324531f7ac4" + digest = "1:7aef6d4ad1b4a613d66ed554010c552a249e9afabcb079f54528a298474549cc" name = "github.com/nesv/go-dynect" packages = ["dynect"] pruneopts = "" @@ -392,7 +423,7 @@ version = "v0.6.0" [[projects]] - digest = "1:70df8e71a953626770223d4982801fa73e7e99cbfcca068b95127f72af9b9edd" + digest = "1:2062e45c462d0327f680340dce46fe11ae2d34bf802e15e397cb1d6c4d159b39" name = "github.com/oracle/oci-go-sdk" packages = [ "common", @@ -404,14 +435,14 @@ [[projects]] branch = "master" - digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc" + digest = "1:b7be9a944fe102bf466420fa8a064534dd12547a0482f5b684d228425b559b56" name = "github.com/petar/GoLLRB" packages = ["llrb"] pruneopts = "" revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4" [[projects]] - digest = "1:b46305723171710475f2dd37547edd57b67b9de9f2a6267cafdd98331fd6897f" + digest = "1:6db21ad64a13fe79220e47fcc895e13b8da923676a3a024f98210fca57a10d9a" name = "github.com/peterbourgon/diskv" packages = ["."] pruneopts = "" @@ -434,7 +465,7 @@ version = "v1.0.0" [[projects]] - digest = "1:4142d94383572e74b42352273652c62afec5b23f325222ed09198f46009022d1" + digest = "1:981835985f655d1d380cc6aa7d9fa9ad7abfaf40c75da200fd40d864cd05a7c3" name = "github.com/prometheus/client_golang" packages = [ "prometheus", @@ -446,7 +477,7 @@ [[projects]] branch = "master" - digest = "1:60aca47f4eeeb972f1b9da7e7db51dee15ff6c59f7b401c1588b8e6771ba15ef" + digest = "1:83bf37d060fca77e959fe5ceee81e58bbd1b01836f4addc70043a948e9912547" name = "github.com/prometheus/client_model" packages = ["go"] pruneopts = "" @@ -454,7 +485,7 @@ [[projects]] branch = "master" - digest = "1:e3aa5178be4fc4ae8cdb37d11c02f7490c00450a9f419e6aa84d02d3b47e90d2" + digest = "1:7221d79e41a24b2245d06f331d0825b479a9acd0bd05a8353806c7bf38395795" name = "github.com/prometheus/common" packages = [ "expfmt", @@ -465,7 +496,7 @@ revision = "2e54d0b93cba2fd133edc32211dcc32c06ef72ca" [[projects]] - digest = "1:a6a85fc81f2a06ccac3d45005523afbeee45138d781d4f3cb7ad9889d5c65aab" + digest = "1:91345f4cce04248cf4998c4f70a82579c1468101767636acf5af2e1556904933" name = "github.com/prometheus/procfs" packages = [ ".", @@ -483,7 +514,7 @@ version = "v1.2.0" [[projects]] - digest = "1:3ac248add5bb40a3c631c5334adcd09aa72d15af2768a5bc0274084ea7b2e5ba" + digest = "1:75e2c10fd48881dc9400b7b70281270923e01c44f1f5cb4bbc5ba8cac8ca3026" name = "github.com/sirupsen/logrus" packages = ["."] pruneopts = "" @@ -498,7 +529,7 @@ revision = "9ff6c6923cfffbcd502984b8e0c80539a94968b7" [[projects]] - digest = "1:306417ea2f31ea733df356a2b895de63776b6a5107085b33458e5cd6eb1d584d" + digest = "1:ba8fed52de60135b7efd5d832b997fb5b10fa09f227fa385174faa69f4219e4e" name = "github.com/stretchr/objx" packages = ["."] pruneopts = "" @@ -506,7 +537,7 @@ version = "v0.1" [[projects]] - digest = "1:a30066593578732a356dc7e5d7f78d69184ca65aeeff5939241a3ab10559bb06" + digest = "1:a70d585d45f695f2e8e6782569bdf181419667a35e6035ceb086706b495aa21a" name = "github.com/stretchr/testify" packages = [ "assert", @@ -527,21 +558,21 @@ revision = "ac974c61c2f990f4115b119354b5e0b47550e888" [[projects]] - digest = "1:cb2800cd5716e9d6172888e0e3ffe1f9c07b7f142eb83d49a391029bcf4f6cc1" + digest = "1:f98e0b7c7bd110a49d8bb56c9eefcef4f547f5d789025d3bfe9bd6b83125221b" name = "github.com/ugorji/go" packages = ["codec"] pruneopts = "" revision = "ded73eae5db7e7a0ef6f55aace87a2873c5d2b74" [[projects]] - digest = "1:0f67b4bbcdf1caaee0450f225a53fd2c2f8793578cc7810eb09c290e008e33ac" + digest = "1:d275cbe5049a91bd638e89ad365617be7eefc6d371fe8873fc658eba81dcfd24" name = "golang.org/x/crypto" packages = ["ssh/terminal"] pruneopts = "" revision = "d172538b2cfce0c13cee31e647d0367aa8cd2486" [[projects]] - digest = "1:8d5d4c9d4b35fe2abfc82ec0bb371d50ac9aef414f09ad756f5dff54dd81df94" + digest = "1:4a5f1ce99f596fe9e26a9d47d61b87642701197ce610100ea628796af88a6544" name = "golang.org/x/net" packages = [ "context", @@ -556,7 +587,7 @@ revision = "e90d6d0afc4c315a0d87a568ae68577cc15149a0" [[projects]] - digest = "1:dad5a319c4710358be1f2bf68f9fb7f90a71d7c641221b79801d5667b28f19e3" + digest = "1:2fef2e19e90f29efd775d58d66b5e100139fedbe24cf749f1c085c0a5aee86d3" name = "golang.org/x/oauth2" packages = [ ".", @@ -569,7 +600,7 @@ revision = "3c3a985cb79f52a3190fbc056984415ca6763d01" [[projects]] - digest = "1:b6e4bf8197b5de25f2ed05c603d39d619f4c41bb4a573ef5274c3446df44d6e4" + digest = "1:3830eb23292100e91d0fad721390e075e23f7ba34ac757d1c237d78ab74dcdb2" name = "golang.org/x/sys" packages = ["unix"] pruneopts = "" @@ -584,7 +615,7 @@ revision = "fbb02b2291d28baffd63558aa44b4b56f178d650" [[projects]] - digest = "1:2ad38d79865e33dde6157b7048debd6e7d47e0709df7b5e11bb888168e316908" + digest = "1:ab84306e7e74b9f01b9f1480d46cca9325f8c512567a0e7b8888d04ff627a5ba" name = "google.golang.org/api" packages = [ "dns/v1", @@ -596,7 +627,7 @@ revision = "a0ff90edab763c86aa88f2b1eb4f3301b82f6336" [[projects]] - digest = "1:41e2b7e287117f6136f75286d48072ecf781ba54823ffeb2098e152e7dc45ef6" + digest = "1:0b45fac4876cbd496ed7b95406b05c8c1eba559b43c82f2dda1b0e1bbe6cd1b6" name = "google.golang.org/appengine" packages = [ ".", @@ -630,7 +661,7 @@ [[projects]] branch = "master" - digest = "1:09ea5dbe52206b2d102fe418b840ca297f63883101c596039d68391596fe9bf1" + digest = "1:6043867e27773bdc28ff2739d3743ad7d52472e356b6fc9406e4a175939a6ecc" name = "k8s.io/api" packages = [ "admissionregistration/v1alpha1", @@ -667,7 +698,7 @@ revision = "183f3326a9353bd6d41430fc80f96259331d029c" [[projects]] - digest = "1:b6b2fb7b4da1ac973b64534ace2299a02504f16bc7820cb48edb8ca4077183e1" + digest = "1:a855f74be59f83ed0950a9a2b70d8c8af01fb5782d060c7dec67ae39033f30dc" name = "k8s.io/apimachinery" packages = [ "pkg/api/errors", @@ -714,7 +745,7 @@ version = "kubernetes-1.11.1" [[projects]] - digest = "1:d04779a8de7d5465e0463bd986506348de5e89677c74777f695d3145a7a8d15e" + digest = "1:3c4611c2b28fdc62391698bba7f212050f0f9ed75f3648f37ec3bcf8a83bf96d" name = "k8s.io/client-go" packages = [ "discovery", @@ -815,7 +846,7 @@ [[projects]] branch = "master" - digest = "1:526095379da1098c3f191a0008cc59c9bf9927492e63da7689e5de424219c162" + digest = "1:d93d8bcb5f04d6b59eafdb9fa1a80f187d2542611670bfabc0ea8e031ab874a2" name = "k8s.io/kube-openapi" packages = ["pkg/util/proto"] pruneopts = "" @@ -832,6 +863,9 @@ "github.com/Azure/go-autorest/autorest/azure", "github.com/Azure/go-autorest/autorest/to", "github.com/alecthomas/kingpin", + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests", + "github.com/aliyun/alibaba-cloud-sdk-go/services/alidns", + "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz", "github.com/aws/aws-sdk-go/aws", "github.com/aws/aws-sdk-go/aws/credentials/stscreds", "github.com/aws/aws-sdk-go/aws/request", @@ -840,6 +874,7 @@ "github.com/aws/aws-sdk-go/service/servicediscovery", "github.com/cloudflare/cloudflare-go", "github.com/coreos/etcd/client", + "github.com/denverdino/aliyungo/metadata", "github.com/digitalocean/godo", "github.com/digitalocean/godo/context", "github.com/dnsimple/dnsimple-go/dnsimple", diff --git a/Gopkg.toml b/Gopkg.toml index b1f1de8c8..a3866511d 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -78,4 +78,8 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"] [[constraint]] name = "github.com/linode/linodego" - version = "0.3.0" \ No newline at end of file + version = "0.3.0" + +[[constraint]] + name = "github.com/aliyun/alibaba-cloud-sdk-go" + version = "1.27.7" \ No newline at end of file From eab3f50802946ec6f564808c4dd187f9355ad791 Mon Sep 17 00:00:00 2001 From: Li Yi Date: Wed, 29 Aug 2018 16:15:13 +0800 Subject: [PATCH 4/9] Fix test case --- provider/alibaba_cloud_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go index 9311bd96f..e8ad7c53a 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibaba_cloud_test.go @@ -373,11 +373,11 @@ func TestAlibabaCloudProvider_splitDNSName(t *testing.T) { func TestAlibabaCloudProvider_TXTEndpoint(t *testing.T) { p := newTestAlibabaCloudProvider(false) - const recordValue = "heritage=external-dns;external-dns/owner=default" + const recordValue = "heritage=external-dns,external-dns/owner=default" const endpointTarget = "\"heritage=external-dns,external-dns/owner=default\"" - if p.normalizedTXTRecordValue(endpointTarget) != recordValue { - t.Errorf("Failed to normalizedTXTRecordValue: %s", p.normalizedTXTRecordValue(endpointTarget)) + if p.escapeTXTRecordValue(endpointTarget) != endpointTarget { + t.Errorf("Failed to escapeTXTRecordValue: %s", p.escapeTXTRecordValue(endpointTarget)) } if p.unescapeTXTRecordValue(recordValue) != endpointTarget { t.Errorf("Failed to unescapeTXTRecordValue: %s", p.unescapeTXTRecordValue(recordValue)) From e488e2bb0fdf9edbdbf57d124079cfbef7684633 Mon Sep 17 00:00:00 2001 From: xianlubird Date: Thu, 30 Aug 2018 14:29:39 +0800 Subject: [PATCH 5/9] Add aliyun sts token support --- README.md | 1 + docs/tutorials/alibabacloud.md | 5 + pkg/apis/externaldns/types_test.go | 2 + provider/alibaba_cloud.go | 331 +++++++++++++++++++++-------- provider/alibaba_cloud_test.go | 77 ++++--- 5 files changed, 293 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index c5473cd95..364aa12a5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Note that all flags can be replaced with environment variables; for instance, The following tutorials are provided: +* [Alibaba Cloud](docs/tutorials/alibabacloud.md) * [AWS (Route53)](docs/tutorials/aws.md) * [AWS (Service Discovery)](docs/tutorials/aws-sd.md) * [Azure](docs/tutorials/azure.md) diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index fc299e3f7..4a9b2bb00 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -28,6 +28,11 @@ This tutorial describes how to setup ExternalDNS for usage within a Kubernetes c "Resource": "*", "Effect": "Allow" }, + { + "Action": "alidns:DescribeDomains", + "Resource": "*", + "Effect": "Allow" + }, { "Action": "pvtz:AddZoneRecord", "Resource": "*", diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index 8824e2aa4..e1e0047f5 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -40,6 +40,7 @@ var ( GoogleProject: "", DomainFilter: []string{""}, ZoneIDFilter: []string{""}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "", AWSAssumeRole: "", AWSMaxChangeCount: 4000, @@ -86,6 +87,7 @@ var ( GoogleProject: "project", DomainFilter: []string{"example.org", "company.com"}, ZoneIDFilter: []string{"/hostedzone/ZTST1", "/hostedzone/ZTST2"}, + AlibabaCloudConfigFile: "/etc/kubernetes/alibaba-cloud.json", AWSZoneType: "private", AWSAssumeRole: "some-other-role", AWSMaxChangeCount: 100, diff --git a/provider/alibaba_cloud.go b/provider/alibaba_cloud.go index 836d83eec..7100bf5bb 100644 --- a/provider/alibaba_cloud.go +++ b/provider/alibaba_cloud.go @@ -33,6 +33,8 @@ import ( "github.com/aliyun/alibaba-cloud-sdk-go/services/pvtz" "github.com/denverdino/aliyungo/metadata" "strings" + "sync" + "time" ) const ( @@ -44,15 +46,15 @@ const ( // AlibabaCloudDNSAPI is a minimal implementation of DNS API that we actually use, used primarily for unit testing. // See https://help.aliyun.com/document_detail/29739.html for descriptions of all of its methods. - type AlibabaCloudDNSAPI interface { AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) + DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) } -// AlibabaCloudDNSAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing. +// AlibabaCloudPrivateZoneAPI is a minimal implementation of Private Zone API that we actually use, used primarily for unit testing. // See https://help.aliyun.com/document_detail/66234.html for descriptions of all of its methods. type AlibabaCloudPrivateZoneAPI interface { AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) @@ -64,7 +66,6 @@ type AlibabaCloudPrivateZoneAPI interface { } // AlibabaCloudProvider implements the DNS provider for Alibaba Cloud. - type AlibabaCloudProvider struct { domainFilter DomainFilter zoneIDFilter ZoneIDFilter // Private Zone only @@ -77,14 +78,18 @@ type AlibabaCloudProvider struct { dnsClient AlibabaCloudDNSAPI pvtzClient AlibabaCloudPrivateZoneAPI privateZone bool + clientLock sync.RWMutex + nextExpire time.Time } type alibabaCloudConfig struct { - RegionID string `json:"regionId" yaml:"regionId"` - AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"` - AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"` - VPCID string `json:"vpcId" yaml:"vpcId"` - RoleName string `json:"-" yaml:"-"` // For ECS RAM role only + RegionID string `json:"regionId" yaml:"regionId"` + AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret" yaml:"accessKeySecret"` + VPCID string `json:"vpcId" yaml:"vpcId"` + RoleName string `json:"-" yaml:"-"` // For ECS RAM role only + StsToken string `json:"-" yaml:"-"` + ExpireTime time.Time `json:"-" yaml:"-"` } // NewAlibabaCloudProvider creates a new Alibaba Cloud provider. @@ -102,24 +107,11 @@ func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneI return nil, fmt.Errorf("Failed to parse Alibaba Cloud config file '%s': %v", configFile, err) } } else { - // Load config from Metadata Service - m := metadata.NewMetaData(nil) - roleName := "" - var err error - if roleName, err = m.RoleName(); err != nil { - return nil, fmt.Errorf("Failed to get role name from Metadata Service: %v", err) + var tmpError error + cfg, tmpError = getCloudConfigFromStsToken() + if tmpError != nil { + return nil, fmt.Errorf("Failed to getCloudConfigFromStsToken: %v", tmpError) } - vpcID, err := m.VpcID() - if err != nil { - return nil, fmt.Errorf("Failed to get VPC ID from Metadata Service: %v", err) - } - regionID, err := m.Region() - if err != nil { - return nil, fmt.Errorf("Failed to get Region ID from Metadata Service: %v", err) - } - cfg.RegionID = regionID - cfg.RoleName = roleName - cfg.VPCID = vpcID } // Public DNS service @@ -133,9 +125,11 @@ func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneI cfg.AccessKeySecret, ) } else { - dnsClient, err = alidns.NewClientWithEcsRamRole( + dnsClient, err = alidns.NewClientWithStsToken( cfg.RegionID, - cfg.RoleName, + cfg.AccessKeyID, + cfg.AccessKeySecret, + cfg.StsToken, ) } @@ -152,9 +146,11 @@ func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneI cfg.AccessKeySecret, ) } else { - pvtzClient, err = pvtz.NewClientWithEcsRamRole( - "cn-hangzhou", // The Private Zone location is fixed - cfg.RoleName, + pvtzClient, err = pvtz.NewClientWithStsToken( + cfg.RegionID, + cfg.AccessKeyID, + cfg.AccessKeySecret, + cfg.StsToken, ) } @@ -165,11 +161,120 @@ func NewAlibabaCloudProvider(configFile string, domainFilter DomainFilter, zoneI dryRun: dryRun, dnsClient: dnsClient, pvtzClient: pvtzClient, - privateZone: (zoneType == "private"), + privateZone: zoneType == "private", + } + + if cfg.RoleName != "" { + provider.setNextExpire(cfg.ExpireTime) + go provider.refreshStsToken(1 * time.Second) } return provider, nil } +func getCloudConfigFromStsToken() (alibabaCloudConfig, error) { + cfg := alibabaCloudConfig{} + // Load config from Metadata Service + m := metadata.NewMetaData(nil) + roleName := "" + var err error + if roleName, err = m.RoleName(); err != nil { + return cfg, fmt.Errorf("Failed to get role name from Metadata Service: %v", err) + } + vpcID, err := m.VpcID() + if err != nil { + return cfg, fmt.Errorf("Failed to get VPC ID from Metadata Service: %v", err) + } + regionID, err := m.Region() + if err != nil { + return cfg, fmt.Errorf("Failed to get Region ID from Metadata Service: %v", err) + } + role, err := m.RamRoleToken(roleName) + if err != nil { + return cfg, fmt.Errorf("Failed to get STS Token from Metadata Service: %v", err) + } + cfg.RegionID = regionID + cfg.RoleName = roleName + cfg.VPCID = vpcID + cfg.AccessKeyID = role.AccessKeyId + cfg.AccessKeySecret = role.AccessKeySecret + cfg.StsToken = role.SecurityToken + cfg.ExpireTime = role.Expiration + return cfg, nil +} + +func (p *AlibabaCloudProvider) getDNSClient() AlibabaCloudDNSAPI { + p.clientLock.RLock() + defer p.clientLock.RUnlock() + return p.dnsClient +} + +func (p *AlibabaCloudProvider) getPvtzClient() AlibabaCloudPrivateZoneAPI { + p.clientLock.RLock() + defer p.clientLock.RUnlock() + return p.pvtzClient +} + +func (p *AlibabaCloudProvider) setNextExpire(expireTime time.Time) { + p.clientLock.Lock() + defer p.clientLock.Unlock() + p.nextExpire = expireTime +} + +func (p *AlibabaCloudProvider) refreshStsToken(sleepTime time.Duration) { + for { + time.Sleep(sleepTime) + now := time.Now() + utcLocation, err := time.LoadLocation("") + if err != nil { + log.Errorf("Get utc time error %v", err) + continue + } + nowTime := now.In(utcLocation) + p.clientLock.RLock() + sleepTime = p.nextExpire.Sub(nowTime) + p.clientLock.RUnlock() + log.Infof("Distance expiration time %v", sleepTime) + if sleepTime < time.Duration(10*time.Minute) { + sleepTime = time.Duration(time.Second * 1) + } else { + sleepTime = time.Duration(9 * time.Minute) + log.Info("Next fetch sts sleep interval : ", sleepTime.String()) + continue + } + cfg, err := getCloudConfigFromStsToken() + if err != nil { + log.Errorf("Failed to getCloudConfigFromStsToken: %v", err) + continue + } + dnsClient, err := alidns.NewClientWithStsToken( + cfg.RegionID, + cfg.AccessKeyID, + cfg.AccessKeySecret, + cfg.StsToken, + ) + if err != nil { + log.Errorf("Failed to new client with sts token %v", err) + continue + } + pvtzClient, err := pvtz.NewClientWithStsToken( + cfg.RegionID, + cfg.AccessKeyID, + cfg.AccessKeySecret, + cfg.StsToken, + ) + if err != nil { + log.Errorf("Failed to new client with sts token %v", err) + continue + } + log.Infof("Refresh client from sts token, next expire time %v", cfg.ExpireTime) + p.clientLock.Lock() + p.dnsClient = dnsClient + p.pvtzClient = pvtzClient + p.nextExpire = cfg.ExpireTime + p.clientLock.Unlock() + } +} + // Records gets the current records. // // Returns the current records or an error if the operation failed. @@ -193,17 +298,15 @@ func (p *AlibabaCloudProvider) ApplyChanges(changes *plan.Changes) error { if p.privateZone { return p.applyChangesForPrivateZone(changes) - } else { - return p.applyChangesForDNS(changes) } + return p.applyChangesForDNS(changes) } func (p *AlibabaCloudProvider) getDNSName(rr, domain string) string { if rr == nullHostAlibabaCloud { return domain - } else { - return rr + "." + domain } + return rr + "." + domain } // recordsForDNS gets the current records. @@ -242,18 +345,15 @@ func getNextPageNumber(pageNumber, pageSize, totalCount int) int { if pageNumber*pageSize >= totalCount { return 0 - } else { - return pageNumber + 1 } - + return pageNumber + 1 } func (p *AlibabaCloudProvider) getRecordKey(record alidns.Record) string { if record.RR == nullHostAlibabaCloud { return record.Type + ":" + record.DomainName - } else { - return record.Type + ":" + record.RR + "." + record.DomainName } + return record.Type + ":" + record.RR + "." + record.DomainName } func (p *AlibabaCloudProvider) getRecordKeyByEndpoint(endpoint *endpoint.Endpoint) string { @@ -277,53 +377,104 @@ func (p *AlibabaCloudProvider) groupRecords(records []alidns.Record) (endpointMa } func (p *AlibabaCloudProvider) records() ([]alidns.Record, error) { - log.Debug("Retrieving Alibaba Cloud DNS Domain Records") + log.Infof("Retrieving Alibaba Cloud DNS Domain Records") var results []alidns.Record - for _, domainName := range p.domainFilter.filters { - request := alidns.CreateDescribeDomainRecordsRequest() - request.DomainName = domainName - request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) - request.PageNumber = "1" - - for { - response, err := p.dnsClient.DescribeDomainRecords(request) - + if len(p.domainFilter.filters) == 1 && p.domainFilter.filters[0] == "" { + domainNames, tmpErr := p.getDomainList() + if tmpErr != nil { + log.Errorf("AlibabaCloudProvider getDomainList error %v", tmpErr) + return results, tmpErr + } + for _, tmpDomainName := range domainNames { + tmpResults, err := p.getDomainRecords(tmpDomainName) if err != nil { - log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err) - return nil, err + log.Errorf("AlibabaCloudProvider getDomainRecords %s error %v", tmpDomainName, err) + continue } - - for _, record := range response.DomainRecords.Record { - - domainName := record.DomainName - recordType := record.Type - - if !p.domainFilter.Match(domainName) { - continue - } - - if !supportedRecordType(recordType) { - continue - } - - //TODO filter Locked record - results = append(results, record) - } - nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalCount) - if nextPage == 0 { - break - } else { - request.PageNumber = requests.NewInteger(nextPage) + results = append(results, tmpResults...) + } + } else { + for _, domainName := range p.domainFilter.filters { + tmpResults, err := p.getDomainRecords(domainName) + if err != nil { + log.Errorf("getDomainRecords %s error %v", domainName, err) + continue } + results = append(results, tmpResults...) } } log.Infof("Found %d Alibaba Cloud DNS record(s).", len(results)) return results, nil } +func (p *AlibabaCloudProvider) getDomainList() ([]string, error) { + var domainNames []string + request := alidns.CreateDescribeDomainsRequest() + request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) + request.PageNumber = "1" + for { + resp, err := p.dnsClient.DescribeDomains(request) + if err != nil { + log.Errorf("Failed to describe domains for Alibaba Cloud DNS: %v", err) + return nil, err + } + for _, tmpDomain := range resp.Domains.Domain { + domainNames = append(domainNames, tmpDomain.DomainName) + } + nextPage := getNextPageNumber(resp.PageNumber, defaultAlibabaCloudPageSize, resp.TotalCount) + if nextPage == 0 { + break + } else { + request.PageNumber = requests.NewInteger(nextPage) + } + } + return domainNames, nil +} + +func (p *AlibabaCloudProvider) getDomainRecords(domainName string) ([]alidns.Record, error) { + var results []alidns.Record + request := alidns.CreateDescribeDomainRecordsRequest() + request.DomainName = domainName + request.PageSize = requests.NewInteger(defaultAlibabaCloudPageSize) + request.PageNumber = "1" + for { + response, err := p.getDNSClient().DescribeDomainRecords(request) + + if err != nil { + log.Errorf("Failed to describe domain records for Alibaba Cloud DNS: %v", err) + return nil, err + } + + for _, record := range response.DomainRecords.Record { + + domainName := record.DomainName + recordType := record.Type + + if !p.domainFilter.Match(domainName) { + continue + } + + if !supportedRecordType(recordType) { + continue + } + + //TODO filter Locked record + results = append(results, record) + } + nextPage := getNextPageNumber(response.PageNumber, defaultAlibabaCloudPageSize, response.TotalCount) + if nextPage == 0 { + break + } else { + request.PageNumber = requests.NewInteger(nextPage) + } + } + + return results, nil +} + func (p *AlibabaCloudProvider) applyChangesForDNS(changes *plan.Changes) error { - log.Debugf("ApplyChanges to Alibaba Cloud DNS: %++v", *changes) + log.Infof("ApplyChanges to Alibaba Cloud DNS: %++v", *changes) records, err := p.records() if err != nil { @@ -373,7 +524,7 @@ func (p *AlibabaCloudProvider) createRecord(endpoint *endpoint.Endpoint, target return nil } - response, err := p.dnsClient.AddDomainRecord(request) + response, err := p.getDNSClient().AddDomainRecord(request) if err == nil { log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud DNS: Record ID=%s", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) } else { @@ -399,11 +550,11 @@ func (p *AlibabaCloudProvider) deleteRecord(recordID string) error { request := alidns.CreateDeleteDomainRecordRequest() request.RecordId = recordID - response, err := p.dnsClient.DeleteDomainRecord(request) + response, err := p.getDNSClient().DeleteDomainRecord(request) if err == nil { - log.Infof("Delete record id '%s' in Alibaba Cloud DNS", response.RecordId) + log.Infof("Delete record id %s in Alibaba Cloud DNS", response.RecordId) } else { - log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", err) + log.Errorf("Failed to delete record '%s' in Alibaba Cloud DNS: %v", response.RecordId, err) } return err } @@ -418,7 +569,7 @@ func (p *AlibabaCloudProvider) updateRecord(record alidns.Record, endpoint *endp if ttl != 0 { request.TTL = requests.NewInteger(ttl) } - response, err := p.dnsClient.UpdateDomainRecord(request) + response, err := p.getDNSClient().UpdateDomainRecord(request) if err == nil { log.Infof("Update record id '%s' in Alibaba Cloud DNS", response.RecordId) } else { @@ -551,7 +702,7 @@ func (p *AlibabaCloudProvider) splitDNSName(endpoint *endpoint.Endpoint) (rr str func (p *AlibabaCloudProvider) matchVPC(zoneID string) bool { request := pvtz.CreateDescribeZoneInfoRequest() request.ZoneId = zoneID - response, err := p.pvtzClient.DescribeZoneInfo(request) + response, err := p.getPvtzClient().DescribeZoneInfo(request) if err != nil { log.Errorf("Failed to describe zone info %s in Alibaba Cloud DNS: %v", zoneID, err) return false @@ -575,7 +726,7 @@ func (p *AlibabaCloudProvider) privateZones() ([]pvtz.Zone, error) { request.PageNumber = "1" for { - response, err := p.pvtzClient.DescribeZones(request) + response, err := p.getPvtzClient().DescribeZones(request) if err != nil { log.Errorf("Failed to describe zones in Alibaba Cloud DNS: %v", err) return nil, err @@ -610,7 +761,7 @@ type alibabaPrivateZone struct { } func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone, error) { - log.Debug("Retrieving Alibaba Cloud Private Zone records") + log.Infof("Retrieving Alibaba Cloud Private Zone records") result := make(map[string]*alibabaPrivateZone) recordsCount := 0 @@ -630,7 +781,7 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone var records []pvtz.Record for { - response, err := p.pvtzClient.DescribeZoneRecords(request) + response, err := p.getPvtzClient().DescribeZoneRecords(request) if err != nil { log.Errorf("Failed to describe zone record '%s' in Alibaba Cloud DNS: %v", zone.ZoneId, err) @@ -663,7 +814,7 @@ func (p *AlibabaCloudProvider) getPrivateZones() (map[string]*alibabaPrivateZone recordsCount += len(records) result[zone.ZoneName] = &privateZone } - log.Debugf("Found %d Alibaba Cloud Private Zone record(s).", recordsCount) + log.Infof("Found %d Alibaba Cloud Private Zone record(s).", recordsCount) return result, nil } @@ -744,7 +895,7 @@ func (p *AlibabaCloudProvider) createPrivateZoneRecord(zones map[string]*alibaba return nil } - response, err := p.pvtzClient.AddZoneRecord(request) + response, err := p.getPvtzClient().AddZoneRecord(request) if err == nil { log.Infof("Create %s record named '%s' to '%s' with ttl %d for Alibaba Cloud Private Zone: Record ID=%d", endpoint.RecordType, endpoint.DNSName, target, ttl, response.RecordId) } else { @@ -771,11 +922,11 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecord(recordID int) error { request := pvtz.CreateDeleteZoneRecordRequest() request.RecordId = requests.NewInteger(recordID) - response, err := p.pvtzClient.DeleteZoneRecord(request) + response, err := p.getPvtzClient().DeleteZoneRecord(request) if err == nil { log.Infof("Delete record id '%d' in Alibaba Cloud Private Zone", response.RecordId) } else { - log.Errorf("Failed to delete record '%s' in Alibaba Cloud Private Zone: %v", err) + log.Errorf("Failed to delete record %d in Alibaba Cloud Private Zone: %v", response.RecordId, err) } return err } @@ -818,7 +969,7 @@ func (p *AlibabaCloudProvider) deletePrivateZoneRecords(zones map[string]*alibab // // Returns nil if the operation was successful or an error if the operation failed. func (p *AlibabaCloudProvider) applyChangesForPrivateZone(changes *plan.Changes) error { - log.Debugf("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes) + log.Infof("ApplyChanges to Alibaba Cloud Private Zone: %++v", *changes) zones, err := p.getPrivateZones() if err != nil { @@ -845,7 +996,7 @@ func (p *AlibabaCloudProvider) updatePrivateZoneRecord(record pvtz.Record, endpo if ttl != 0 { request.Ttl = requests.NewInteger(ttl) } - response, err := p.pvtzClient.UpdateZoneRecord(request) + response, err := p.getPvtzClient().UpdateZoneRecord(request) if err == nil { log.Infof("Update record id '%d' in Alibaba Cloud Private Zone", response.RecordId) } else { diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go index e8ad7c53a..a19b5cf83 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibaba_cloud_test.go @@ -25,14 +25,14 @@ import ( "github.com/kubernetes-incubator/external-dns/plan" ) -type mockAlibabaCloudDNSAPI struct { +type MockAlibabaCloudDNSAPI struct { records []alidns.Record } -func NewMockAlibabaCloudDNSAPI() *mockAlibabaCloudDNSAPI { - api := mockAlibabaCloudDNSAPI{} +func NewMockAlibabaCloudDNSAPI() *MockAlibabaCloudDNSAPI { + api := MockAlibabaCloudDNSAPI{} api.records = []alidns.Record{ - alidns.Record{ + { RecordId: "1", DomainName: "container-service.top", Type: "A", @@ -40,7 +40,7 @@ func NewMockAlibabaCloudDNSAPI() *mockAlibabaCloudDNSAPI { RR: "abc", Value: "1.2.3.4", }, - alidns.Record{ + { RecordId: "2", DomainName: "container-service.top", Type: "TXT", @@ -52,7 +52,7 @@ func NewMockAlibabaCloudDNSAPI() *mockAlibabaCloudDNSAPI { return &api } -func (m *mockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecordRequest) (response *alidns.AddDomainRecordResponse, err error) { ttl, _ := request.TTL.GetValue() m.records = append(m.records, alidns.Record{ RecordId: "3", @@ -66,7 +66,7 @@ func (m *mockAlibabaCloudDNSAPI) AddDomainRecord(request *alidns.AddDomainRecord return response, nil } -func (m *mockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomainRecordRequest) (response *alidns.DeleteDomainRecordResponse, err error) { var result []alidns.Record for _, record := range m.records { if record.RecordId != request.RecordId { @@ -79,9 +79,9 @@ func (m *mockAlibabaCloudDNSAPI) DeleteDomainRecord(request *alidns.DeleteDomain return response, nil } -func (m *mockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomainRecordRequest) (response *alidns.UpdateDomainRecordResponse, err error) { ttl, _ := request.TTL.GetValue() - for i, _ := range m.records { + for i := range m.records { if m.records[i].RecordId == request.RecordId { m.records[i].TTL = ttl } @@ -91,7 +91,18 @@ func (m *mockAlibabaCloudDNSAPI) UpdateDomainRecord(request *alidns.UpdateDomain return response, nil } -func (m *mockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) { +func (m *MockAlibabaCloudDNSAPI) DescribeDomains(request *alidns.DescribeDomainsRequest) (response *alidns.DescribeDomainsResponse, err error) { + var result alidns.Domains + for _, record := range m.records { + domain := alidns.Domain{} + domain.DomainName = record.DomainName + } + response = alidns.CreateDescribeDomainsResponse() + response.Domains = result + return response, nil +} + +func (m *MockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeDomainRecordsRequest) (response *alidns.DescribeDomainRecordsResponse, err error) { var result []alidns.Record for _, record := range m.records { if record.DomainName == request.DomainName { @@ -103,26 +114,26 @@ func (m *mockAlibabaCloudDNSAPI) DescribeDomainRecords(request *alidns.DescribeD return response, nil } -type mockAlibabaCloudPrivateZoneAPI struct { +type MockAlibabaCloudPrivateZoneAPI struct { zone pvtz.Zone records []pvtz.Record } -func NewMockAlibabaCloudPrivateZoneAPI() *mockAlibabaCloudPrivateZoneAPI { - api := mockAlibabaCloudPrivateZoneAPI{} +func NewMockAlibabaCloudPrivateZoneAPI() *MockAlibabaCloudPrivateZoneAPI { + api := MockAlibabaCloudPrivateZoneAPI{} api.zone = pvtz.Zone{ ZoneId: "test-zone", ZoneName: "container-service.top", } api.records = []pvtz.Record{ - pvtz.Record{ + { RecordId: 1, Type: "A", Ttl: 300, Rr: "abc", Value: "1.2.3.4", }, - pvtz.Record{ + { RecordId: 2, Type: "TXT", Ttl: 300, @@ -133,7 +144,7 @@ func NewMockAlibabaCloudPrivateZoneAPI() *mockAlibabaCloudPrivateZoneAPI { return &api } -func (m *mockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneRecordRequest) (response *pvtz.AddZoneRecordResponse, err error) { ttl, _ := request.Ttl.GetValue() m.records = append(m.records, pvtz.Record{ RecordId: 3, @@ -146,12 +157,12 @@ func (m *mockAlibabaCloudPrivateZoneAPI) AddZoneRecord(request *pvtz.AddZoneReco return response, nil } -func (m *mockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) { - recordId, _ := request.RecordId.GetValue() +func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZoneRecordRequest) (response *pvtz.DeleteZoneRecordResponse, err error) { + recordID, _ := request.RecordId.GetValue() var result []pvtz.Record for _, record := range m.records { - if record.RecordId != recordId { + if record.RecordId != recordID { result = append(result, record) } } @@ -159,11 +170,11 @@ func (m *mockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZo return response, nil } -func (m *mockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) { - recordId, _ := request.RecordId.GetValue() +func (m *MockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZoneRecordRequest) (response *pvtz.UpdateZoneRecordResponse, err error) { + recordID, _ := request.RecordId.GetValue() ttl, _ := request.Ttl.GetValue() - for i, _ := range m.records { - if m.records[i].RecordId == recordId { + for i := range m.records { + if m.records[i].RecordId == recordID { m.records[i].Ttl = ttl } } @@ -171,19 +182,19 @@ func (m *mockAlibabaCloudPrivateZoneAPI) UpdateZoneRecord(request *pvtz.UpdateZo return response, nil } -func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneRecords(request *pvtz.DescribeZoneRecordsRequest) (response *pvtz.DescribeZoneRecordsResponse, err error) { response = pvtz.CreateDescribeZoneRecordsResponse() response.Records.Record = append(response.Records.Record, m.records...) return response, nil } -func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZones(request *pvtz.DescribeZonesRequest) (response *pvtz.DescribeZonesResponse, err error) { response = pvtz.CreateDescribeZonesResponse() response.Zones.Zone = append(response.Zones.Zone, m.zone) return response, nil } -func (m *mockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) { +func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.DescribeZoneInfoRequest) (response *pvtz.DescribeZoneInfoResponse, err error) { response = pvtz.CreateDescribeZoneInfoResponse() response.ZoneId = m.zone.ZoneId response.ZoneName = m.zone.ZoneName @@ -216,7 +227,7 @@ func newTestAlibabaCloudProvider(private bool) *AlibabaCloudProvider { vpcID: cfg.VPCID, dryRun: false, dnsClient: NewMockAlibabaCloudDNSAPI(), - pvtzClient: &mockAlibabaCloudPrivateZoneAPI{}, + pvtzClient: &MockAlibabaCloudPrivateZoneAPI{}, privateZone: false, } } @@ -240,7 +251,7 @@ func TestAlibabaCloudProvider_ApplyChanges(t *testing.T) { p := newTestAlibabaCloudProvider(false) changes := plan.Changes{ Create: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "xyz.container-service.top", RecordType: "A", RecordTTL: 300, @@ -248,7 +259,7 @@ func TestAlibabaCloudProvider_ApplyChanges(t *testing.T) { }, }, UpdateNew: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "abc.container-service.top", RecordType: "A", RecordTTL: 500, @@ -256,7 +267,7 @@ func TestAlibabaCloudProvider_ApplyChanges(t *testing.T) { }, }, Delete: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "abc.container-service.top", RecordType: "TXT", RecordTTL: 300, @@ -297,7 +308,7 @@ func TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) { p := newTestAlibabaCloudProvider(true) changes := plan.Changes{ Create: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "xyz.container-service.top", RecordType: "A", RecordTTL: 300, @@ -305,7 +316,7 @@ func TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) { }, }, UpdateNew: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "abc.container-service.top", RecordType: "A", RecordTTL: 500, @@ -313,7 +324,7 @@ func TestAlibabaCloudProvider_ApplyChanges_PrivateZone(t *testing.T) { }, }, Delete: []*endpoint.Endpoint{ - &endpoint.Endpoint{ + { DNSName: "abc.container-service.top", RecordType: "TXT", RecordTTL: 300, From 98f0013b2a785b856023621d5b8c0e6dc4fe3e2f Mon Sep 17 00:00:00 2001 From: xianlubird Date: Thu, 30 Aug 2018 19:58:49 +0800 Subject: [PATCH 6/9] Fix google file format problem --- provider/google.go | 8 ++++---- provider/google_test.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/provider/google.go b/provider/google.go index 017b22da4..47dab7334 100644 --- a/provider/google.go +++ b/provider/google.go @@ -143,10 +143,10 @@ func NewGoogleProvider(project string, domainFilter DomainFilter, zoneIDFilter Z } provider := &GoogleProvider{ - project: project, - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - dryRun: dryRun, + project: project, + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + dryRun: dryRun, resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets}, managedZonesClient: managedZonesService{dnsClient.ManagedZones}, changesClient: changesService{dnsClient.Changes}, diff --git a/provider/google_test.go b/provider/google_test.go index 102debfbf..8791a1b9b 100644 --- a/provider/google_test.go +++ b/provider/google_test.go @@ -569,10 +569,10 @@ func validateChangeRecord(t *testing.T, record *dns.ResourceRecordSet, expected func newGoogleProvider(t *testing.T, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool, records []*endpoint.Endpoint) *GoogleProvider { provider := &GoogleProvider{ - project: "zalando-external-dns-test", - domainFilter: domainFilter, - zoneIDFilter: zoneIDFilter, - dryRun: false, + project: "zalando-external-dns-test", + domainFilter: domainFilter, + zoneIDFilter: zoneIDFilter, + dryRun: false, resourceRecordSetsClient: &mockResourceRecordSetsClient{}, managedZonesClient: &mockManagedZonesClient{}, changesClient: &mockChangesClient{}, From 3e771e5b72fbb8119d51814a67da62673a7ed737 Mon Sep 17 00:00:00 2001 From: xianlubird Date: Fri, 31 Aug 2018 10:06:25 +0800 Subject: [PATCH 7/9] Change README.md from 0.4 to 0.5.6 --- docs/tutorials/alibabacloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index 4a9b2bb00..9773f54a0 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -1,6 +1,6 @@ # Setting up ExternalDNS for Services on Alibaba Cloud -This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use **>=0.4** version of ExternalDNS for this tutorial +This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster on Alibaba Cloud. Make sure to use **>=0.5.6** version of ExternalDNS for this tutorial ## RAM Permissions From a77dcddd9896fdf1de34865904a4606d231f72f9 Mon Sep 17 00:00:00 2001 From: xianlubird Date: Fri, 31 Aug 2018 12:41:57 +0800 Subject: [PATCH 8/9] Add some test case --- provider/alibaba_cloud_test.go | 50 ++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/provider/alibaba_cloud_test.go b/provider/alibaba_cloud_test.go index a19b5cf83..5d4b97a10 100644 --- a/provider/alibaba_cloud_test.go +++ b/provider/alibaba_cloud_test.go @@ -120,11 +120,19 @@ type MockAlibabaCloudPrivateZoneAPI struct { } func NewMockAlibabaCloudPrivateZoneAPI() *MockAlibabaCloudPrivateZoneAPI { - api := MockAlibabaCloudPrivateZoneAPI{} - api.zone = pvtz.Zone{ + vpc := pvtz.Vpc{ + RegionId: "cn-beijing", + VpcId: "vpc-xxxxxx", + } + + api := MockAlibabaCloudPrivateZoneAPI{zone: pvtz.Zone{ ZoneId: "test-zone", ZoneName: "container-service.top", - } + Vpcs: pvtz.Vpcs{ + Vpc: []pvtz.Vpc{vpc}, + }, + }} + api.records = []pvtz.Record{ { RecordId: 1, @@ -166,6 +174,7 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DeleteZoneRecord(request *pvtz.DeleteZo result = append(result, record) } } + m.records = result response = pvtz.CreateDeleteZoneRecordResponse() return response, nil } @@ -198,6 +207,7 @@ func (m *MockAlibabaCloudPrivateZoneAPI) DescribeZoneInfo(request *pvtz.Describe response = pvtz.CreateDescribeZoneInfoResponse() response.ZoneId = m.zone.ZoneId response.ZoneName = m.zone.ZoneName + response.BindVpcs = pvtz.BindVpcs{Vpc: m.zone.Vpcs.Vpc} return response, nil } @@ -227,8 +237,23 @@ func newTestAlibabaCloudProvider(private bool) *AlibabaCloudProvider { vpcID: cfg.VPCID, dryRun: false, dnsClient: NewMockAlibabaCloudDNSAPI(), - pvtzClient: &MockAlibabaCloudPrivateZoneAPI{}, - privateZone: false, + pvtzClient: NewMockAlibabaCloudPrivateZoneAPI(), + privateZone: private, + } +} + +func TestAlibabaCloudPrivateProvider_Records(t *testing.T) { + p := newTestAlibabaCloudProvider(true) + endpoints, err := p.Records() + 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) + } } } @@ -394,3 +419,18 @@ func TestAlibabaCloudProvider_TXTEndpoint(t *testing.T) { t.Errorf("Failed to unescapeTXTRecordValue: %s", p.unescapeTXTRecordValue(recordValue)) } } + +//TestAlibabaCloudProvider_TXTEndpoint_PrivateZone +func TestAlibabaCloudProvider_TXTEndpoint_PrivateZone(t *testing.T) { + + p := newTestAlibabaCloudProvider(true) + const recordValue = "heritage=external-dns,external-dns/owner=default" + const endpointTarget = "\"heritage=external-dns,external-dns/owner=default\"" + + if p.escapeTXTRecordValue(endpointTarget) != endpointTarget { + t.Errorf("Failed to escapeTXTRecordValue: %s", p.escapeTXTRecordValue(endpointTarget)) + } + if p.unescapeTXTRecordValue(recordValue) != endpointTarget { + t.Errorf("Failed to unescapeTXTRecordValue: %s", p.unescapeTXTRecordValue(recordValue)) + } +} From ae9c135d755dca01870c2428649a9645a02cbf08 Mon Sep 17 00:00:00 2001 From: xianlubird Date: Mon, 3 Sep 2018 16:20:26 +0800 Subject: [PATCH 9/9] Add zone info to deployment --- docs/tutorials/alibabacloud.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/tutorials/alibabacloud.md b/docs/tutorials/alibabacloud.md index 9773f54a0..b9b5a5789 100644 --- a/docs/tutorials/alibabacloud.md +++ b/docs/tutorials/alibabacloud.md @@ -120,6 +120,14 @@ spec: - --alibaba-cloud-zone=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier + volumeMounts: + - mountPath: /usr/share/zoneinfo + name: hostpath + volumes: + - name: hostpath + hostPath: + path: /usr/share/zoneinfo + type: Directory ``` ### Manifest (for clusters with RBAC enabled) @@ -186,6 +194,14 @@ spec: - --alibaba-cloud-zone-type=public # only look at public hosted zones (valid values are public, private or no value for both) - --registry=txt - --txt-owner-id=my-identifier + volumeMounts: + - mountPath: /usr/share/zoneinfo + name: hostpath + volumes: + - name: hostpath + hostPath: + path: /usr/share/zoneinfo + type: Directory ```