diff --git a/README.md b/README.md index 07a613b15..90aa89249 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,6 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz - [CloudFlare](https://www.cloudflare.com/dns) - [DigitalOcean](https://www.digitalocean.com/products/networking) - [DNSimple](https://dnsimple.com/) -- [OpenStack Designate](https://docs.openstack.org/designate/latest/) - [PowerDNS](https://www.powerdns.com/) - [CoreDNS](https://coredns.io/) - [Exoscale](https://www.exoscale.com/dns/) @@ -137,7 +136,6 @@ The following table clarifies the current status of the providers according to t | CloudFlare | Beta | | | DigitalOcean | Alpha | | | DNSimple | Alpha | | -| OpenStack Designate | Alpha | | | PowerDNS | Alpha | | | CoreDNS | Alpha | | | Exoscale | Alpha | | @@ -204,7 +202,6 @@ The following tutorials are provided: - [NS Record Creation with CRD Source](docs/sources/ns-record.md) - [MX Record Creation with CRD Source](docs/sources/mx-record.md) - [TXT Record Creation with CRD Source](docs/sources/txt-record.md) -- [OpenStack Designate](docs/tutorials/designate.md) - [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md) - [PowerDNS](docs/tutorials/pdns.md) - [RFC2136](docs/tutorials/rfc2136.md) diff --git a/docs/flags.md b/docs/flags.md index bbe78c71d..3587b686c 100644 --- a/docs/flags.md +++ b/docs/flags.md @@ -49,7 +49,7 @@ | `--[no-]traefik-disable-legacy` | Disable listeners on Resources under the traefik.containo.us API Group | | `--[no-]traefik-disable-new` | Disable listeners on Resources under the traefik.io API Group | | `--nat64-networks=NAT64-NETWORKS` | Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional) | -| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, designate, digitalocean, dnsimple, exoscale, gandi, godaddy, google, ibmcloud, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) | +| `--provider=provider` | The DNS provider where the DNS records will be created (required, options: akamai, alibabacloud, aws, aws-sd, azure, azure-dns, azure-private-dns, civo, cloudflare, coredns, digitalocean, dnsimple, exoscale, gandi, godaddy, google, ibmcloud, inmemory, linode, ns1, oci, ovh, pdns, pihole, plural, rfc2136, scaleway, skydns, tencentcloud, transip, ultradns, webhook) | | `--provider-cache-time=0s` | The time to cache the DNS provider record list requests. | | `--domain-filter=` | Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional) | | `--exclude-domains=` | Exclude subdomains (optional) | diff --git a/docs/tutorials/designate.md b/docs/tutorials/designate.md deleted file mode 100644 index 650eab39b..000000000 --- a/docs/tutorials/designate.md +++ /dev/null @@ -1,258 +0,0 @@ -# Designate DNS from OpenStack - -This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OpenStack Designate DNS. - -## Authenticating with OpenStack - -We are going to use OpenStack CLI - `openstack` utility, which is an umbrella application for most of OpenStack clients including `designate`. - -All OpenStack CLIs require authentication parameters to be provided. These parameters include: - -* URL of the OpenStack identity service (`keystone`) which is responsible for user authentication and also served as a registry for other - OpenStack services. Designate endpoints must be registered in `keystone` in order to ExternalDNS and OpenStack CLI be able to find them. -* OpenStack region name -* User login name. -* User project (tenant) name. -* User domain (only when using keystone API v3) - -Although these parameters can be passed explicitly through the CLI flags, traditionally it is done by sourcing `openrc` file (`source ~/openrc`) that is a -shell snippet that sets environment variables that all OpenStack CLI understand by convention. - -Recent versions of OpenStack Dashboard have a nice UI to download `openrc` file for both v2 and v3 auth protocols. Both protocols can be used with ExternalDNS. -v3 is generally preferred over v2, but might not be available in some OpenStack installations. - -## Installing OpenStack Designate - -Please refer to the Designate deployment [tutorial](https://docs.openstack.org/project-install-guide/dns/ocata/install.html) for instructions on how -to install and test Designate with BIND backend. You will be required to have admin rights in existing OpenStack installation to do this. One convenient -way to get yourself an OpenStack installation to play with is to use [DevStack](https://docs.openstack.org/devstack/latest/). - -## Creating DNS zones - -All domain names that are ExternalDNS is going to create must belong to one of DNS zones created in advance. Here is an example of how to create `example.com` DNS zone: - -```console -openstack zone create --email dnsmaster@example.com example.com. -``` - -It is important to manually create all the zones that are going to be used for kubernetes entities (ExternalDNS sources) before starting ExternalDNS. - -## Deploy ExternalDNS - -Create a deployment file called `externaldns.yaml` with the following contents: - -### Manifest (for clusters without RBAC enabled) - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - strategy: - type: Recreate - selector: - matchLabels: - app: external-dns - template: - metadata: - labels: - app: external-dns - spec: - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.16.1 - args: - - --source=service # ingress is also possible - - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - - --provider=designate - env: # values from openrc file - - name: OS_AUTH_URL - value: https://controller/identity/v3 - - name: OS_REGION_NAME - value: RegionOne - - name: OS_USERNAME - value: admin - - name: OS_PASSWORD - value: p@ssw0rd - - name: OS_PROJECT_NAME - value: demo - - name: OS_USER_DOMAIN_NAME - value: Default -``` - -### Manifest (for clusters with RBAC enabled) - -```yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: external-dns ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: external-dns -rules: -- apiGroups: [""] - resources: ["services","endpoints","pods"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","watch","list"] -- apiGroups: ["extensions","networking.k8s.io"] - resources: ["ingresses"] - verbs: ["get","watch","list"] -- apiGroups: [""] - resources: ["nodes"] - verbs: ["watch","list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: external-dns-viewer -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: external-dns -subjects: -- kind: ServiceAccount - name: external-dns - namespace: default ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: external-dns -spec: - selector: - matchLabels: - app: external-dns - strategy: - type: Recreate - template: - metadata: - labels: - app: external-dns - spec: - serviceAccountName: external-dns - containers: - - name: external-dns - image: registry.k8s.io/external-dns/external-dns:v0.16.1 - args: - - --source=service # ingress is also possible - - --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above. - - --provider=designate - env: # values from openrc file - - name: OS_AUTH_URL - value: https://controller/identity/v3 - - name: OS_REGION_NAME - value: RegionOne - - name: OS_USERNAME - value: admin - - name: OS_PASSWORD - value: p@ssw0rd - - name: OS_PROJECT_NAME - value: demo - - name: OS_USER_DOMAIN_NAME - value: Default -``` - -Create the deployment for ExternalDNS: - -```console -kubectl create -f externaldns.yaml -``` - -### Optional: Trust self-sign certificates - -If your OpenStack-Installation is configured with a self-sign certificate, you could extend the `pod.spec` with following secret-mount: - -```yaml - volumeMounts: - - mountPath: /etc/ssl/certs/ - name: cacerts - volumes: - - name: cacerts - secret: - defaultMode: 420 - secretName: self-sign-certs -``` - -content of the secret `self-sign-certs` must be the certificate/chain in PEM format. - -## Deploying an Nginx Service - -Create a service file called 'nginx.yaml' with the following contents: - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx -spec: - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - image: nginx - name: nginx - ports: - - containerPort: 80 ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx - annotations: - external-dns.alpha.kubernetes.io/hostname: my-app.example.com -spec: - selector: - app: nginx - type: LoadBalancer - ports: - - protocol: TCP - port: 80 - targetPort: 80 -``` - -Note the annotation on the service; use the same hostname as the DNS zone created above. - -ExternalDNS uses this annotation to determine what services should be registered with DNS. Removing the annotation will cause ExternalDNS to remove the corresponding DNS records. - -Create the deployment and service: - -```console -kubectl create -f nginx.yaml -``` - -Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and notify Designate, -which in turn synchronize DNS records with underlying DNS server backend. - -## Verifying DNS records - -To verify that DNS record was indeed created, you can use the following command: - -```console -openstack recordset list example.com. -``` - -There should be a record for my-app.example.com having `ACTIVE` status. And of course, the ultimate method to verify is to issue a DNS query: - -```console -dig my-app.example.com @controller -``` - -## Cleanup - -Now that we have verified that ExternalDNS created all DNS records, we can delete the tutorial's example: - -```console -kubectl delete service -f nginx.yaml -kubectl delete service -f externaldns.yaml -``` diff --git a/main.go b/main.go index 47d1695dc..ef98304aa 100644 --- a/main.go +++ b/main.go @@ -50,7 +50,6 @@ import ( "sigs.k8s.io/external-dns/provider/civo" "sigs.k8s.io/external-dns/provider/cloudflare" "sigs.k8s.io/external-dns/provider/coredns" - "sigs.k8s.io/external-dns/provider/designate" "sigs.k8s.io/external-dns/provider/digitalocean" "sigs.k8s.io/external-dns/provider/dnsimple" "sigs.k8s.io/external-dns/provider/exoscale" @@ -287,8 +286,6 @@ func main() { ) case "inmemory": p, err = inmemory.NewInMemoryProvider(inmemory.InMemoryInitZones(cfg.InMemoryZones), inmemory.InMemoryWithDomain(domainFilter), inmemory.InMemoryWithLogging()), nil - case "designate": - p, err = designate.NewDesignateProvider(domainFilter, cfg.DryRun) case "pdns": p, err = pdns.NewPDNSProvider( ctx, diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index f0697d498..962e38721 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -484,7 +484,7 @@ func App(cfg *Config) *kingpin.Application { app.Flag("nat64-networks", "Adding an A record for each AAAA record in NAT64-enabled networks; specify multiple times for multiple possible nets (optional)").StringsVar(&cfg.NAT64Networks) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "civo", "cloudflare", "coredns", "digitalocean", "dnsimple", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rfc2136", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "webhook"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) app.Flag("provider-cache-time", "The time to cache the DNS provider record list requests.").Default(defaultConfig.ProviderCacheTime.String()).DurationVar(&cfg.ProviderCacheTime) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) diff --git a/provider/designate/designate.go b/provider/designate/designate.go deleted file mode 100644 index ee3db2af3..000000000 --- a/provider/designate/designate.go +++ /dev/null @@ -1,496 +0,0 @@ -/* -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 designate - -import ( - "context" - "fmt" - "net" - "net/http" - "os" - "strings" - "time" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" - "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" - "github.com/gophercloud/gophercloud/pagination" - log "github.com/sirupsen/logrus" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/pkg/tlsutils" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -const ( - // ID of the RecordSet from which endpoint was created - designateRecordSetID = "designate-recordset-id" - // Zone ID of the RecordSet - designateZoneID = "designate-record-id" - - // Initial records values of the RecordSet. This label is required in order not to loose records that haven't - // changed where there are several targets per domain and only some of them changed. - // Values are joined by zero-byte to in order to get a single string - designateOriginalRecords = "designate-original-records" -) - -// interface between provider and OpenStack DNS API -type designateClientInterface interface { - // ForEachZone calls handler for each zone managed by the Designate - ForEachZone(handler func(zone *zones.Zone) error) error - - // ForEachRecordSet calls handler for each recordset in the given DNS zone - ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error - - // CreateRecordSet creates recordset in the given DNS zone - CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) - - // UpdateRecordSet updates recordset in the given DNS zone - UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error - - // DeleteRecordSet deletes recordset in the given DNS zone - DeleteRecordSet(zoneID, recordSetID string) error -} - -// implementation of the designateClientInterface -type designateClient struct { - serviceClient *gophercloud.ServiceClient -} - -// factory function for the designateClientInterface -func newDesignateClient() (designateClientInterface, error) { - serviceClient, err := createDesignateServiceClient() - if err != nil { - return nil, err - } - return &designateClient{serviceClient}, nil -} - -// copies environment variables to new names without overwriting existing values -func remapEnv(mapping map[string]string) { - for k, v := range mapping { - currentVal := os.Getenv(k) - newVal := os.Getenv(v) - if currentVal == "" && newVal != "" { - os.Setenv(k, newVal) - } - } -} - -// returns OpenStack Keystone authentication settings by obtaining values from standard environment variables. -// also fixes incompatibilities between gophercloud implementation and *-stackrc files that can be downloaded -// from OpenStack dashboard in latest versions -func getAuthSettings() (gophercloud.AuthOptions, error) { - remapEnv(map[string]string{ - "OS_TENANT_NAME": "OS_PROJECT_NAME", - "OS_TENANT_ID": "OS_PROJECT_ID", - "OS_DOMAIN_NAME": "OS_USER_DOMAIN_NAME", - "OS_DOMAIN_ID": "OS_USER_DOMAIN_ID", - }) - - opts, err := openstack.AuthOptionsFromEnv() - if err != nil { - return gophercloud.AuthOptions{}, err - } - opts.AllowReauth = true - if !strings.HasSuffix(opts.IdentityEndpoint, "/") { - opts.IdentityEndpoint += "/" - } - if !strings.HasSuffix(opts.IdentityEndpoint, "/v2.0/") && !strings.HasSuffix(opts.IdentityEndpoint, "/v3/") { - opts.IdentityEndpoint += "v2.0/" - } - return opts, nil -} - -// authenticate in OpenStack and obtain Designate service endpoint -func createDesignateServiceClient() (*gophercloud.ServiceClient, error) { - opts, err := getAuthSettings() - if err != nil { - return nil, err - } - log.Infof("Using OpenStack Keystone at %s", opts.IdentityEndpoint) - authProvider, err := openstack.NewClient(opts.IdentityEndpoint) - if err != nil { - return nil, err - } - - tlsConfig, err := tlsutils.CreateTLSConfig("OPENSTACK") - if err != nil { - return nil, err - } - - transport := &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: tlsConfig, - } - authProvider.HTTPClient.Transport = transport - - if err = openstack.Authenticate(authProvider, opts); err != nil { - return nil, err - } - - eo := gophercloud.EndpointOpts{ - Region: os.Getenv("OS_REGION_NAME"), - } - - client, err := openstack.NewDNSV2(authProvider, eo) - if err != nil { - return nil, err - } - log.Infof("Found OpenStack Designate service at %s", client.Endpoint) - return client, nil -} - -// ForEachZone calls handler for each zone managed by the Designate -func (c designateClient) ForEachZone(handler func(zone *zones.Zone) error) error { - pager := zones.List(c.serviceClient, zones.ListOpts{}) - return pager.EachPage( - func(page pagination.Page) (bool, error) { - list, err := zones.ExtractZones(page) - if err != nil { - return false, err - } - for _, zone := range list { - err := handler(&zone) - if err != nil { - return false, err - } - } - return true, nil - }, - ) -} - -// ForEachRecordSet calls handler for each recordset in the given DNS zone -func (c designateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error { - pager := recordsets.ListByZone(c.serviceClient, zoneID, recordsets.ListOpts{}) - return pager.EachPage( - func(page pagination.Page) (bool, error) { - list, err := recordsets.ExtractRecordSets(page) - if err != nil { - return false, err - } - for _, recordSet := range list { - err := handler(&recordSet) - if err != nil { - return false, err - } - } - return true, nil - }, - ) -} - -// CreateRecordSet creates recordset in the given DNS zone -func (c designateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) { - r, err := recordsets.Create(c.serviceClient, zoneID, opts).Extract() - if err != nil { - return "", err - } - return r.ID, nil -} - -// UpdateRecordSet updates recordset in the given DNS zone -func (c designateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error { - _, err := recordsets.Update(c.serviceClient, zoneID, recordSetID, opts).Extract() - return err -} - -// DeleteRecordSet deletes recordset in the given DNS zone -func (c designateClient) DeleteRecordSet(zoneID, recordSetID string) error { - return recordsets.Delete(c.serviceClient, zoneID, recordSetID).ExtractErr() -} - -// designate provider type -type designateProvider struct { - provider.BaseProvider - client designateClientInterface - - // only consider hosted zones managing domains ending in this suffix - domainFilter endpoint.DomainFilter - dryRun bool -} - -// NewDesignateProvider is a factory function for OpenStack designate providers -func NewDesignateProvider(domainFilter endpoint.DomainFilter, dryRun bool) (provider.Provider, error) { - client, err := newDesignateClient() - if err != nil { - return nil, err - } - return &designateProvider{ - client: client, - domainFilter: domainFilter, - dryRun: dryRun, - }, nil -} - -// converts domain names to FQDN -func canonicalizeDomainNames(domains []string) []string { - var cDomains []string - for _, d := range domains { - if !strings.HasSuffix(d, ".") { - d += "." - cDomains = append(cDomains, strings.ToLower(d)) - } - } - return cDomains -} - -// converts domain name to FQDN -func canonicalizeDomainName(d string) string { - if !strings.HasSuffix(d, ".") { - d += "." - } - return strings.ToLower(d) -} - -// returns ZoneID -> ZoneName mapping for zones that are managed by the Designate and match domain filter -func (p designateProvider) getZones() (map[string]string, error) { - result := map[string]string{} - - err := p.client.ForEachZone( - func(zone *zones.Zone) error { - if zone.Type != "" && strings.ToUpper(zone.Type) != "PRIMARY" || zone.Status != "ACTIVE" { - return nil - } - - zoneName := canonicalizeDomainName(zone.Name) - if !p.domainFilter.Match(zoneName) { - return nil - } - result[zone.ID] = zoneName - return nil - }, - ) - - return result, err -} - -// finds best suitable DNS zone for the hostname -func (p designateProvider) getHostZoneID(hostname string, managedZones map[string]string) (string, error) { - longestZoneLength := 0 - resultID := "" - - for zoneID, zoneName := range managedZones { - if !strings.HasSuffix(hostname, zoneName) { - continue - } - ln := len(zoneName) - if ln > longestZoneLength { - resultID = zoneID - longestZoneLength = ln - } - } - - return resultID, nil -} - -// Records returns the list of records. -func (p designateProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - var result []*endpoint.Endpoint - managedZones, err := p.getZones() - if err != nil { - return nil, err - } - for zoneID := range managedZones { - err = p.client.ForEachRecordSet(zoneID, - func(recordSet *recordsets.RecordSet) error { - if recordSet.Type != endpoint.RecordTypeA && recordSet.Type != endpoint.RecordTypeTXT && recordSet.Type != endpoint.RecordTypeCNAME { - return nil - } - - ep := endpoint.NewEndpoint(recordSet.Name, recordSet.Type, recordSet.Records...) - ep.Labels[designateRecordSetID] = recordSet.ID - ep.Labels[designateZoneID] = recordSet.ZoneID - ep.Labels[designateOriginalRecords] = strings.Join(recordSet.Records, "\000") - result = append(result, ep) - - return nil - }, - ) - if err != nil { - return nil, err - } - } - - return result, nil -} - -// temporary structure to hold recordset parameters so that we could aggregate endpoints into recordsets -type recordSet struct { - dnsName string - recordType string - zoneID string - recordSetID string - names map[string]bool -} - -// adds endpoint into recordset aggregation, loading original values from endpoint labels first -func addEndpoint(ep *endpoint.Endpoint, recordSets map[string]*recordSet, oldEndpoints []*endpoint.Endpoint, delete bool) { - key := fmt.Sprintf("%s/%s", ep.DNSName, ep.RecordType) - rs := recordSets[key] - if rs == nil { - rs = &recordSet{ - dnsName: canonicalizeDomainName(ep.DNSName), - recordType: ep.RecordType, - names: make(map[string]bool), - } - } - - addDesignateIDLabelsFromExistingEndpoints(oldEndpoints, ep) - - if rs.zoneID == "" { - rs.zoneID = ep.Labels[designateZoneID] - } - if rs.recordSetID == "" { - rs.recordSetID = ep.Labels[designateRecordSetID] - } - for _, rec := range strings.Split(ep.Labels[designateOriginalRecords], "\000") { - if _, ok := rs.names[rec]; !ok && rec != "" { - rs.names[rec] = true - } - } - targets := ep.Targets - if ep.RecordType == endpoint.RecordTypeCNAME { - targets = canonicalizeDomainNames(targets) - } - for _, t := range targets { - rs.names[t] = !delete - } - recordSets[key] = rs -} - -// addDesignateIDLabelsFromExistingEndpoints adds the labels identified by the constants designateZoneID and designateRecordSetID -// to an Endpoint. Therefore, it searches all given existing endpoints for an endpoint with the same record type and record -// value. If the given Endpoint already has the labels set, they are left untouched. This fixes an issue with the -// TXTRegistry which generates new TXT entries instead of updating the old ones. -func addDesignateIDLabelsFromExistingEndpoints(existingEndpoints []*endpoint.Endpoint, ep *endpoint.Endpoint) { - _, hasZoneIDLabel := ep.Labels[designateZoneID] - _, hasRecordSetIDLabel := ep.Labels[designateRecordSetID] - if hasZoneIDLabel && hasRecordSetIDLabel { - return - } - for _, oep := range existingEndpoints { - if ep.RecordType == oep.RecordType && ep.DNSName == oep.DNSName { - if !hasZoneIDLabel { - ep.Labels[designateZoneID] = oep.Labels[designateZoneID] - } - if !hasRecordSetIDLabel { - ep.Labels[designateRecordSetID] = oep.Labels[designateRecordSetID] - } - return - } - } -} - -// ApplyChanges applies a given set of changes in a given zone. -func (p designateProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - managedZones, err := p.getZones() - if err != nil { - return err - } - - endpoints, err := p.Records(ctx) - if err != nil { - return fmt.Errorf("failed to fetch active records: %w", err) - } - - recordSets := map[string]*recordSet{} - for _, ep := range changes.Create { - addEndpoint(ep, recordSets, endpoints, false) - } - for _, ep := range changes.UpdateOld { - addEndpoint(ep, recordSets, endpoints, true) - } - for _, ep := range changes.UpdateNew { - addEndpoint(ep, recordSets, endpoints, false) - } - for _, ep := range changes.Delete { - addEndpoint(ep, recordSets, endpoints, true) - } - - for _, rs := range recordSets { - if err2 := p.upsertRecordSet(rs, managedZones); err == nil { - err = err2 - } - } - return err -} - -// apply recordset changes by inserting/updating/deleting recordsets -func (p designateProvider) upsertRecordSet(rs *recordSet, managedZones map[string]string) error { - if rs.zoneID == "" { - var err error - rs.zoneID, err = p.getHostZoneID(rs.dnsName, managedZones) - if err != nil { - return err - } - if rs.zoneID == "" { - log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", rs.dnsName) - return nil - } - } - var records []string - for rec, v := range rs.names { - if v { - records = append(records, rec) - } - } - if rs.recordSetID == "" && records == nil { - return nil - } - if rs.recordSetID == "" { - opts := recordsets.CreateOpts{ - Name: rs.dnsName, - Type: rs.recordType, - Records: records, - } - log.Infof("Creating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ",")) - if p.dryRun { - return nil - } - _, err := p.client.CreateRecordSet(rs.zoneID, opts) - return err - } else if len(records) == 0 { - log.Infof("Deleting records for %s/%s", rs.dnsName, rs.recordType) - if p.dryRun { - return nil - } - return p.client.DeleteRecordSet(rs.zoneID, rs.recordSetID) - } else { - ttl := 0 - opts := recordsets.UpdateOpts{ - Records: records, - TTL: &ttl, - } - log.Infof("Updating records: %s/%s: %s", rs.dnsName, rs.recordType, strings.Join(records, ",")) - if p.dryRun { - return nil - } - return p.client.UpdateRecordSet(rs.zoneID, rs.recordSetID, opts) - } -} diff --git a/provider/designate/designate_test.go b/provider/designate/designate_test.go deleted file mode 100644 index 80106489a..000000000 --- a/provider/designate/designate_test.go +++ /dev/null @@ -1,584 +0,0 @@ -/* -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 designate - -import ( - "context" - "encoding/pem" - "fmt" - "net/http" - "net/http/httptest" - "os" - "reflect" - "sort" - "sync/atomic" - "testing" - - "github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets" - "github.com/gophercloud/gophercloud/openstack/dns/v2/zones" - - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" - "sigs.k8s.io/external-dns/provider" -) - -var lastGeneratedDesignateID int32 - -func generateDesignateID() string { - return fmt.Sprintf("id-%d", atomic.AddInt32(&lastGeneratedDesignateID, 1)) -} - -type fakeDesignateClient struct { - managedZones map[string]*struct { - zone *zones.Zone - recordSets map[string]*recordsets.RecordSet - } -} - -func (c fakeDesignateClient) AddZone(zone zones.Zone) string { - if zone.ID == "" { - zone.ID = zone.Name - } - c.managedZones[zone.ID] = &struct { - zone *zones.Zone - recordSets map[string]*recordsets.RecordSet - }{ - zone: &zone, - recordSets: make(map[string]*recordsets.RecordSet), - } - return zone.ID -} - -func (c fakeDesignateClient) ForEachZone(handler func(zone *zones.Zone) error) error { - for _, zone := range c.managedZones { - if err := handler(zone.zone); err != nil { - return err - } - } - return nil -} - -func (c fakeDesignateClient) ForEachRecordSet(zoneID string, handler func(recordSet *recordsets.RecordSet) error) error { - zone := c.managedZones[zoneID] - if zone == nil { - return fmt.Errorf("unknown zone %s", zoneID) - } - for _, recordSet := range zone.recordSets { - if err := handler(recordSet); err != nil { - return err - } - } - return nil -} - -func (c fakeDesignateClient) CreateRecordSet(zoneID string, opts recordsets.CreateOpts) (string, error) { - zone := c.managedZones[zoneID] - if zone == nil { - return "", fmt.Errorf("unknown zone %s", zoneID) - } - rs := &recordsets.RecordSet{ - ID: generateDesignateID(), - ZoneID: zoneID, - Name: opts.Name, - Description: opts.Description, - Records: opts.Records, - TTL: opts.TTL, - Type: opts.Type, - } - zone.recordSets[rs.ID] = rs - return rs.ID, nil -} - -func (c fakeDesignateClient) UpdateRecordSet(zoneID, recordSetID string, opts recordsets.UpdateOpts) error { - zone := c.managedZones[zoneID] - if zone == nil { - return fmt.Errorf("unknown zone %s", zoneID) - } - rs := zone.recordSets[recordSetID] - if rs == nil { - return fmt.Errorf("unknown record-set %s", recordSetID) - } - if opts.Description != nil { - rs.Description = *opts.Description - } - rs.TTL = *opts.TTL - - rs.Records = opts.Records - return nil -} - -func (c fakeDesignateClient) DeleteRecordSet(zoneID, recordSetID string) error { - zone := c.managedZones[zoneID] - if zone == nil { - return fmt.Errorf("unknown zone %s", zoneID) - } - delete(zone.recordSets, recordSetID) - return nil -} - -func (c fakeDesignateClient) ToProvider() provider.Provider { - return &designateProvider{client: c} -} - -func newFakeDesignateClient() *fakeDesignateClient { - return &fakeDesignateClient{ - make(map[string]*struct { - zone *zones.Zone - recordSets map[string]*recordsets.RecordSet - }), - } -} - -func TestNewDesignateProvider(t *testing.T) { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusAccepted) - w.Write([]byte(`{ - "token": { - "catalog": [ - { - "id": "9615c2dfac3b4b19935226d4c9d4afce", - "name": "designate", - "type": "dns", - "endpoints": [ - { - "id": "3d3cc3a273b54d0490ac43d6572e4c48", - "region": "RegionOne", - "region_id": "RegionOne", - "interface": "public", - "url": "https://example.com:9001" - } - ] - } - ] - } - }`)) - })) - defer ts.Close() - - block := &pem.Block{ - Type: "CERTIFICATE", - Bytes: ts.Certificate().Raw, - } - tmpfile, err := os.CreateTemp("", "os-test.crt") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpfile.Name()) - if err := pem.Encode(tmpfile, block); err != nil { - t.Fatal(err) - } - if err := tmpfile.Close(); err != nil { - t.Fatal(err) - } - - os.Setenv("OS_AUTH_URL", ts.URL+"/v3") - os.Setenv("OS_USERNAME", "username") - os.Setenv("OS_PASSWORD", "password") - os.Setenv("OS_USER_DOMAIN_NAME", "Default") - os.Setenv("OPENSTACK_CA_FILE", tmpfile.Name()) - - if _, err := NewDesignateProvider(endpoint.DomainFilter{}, true); err != nil { - t.Fatalf("Failed to initialize Designate provider: %s", err) - } -} - -func TestDesignateRecords(t *testing.T) { - client := newFakeDesignateClient() - - zone1ID := client.AddZone(zones.Zone{ - Name: "example.com.", - Type: "PRIMARY", - Status: "ACTIVE", - }) - rs11ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ - Name: "www.example.com.", - Type: endpoint.RecordTypeA, - Records: []string{"10.1.1.1"}, - }) - rs12ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ - Name: "www.example.com.", - Type: endpoint.RecordTypeTXT, - Records: []string{"text1"}, - }) - client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ - Name: "xxx.example.com.", - Type: "SRV", - Records: []string{"http://test.com:1234"}, - }) - rs14ID, _ := client.CreateRecordSet(zone1ID, recordsets.CreateOpts{ - Name: "ftp.example.com.", - Type: endpoint.RecordTypeA, - Records: []string{"10.1.1.2"}, - }) - - zone2ID := client.AddZone(zones.Zone{ - Name: "test.net.", - Type: "PRIMARY", - Status: "ACTIVE", - }) - rs21ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{ - Name: "srv.test.net.", - Type: endpoint.RecordTypeA, - Records: []string{"10.2.1.1", "10.2.1.2"}, - }) - rs22ID, _ := client.CreateRecordSet(zone2ID, recordsets.CreateOpts{ - Name: "db.test.net.", - Type: endpoint.RecordTypeCNAME, - Records: []string{"sql.test.net."}, - }) - expected := []*endpoint.Endpoint{ - { - DNSName: "www.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.1"}, - Labels: map[string]string{ - designateRecordSetID: rs11ID, - designateZoneID: zone1ID, - designateOriginalRecords: "10.1.1.1", - }, - }, - { - DNSName: "www.example.com", - RecordType: endpoint.RecordTypeTXT, - Targets: endpoint.Targets{"text1"}, - Labels: map[string]string{ - designateRecordSetID: rs12ID, - designateZoneID: zone1ID, - designateOriginalRecords: "text1", - }, - }, - { - DNSName: "ftp.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.2"}, - Labels: map[string]string{ - designateRecordSetID: rs14ID, - designateZoneID: zone1ID, - designateOriginalRecords: "10.1.1.2", - }, - }, - { - DNSName: "srv.test.net", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.1", "10.2.1.2"}, - Labels: map[string]string{ - designateRecordSetID: rs21ID, - designateZoneID: zone2ID, - designateOriginalRecords: "10.2.1.1\00010.2.1.2", - }, - }, - { - DNSName: "db.test.net", - RecordType: endpoint.RecordTypeCNAME, - Targets: endpoint.Targets{"sql.test.net"}, - Labels: map[string]string{ - designateRecordSetID: rs22ID, - designateZoneID: zone2ID, - designateOriginalRecords: "sql.test.net.", - }, - }, - } - - endpoints, err := client.ToProvider().Records(context.Background()) - if err != nil { - t.Fatal(err) - } -out: - for _, ep := range endpoints { - for i, ex := range expected { - if reflect.DeepEqual(ep, ex) { - expected = append(expected[:i], expected[i+1:]...) - continue out - } - } - t.Errorf("unexpected endpoint %s/%s -> %s", ep.DNSName, ep.RecordType, ep.Targets) - } - if len(expected) != 0 { - t.Errorf("not all expected endpoints were returned. Remained: %v", expected) - } -} - -func TestDesignateCreateRecords(t *testing.T) { - client := newFakeDesignateClient() - testDesignateCreateRecords(t, client) -} - -func testDesignateCreateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet { - for i, zoneName := range []string{"example.com.", "test.net."} { - client.AddZone(zones.Zone{ - ID: fmt.Sprintf("zone-%d", i+1), - Name: zoneName, - Type: "PRIMARY", - Status: "ACTIVE", - }) - } - - _, err := client.CreateRecordSet("zone-1", recordsets.CreateOpts{ - Name: "www.example.com.", - Description: "", - Records: []string{"foo"}, - TTL: 60, - Type: endpoint.RecordTypeTXT, - }) - - if err != nil { - t.Fatal("failed to prefil records") - } - - endpoints := []*endpoint.Endpoint{ - { - DNSName: "www.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.1"}, - Labels: map[string]string{}, - }, - { - DNSName: "www.example.com", - RecordType: endpoint.RecordTypeTXT, - Targets: endpoint.Targets{"text1"}, - Labels: map[string]string{}, - }, - { - DNSName: "ftp.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.2"}, - Labels: map[string]string{}, - }, - { - DNSName: "srv.test.net", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.1"}, - Labels: map[string]string{}, - }, - { - DNSName: "srv.test.net", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.2"}, - Labels: map[string]string{}, - }, - { - DNSName: "db.test.net", - RecordType: endpoint.RecordTypeCNAME, - Targets: endpoint.Targets{"sql.test.net"}, - Labels: map[string]string{}, - }, - } - expected := []*recordsets.RecordSet{ - { - Name: "www.example.com.", - Type: endpoint.RecordTypeA, - Records: []string{"10.1.1.1"}, - ZoneID: "zone-1", - }, - { - Name: "www.example.com.", - Type: endpoint.RecordTypeTXT, - Records: []string{"text1"}, - ZoneID: "zone-1", - }, - { - Name: "ftp.example.com.", - Type: endpoint.RecordTypeA, - Records: []string{"10.1.1.2"}, - ZoneID: "zone-1", - }, - { - Name: "srv.test.net.", - Type: endpoint.RecordTypeA, - Records: []string{"10.2.1.1", "10.2.1.2"}, - ZoneID: "zone-2", - }, - { - Name: "db.test.net.", - Type: endpoint.RecordTypeCNAME, - Records: []string{"sql.test.net."}, - ZoneID: "zone-2", - }, - } - expectedCopy := make([]*recordsets.RecordSet, len(expected)) - copy(expectedCopy, expected) - - err = client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Create: endpoints}) - if err != nil { - t.Fatal(err) - } - - client.ForEachZone(func(zone *zones.Zone) error { - client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { - id := recordSet.ID - recordSet.ID = "" - for i, ex := range expected { - sort.Strings(recordSet.Records) - if reflect.DeepEqual(ex, recordSet) { - ex.ID = id - recordSet.ID = id - expected = append(expected[:i], expected[i+1:]...) - return nil - } - } - t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) - return nil - }) - return nil - }) - - if len(expected) != 0 { - t.Errorf("not all expected record-sets were created. Remained: %v", expected) - } - return expectedCopy -} - -func TestDesignateUpdateRecords(t *testing.T) { - client := newFakeDesignateClient() - testDesignateUpdateRecords(t, client) -} - -func testDesignateUpdateRecords(t *testing.T, client *fakeDesignateClient) []*recordsets.RecordSet { - expected := testDesignateCreateRecords(t, client) - - updatesOld := []*endpoint.Endpoint{ - { - DNSName: "ftp.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.2"}, - Labels: map[string]string{ - designateZoneID: "zone-1", - designateRecordSetID: expected[2].ID, - designateOriginalRecords: "10.1.1.2", - }, - }, - { - DNSName: "srv.test.net.", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.2"}, - Labels: map[string]string{ - designateZoneID: "zone-2", - designateRecordSetID: expected[3].ID, - designateOriginalRecords: "10.2.1.1\00010.2.1.2", - }, - }, - } - updatesNew := []*endpoint.Endpoint{ - { - DNSName: "ftp.example.com", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.3.3.1"}, - Labels: map[string]string{ - designateZoneID: "zone-1", - designateRecordSetID: expected[2].ID, - designateOriginalRecords: "10.1.1.2", - }, - }, - { - DNSName: "srv.test.net.", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.3.3.2"}, - Labels: map[string]string{ - designateZoneID: "zone-2", - designateRecordSetID: expected[3].ID, - designateOriginalRecords: "10.2.1.1\00010.2.1.2", - }, - }, - } - expectedCopy := make([]*recordsets.RecordSet, len(expected)) - copy(expectedCopy, expected) - - expected[2].Records = []string{"10.3.3.1"} - expected[3].Records = []string{"10.2.1.1", "10.3.3.2"} - - err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{UpdateOld: updatesOld, UpdateNew: updatesNew}) - if err != nil { - t.Fatal(err) - } - - client.ForEachZone(func(zone *zones.Zone) error { - client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { - for i, ex := range expected { - sort.Strings(recordSet.Records) - if reflect.DeepEqual(ex, recordSet) { - expected = append(expected[:i], expected[i+1:]...) - return nil - } - } - t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) - return nil - }) - return nil - }) - - if len(expected) != 0 { - t.Errorf("not all expected record-sets were updated. Remained: %v", expected) - } - return expectedCopy -} - -func TestDesignateDeleteRecords(t *testing.T) { - client := newFakeDesignateClient() - testDesignateDeleteRecords(t, client) -} - -func testDesignateDeleteRecords(t *testing.T, client *fakeDesignateClient) { - expected := testDesignateUpdateRecords(t, client) - deletes := []*endpoint.Endpoint{ - { - DNSName: "www.example.com.", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.1.1.1"}, - Labels: map[string]string{ - designateZoneID: "zone-1", - designateRecordSetID: expected[0].ID, - designateOriginalRecords: "10.1.1.1", - }, - }, - { - DNSName: "srv.test.net.", - RecordType: endpoint.RecordTypeA, - Targets: endpoint.Targets{"10.2.1.1"}, - Labels: map[string]string{ - designateZoneID: "zone-2", - designateRecordSetID: expected[3].ID, - designateOriginalRecords: "10.2.1.1\00010.3.3.2", - }, - }, - } - expected[3].Records = []string{"10.3.3.2"} - expected = expected[1:] - - err := client.ToProvider().ApplyChanges(context.Background(), &plan.Changes{Delete: deletes}) - if err != nil { - t.Fatal(err) - } - - client.ForEachZone(func(zone *zones.Zone) error { - client.ForEachRecordSet(zone.ID, func(recordSet *recordsets.RecordSet) error { - for i, ex := range expected { - sort.Strings(recordSet.Records) - if reflect.DeepEqual(ex, recordSet) { - expected = append(expected[:i], expected[i+1:]...) - return nil - } - } - t.Errorf("unexpected record-set %s/%s -> %v", recordSet.Name, recordSet.Type, recordSet.Records) - return nil - }) - return nil - }) - - if len(expected) != 0 { - t.Errorf("not all expected record-sets were deleted. Remained: %v", expected) - } -}