Merge branch 'master' into external-dns-exoscale

This commit is contained in:
Nick Jüttner 2018-07-12 12:21:12 +02:00 committed by GitHub
commit 717ee8440c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1333 additions and 7 deletions

22
Gopkg.lock generated
View File

@ -149,7 +149,8 @@
[[projects]]
name = "github.com/davecgh/go-spew"
packages = ["spew"]
revision = "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
revision = "346938d642f2ec3594ed81d874461961cd0faa76"
version = "v1.1.0"
[[projects]]
name = "github.com/dgrijalva/jwt-go"
@ -346,6 +347,15 @@
revision = "cdd946344b54bdf7dbeac406c2f1fe93150f08ea"
version = "v0.6.0"
[[projects]]
name = "github.com/oracle/oci-go-sdk"
packages = [
"common",
"dns"
]
revision = "a2ded717dc4bb4916c0416ec79f81718b576dbc4"
version = "v1.8.0"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
@ -354,7 +364,8 @@
[[projects]]
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
revision = "d8ed2627bdf02c080bf22230dbb337003b7aba2d"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
name = "github.com/prometheus/client_golang"
@ -403,7 +414,8 @@
[[projects]]
name = "github.com/stretchr/objx"
packages = ["."]
revision = "cbeaeb16a013161a98496fad62933b1d21786672"
revision = "facf9a85c22f48d2f52f2380e4efce1768749a89"
version = "v0.1"
[[projects]]
name = "github.com/stretchr/testify"
@ -413,8 +425,8 @@
"require",
"suite"
]
revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0"
version = "v1.1.4"
revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71"
version = "v1.2.1"
[[projects]]
branch = "master"

View File

@ -54,7 +54,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/stretchr/testify"
version = "~1.1.4"
version = "~1.2.1"
[[constraint]]
name = "k8s.io/client-go"
@ -71,3 +71,7 @@ ignored = ["github.com/kubernetes/repo-infra/kazel"]
[[constraint]]
name = "github.com/exoscale/egoscale"
version = "~0.9.31"
[[constraint]]
name = "github.com/oracle/oci-go-sdk"
version = "1.8.0"

View File

@ -37,6 +37,7 @@ ExternalDNS' current release is `v0.5`. This version allows you to keep selected
* [PowerDNS](https://www.powerdns.com/)
* [CoreDNS](https://coredns.io/)
* [Exoscale](https://www.exoscale.com/dns/)
* [Oracle Cloud Infrastructure DNS](https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm)
From this release, ExternalDNS can become aware of the records it is managing (enabled via `--registry=txt`), therefore ExternalDNS can safely manage non-empty hosted zones. We strongly encourage you to use `v0.5` (or greater) with `--registry=txt` enabled and `--txt-owner-id` set to a unique value that doesn't change for the lifetime of your cluster. You might also want to run ExternalDNS in a dry run mode (`--dry-run` flag) to see the changes to be submitted to your DNS Provider API.
@ -59,6 +60,7 @@ The following tutorials are provided:
* [Using Google's Default Ingress Controller](docs/tutorials/gke.md)
* [Using the Nginx Ingress Controller](docs/tutorials/nginx-ingress.md)
* [Exoscale](docs/tutorials/exoscale.md)
* [Oracle Cloud Infrastructure (OCI) DNS](docs/tutorials/oracle.md)
## Running Locally

155
docs/tutorials/oracle.md Normal file
View File

@ -0,0 +1,155 @@
# Setting up ExternalDNS for Oracle Cloud Infrastructure (OCI)
This tutorial describes how to setup ExternalDNS for usage within a Kubernetes cluster using OCI DNS.
Make sure to use the latest version of ExternalDNS for this tutorial.
## Creating an OCI DNS Zone
Create a DNS zone which will contain the managed DNS records. Let's use `example.com` as an reference here.
For more information about OCI DNS see the documentation [here][1].
## Deploy ExternalDNS
Connect your `kubectl` client to the cluster you want to test ExternalDNS with.
We first need to create a config file containing the information needed to connect with the OCI API.
Create a new file (oci.yaml) and modify the contents to match the example below. Be sure to adjust the values to match your own credentials:
```yaml
auth:
region: us-phoenix-1
tenancy: ocid1.tenancy.oc1...
user: ocid1.user.oc1...
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
fingerprint: af:81:71:8e...
compartment: ocid1.compartment.oc1...
```
Create a secret using the config file above:
```shell
$ kubectl create secret generic external-dns-config --from-file=oci.yaml
```
### Manifest (for clusters with RBAC enabled)
Apply the following manifest to deploy ExternalDNS.
```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
- --source=ingress
- --provider=oci
- --policy=upsert-only # prevent ExternalDNSfrom deleting any records, omit to enable full synchronization
- --txt-owner-id=my-identifier
volumeMounts:
- name: config
mountPath: /etc/kubernetes/
volumes:
- name: config
secret:
secretName: external-dns-config
```
## Verify ExternalDNS works (Service example)
Create the following sample application to test that ExternalDNS works.
> For services ExternalDNS will look for the annotation `external-dns.alpha.kubernetes.io/hostname` on the service and use the corresponding value.
```yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
annotations:
external-dns.alpha.kubernetes.io/hostname: example.com
spec:
type: LoadBalancer
ports:
- port: 80
name: http
targetPort: 80
selector:
app: nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx
spec:
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: http
```
Apply the manifest above and wait roughly two minutes and check that a corresponding DNS record for your service was created.
```
$ kubectl apply -f nginx.yaml
```
[1]: https://docs.cloud.oracle.com/iaas/Content/DNS/Concepts/dnszonemanagement.htm

View File

@ -172,6 +172,12 @@ func main() {
},
},
)
case "oci":
var config *provider.OCIConfig
config, err = provider.LoadOCIConfig(cfg.OCIConfigFile)
if err == nil {
p, err = provider.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
}
default:
log.Fatalf("unknown dns provider: %s", cfg.Provider)
}

View File

@ -67,6 +67,7 @@ type Config struct {
DynUsername string
DynPassword string
DynMinTTLSeconds int
OCIConfigFile string
InMemoryZones []string
PDNSServer string
PDNSAPIKey string
@ -117,6 +118,7 @@ var defaultConfig = &Config{
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
@ -188,7 +190,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)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "cloudflare", "digitalocean", "dnsimple", "infoblox", "dyn", "designate", "coredns", "skydns", "exoscale", "inmemory", "pdns")
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("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)
@ -209,6 +211,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("dyn-username", "When using the Dyn provider, specify the Username").Default("").StringVar(&cfg.DynUsername)
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("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)

View File

@ -52,6 +52,7 @@ var (
InfobloxWapiPassword: "",
InfobloxWapiVersion: "2.3.1",
InfobloxSSLVerify: true,
OCIConfigFile: "/etc/kubernetes/oci.yaml",
InMemoryZones: []string{""},
PDNSServer: "http://localhost:8081",
PDNSAPIKey: "",
@ -93,6 +94,7 @@ var (
InfobloxWapiPassword: "infoblox",
InfobloxWapiVersion: "2.6.1",
InfobloxSSLVerify: false,
OCIConfigFile: "oci.yaml",
InMemoryZones: []string{"example.org", "company.com"},
PDNSServer: "http://ns.example.com:8081",
PDNSAPIKey: "some-secret-key",
@ -157,6 +159,7 @@ func TestParseFlags(t *testing.T) {
"--pdns-server=http://ns.example.com:8081",
"--pdns-api-key=some-secret-key",
"--pdns-tls-enabled",
"--oci-config-file=oci.yaml",
"--tls-ca=/path/to/ca.crt",
"--tls-client-cert=/path/to/cert.pem",
"--tls-client-cert-key=/path/to/key.pem",
@ -206,6 +209,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_INFOBLOX_WAPI_PASSWORD": "infoblox",
"EXTERNAL_DNS_INFOBLOX_WAPI_VERSION": "2.6.1",
"EXTERNAL_DNS_INFOBLOX_SSL_VERIFY": "0",
"EXTERNAL_DNS_OCI_CONFIG_FILE": "oci.yaml",
"EXTERNAL_DNS_INMEMORY_ZONE": "example.org\ncompany.com",
"EXTERNAL_DNS_DOMAIN_FILTER": "example.org\ncompany.com",
"EXTERNAL_DNS_PDNS_SERVER": "http://ns.example.com:8081",

301
provider/oci.go Normal file
View File

@ -0,0 +1,301 @@
/*
Copyright 2018 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"
"io/ioutil"
"strings"
"github.com/oracle/oci-go-sdk/common"
"github.com/oracle/oci-go-sdk/dns"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
const ociRecordTTL = 300
// OCIAuthConfig holds connection parameters for the OCI API.
type OCIAuthConfig struct {
Region string `yaml:"region"`
TenancyID string `yaml:"tenancy"`
UserID string `yaml:"user"`
PrivateKey string `yaml:"key"`
Fingerprint string `yaml:"fingerprint"`
Passphrase string `yaml:"passphrase"`
}
// OCIConfig holds the configuration for the OCI Provider.
type OCIConfig struct {
Auth OCIAuthConfig `yaml:"auth"`
CompartmentID string `yaml:"compartment"`
}
// OCIProvider is an implementation of Provider for Oracle Cloud Infrastructure
// (OCI) DNS.
type OCIProvider struct {
client ociDNSClient
cfg OCIConfig
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
dryRun bool
}
// ociDNSClient is the subset of the OCI DNS API required by the OCI Provider.
type ociDNSClient interface {
ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error)
GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error)
PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error)
}
// LoadOCIConfig reads and parses the OCI ExternalDNS config file at the given
// path.
func LoadOCIConfig(path string) (*OCIConfig, error) {
contents, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "reading OCI config file %q", path)
}
cfg := OCIConfig{}
if err := yaml.Unmarshal(contents, &cfg); err != nil {
return nil, errors.Wrapf(err, "parsing OCI config file %q", path)
}
return &cfg, nil
}
// NewOCIProvider initialises a new OCI DNS based Provider.
func NewOCIProvider(cfg OCIConfig, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) (*OCIProvider, error) {
var client ociDNSClient
client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider(
cfg.Auth.TenancyID,
cfg.Auth.UserID,
cfg.Auth.Region,
cfg.Auth.Fingerprint,
cfg.Auth.PrivateKey,
&cfg.Auth.Passphrase,
))
if err != nil {
return nil, errors.Wrap(err, "initialising OCI DNS API client")
}
return &OCIProvider{
client: client,
cfg: cfg,
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
}, nil
}
func (p *OCIProvider) zones(ctx context.Context) (map[string]*dns.ZoneSummary, error) {
zones := make(map[string]*dns.ZoneSummary)
log.Debugf("Matching zones against domain filters: %v", p.domainFilter.filters)
var page *string
for {
resp, err := p.client.ListZones(ctx, dns.ListZonesRequest{
CompartmentId: &p.cfg.CompartmentID,
ZoneType: dns.ListZonesZoneTypePrimary,
Page: page,
})
if err != nil {
return nil, errors.Wrapf(err, "listing zones in %q", p.cfg.CompartmentID)
}
for _, zone := range resp.Items {
if p.domainFilter.Match(*zone.Name) && p.zoneIDFilter.Match(*zone.Id) {
zones[*zone.Name] = &zone
log.Debugf("Matched %q (%q)", *zone.Name, *zone.Id)
} else {
log.Debugf("Filtered %q (%q)", *zone.Name, *zone.Id)
}
}
if page = resp.OpcNextPage; resp.OpcNextPage == nil {
break
}
}
if len(zones) == 0 {
if p.domainFilter.IsConfigured() {
log.Warnf("No zones in compartment %q match domain filters %v", p.cfg.CompartmentID, p.domainFilter.filters)
} else {
log.Warnf("No zones found in compartment %q", p.cfg.CompartmentID)
}
}
return zones, nil
}
func (p *OCIProvider) newFilteredRecordOperations(endpoints []*endpoint.Endpoint, opType dns.RecordOperationOperationEnum) []dns.RecordOperation {
ops := []dns.RecordOperation{}
for _, endpoint := range endpoints {
if p.domainFilter.Match(endpoint.DNSName) {
ops = append(ops, newRecordOperation(endpoint, opType))
}
}
return ops
}
// Records returns the list of records in a given hosted zone.
func (p *OCIProvider) Records() ([]*endpoint.Endpoint, error) {
ctx := context.Background()
zones, err := p.zones(ctx)
if err != nil {
return nil, errors.Wrap(err, "getting zones")
}
endpoints := []*endpoint.Endpoint{}
for _, zone := range zones {
var page *string
for {
resp, err := p.client.GetZoneRecords(ctx, dns.GetZoneRecordsRequest{
ZoneNameOrId: zone.Id,
Page: page,
CompartmentId: &p.cfg.CompartmentID,
})
if err != nil {
return nil, errors.Wrapf(err, "getting records for zone %q", *zone.Id)
}
for _, record := range resp.Items {
if !supportedRecordType(*record.Rtype) {
continue
}
endpoints = append(endpoints,
endpoint.NewEndpointWithTTL(
*record.Domain,
*record.Rtype,
endpoint.TTL(*record.Ttl),
*record.Rdata,
),
)
}
if page = resp.OpcNextPage; resp.OpcNextPage == nil {
break
}
}
}
return endpoints, nil
}
// ApplyChanges applies a given set of changes to a given zone.
func (p *OCIProvider) ApplyChanges(changes *plan.Changes) error {
log.Debugf("Processing chages: %+v", changes)
ops := []dns.RecordOperation{}
ops = append(ops, p.newFilteredRecordOperations(changes.Create, dns.RecordOperationOperationAdd)...)
ops = append(ops, p.newFilteredRecordOperations(changes.UpdateNew, dns.RecordOperationOperationAdd)...)
ops = append(ops, p.newFilteredRecordOperations(changes.UpdateOld, dns.RecordOperationOperationRemove)...)
ops = append(ops, p.newFilteredRecordOperations(changes.Delete, dns.RecordOperationOperationRemove)...)
if len(ops) == 0 {
log.Info("All records are already up to date")
return nil
}
ctx := context.Background()
zones, err := p.zones(ctx)
if err != nil {
return errors.Wrap(err, "fetching zones")
}
// Separate into per-zone change sets to be passed to OCI API.
opsByZone := operationsByZone(zones, ops)
for zoneID, ops := range opsByZone {
log.Infof("Change zone: %q", zoneID)
for _, op := range ops {
log.Info(op)
}
}
if p.dryRun {
return nil
}
for zoneID, ops := range opsByZone {
if _, err := p.client.PatchZoneRecords(ctx, dns.PatchZoneRecordsRequest{
CompartmentId: &p.cfg.CompartmentID,
ZoneNameOrId: &zoneID,
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{Items: ops},
}); err != nil {
return err
}
}
return nil
}
// newRecordOperation returns a RecordOperation based on a given endpoint.
func newRecordOperation(ep *endpoint.Endpoint, opType dns.RecordOperationOperationEnum) dns.RecordOperation {
targets := make([]string, len(ep.Targets))
copy(targets, []string(ep.Targets))
if ep.RecordType == endpoint.RecordTypeCNAME {
targets[0] = ensureTrailingDot(targets[0])
}
rdata := strings.Join(targets, " ")
ttl := ociRecordTTL
if ep.RecordTTL.IsConfigured() {
ttl = int(ep.RecordTTL)
}
return dns.RecordOperation{
Domain: &ep.DNSName,
Rdata: &rdata,
Ttl: &ttl,
Rtype: &ep.RecordType,
Operation: opType,
}
}
// operationsByZone segments a slice of RecordOperations by their zone.
func operationsByZone(zones map[string]*dns.ZoneSummary, ops []dns.RecordOperation) map[string][]dns.RecordOperation {
changes := make(map[string][]dns.RecordOperation)
zoneNameIDMapper := zoneIDName{}
for _, z := range zones {
zoneNameIDMapper.Add(*z.Id, *z.Name)
changes[*z.Id] = []dns.RecordOperation{}
}
for _, op := range ops {
if zoneID, _ := zoneNameIDMapper.FindZone(*op.Domain); zoneID != "" {
changes[zoneID] = append(changes[zoneID], op)
} else {
log.Warnf("No matching zone for record operation %s", op)
}
}
// Remove zones that don't have have any changes.
for zone, ops := range changes {
if len(ops) == 0 {
delete(changes, zone)
}
}
return changes
}

839
provider/oci_test.go Normal file
View File

@ -0,0 +1,839 @@
/*
Copyright 2018 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"
"sort"
"testing"
"github.com/oracle/oci-go-sdk/common"
"github.com/oracle/oci-go-sdk/dns"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/kubernetes-incubator/external-dns/endpoint"
"github.com/kubernetes-incubator/external-dns/plan"
)
type mockOCIDNSClient struct{}
func (c *mockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
if request.Page == nil || *request.Page == "0" {
return dns.ListZonesResponse{
Items: []dns.ZoneSummary{
{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
},
},
OpcNextPage: common.String("1"),
}, nil
}
return dns.ListZonesResponse{
Items: []dns.ZoneSummary{
{
Id: common.String("ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"),
Name: common.String("bar.com"),
},
},
}, nil
}
func (c *mockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
if request.ZoneNameOrId == nil {
return
}
switch *request.ZoneNameOrId {
case "ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959":
if request.Page == nil || *request.Page == "0" {
response.Items = []dns.Record{{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}, {
Domain: common.String("foo.foo.com"),
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
Rtype: common.String(endpoint.RecordTypeTXT),
Ttl: common.Int(ociRecordTTL),
}}
response.OpcNextPage = common.String("1")
} else {
response.Items = []dns.Record{{Domain: common.String("bar.foo.com"),
Rdata: common.String("bar.com."),
Rtype: common.String(endpoint.RecordTypeCNAME),
Ttl: common.Int(ociRecordTTL),
}}
}
case "ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404":
if request.Page == nil || *request.Page == "0" {
response.Items = []dns.Record{{
Domain: common.String("foo.bar.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}}
}
}
return
}
func (c *mockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
return // Provider does not use the response so nothing to do here.
}
// newOCIProvider creates an OCI provider with API calls mocked out.
func newOCIProvider(client ociDNSClient, domainFilter DomainFilter, zoneIDFilter ZoneIDFilter, dryRun bool) *OCIProvider {
return &OCIProvider{
client: client,
cfg: OCIConfig{
CompartmentID: "ocid1.compartment.oc1..aaaaaaaaujjg4lf3v6uaqeml7xfk7stzvrxeweaeyolhh75exuoqxpqjb4qq",
},
domainFilter: domainFilter,
zoneIDFilter: zoneIDFilter,
dryRun: dryRun,
}
}
func validateOCIZones(t *testing.T, actual, expected map[string]*dns.ZoneSummary) {
require.Len(t, actual, len(expected))
for k, a := range actual {
e, ok := expected[k]
require.True(t, ok, "unexpected zone %q (%q)", *a.Name, *a.Id)
require.Equal(t, e, a)
}
}
func TestNewOCIProvider(t *testing.T) {
testCases := map[string]struct {
config OCIConfig
err error
}{
"valid": {
config: OCIConfig{
Auth: OCIAuthConfig{
TenancyID: "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
UserID: "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
Region: "us-ashburn-1",
Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAv2JspZyO14kqcO/X4iz3ZdcyAf1GQJqYsBb6wyrlU0PB9Fee
H23/HLtMSqeqo+2KQHmdV1OHFQ/S6tx7zcBaby/+2b+z3/gJO4PGxohe2812AJ/J
W8Fp/4EnwbaRqDhoLN7ms0/e566zE3z40kCSW0NAIzv/F+0nNaka1xrypBqzvaNm
N49dAGvqWRpzFFUb8CbvKmgE6c/H4a2zVNW3G7/K6Og4HQGeEP3NKSVvi0BiQlvd
tVJTg7084kKcrngsS2N3qI3pzsr5wgpzPPefuPHWRKokZ20kpu8tXdFt+mAC2NHh
eWbtY3jsR6JFaXCyZLMXInwDvRgdP0T5+uh8WwIDAQABAoIBAG0rr94omDLKw7L4
naUfEWC+iIAqAdEIXuDTuudpqLb+h7zh3gj/re6tyK8tRWGNNrfgp6gQtZWGGUJv
0w9jEjMqpa2AdRLlYh7Y5KKLV9D6Or3QaAQ3KEffXNZbVmsnAgXWgLL4dKakOPJ8
71LAEryMeCGhL7puRVeOxwi9Dnwc4pcloimdggw/uwVHMK9eY5ylyt5ziiiWfhAo
cnNJNPHRSTqSiCoEhk/8BLZT5gxf1YX0hVSEdQh2WNyxmPmVSC9uuzKOqcEBfHf5
hmLnsUET1REM9IxCLqC9ebW263lIO/KdGiCu+YgIdwIi3wrLhaKXAZQmp4oMvWlE
n5eYlcECgYEA5AhctPWCQBCJhcD39pSWgnSq1O9bt8yQi2P2stqlxKV9ZBepCK49
OT42OYPUgWn7/y//6/LLzsPY58VTDHF3xZN1qu+fU0IM22D3Jqc19pnfVEb6TXSc
0jJIiaYCWTdqRQ4p2DuDcI+EzRB+V1Z7tFWxshZWXwNvtMXNoYPOYaUCgYEA1ttn
R3pCuGYJ5XbBwPzD5J+hvdZ6TQf8oTDraUBPxjtFOr7ea42T6KeYRFvnK2AQDnKL
Mw3I55lNO4I2W9gahUFG28dhxEuxeyvXGqXEJvPCUYePstab/BkUrm7/jkS3CLcJ
dlRXjqOfGwi5+NPUZMoOkZ54ZR4ZpdhIAeEpBf8CgYEAyMyMRlVCowNs9jkcoSfq
+Wme3O8BhvI9/mDCZnCfNHC94Bvtn1U/WF7uBOuPf35Ch05PQAiHa8WOBVn/bZ+l
ZngZT7K+S+SHyc6zFHh9zm9k96Og2f/r8DSTJ5Ll0oY3sCNuuZh+f+oBeUoi1umy
+PPVDAsbd4NhJIBiOO4GGHkCgYA1p4i9Es0Cm4ixItzzwqtwtmR/scXM4se1wS+o
kwTY7gg1yWBl328mVGPz/jdWX6Di2rvkPfcDzwa4a6YDfY3x5QE69Sl3CagCqEoJ
P4giahEGpyG9eVZuuBywCswKzSIgLQVR5XIQDtA2whEfEFcj7EmDF93c8o1ZGw+w
WHgUJQKBgEXr0HgxGG+v8bsXdrJ87Avx/nuA2rrFfECDPa4zuPkEK+cSFibdAq/H
u6OIV+z59AD2s84gxR+KLzEDfQAqBt7cVA5ZH6hrO+bkCtK9ycLL+koOuB+1EV+Y
hKRtDhmSdWBo3tJK12RrAe4t7CUe8gMgTvU7ExlcA3xQkseFPx9K
-----END RSA PRIVATE KEY-----
`,
},
},
},
"invalid": {
config: OCIConfig{
Auth: OCIAuthConfig{
TenancyID: "ocid1.tenancy.oc1..aaaaaaaaxf3fuazosc6xng7l75rj6uist5jb6ken64t3qltimxnkymddqbma",
UserID: "ocid1.user.oc1..aaaaaaaahx2vpvm4of5nqq3t274ike7ygyk2aexvokk3gyv4eyumzqajcrvq",
Region: "us-ashburn-1",
Fingerprint: "48:ba:d4:21:63:53:db:10:65:20:d4:09:ce:01:f5:97",
PrivateKey: `-----BEGIN RSA PRIVATE KEY-----
`,
},
},
err: errors.New("initialising OCI DNS API client: can not create client, bad configuration: PEM data was not found in buffer"),
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
_, err := NewOCIProvider(
tc.config,
NewDomainFilter([]string{"com"}),
NewZoneIDFilter([]string{""}),
false,
)
if err == nil {
require.NoError(t, err)
} else {
require.Equal(t, tc.err.Error(), err.Error())
}
})
}
}
func TestOCIZones(t *testing.T) {
testCases := []struct {
name string
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
expected map[string]*dns.ZoneSummary
}{
{
name: "DomainFilter_com",
domainFilter: NewDomainFilter([]string{"com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
expected: map[string]*dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
},
"bar.com": {
Id: common.String("ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"),
Name: common.String("bar.com"),
},
},
}, {
name: "DomainFilter_foo.com",
domainFilter: NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
expected: map[string]*dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
},
},
}, {
name: "ZoneIDFilter_ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959",
domainFilter: NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"}),
expected: map[string]*dns.ZoneSummary{
"foo.com": {
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, false)
zones, err := provider.zones(context.Background())
require.NoError(t, err)
validateOCIZones(t, zones, tc.expected)
})
}
}
func TestOCIRecords(t *testing.T) {
testCases := []struct {
name string
domainFilter DomainFilter
zoneIDFilter ZoneIDFilter
expected []*endpoint.Endpoint
}{
{
name: "unfiltered",
domainFilter: NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
},
}, {
name: "DomainFilter_foo.com",
domainFilter: NewDomainFilter([]string{"foo.com"}),
zoneIDFilter: NewZoneIDFilter([]string{""}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
endpoint.NewEndpointWithTTL("foo.foo.com", endpoint.RecordTypeTXT, endpoint.TTL(ociRecordTTL), "heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
endpoint.NewEndpointWithTTL("bar.foo.com", endpoint.RecordTypeCNAME, endpoint.TTL(ociRecordTTL), "bar.com."),
},
}, {
name: "ZoneIDFilter_ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404",
domainFilter: NewDomainFilter([]string{""}),
zoneIDFilter: NewZoneIDFilter([]string{"ocid1.dns-zone.oc1..502aeddba262b92fd13ed7874f6f1404"}),
expected: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("foo.bar.com", endpoint.RecordTypeA, endpoint.TTL(ociRecordTTL), "127.0.0.1"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
provider := newOCIProvider(&mockOCIDNSClient{}, tc.domainFilter, tc.zoneIDFilter, false)
endpoints, err := provider.Records()
require.NoError(t, err)
require.ElementsMatch(t, tc.expected, endpoints)
})
}
}
func TestNewRecordOperation(t *testing.T) {
testCases := []struct {
name string
ep *endpoint.Endpoint
opType dns.RecordOperationOperationEnum
expected dns.RecordOperation
}{
{
name: "A_record",
opType: dns.RecordOperationOperationAdd,
ep: endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1"),
expected: dns.RecordOperation{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
}, {
name: "TXT_record",
opType: dns.RecordOperationOperationAdd,
ep: endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeTXT,
endpoint.TTL(ociRecordTTL),
"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
expected: dns.RecordOperation{
Domain: common.String("foo.foo.com"),
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
Rtype: common.String("TXT"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
}, {
name: "CNAME_record",
opType: dns.RecordOperationOperationAdd,
ep: endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeCNAME,
endpoint.TTL(ociRecordTTL),
"bar.com."),
expected: dns.RecordOperation{
Domain: common.String("foo.foo.com"),
Rdata: common.String("bar.com."),
Rtype: common.String("CNAME"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
op := newRecordOperation(tc.ep, tc.opType)
require.Equal(t, tc.expected, op)
})
}
}
func TestOperationsByZone(t *testing.T) {
testCases := []struct {
name string
zones map[string]*dns.ZoneSummary
ops []dns.RecordOperation
expected map[string][]dns.RecordOperation
}{
{
name: "basic",
zones: map[string]*dns.ZoneSummary{
"foo": {
Id: common.String("foo"),
Name: common.String("foo.com"),
},
"bar": {
Id: common.String("bar"),
Name: common.String("bar.com"),
},
},
ops: []dns.RecordOperation{
{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
{
Domain: common.String("foo.bar.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
expected: map[string][]dns.RecordOperation{
"foo": {
{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
"bar": {
{
Domain: common.String("foo.bar.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
},
}, {
name: "does_not_include_zones_with_no_changes",
zones: map[string]*dns.ZoneSummary{
"foo": {
Id: common.String("foo"),
Name: common.String("foo.com"),
},
"bar": {
Id: common.String("bar"),
Name: common.String("bar.com"),
},
},
ops: []dns.RecordOperation{
{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
expected: map[string][]dns.RecordOperation{
"foo": {
{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := operationsByZone(tc.zones, tc.ops)
require.Equal(t, tc.expected, result)
})
}
}
type mutableMockOCIDNSClient struct {
zones map[string]dns.ZoneSummary
records map[string]map[string]dns.Record
}
func newMutableMockOCIDNSClient(zones []dns.ZoneSummary, recordsByZone map[string][]dns.Record) *mutableMockOCIDNSClient {
c := &mutableMockOCIDNSClient{
zones: make(map[string]dns.ZoneSummary),
records: make(map[string]map[string]dns.Record),
}
for _, zone := range zones {
c.zones[*zone.Id] = zone
c.records[*zone.Id] = make(map[string]dns.Record)
}
for zoneID, records := range recordsByZone {
for _, record := range records {
c.records[zoneID][ociRecordKey(*record.Rtype, *record.Domain)] = record
}
}
return c
}
func (c *mutableMockOCIDNSClient) ListZones(ctx context.Context, request dns.ListZonesRequest) (response dns.ListZonesResponse, err error) {
var zones []dns.ZoneSummary
for _, v := range c.zones {
zones = append(zones, v)
}
return dns.ListZonesResponse{Items: zones}, nil
}
func (c *mutableMockOCIDNSClient) GetZoneRecords(ctx context.Context, request dns.GetZoneRecordsRequest) (response dns.GetZoneRecordsResponse, err error) {
if request.ZoneNameOrId == nil {
err = errors.New("no name or id")
return
}
records, ok := c.records[*request.ZoneNameOrId]
if !ok {
err = errors.New("zone not found")
return
}
var items []dns.Record
for _, v := range records {
items = append(items, v)
}
response.Items = items
return
}
func ociRecordKey(rType, domain string) string {
return rType + "/" + domain
}
func (c *mutableMockOCIDNSClient) PatchZoneRecords(ctx context.Context, request dns.PatchZoneRecordsRequest) (response dns.PatchZoneRecordsResponse, err error) {
if request.ZoneNameOrId == nil {
err = errors.New("no name or id")
return
}
records, ok := c.records[*request.ZoneNameOrId]
if !ok {
err = errors.New("zone not found")
return
}
// Ensure that ADD operations occur after REMOVE.
sort.Slice(request.Items, func(i, j int) bool {
return request.Items[i].Operation > request.Items[j].Operation
})
for _, op := range request.Items {
k := ociRecordKey(*op.Rtype, *op.Domain)
switch op.Operation {
case dns.RecordOperationOperationAdd:
records[k] = dns.Record{
Domain: op.Domain,
Rtype: op.Rtype,
Rdata: op.Rdata,
Ttl: op.Ttl,
}
case dns.RecordOperationOperationRemove:
delete(records, k)
default:
err = errors.Errorf("unsupported operation %q", op.Operation)
return
}
}
return
}
// TestMutableMockOCIDNSClient exists because one must always test one's tests
// right...?
func TestMutableMockOCIDNSClient(t *testing.T) {
zones := []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}}
records := map[string][]dns.Record{
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}, {
Domain: common.String("foo.foo.com"),
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
Rtype: common.String(endpoint.RecordTypeTXT),
Ttl: common.Int(ociRecordTTL),
}},
}
client := newMutableMockOCIDNSClient(zones, records)
// First ListZones.
zonesResponse, err := client.ListZones(context.Background(), dns.ListZonesRequest{})
require.NoError(t, err)
require.Len(t, zonesResponse.Items, 1)
require.Equal(t, zonesResponse.Items, zones)
// GetZoneRecords for that zone.
recordsResponse, err := client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
ZoneNameOrId: zones[0].Id,
})
require.NoError(t, err)
require.Len(t, recordsResponse.Items, 2)
require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
// Remove the A record.
_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
Items: []dns.RecordOperation{{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationRemove,
}},
},
})
require.NoError(t, err)
// GetZoneRecords again and check the A record was removed.
recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
ZoneNameOrId: zones[0].Id,
})
require.NoError(t, err)
require.Len(t, recordsResponse.Items, 1)
require.Equal(t, recordsResponse.Items[0], records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"][1])
// Add the A record back.
_, err = client.PatchZoneRecords(context.Background(), dns.PatchZoneRecordsRequest{
ZoneNameOrId: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{
Items: []dns.RecordOperation{{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String("A"),
Ttl: common.Int(300),
Operation: dns.RecordOperationOperationAdd,
}},
},
})
require.NoError(t, err)
// GetZoneRecords and check we're back in the origional state
recordsResponse, err = client.GetZoneRecords(context.Background(), dns.GetZoneRecordsRequest{
ZoneNameOrId: zones[0].Id,
})
require.NoError(t, err)
require.Len(t, recordsResponse.Items, 2)
require.ElementsMatch(t, recordsResponse.Items, records["ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"])
}
func TestOCIApplyChanges(t *testing.T) {
testCases := []struct {
name string
zones []dns.ZoneSummary
records map[string][]dns.Record
changes *plan.Changes
dryRun bool
err error
expectedEndpoints []*endpoint.Endpoint
}{
{
name: "add",
zones: []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}},
changes: &plan.Changes{
Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
},
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
}, {
name: "remove",
zones: []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}},
records: map[string][]dns.Record{
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}, {
Domain: common.String("foo.foo.com"),
Rdata: common.String("heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/my-svc"),
Rtype: common.String(endpoint.RecordTypeTXT),
Ttl: common.Int(ociRecordTTL),
}},
},
changes: &plan.Changes{
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeTXT,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
},
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
}, {
name: "update",
zones: []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}},
records: map[string][]dns.Record{
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}},
},
changes: &plan.Changes{
UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"10.0.0.1",
)},
},
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"10.0.0.1",
)},
}, {
name: "dry_run_no_changes",
zones: []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}},
records: map[string][]dns.Record{
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}},
},
changes: &plan.Changes{
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
},
dryRun: true,
expectedEndpoints: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
}, {
name: "add_remove_update",
zones: []dns.ZoneSummary{{
Id: common.String("ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959"),
Name: common.String("foo.com"),
}},
records: map[string][]dns.Record{
"ocid1.dns-zone.oc1..e1e042ef0bfbb5c251b9713fd7bf8959": {{
Domain: common.String("foo.foo.com"),
Rdata: common.String("127.0.0.1"),
Rtype: common.String(endpoint.RecordTypeA),
Ttl: common.Int(ociRecordTTL),
}, {
Domain: common.String("bar.foo.com"),
Rdata: common.String("bar.com."),
Rtype: common.String(endpoint.RecordTypeCNAME),
Ttl: common.Int(ociRecordTTL),
}},
},
changes: &plan.Changes{
Delete: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"foo.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"baz.com.",
)},
UpdateOld: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"bar.foo.com",
endpoint.RecordTypeCNAME,
endpoint.TTL(ociRecordTTL),
"baz.com.",
)},
UpdateNew: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"bar.foo.com",
endpoint.RecordTypeCNAME,
endpoint.TTL(ociRecordTTL),
"foo.bar.com.",
)},
Create: []*endpoint.Endpoint{endpoint.NewEndpointWithTTL(
"baz.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1",
)},
},
expectedEndpoints: []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL(
"bar.foo.com",
endpoint.RecordTypeCNAME,
endpoint.TTL(ociRecordTTL),
"foo.bar.com.",
),
endpoint.NewEndpointWithTTL(
"baz.foo.com",
endpoint.RecordTypeA,
endpoint.TTL(ociRecordTTL),
"127.0.0.1"),
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := newMutableMockOCIDNSClient(tc.zones, tc.records)
provider := newOCIProvider(
client,
NewDomainFilter([]string{""}),
NewZoneIDFilter([]string{""}),
tc.dryRun,
)
err := provider.ApplyChanges(tc.changes)
require.Equal(t, tc.err, err)
endpoints, err := provider.Records()
require.NoError(t, err)
require.ElementsMatch(t, tc.expectedEndpoints, endpoints)
})
}
}