mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
Merge branch 'master' into external-dns-exoscale
This commit is contained in:
commit
717ee8440c
22
Gopkg.lock
generated
22
Gopkg.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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
155
docs/tutorials/oracle.md
Normal 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
|
6
main.go
6
main.go
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
301
provider/oci.go
Normal 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
839
provider/oci_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user