mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-05-05 06:36:11 +02:00
Resolving merge conflicts
This commit is contained in:
commit
5fc7d31a57
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -226,6 +226,12 @@
|
||||
packages = ["."]
|
||||
revision = "44d81051d367757e1c7c6a5a86423ece9afcf63c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/gophercloud/gophercloud"
|
||||
packages = ["."]
|
||||
revision = "bfc4756e1a693a850d7d459f4b28b21f35a24b5a"
|
||||
|
||||
[[projects]]
|
||||
name = "github.com/howeyc/gopass"
|
||||
packages = ["."]
|
||||
@ -622,6 +628,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "7af57b8d195abce34060c82b92243ddba947f5d882be8f9a08b1aa827a9061fa"
|
||||
inputs-digest = "3cc043ee8be5b7cdba42793f12259ad23e13608d4b6a18523e540e14f029c0b0"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
||||
155
docs/tutorials/designate.md
Normal file
155
docs/tutorials/designate.md
Normal file
@ -0,0 +1,155 @@
|
||||
# Setting up ExternalDNS for Services on OpenStack Designate
|
||||
|
||||
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:
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: registry.opensource.zalan.do/teapot/external-dns
|
||||
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: http://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
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
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
|
||||
```
|
||||
2
main.go
2
main.go
@ -134,6 +134,8 @@ func main() {
|
||||
)
|
||||
case "inmemory":
|
||||
p, err = provider.NewInMemoryProvider(provider.InMemoryInitZones(cfg.InMemoryZones), provider.InMemoryWithDomain(domainFilter), provider.InMemoryWithLogging()), nil
|
||||
case "designate":
|
||||
p, err = provider.NewDesignateProvider(domainFilter, cfg.DryRun)
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
|
||||
@ -159,7 +159,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("publish-internal-services", "Allow external-dns to publish DNS records for ClusterIP services (optional)").BoolVar(&cfg.PublishInternal)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "inmemory")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, inmemory)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "desginate", "inmemory")
|
||||
app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter)
|
||||
app.Flag("zone-id-filter", "Filter target zones by hosted zone id; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneIDFilter)
|
||||
app.Flag("google-project", "When using the Google provider, current project is auto-detected, when running on GCP. Specify other project with this. Must be specified when running outside GCP.").Default(defaultConfig.GoogleProject).StringVar(&cfg.GoogleProject)
|
||||
|
||||
85
pkg/tlsutils/tlsconfig.go
Normal file
85
pkg/tlsutils/tlsconfig.go
Normal file
@ -0,0 +1,85 @@
|
||||
/*
|
||||
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 tlsutils
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CreateTLSConfig creates tls.Config instance from TLS parameters passed in environment variables with the given prefix
|
||||
func CreateTLSConfig(prefix string) (*tls.Config, error) {
|
||||
caFile := os.Getenv(fmt.Sprintf("%s_CA_FILE", prefix))
|
||||
certFile := os.Getenv(fmt.Sprintf("%s_CERT_FILE", prefix))
|
||||
keyFile := os.Getenv(fmt.Sprintf("%s_KEY_FILE", prefix))
|
||||
serverName := os.Getenv(fmt.Sprintf("%s_TLS_SERVER_NAME", prefix))
|
||||
isInsecureStr := strings.ToLower(os.Getenv(fmt.Sprintf("%s_TLS_INSECURE", prefix)))
|
||||
isInsecure := isInsecureStr == "true" || isInsecureStr == "yes" || isInsecureStr == "1"
|
||||
tlsConfig, err := newTLSConfig(certFile, keyFile, caFile, serverName, isInsecure)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func newTLSConfig(certPath, keyPath, caPath, serverName string, insecure bool) (*tls.Config, error) {
|
||||
if certPath != "" && keyPath == "" || certPath == "" && keyPath != "" {
|
||||
return nil, errors.New("either both cert and key or none must be provided")
|
||||
}
|
||||
var certificates []tls.Certificate
|
||||
if certPath != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load TLS cert: %s", err)
|
||||
}
|
||||
certificates = append(certificates, cert)
|
||||
}
|
||||
roots, err := loadRoots(caPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: certificates,
|
||||
RootCAs: roots,
|
||||
InsecureSkipVerify: insecure,
|
||||
ServerName: serverName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// loads CA cert
|
||||
func loadRoots(caPath string) (*x509.CertPool, error) {
|
||||
if caPath == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
roots := x509.NewCertPool()
|
||||
pem, err := ioutil.ReadFile(caPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error reading %s: %s", caPath, err)
|
||||
}
|
||||
ok := roots.AppendCertsFromPEM(pem)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not read root certs: %s", err)
|
||||
}
|
||||
return roots, nil
|
||||
}
|
||||
440
provider/designate.go
Normal file
440
provider/designate.go
Normal file
@ -0,0 +1,440 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"
|
||||
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/pkg/tlsutils"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
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.AuthenticatedClient(opts)
|
||||
if 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
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
client.ProviderClient.HTTPClient.Transport = transport
|
||||
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 {
|
||||
client designateClientInterface
|
||||
|
||||
// only consider hosted zones managing domains ending in this suffix
|
||||
domainFilter DomainFilter
|
||||
dryRun bool
|
||||
}
|
||||
|
||||
// NewDesignateProvider is a factory function for OpenStack designate providers
|
||||
func NewDesignateProvider(domainFilter DomainFilter, dryRun bool) (Provider, error) {
|
||||
client, err := newDesignateClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &designateProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
dryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// converts domain name to FQDN
|
||||
func canonicalizeDomainName(domain string) string {
|
||||
if !strings.HasSuffix(domain, ".") {
|
||||
domain += "."
|
||||
}
|
||||
return strings.ToLower(domain)
|
||||
}
|
||||
|
||||
// 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() ([]*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
|
||||
}
|
||||
for _, record := range recordSet.Records {
|
||||
ep := endpoint.NewEndpoint(recordSet.Name, record, recordSet.Type)
|
||||
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, 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),
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
target := ep.Target
|
||||
if ep.RecordType == endpoint.RecordTypeCNAME {
|
||||
target = canonicalizeDomainName(target)
|
||||
}
|
||||
rs.names[target] = !delete
|
||||
recordSets[key] = rs
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p designateProvider) ApplyChanges(changes *plan.Changes) error {
|
||||
managedZones, err := p.getZones()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recordSets := map[string]*recordSet{}
|
||||
for _, ep := range changes.Create {
|
||||
addEndpoint(ep, recordSets, false)
|
||||
}
|
||||
for _, ep := range changes.UpdateNew {
|
||||
addEndpoint(ep, recordSets, false)
|
||||
}
|
||||
for _, ep := range changes.UpdateOld {
|
||||
addEndpoint(ep, recordSets, true)
|
||||
}
|
||||
for _, ep := range changes.Delete {
|
||||
addEndpoint(ep, recordSets, 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 {
|
||||
opts := recordsets.UpdateOpts{
|
||||
Records: records,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
519
provider/designate_test.go
Normal file
519
provider/designate_test.go
Normal file
@ -0,0 +1,519 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/gophercloud/gophercloud/openstack/dns/v2/recordsets"
|
||||
"github.com/gophercloud/gophercloud/openstack/dns/v2/zones"
|
||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||
"github.com/kubernetes-incubator/external-dns/plan"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
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 {
|
||||
return &designateProvider{client: c}
|
||||
}
|
||||
|
||||
func newFakeDesignateClient() *fakeDesignateClient {
|
||||
return &fakeDesignateClient{
|
||||
make(map[string]*struct {
|
||||
zone *zones.Zone
|
||||
recordSets map[string]*recordsets.RecordSet
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Target: "10.1.1.1",
|
||||
Labels: map[string]string{
|
||||
designateRecordSetID: rs11ID,
|
||||
designateZoneID: zone1ID,
|
||||
designateOriginalRecords: "10.1.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "www.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Target: "text1",
|
||||
Labels: map[string]string{
|
||||
designateRecordSetID: rs12ID,
|
||||
designateZoneID: zone1ID,
|
||||
designateOriginalRecords: "text1",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "ftp.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.1.1.2",
|
||||
Labels: map[string]string{
|
||||
designateRecordSetID: rs14ID,
|
||||
designateZoneID: zone1ID,
|
||||
designateOriginalRecords: "10.1.1.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "srv.test.net",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.2.1.1",
|
||||
Labels: map[string]string{
|
||||
designateRecordSetID: rs21ID,
|
||||
designateZoneID: zone2ID,
|
||||
designateOriginalRecords: "10.2.1.1\00010.2.1.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "srv.test.net",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "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,
|
||||
Target: "sql.test.net",
|
||||
Labels: map[string]string{
|
||||
designateRecordSetID: rs22ID,
|
||||
designateZoneID: zone2ID,
|
||||
designateOriginalRecords: "sql.test.net.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
endpoints, err := client.ToProvider().Records()
|
||||
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.Target)
|
||||
}
|
||||
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",
|
||||
})
|
||||
}
|
||||
endpoints := []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "www.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.1.1.1",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
{
|
||||
DNSName: "www.example.com",
|
||||
RecordType: endpoint.RecordTypeTXT,
|
||||
Target: "text1",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
{
|
||||
DNSName: "ftp.example.com",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.1.1.2",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
{
|
||||
DNSName: "srv.test.net",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.2.1.1",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
{
|
||||
DNSName: "srv.test.net",
|
||||
RecordType: endpoint.RecordTypeA,
|
||||
Target: "10.2.1.2",
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
{
|
||||
DNSName: "db.test.net",
|
||||
RecordType: endpoint.RecordTypeCNAME,
|
||||
Target: "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(&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,
|
||||
Target: "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,
|
||||
Target: "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,
|
||||
Target: "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,
|
||||
Target: "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(&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,
|
||||
Target: "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,
|
||||
Target: "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(&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…
x
Reference in New Issue
Block a user