mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 09:36:58 +02:00
Add RcodeZero Anycast DNS provider
This commit is contained in:
parent
623ecd07f0
commit
79bf8c807e
@ -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
194
docs/tutorials/rcodezero.md
Normal 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
|
||||
```
|
4
main.go
4
main.go
@ -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)
|
||||
|
@ -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
336
provider/rcode0.go
Normal 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
417
provider/rcode0_test.go
Normal 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 }
|
||||
|
Loading…
Reference in New Issue
Block a user