Add RcodeZero Anycast DNS provider

This commit is contained in:
Dimitrij Klesev 2019-01-23 11:36:13 +01:00
parent 623ecd07f0
commit 79bf8c807e
No known key found for this signature in database
GPG Key ID: 677B420908DB1D5B
6 changed files with 956 additions and 2 deletions

View File

@ -29,6 +29,7 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected
* [AWS Service Discovery](https://docs.aws.amazon.com/Route53/latest/APIReference/overview-service-discovery.html)
* [AzureDNS](https://azure.microsoft.com/en-us/services/dns)
* [CloudFlare](https://www.cloudflare.com/dns)
* [RcodeZero](https://www.rcodezero.at/)
* [DigitalOcean](https://www.digitalocean.com/products/networking)
* [DNSimple](https://dnsimple.com/)
* [Infoblox](https://www.infoblox.com/products/dns/)
@ -57,6 +58,7 @@ The following tutorials are provided:
* [Azure](docs/tutorials/azure.md)
* [CoreDNS](docs/tutorials/coredns.md)
* [Cloudflare](docs/tutorials/cloudflare.md)
* [RcodeZero](docs/tutorials/rcodezero.md)
* [DigitalOcean](docs/tutorials/digitalocean.md)
* [Infoblox](docs/tutorials/infoblox.md)
* [Dyn](docs/tutorials/dyn.md)

194
docs/tutorials/rcodezero.md Normal file
View File

@ -0,0 +1,194 @@
# Setting up ExternalDNS for Services on RcodeZero
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using RcodeZero Anycast DNS.
Make sure to use **>=0.5.0** version of ExternalDNS for this tutorial.
## Creating a RcodeZero DNS zone
After logging into RcodeZero Dashboard add a master domain under [RcodeZero Add Zone](https://my.rcodezero.at/domain/create). Use it throughout this guide (substitute example.com).
## Creating RcodeZero Credentials
> The RcodeZero Anycast-Network is provisioned via web interface or REST-API.
RcodeZero API can be enabled and a key generated on [RcodeZero API](https://my.rcodezero.at/enableapi)
The environment var `RC0_API_KEY` will be needed to run ExternalDNS with RcodeZero.
## 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=rcodezero
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
env:
- name: RC0_API_KEY
value: "YOUR_RCODEZERO_API_KEY"
- name: RC0_ENC_VAR
value: "YOUR_ENCRYPTION_KEY_STRING"
```
### 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=rcodezero
- --rc0-enc-txt # (optional) encrypt TXT records; encryption key has to be provided with RC0_ENC_KEY env var.
env:
- name: RC0_API_KEY
value: "YOUR_RCODEZERO_API_KEY"
- name: RC0_ENC_VAR
value: "YOUR_ENCRYPTION_KEY_STRING"
```
## 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: example.com
external-dns.alpha.kubernetes.io/ttl: "120" #optional
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 RcodeZero DNS zone created above. The annotation may also be a subdomain
of the DNS zone (e.g. 'www.example.com').
By setting the TTL annotation on the service, you have to pass a valid TTL, which must be 120 or above.
This annotation is optional, if you won't set it, it will be 1 (automatic) which is 300.
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:
```
$ 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 RcodeZero DNS records.
## Verifying RcodeZero DNS records
Check your [RcodeZero Configured Zones](https://my.rcodezero.at/domain) and select the ExternalDNS managed domain.
Substitute 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 RcodeZero DNS records, we can delete the tutorial's example:
```
$ kubectl delete -f nginx.yaml
$ kubectl delete -f externaldns.yaml
```

View File

@ -132,7 +132,9 @@ func main() {
p, err = provider.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.DryRun)
case "cloudflare":
p, err = provider.NewCloudFlareProvider(domainFilter, zoneIDFilter, cfg.CloudflareZonesPerPage, cfg.CloudflareProxied, cfg.DryRun)
case "google":
case "rcodezero":
p, err = provider.NewRcodeZeroProvider(domainFilter, cfg.DryRun, cfg.RcodezeroTXTEncrypt)
case "google":
p, err = provider.NewGoogleProvider(cfg.GoogleProject, domainFilter, zoneIDFilter, cfg.DryRun)
case "digitalocean":
p, err = provider.NewDigitalOceanProvider(domainFilter, cfg.DryRun)

View File

@ -67,6 +67,7 @@ type Config struct {
AzureResourceGroup string
CloudflareProxied bool
CloudflareZonesPerPage int
RcodezeroTXTEncrypt bool
InfobloxGridHost string
InfobloxWapiPort int
InfobloxWapiUsername string
@ -142,6 +143,7 @@ var defaultConfig = &Config{
AzureResourceGroup: "",
CloudflareProxied: false,
CloudflareZonesPerPage: 50,
RcodezeroTXTEncrypt: false,
InfobloxGridHost: "",
InfobloxWapiPort: 443,
InfobloxWapiUsername: "admin",
@ -243,7 +245,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
// 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, linode, rfc2136)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136")
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, cloudflare, rcodezero, digitalocean, dnsimple, infoblox, dyn, designate, coredns, skydns, inmemory, pdns, oci, exoscale, linode, rfc2136)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "pdns", "oci", "exoscale", "linode", "rfc2136")
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)
@ -271,6 +273,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("dyn-password", "When using the Dyn provider, specify the pasword").Default("").StringVar(&cfg.DynPassword)
app.Flag("dyn-min-ttl", "Minimal TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is lower than this.").IntVar(&cfg.DynMinTTLSeconds)
app.Flag("oci-config-file", "When using the OCI provider, specify the OCI configuration file (required when --provider=oci").Default(defaultConfig.OCIConfigFile).StringVar(&cfg.OCIConfigFile)
app.Flag("rc0-txt-enc", "When using the Rcodezero provider with txt registry option, set if TXT rrs are encrypted (default: false)").BoolVar(&cfg.RcodezeroTXTEncrypt)
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)

336
provider/rcode0.go Normal file
View File

@ -0,0 +1,336 @@
/*
Copyright 2019 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"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
rc0 "github.com/nic-at/rc0go"
log "github.com/sirupsen/logrus"
"net/url"
"os"
"strings"
)
// RcodeZeroProvider implements the DNS provider for RcodeZero Anycast DNS.
type RcodeZeroProvider struct {
Client *rc0.Client
DomainFilter DomainFilter
DryRun bool
TXTEncrypt bool
Key []byte
}
// NewRcodeZeroProvider creates a new RcodeZero Anycast DNS provider.
//
// Returns the provider or an error if a provider could not be created.
func NewRcodeZeroProvider(domainFilter DomainFilter, dryRun bool, txtEnc bool) (*RcodeZeroProvider, error){
client, err := rc0.NewClient(os.Getenv("RC0_API_KEY"))
if err != nil {
return nil, err
}
value := os.Getenv("RC0_BASE_URL")
if len(value) != 0 {
client.BaseURL, _ = url.Parse(os.Getenv("RC0_BASE_URL"))
}
if err != nil {
return nil, fmt.Errorf("failed to initialize rcodezero provider: %v", err)
}
provider := &RcodeZeroProvider{
Client: client,
DomainFilter: domainFilter,
DryRun: dryRun,
TXTEncrypt: txtEnc,
}
if txtEnc {
provider.Key = []byte(os.Getenv("RC0_ENC_KEY"))
}
return provider, nil
}
// Returns filtered zones if filter is set
func (p *RcodeZeroProvider) Zones() ([]*rc0.Zone, error) {
var result []*rc0.Zone
zones, err := p.fetchZones()
if err != nil {
return nil, err
}
for _, zone := range zones {
if p.DomainFilter.Match(zone.Domain) {
result = append(result, zone)
}
}
return result, nil
}
// Returns resource records
//
// Decrypts TXT records if TXT-Encrypt flag is set and key is provided
func (p *RcodeZeroProvider) Records() ([]*endpoint.Endpoint, error) {
zones, err := p.Zones()
if err != nil {
return nil, err
}
var endpoints []*endpoint.Endpoint
for _, zone := range zones {
rrset, err := p.fetchRecords(zone.Domain)
if err != nil {
return nil, err
}
for _, r := range rrset {
if supportedRecordType(r.Type) {
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(r.Type, "TXT") {
p.Client.RRSet.DecryptTXT(p.Key, r)
}
if len(r.Records) > 1 {
for _, _r := range r.Records {
if !_r.Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), _r.Content))
}
}
} else {
if !r.Records[0].Disabled {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(r.Name, r.Type, endpoint.TTL(r.TTL), r.Records[0].Content))
}
}
}
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes in a given zone.
func (p *RcodeZeroProvider) ApplyChanges(changes *plan.Changes) error {
combinedChanges := make([]*rc0.RRSetChange, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete))
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeADD, changes.Create)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeUPDATE, changes.UpdateNew)...)
combinedChanges = append(combinedChanges, p.NewRcodezeroChanges(rc0.ChangeTypeDELETE, changes.Delete)...)
return p.submitChanges(combinedChanges)
}
// Helper function
func rcodezeroChangesByZone(zones []*rc0.Zone, changeSet []*rc0.RRSetChange) map[string][]*rc0.RRSetChange {
changes := make(map[string][]*rc0.RRSetChange)
zoneNameIDMapper := zoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(z.Domain, z.Domain)
changes[z.Domain] = []*rc0.RRSetChange{}
}
for _, c := range changeSet {
zone, _ := zoneNameIDMapper.FindZone(c.Name)
if zone == "" {
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected ", c.Name)
continue
}
changes[zone] = append(changes[zone], c)
}
return changes
}
// Helper function
func (p *RcodeZeroProvider) fetchRecords(zoneName string) ([]*rc0.RRType, error) {
var allRecords []*rc0.RRType
listOptions := rc0.NewListOptions()
for {
records, page, err := p.Client.RRSet.List(zoneName, listOptions)
if err != nil {
return nil, err
}
allRecords = append(allRecords, records...)
if page == nil || (page.CurrentPage == page.LastPage) {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allRecords, nil
}
// Helper function
func (p *RcodeZeroProvider) fetchZones() ([]*rc0.Zone, error) {
var allZones []*rc0.Zone
listOptions := rc0.NewListOptions()
for {
zones, page, err := p.Client.Zones.List(listOptions)
if err != nil {
return nil, err
}
allZones = append(allZones, zones...)
if page == nil || page.IsLastPage() {
break
}
listOptions.SetPageNumber(page.CurrentPage + 1)
}
return allZones, nil
}
// Helper function to submit changes.
//
// Changes are submitted by change type.
func (p *RcodeZeroProvider) submitChanges(changes []*rc0.RRSetChange) error {
if len(changes) == 0 {
return nil
}
zones, err := p.Zones()
if err != nil {
return err
}
// separate into per-zone change sets to be passed to the API.
changesByZone := rcodezeroChangesByZone(zones, changes)
for zoneName, changes := range changesByZone {
for _, change := range changes {
logFields := log.Fields{
"record" : change.Name,
"content" : change.Records[0].Content,
"type" : change.Type,
"action" : change.ChangeType,
"zone" : zoneName,
}
log.WithFields(logFields).Info("Changing record.")
if p.DryRun {
continue
}
// to avoid accidentally adding extra dot if already present
change.Name = strings.TrimSuffix(change.Name, ".") + "."
switch change.ChangeType {
case rc0.ChangeTypeADD:
sr, err := p.Client.RRSet.Create(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("adding new RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeUPDATE:
sr, err := p.Client.RRSet.Edit(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("updating existing RR resulted in an error: %v", sr.Message)
}
case rc0.ChangeTypeDELETE:
sr, err := p.Client.RRSet.Delete(zoneName, []*rc0.RRSetChange{change})
if err != nil {
return err
}
if sr.HasError() {
return fmt.Errorf("deleting existing RR resulted in an error: %v", sr.Message)
}
default:
return fmt.Errorf("unsupported changeType submitted: %v", change.ChangeType)
}
}
}
return nil
}
// Returns a RcodeZero specific array with rrset change objects.
func (p *RcodeZeroProvider) NewRcodezeroChanges(action string, endpoints []*endpoint.Endpoint) []*rc0.RRSetChange {
changes := make([]*rc0.RRSetChange, 0, len(endpoints))
for _, _endpoint := range endpoints {
changes = append(changes, p.NewRcodezeroChange(action, _endpoint))
}
return changes
}
// Returns a RcodeZero specific rrset change object.
func (p *RcodeZeroProvider) NewRcodezeroChange(action string, endpoint *endpoint.Endpoint) *rc0.RRSetChange {
change := &rc0.RRSetChange{
Type: endpoint.RecordType,
ChangeType: action,
Name: endpoint.DNSName,
Records: []*rc0.Record{{
Disabled: false,
Content: endpoint.Targets[0],
}},
}
if p.TXTEncrypt && (p.Key != nil) && strings.EqualFold(endpoint.RecordType, "TXT") {
p.Client.RRSet.EncryptTXT(p.Key, change)
}
return change
}

417
provider/rcode0_test.go Normal file
View File

@ -0,0 +1,417 @@
/*
Copyright 2019 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"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
rc0 "github.com/nic-at/rc0go"
"github.com/stretchr/testify/require"
"os"
"testing"
)
const (
testZoneOne = "testzone1.at"
testZoneTwo = "testzone2.at"
rrsetChangesUnsupportedChangeType = 0
)
type mockRcodeZeroClient rc0.Client
type mockZoneManagementService struct {
TestNilZonesReturned bool
TestErrorReturned bool
}
type mockRRSetService struct {
TestErrorReturned bool
}
func (m *mockRcodeZeroClient) resetMockServices() {
m.Zones = &mockZoneManagementService{}
m.RRSet = &mockRRSetService{}
}
func (m *mockZoneManagementService) resetTestConditions() {
m.TestNilZonesReturned = false
m.TestErrorReturned = false
}
func (m *mockRRSetService) resetTestConditions() {
m.TestErrorReturned = false
}
func TestRcodeZeroProvider_Records(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
}
endpoints, err := provider.Records() // should return 6 rrs
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, 6, len(endpoints))
mockRRSetService.TestErrorReturned = true
_, err = provider.Records()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func TestRcodeZeroProvider_ApplyChanges(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
DomainFilter: NewDomainFilter([]string{testZoneOne}),
}
changes := mockChanges()
err := provider.ApplyChanges(changes)
if err != nil {
t.Errorf("should not fail, %s", err)
}
}
func TestRcodeZeroProvider_NewRcodezeroChanges(t *testing.T) {
provider := &RcodeZeroProvider{}
changes := mockChanges()
createChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Create)
require.Equal(t, 4, len(createChanges))
deleteChanges := provider.NewRcodezeroChanges(testZoneOne, changes.Delete)
require.Equal(t, 1, len(deleteChanges))
updateOldChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateOld)
require.Equal(t, 1, len(updateOldChanges))
updateNewChanges := provider.NewRcodezeroChanges(testZoneOne, changes.UpdateNew)
require.Equal(t, 1, len(updateNewChanges))
}
func TestRcodeZeroProvider_NewRcodezeroChange(t *testing.T) {
_endpoint := &endpoint.Endpoint{
RecordType: "A",
DNSName: "app." + testZoneOne,
RecordTTL: 300,
Targets: endpoint.Targets{"target"},
}
provider := &RcodeZeroProvider{}
rrsetChange := provider.NewRcodezeroChange(testZoneOne, _endpoint)
require.Equal(t, _endpoint.RecordType, rrsetChange.Type)
require.Equal(t, _endpoint.DNSName, rrsetChange.Name)
require.Equal(t, _endpoint.Targets[0], rrsetChange.Records[0].Content)
//require.Equal(t, endpoint.RecordTTL, rrsetChange.TTL)
}
func Test_submitChanges(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
DomainFilter: NewDomainFilter([]string{testZoneOne}),
}
changes := mockRRSetChanges(rrsetChangesUnsupportedChangeType)
err := provider.submitChanges(changes)
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func mockRRSetChanges(condition int) []*rc0.RRSetChange {
switch condition {
case rrsetChangesUnsupportedChangeType:
return []*rc0.RRSetChange{
{
Name: testZoneOne,
Type: "A",
ChangeType: "UNSUPPORTED",
Records: []*rc0.Record{{Content:"fail"}},
},
}
default:
return nil
}
}
func mockChanges() *plan.Changes {
changes := &plan.Changes{}
changes.Create = []*endpoint.Endpoint{
{DNSName: "new.ext-dns-test."+testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A"},
{DNSName: "new.ext-dns-test-with-ttl."+testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "A", RecordTTL: 100},
{DNSName: "new.ext-dns-test.unexpected.com", Targets: endpoint.Targets{"target"}, RecordType: "AAAA"},
{DNSName: testZoneOne, Targets: endpoint.Targets{"target"}, RecordType: "CNAME"},
}
changes.Delete = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test."+testZoneOne, Targets: endpoint.Targets{"target"}}}
changes.UpdateOld = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test."+testZoneOne, Targets: endpoint.Targets{"target-old"}}}
changes.UpdateNew = []*endpoint.Endpoint{{DNSName: "foobar.ext-dns-test."+testZoneOne, Targets: endpoint.Targets{"target-new"}, RecordType: "CNAME", RecordTTL: 100}}
return changes
}
func TestRcodeZeroProvider_Zones(t *testing.T) {
mockRRSetService := &mockRRSetService{}
mockZoneManagementService := &mockZoneManagementService{}
provider := &RcodeZeroProvider{
Client: (*rc0.Client)(&mockRcodeZeroClient{
Zones: mockZoneManagementService,
RRSet: mockRRSetService,
}),
}
mockZoneManagementService.TestNilZonesReturned = true
zones, err := provider.Zones()
if err != nil {
t.Fatal(err)
}
require.Equal(t, 0, len(zones))
mockZoneManagementService.resetTestConditions()
mockZoneManagementService.TestErrorReturned = true
zones, err = provider.Zones()
if err == nil {
t.Errorf("expected to fail, %s", err)
}
}
func TestNewRcodeZeroProvider(t *testing.T) {
_ = os.Setenv("RC0_API_KEY", "123")
p, err := NewRcodeZeroProvider(NewDomainFilter([]string{"ext-dns-test."+testZoneOne+"."}), true, true)
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, true, p.DryRun)
require.Equal(t, true, p.TXTEncrypt)
require.Equal(t, true, p.DomainFilter.IsConfigured())
require.Equal(t, false, p.DomainFilter.Match("ext-dns-test."+testZoneTwo+".")) // filter is set, so it should match only provided domains
p, err = NewRcodeZeroProvider(DomainFilter{}, false, false)
if err != nil {
t.Errorf("should not fail, %s", err)
}
require.Equal(t, false, p.DryRun)
require.Equal(t, false, p.DomainFilter.IsConfigured())
require.Equal(t, true, p.DomainFilter.Match("ext-dns-test."+testZoneOne+".")) // filter is not set, so it should match any
_ = os.Unsetenv("RC0_API_KEY")
_, err = NewRcodeZeroProvider(DomainFilter{}, false, false)
if err == nil {
t.Errorf("expected to fail")
}
}
/* mocking mockRRSetServiceInterface */
func (m *mockRRSetService) List(zone string, options *rc0.ListOptions) ([]*rc0.RRType, *rc0.Page, error) {
if m.TestErrorReturned {
return nil, nil, fmt.Errorf("operation RRSet.List failed")
}
return mockRRSet(zone), nil, nil
}
func mockRRSet(zone string) []*rc0.RRType {
return []*rc0.RRType{
{
Name: "app."+zone+".",
Type: "TXT",
TTL: 300,
Records: []*rc0.Record{
{
Content: "\"heritage=external-dns,external-dns/owner=default,external-dns/resource=ingress/default/app\"",
Disabled: false,
},
},
},
{
Name: "app."+zone+".",
Type: "A",
TTL: 300,
Records: []*rc0.Record{
{
Content: "127.0.0.1",
Disabled: false,
},
},
},
{
Name: "www."+zone+".",
Type: "A",
TTL: 300,
Records: []*rc0.Record{
{
Content: "127.0.0.1",
Disabled: false,
},
},
},
{
Name: zone+".",
Type: "SOA",
TTL: 3600,
Records: []*rc0.Record{
{
Content: "sec1.rcode0.net. rcodezero-soa.ipcom.at. 2019011616 10800 3600 604800 3600",
Disabled: false,
},
},
},
{
Name: zone+".",
Type: "NS",
TTL: 3600,
Records: []*rc0.Record{
{
Content: "sec2.rcode0.net.",
Disabled: false,
},
{
Content: "sec1.rcode0.net.",
Disabled: false,
},
},
},
}
}
func (m *mockRRSetService) Create(zone string, rrsetCreate []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) Edit(zone string, rrsetEdit []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) Delete(zone string, rrsetDelete []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) SubmitChangeSet(zone string, changeSet []*rc0.RRSetChange) (*rc0.StatusResponse, error) {
return &rc0.StatusResponse{Status: "ok", Message: "pass"}, nil
}
func (m *mockRRSetService) EncryptTXT(key []byte, rrType *rc0.RRSetChange) {}
func (m *mockRRSetService) DecryptTXT(key []byte, rrType *rc0.RRType) {}
/* mocking ZoneManagementServiceInterface */
func (m *mockZoneManagementService) List(options *rc0.ListOptions) ([]*rc0.Zone, *rc0.Page, error) {
if m.TestNilZonesReturned {
return nil, nil, nil
}
if m.TestErrorReturned {
return nil, nil, fmt.Errorf("operation Zone.List failed")
}
zones := []*rc0.Zone{
{
Domain: testZoneOne,
Type: "SLAVE",
// "dnssec": "yes", @todo: add this
// "created": "2018-04-09T09:27:31Z", @todo: add this
LastCheck: "",
Serial: 20180411,
Masters: []string{
"193.0.2.2",
"2001:db8::2",
},
},
{
Domain: testZoneTwo,
Type: "MASTER",
// "dnssec": "no", @todo: add this
// "created": "2019-01-15T13:20:10Z", @todo: add this
LastCheck: "",
Serial: 2019011616,
Masters: []string{
"",
},
},
}
return zones, nil, nil
}
func (m *mockZoneManagementService) Get(zone string) (*rc0.Zone, error) { return nil, nil }
func (m *mockZoneManagementService) Create(zoneCreate *rc0.ZoneCreate) (*rc0.StatusResponse, error) { return nil, nil }
func (m *mockZoneManagementService) Edit(zone string, zoneEdit *rc0.ZoneEdit) (*rc0.StatusResponse, error) { return nil, nil }
func (m *mockZoneManagementService) Delete(zone string) (*rc0.StatusResponse, error) { return nil, nil }
func (m *mockZoneManagementService) Transfer(zone string) (*rc0.StatusResponse, error) { return nil ,nil }