Commit b9c1e433 authored by cliedeman's avatar cliedeman
Browse files

Linode Provider Implementation

parent c090c989
......@@ -107,6 +107,14 @@
pruneopts = ""
revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
[[projects]]
digest = "1:e62a9159a86473538057a322c49b65824cc32da865ace6cfc5ef1cacf3245226"
name = "github.com/chiefy/linodego"
packages = ["."]
pruneopts = ""
revision = "e7bd44d89fc5db41b43b8bce7278550a589d7ab6"
version = "v0.2.0"
[[projects]]
digest = "1:85fd00554a6ed5b33687684b76635d532c74141508b5bce2843d85e8a3c9dc91"
name = "github.com/cloudflare/cloudflare-go"
......@@ -203,6 +211,14 @@
revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a"
version = "v1.32.0"
[[projects]]
digest = "1:8e67153fc0a9fb0d6c9707e36cf80e217a012364307b222eb4ba6828f7e881e6"
name = "github.com/go-resty/resty"
packages = ["."]
pruneopts = ""
revision = "97a15579492cd5f35632499f315d7a8df94160a1"
version = "v1.8.0"
[[projects]]
digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918"
name = "github.com/gogo/protobuf"
......@@ -822,6 +838,7 @@
"github.com/aws/aws-sdk-go/aws/session",
"github.com/aws/aws-sdk-go/service/route53",
"github.com/aws/aws-sdk-go/service/servicediscovery",
"github.com/chiefy/linodego",
"github.com/cloudflare/cloudflare-go",
"github.com/coreos/etcd/client",
"github.com/digitalocean/godo",
......
......@@ -75,3 +75,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/oracle/oci-go-sdk"
version = "1.8.0"
[[constraint]]
name = "github.com/chiefy/linodego"
version = "0.2.0"
\ No newline at end of file
......@@ -61,6 +61,7 @@ The following tutorials are provided:
* [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
* [Exoscale](docs/tutorials/exoscale.md)
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
* [Linode](docs/tutorials/linode.md)
## Running Locally
......
# Setting up ExternalDNS for Services on Linode
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using Linode DNS Manager.
Make sure to use **>=0.6.0** version of ExternalDNS for this tutorial.
## Managing DNS with Linode
If you want to learn about how to use Linode DNS Manager read the following tutorials:
[An Introduction to Managing DNS](https://www.linode.com/docs/platform/manager/dns-manager/), and [general documentation](https://www.linode.com/docs/networking/dns/)
## Creating Linode Credentials
Generate a new oauth token by following the instructions at [Access-and-Authentication](https://developers.linode.com/api/v4#section/Access-and-Authentication)
The environment variable `LINODE_TOKEN` will be needed to run ExternalDNS with Linode.
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
Then apply one of the following manifests file to deploy ExternalDNS.
### Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=linode
env:
- name: LINODE_TOKEN
value: "YOUR_LINODE_API_KEY"
```
### Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.opensource.zalan.do/teapot/external-dns:latest
args:
- --source=service # ingress is also possible
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
- --provider=linode
env:
- name: LINODE_TOKEN
value: "YOUR_LINODE_API_KEY"
```
## 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 Linode 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
```
Depending where you run your service it can take a little while for your cloud provider to create an external IP for the service.
Once the service has an external IP assigned, ExternalDNS will notice the new service IP address and synchronize the Linode DNS records.
## Verifying Linode DNS records
Check your [Linode UI](https://manager.linode.com/dns) to view the records for your Linode DNS zone.
Click on the zone for the one created above if a different domain was used.
This should show the external IP address of the service as the A record for your domain.
## Cleanup
Now that we have verified that ExternalDNS will automatically manage Linode DNS records, we can delete the tutorial's example:
```
$ kubectl delete service -f nginx.yaml
$ kubectl delete service -f externaldns.yaml
```
......@@ -121,6 +121,8 @@ func main() {
p, err = provider.NewGoogleProvider(cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.DryRun)
case "digitalocean":
p, err = provider.NewDigitalOceanProvider(domainFilter, cfg.DryRun)
case "linode":
p, err = provider.NewLinodeProvider(domainFilter, cfg.DryRun)
case "dnsimple":
p, err = provider.NewDnsimpleProvider(domainFilter, zoneIDFilter, cfg.DryRun)
case "infoblox":
......
......@@ -196,7 +196,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("connector-source-server", "The server to connect for connector source, valid only when using connector source").Default(defaultConfig.ConnectorSourceServer).StringVar(&cfg.ConnectorSourceServer)
// Flags related to providers
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode")
app.Flag("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 provider
import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"github.com/chiefy/linodego"
log "github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"strings"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
// LinodeDomainClient interface to ease testing
type LinodeDomainClient interface {
ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]*linodego.DomainRecord, error)
ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]*linodego.Domain, error)
CreateDomainRecord(ctx context.Context, domainID int, domainrecord linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error)
DeleteDomainRecord(ctx context.Context, domainID int, id int) error
UpdateDomainRecord(ctx context.Context, domainID int, id int, domainrecord linodego.DomainRecordUpdateOptions) (*linodego.DomainRecord, error)
}
// LinodeProvider is an implementation of Provider for Digital Ocean's DNS.
type LinodeProvider struct {
Client LinodeDomainClient
domainFilter DomainFilter
DryRun bool
}
// LinodeChanges All API calls calculated from the plan
type LinodeChanges struct {
Creates []*LinodeChangeCreate
Deletes []*LinodeChangeDelete
Updates []*LinodeChangeUpdate
}
// LinodeChangeCreate Linode Domain Record Creates
type LinodeChangeCreate struct {
Domain *linodego.Domain
Options linodego.DomainRecordCreateOptions
}
// LinodeChangeUpdate Linode Domain Record Updates
type LinodeChangeUpdate struct {
Domain *linodego.Domain
DomainRecord *linodego.DomainRecord
Options linodego.DomainRecordUpdateOptions
}
// LinodeChangeDelete Linode Domain Record Deletes
type LinodeChangeDelete struct {
Domain *linodego.Domain
DomainRecord *linodego.DomainRecord
}
// NewLinodeProvider initializes a new Linode DNS based Provider.
func NewLinodeProvider(domainFilter DomainFilter, dryRun bool) (*LinodeProvider, error) {
token, ok := os.LookupEnv("LINODE_TOKEN")
if !ok {
return nil, fmt.Errorf("no token found")
}
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
oauth2Client := &http.Client{
Transport: &oauth2.Transport{
Source: tokenSource,
},
}
linodeClient := linodego.NewClient(oauth2Client)
provider := &LinodeProvider{
Client: &linodeClient,
domainFilter: domainFilter,
DryRun: dryRun,
}
return provider, nil
}
// Zones returns the list of hosted zones.
func (p *LinodeProvider) Zones() ([]*linodego.Domain, error) {
zones, err := p.fetchZones()
if err != nil {
return nil, err
}
return zones, nil
}
// Records returns the list of records in a given zone.
func (p *LinodeProvider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.Zones()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
for _, zone := range zones {
records, err := p.fetchRecords(zone.ID)
if err != nil {
return nil, err
}
for _, r := range records {
if supportedRecordType(string(r.Type)) {
name := fmt.Sprintf("%s.%s", r.Name, zone.Domain)
// root name is identified by the empty string and should be
// translated to zone name for the endpoint entry.
if r.Name == "" {
name = zone.Domain
}
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(name, string(r.Type), endpoint.TTL(r.TTLSec), r.Target))
}
}
}
return endpoints, nil
}
func (p *LinodeProvider) fetchRecords(domainID int) ([]*linodego.DomainRecord, error) {
records, err := p.Client.ListDomainRecords(context.TODO(), domainID, nil)
if err != nil {
return nil, err
}
return records, nil
}
func (p *LinodeProvider) fetchZones() ([]*linodego.Domain, error) {
var zones []*linodego.Domain
allZones, err := p.Client.ListDomains(context.TODO(), linodego.NewListOptions(0, ""))
if err != nil {
return nil, err
}
for _, zone := range allZones {
if !p.domainFilter.Match(zone.Domain) {
continue
}
zones = append(zones, zone)
}
return zones, nil
}
// submitChanges takes a zone and a collection of Changes and sends them as a single transaction.
func (p *LinodeProvider) submitChanges(changes LinodeChanges) error {
for _, change := range changes.Creates {
logFields := log.Fields{
"record": change.Options.Name,
"type": change.Options.Type,
"action": "Create",
"zoneName": change.Domain.Domain,
"zoneID": change.Domain.ID,
}
log.WithFields(logFields).Info("Creating record.")
if p.DryRun {
log.WithFields(logFields).Info("Would create record.")
} else {
if _, err := p.Client.CreateDomainRecord(context.TODO(), change.Domain.ID, change.Options); err != nil {
log.WithFields(logFields).Errorf(
"Failed to Create record: %v",
err,
)
}
}
}
for _, change := range changes.Deletes {
logFields := log.Fields{
"record": change.DomainRecord.Name,
"type": change.DomainRecord.Type,
"action": "Delete",
"zoneName": change.Domain.Domain,
"zoneID": change.Domain.ID,
}
log.WithFields(logFields).Info("Deleting record.")
if p.DryRun {
log.WithFields(logFields).Info("Would delete record.")
} else {
if err := p.Client.DeleteDomainRecord(context.TODO(), change.Domain.ID, change.DomainRecord.ID); err != nil {
log.WithFields(logFields).Errorf(
"Failed to Delete record: %v",
err,
)
}
}
}
for _, change := range changes.Updates {
logFields := log.Fields{
"record": change.Options.Name,
"type": change.Options.Type,
"action": "Update",
"zoneName": change.Domain.Domain,
"zoneID": change.Domain.ID,
}
log.WithFields(logFields).Info("Updating record.")
if p.DryRun {
log.WithFields(logFields).Info("Would update record.")
} else {
if _, err := p.Client.UpdateDomainRecord(context.TODO(), change.Domain.ID, change.DomainRecord.ID, change.Options); err != nil {
log.WithFields(logFields).Errorf(
"Failed to Update record: %v",
err,
)
}
}
}
return nil
}
func getWeight() *int {
weight := 1
return &weight
}
func getPort() *int {
port := 0
return &port
}
func getPriority() *int {
priority := 0
return &priority
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *LinodeProvider) ApplyChanges(changes *plan.Changes) error {
recordsByZoneID := make(map[string][]*linodego.DomainRecord)
zones, err := p.fetchZones()
if err != nil {
return err
}
zonesByID := make(map[string]*linodego.Domain)
zoneNameIDMapper := zoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(strconv.Itoa(z.ID), z.Domain)
zonesByID[strconv.Itoa(z.ID)] = z
}
// Fetch records for each zone
for _, zone := range zones {
records, err := p.fetchRecords(zone.ID)
if err != nil {
return err
}
recordsByZoneID[strconv.Itoa(zone.ID)] = append(recordsByZoneID[strconv.Itoa(zone.ID)], records...)
}
createsByZone := endpointsByZone(zoneNameIDMapper, changes.Create)
updatesByZone := endpointsByZone(zoneNameIDMapper, changes.UpdateNew)
deletesByZone := endpointsByZone(zoneNameIDMapper, changes.Delete)
var linodeCreates []*LinodeChangeCreate
var linodeUpdates []*LinodeChangeUpdate
var linodeDeletes []*LinodeChangeDelete
// Generate Creates
for zoneID, creates := range createsByZone {
zone := zonesByID[zoneID]
if len(creates) == 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"zoneName": zone.Domain,
}).Debug("Skipping Zone, no creates found.")
continue
}
records := recordsByZoneID[zoneID]
for _, ep := range creates {
matchedRecords := getRecordID(records, zone, ep)
if len(matchedRecords) != 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"zoneName": zone.Domain,
"dnsName": ep.DNSName,
"recordType": ep.RecordType,
}).Warn("Records found which should not exist")
}
recordType, err := convertRecordType(ep.RecordType)
if err != nil {
return err
}
for _, target := range ep.Targets {
linodeCreates = append(linodeCreates, &LinodeChangeCreate{
Domain: zone,
Options: linodego.DomainRecordCreateOptions{
Target: target,
Name: getStrippedRecordName(zone, ep),
Type: recordType,
Weight: getWeight(),
Port: getPort(),
Priority: getPriority(),
TTLSec: int(ep.RecordTTL),
},
})
}
}
}
// Generate Updates
for zoneID, updates := range updatesByZone {
zone := zonesByID[zoneID]
if len(updates) == 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"zoneName": zone.Domain,
}).Debug("Skipping Zone, no updates found.")
continue
}
records := recordsByZoneID[zoneID]
for _, ep := range updates {
matchedRecords := getRecordID(records, zone, ep)
if len(matchedRecords) == 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"dnsName": ep.DNSName,
"zoneName": zone.Domain,
"recordType": ep.RecordType,
}).Warn("Update Records not found.")
}
recordType, err := convertRecordType(ep.RecordType)
if err != nil {
return err
}
matchedRecordsByTarget := make(map[string]*linodego.DomainRecord)
for _, record := range matchedRecords {
matchedRecordsByTarget[record.Target] = record
}
for _, target := range ep.Targets {
if record, ok := matchedRecordsByTarget[target]; ok {
log.WithFields(log.Fields{
"zoneID": zoneID,
"dnsName": ep.DNSName,
"zoneName": zone.Domain,
"recordType": ep.RecordType,
"target": target,
}).Warn("Updating Existing Target")
linodeUpdates = append(linodeUpdates, &LinodeChangeUpdate{
Domain: zone,
DomainRecord: record,
Options: linodego.DomainRecordUpdateOptions{
Target: target,
Name: getStrippedRecordName(zone, ep),
Type: recordType,
Weight: getWeight(),
Port: getPort(),
Priority: getPriority(),
TTLSec: int(ep.RecordTTL),
},
})
delete(matchedRecordsByTarget, target)
} else {
// Record did not previously exist, create new 'target'
log.WithFields(log.Fields{
"zoneID": zoneID,
"dnsName": ep.DNSName,
"zoneName": zone.Domain,
"recordType": ep.RecordType,
"target": target,
}).Warn("Creating New Target")
linodeCreates = append(linodeCreates, &LinodeChangeCreate{
Domain: zone,
Options: linodego.DomainRecordCreateOptions{
Target: target,
Name: getStrippedRecordName(zone, ep),
Type: recordType,
Weight: getWeight(),
Port: getPort(),
Priority: getPriority(),
TTLSec: int(ep.RecordTTL),
},
})
}
}
// Any remaining records have been removed, delete them
for _, record := range matchedRecordsByTarget {
log.WithFields(log.Fields{
"zoneID": zoneID,
"dnsName": ep.DNSName,
"zoneName": zone.Domain,
"recordType": ep.RecordType,
"target": record.Target,
}).Warn("Deleting Target")
linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
Domain: zone,
DomainRecord: record,
})
}
}
}
// Generate Deletes
for zoneID, deletes := range deletesByZone {
zone := zonesByID[zoneID]
if len(deletes) == 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"zoneName": zone.Domain,
}).Debug("Skipping Zone, no deletes found.")
continue
}
records := recordsByZoneID[zoneID]
for _, ep := range deletes {
matchedRecords := getRecordID(records, zone, ep)
if len(matchedRecords) == 0 {
log.WithFields(log.Fields{
"zoneID": zoneID,
"dnsName": ep.DNSName,
"zoneName": zone.Domain,
"recordType": ep.RecordType,
}).Warn("Records to Delete not found.")
}
for _, record := range matchedRecords {
linodeDeletes = append(linodeDeletes, &LinodeChangeDelete{
Domain: zone,
DomainRecord: record,
})
}
}
}
return p.submitChanges(LinodeChanges{
Creates: linodeCreates,
Deletes: linodeDeletes,
Updates: linodeUpdates,
})
}
func endpointsByZone(zoneNameIDMapper zoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
endpointsByZone := make(map[string][]*endpoint.Endpoint)
for _, ep := range endpoints {
zoneID, _ := zoneNameIDMapper.FindZone(ep.DNSName)
if zoneID == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", ep.DNSName)
continue
}
endpointsByZone[zoneID] = append(endpointsByZone[zoneID], ep)
}
return endpointsByZone
}
func convertRecordType(recordType string) (linodego.DomainRecordType, error) {
switch recordType {
case "A":
return linodego.RecordTypeA, nil
case "AAAA":
return linodego.RecordTypeAAAA, nil
case "CNAME":
return linodego.RecordTypeCNAME, nil
case "TXT":
return linodego.RecordTypeTXT, nil
case "SRV":
return linodego.RecordTypeSRV, nil
default:
return "", fmt.Errorf("invalid Record Type: %s", recordType)
}
}
func getStrippedRecordName(zone *linodego.Domain, ep *endpoint.Endpoint) string {
// Handle root
if ep.DNSName == zone.Domain {
return ""
}
return strings.TrimSuffix(ep.DNSName, "."+zone.Domain)
}
func getRecordID(records []*linodego.DomainRecord, zone *linodego.Domain, ep *endpoint.Endpoint) []*linodego.DomainRecord {
var matchedRecords []*linodego.DomainRecord
for _, record := range records {
if record.Name == getStrippedRecordName(zone, ep) && string(record.Type) == ep.RecordType {
matchedRecords = append(matchedRecords, record)
}
}
return matchedRecords
}
/*
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 (
"context"
"os"
"testing"
"github.com/chiefy/linodego"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type MockDomainClient struct {
mock.Mock
}
func (m *MockDomainClient) ListDomainRecords(ctx context.Context, domainID int, opts *linodego.ListOptions) ([]*linodego.DomainRecord, error) {
args := m.Called(ctx, domainID, opts)
return args.Get(0).([]*linodego.DomainRecord), args.Error(1)
}
func (m *MockDomainClient) ListDomains(ctx context.Context, opts *linodego.ListOptions) ([]*linodego.Domain, error) {
args := m.Called(ctx, opts)
return args.Get(0).([]*linodego.Domain), args.Error(1)
}
func (m *MockDomainClient) CreateDomainRecord(ctx context.Context, domainID int, opts linodego.DomainRecordCreateOptions) (*linodego.DomainRecord, error) {
args := m.Called(ctx, domainID, opts)
return args.Get(0).(*linodego.DomainRecord), args.Error(1)
}
func (m *MockDomainClient) DeleteDomainRecord(ctx context.Context, domainID int, recordID int) error {
args := m.Called(ctx, domainID, recordID)
return args.Error(0)
}
func (m *MockDomainClient) UpdateDomainRecord(ctx context.Context, domainID int, recordID int, opts linodego.DomainRecordUpdateOptions) (*linodego.DomainRecord, error) {
args := m.Called(ctx, domainID, recordID, opts)
return args.Get(0).(*linodego.DomainRecord), args.Error(1)
}
func createZones() []*linodego.Domain {
return []*linodego.Domain{
{ID: 1, Domain: "foo.com"},
{ID: 2, Domain: "bar.io"},
{ID: 3, Domain: "baz.com"},
}
}
func createFooRecords() []*linodego.DomainRecord {
return []*linodego.DomainRecord{{
ID: 11,
Type: linodego.RecordTypeA,
Name: "",
Target: "targetFoo",
}, {
ID: 12,
Type: linodego.RecordTypeTXT,
Name: "",
Target: "txt",
}, {
ID: 13,
Type: linodego.RecordTypeCAA,
Name: "foo.com",
Target: "",
}}
}
func createBarRecords() []*linodego.DomainRecord {
return []*linodego.DomainRecord{}
}
func createBazRecords() []*linodego.DomainRecord {
return []*linodego.DomainRecord{{
ID: 31,
Type: linodego.RecordTypeA,
Name: "",
Target: "targetBaz",
}, {
ID: 32,
Type: linodego.RecordTypeTXT,
Name: "",
Target: "txt",
}, {
ID: 33,
Type: linodego.RecordTypeA,
Name: "api",
Target: "targetBaz",
}, {
ID: 34,
Type: linodego.RecordTypeTXT,
Name: "api",
Target: "txt",
}}
}
func TestNewLinodeProvider(t *testing.T) {
_ = os.Setenv("LINODE_TOKEN", "xxxxxxxxxxxxxxxxx")
_, err := NewLinodeProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.NoError(t, err)
_ = os.Unsetenv("LINODE_TOKEN")
_, err = NewLinodeProvider(NewDomainFilter([]string{"ext-dns-test.zalando.to."}), true)
require.Error(t, err)
}
func TestLinodeStripRecordName(t *testing.T) {
assert.Equal(t, "api", getStrippedRecordName(&linodego.Domain{
Domain: "example.com",
}, &endpoint.Endpoint{
DNSName: "api.example.com",
}))
assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{
Domain: "example.com",
}, &endpoint.Endpoint{
DNSName: "example.com",
}))
}
func TestLinodeFetchZonesNoFiilters(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return(createZones(), nil).Once()
expected := createZones()
actual, err := provider.fetchZones()
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
assert.Equal(t, expected, actual)
}
func TestLinodeFetchZonesWithFilter(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{".com"}),
DryRun: false,
}
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return(createZones(), nil).Once()
expected := []*linodego.Domain{
{ID: 1, Domain: "foo.com"},
{ID: 3, Domain: "baz.com"},
}
actual, err := provider.fetchZones()
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
assert.Equal(t, expected, actual)
}
func TestLinodeGetStrippedRecordName(t *testing.T) {
assert.Equal(t, "", getStrippedRecordName(&linodego.Domain{
Domain: "foo.com",
}, &endpoint.Endpoint{
DNSName: "foo.com",
}))
assert.Equal(t, "api", getStrippedRecordName(&linodego.Domain{
Domain: "foo.com",
}, &endpoint.Endpoint{
DNSName: "api.foo.com",
}))
}
func TestLinodeRecords(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return(createZones(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
1,
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
2,
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
3,
mock.Anything,
).Return(createBazRecords(), nil).Once()
actual, err := provider.Records()
require.NoError(t, err)
expected := []*endpoint.Endpoint{
{DNSName: "foo.com", Targets: []string{"targetFoo"}, RecordType: "A", RecordTTL: 0, Labels: endpoint.NewLabels()},
{DNSName: "foo.com", Targets: []string{"txt"}, RecordType: "TXT", RecordTTL: 0, Labels: endpoint.NewLabels()},
{DNSName: "baz.com", Targets: []string{"targetBaz"}, RecordType: "A", RecordTTL: 0, Labels: endpoint.NewLabels()},
{DNSName: "baz.com", Targets: []string{"txt"}, RecordType: "TXT", RecordTTL: 0, Labels: endpoint.NewLabels()},
{DNSName: "api.baz.com", Targets: []string{"targetBaz"}, RecordType: "A", RecordTTL: 0, Labels: endpoint.NewLabels()},
{DNSName: "api.baz.com", Targets: []string{"txt"}, RecordType: "TXT", RecordTTL: 0, Labels: endpoint.NewLabels()},
}
mockDomainClient.AssertExpectations(t)
assert.Equal(t, expected, actual)
}
func TestLinodeApplyChanges(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy Data
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return(createZones(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
1,
mock.Anything,
).Return(createFooRecords(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
2,
mock.Anything,
).Return(createBarRecords(), nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
3,
mock.Anything,
).Return(createBazRecords(), nil).Once()
// Apply Actions
mockDomainClient.On(
"DeleteDomainRecord",
mock.Anything,
3,
33,
).Return(nil).Once()
mockDomainClient.On(
"DeleteDomainRecord",
mock.Anything,
3,
34,
).Return(nil).Once()
mockDomainClient.On(
"UpdateDomainRecord",
mock.Anything,
1,
11,
linodego.DomainRecordUpdateOptions{
Type: "A", Name: "", Target: "targetFoo",
Priority: getPriority(), Weight: getWeight(), Port: getPort(), TTLSec: 300,
},
).Return(&linodego.DomainRecord{}, nil).Once()
mockDomainClient.On(
"CreateDomainRecord",
mock.Anything,
2,
linodego.DomainRecordCreateOptions{
Type: "A", Name: "create", Target: "targetBar",
Priority: getPriority(), Weight: getWeight(), Port: getPort(), TTLSec: 0,
},
).Return(&linodego.DomainRecord{}, nil).Once()
mockDomainClient.On(
"CreateDomainRecord",
mock.Anything,
2,
linodego.DomainRecordCreateOptions{
Type: "A", Name: "", Target: "targetBar",
Priority: getPriority(), Weight: getWeight(), Port: getPort(), TTLSec: 0,
},
).Return(&linodego.DomainRecord{}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{
Create: []*endpoint.Endpoint{{
DNSName: "create.bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
}, {
DNSName: "bar.io",
RecordType: "A",
Targets: []string{"targetBar"},
}},
Delete: []*endpoint.Endpoint{{
DNSName: "api.baz.com",
RecordType: "A",
}, {
DNSName: "api.baz.com",
RecordType: "TXT",
}},
UpdateNew: []*endpoint.Endpoint{{
DNSName: "foo.com",
RecordType: "A",
RecordTTL: 300,
Targets: []string{"targetFoo"},
}},
UpdateOld: []*endpoint.Endpoint{},
})
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
}
func TestLinodeApplyChangesTargetAdded(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy Data
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
1,
mock.Anything,
).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once()
// Apply Actions
mockDomainClient.On(
"UpdateDomainRecord",
mock.Anything,
1,
11,
linodego.DomainRecordUpdateOptions{
Type: "A", Name: "", Target: "targetA",
Priority: getPriority(), Weight: getWeight(), Port: getPort(),
},
).Return(&linodego.DomainRecord{}, nil).Once()
mockDomainClient.On(
"CreateDomainRecord",
mock.Anything,
1,
linodego.DomainRecordCreateOptions{
Type: "A", Name: "", Target: "targetB",
Priority: getPriority(), Weight: getWeight(), Port: getPort(),
},
).Return(&linodego.DomainRecord{}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{
// From 1 target to 2
UpdateNew: []*endpoint.Endpoint{{
DNSName: "example.com",
RecordType: "A",
Targets: []string{"targetA", "targetB"},
}},
UpdateOld: []*endpoint.Endpoint{},
})
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
}
func TestLinodeApplyChangesTargetRemoved(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy Data
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
1,
mock.Anything,
).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}, {ID: 12, Type: "A", Name: "", Target: "targetB"}}, nil).Once()
// Apply Actions
mockDomainClient.On(
"UpdateDomainRecord",
mock.Anything,
1,
12,
linodego.DomainRecordUpdateOptions{
Type: "A", Name: "", Target: "targetB",
Priority: getPriority(), Weight: getWeight(), Port: getPort(),
},
).Return(&linodego.DomainRecord{}, nil).Once()
mockDomainClient.On(
"DeleteDomainRecord",
mock.Anything,
1,
11,
).Return(nil).Once()
err := provider.ApplyChanges(&plan.Changes{
// From 2 targets to 1
UpdateNew: []*endpoint.Endpoint{{
DNSName: "example.com",
RecordType: "A",
Targets: []string{"targetB"},
}},
UpdateOld: []*endpoint.Endpoint{},
})
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
}
func TestLinodeApplyChangesNoChanges(t *testing.T) {
mockDomainClient := MockDomainClient{}
provider := &LinodeProvider{
Client: &mockDomainClient,
domainFilter: NewDomainFilter([]string{}),
DryRun: false,
}
// Dummy Data
mockDomainClient.On(
"ListDomains",
mock.Anything,
mock.Anything,
).Return([]*linodego.Domain{{Domain: "example.com", ID: 1}}, nil).Once()
mockDomainClient.On(
"ListDomainRecords",
mock.Anything,
1,
mock.Anything,
).Return([]*linodego.DomainRecord{{ID: 11, Name: "", Type: "A", Target: "targetA"}}, nil).Once()
err := provider.ApplyChanges(&plan.Changes{})
require.NoError(t, err)
mockDomainClient.AssertExpectations(t)
}
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