mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
chore(openstack designate)!: remove in-tree provider
This commit is contained in:
parent
a4fac0ae78
commit
313e15ac2c
@ -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)
|
||||
|
@ -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) |
|
||||
|
@ -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
|
||||
```
|
3
main.go
3
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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user