Commit 5fc7d31a authored by Nick Jüttner's avatar Nick Jüttner
Browse files

Resolving merge conflicts

parents 2319a44f 09a6f6ba
No related merge requests found
Showing with 1209 additions and 2 deletions
+1209 -2
......@@ -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
# 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
```
......@@ -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)
......
/*
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
}
/*
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)
}
}
/*
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)
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment