mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
Merge branch 'master' into fix-1906
This commit is contained in:
commit
469f07eb8b
@ -16,6 +16,8 @@
|
||||
- Fix NodePort with externaltrafficpolicy targets duplication @codearky
|
||||
- Update contributing section in README (#1760) @seanmalloy
|
||||
- Option to cache AWS zones list @bpineau
|
||||
- Refactor, enhance and test Akamai provider and documentation (#1846) @edglynes
|
||||
- Fix: only use absolute CNAMEs in Scaleway provider (#1859) @Sh4d1
|
||||
|
||||
## v0.7.3 - 2020-08-05
|
||||
|
||||
|
@ -48,6 +48,8 @@ ExternalDNS' current release is `v0.7`. This version allows you to keep selected
|
||||
* [Vultr](https://www.vultr.com)
|
||||
* [OVH](https://www.ovh.com)
|
||||
* [Scaleway](https://www.scaleway.com)
|
||||
* [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
|
||||
* [GoDaddy](https://www.godaddy.com)
|
||||
|
||||
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.
|
||||
|
||||
@ -78,6 +80,7 @@ The following table clarifies the current status of the providers according to t
|
||||
| Google Cloud DNS | Stable | |
|
||||
| AWS Route 53 | Stable | |
|
||||
| AWS Cloud Map | Beta | |
|
||||
| Akamai Edge DNS | Beta | |
|
||||
| AzureDNS | Beta | |
|
||||
| CloudFlare | Beta | |
|
||||
| RcodeZero | Alpha | |
|
||||
@ -97,11 +100,11 @@ The following table clarifies the current status of the providers according to t
|
||||
| TransIP | Alpha | |
|
||||
| VinylDNS | Alpha | |
|
||||
| RancherDNS | Alpha | |
|
||||
| Akamai FastDNS | Alpha | |
|
||||
| OVH | Alpha | |
|
||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||
| Vultr | Alpha | |
|
||||
| UltraDNS | Alpha | |
|
||||
| GoDaddy | Alpha | |
|
||||
|
||||
## Running ExternalDNS:
|
||||
|
||||
@ -154,6 +157,7 @@ The following tutorials are provided:
|
||||
* [Scaleway](docs/tutorials/scaleway.md)
|
||||
* [Vultr](docs/tutorials/vultr.md)
|
||||
* [UltraDNS](docs/tutorials/ultradns.md)
|
||||
* [GoDaddy](docs/tutorials/godaddy.md)
|
||||
|
||||
### Running Locally
|
||||
|
||||
|
@ -117,6 +117,8 @@ type Controller struct {
|
||||
nextRunAt time.Time
|
||||
// The nextRunAtMux is for atomic updating of nextRunAt
|
||||
nextRunAtMux sync.Mutex
|
||||
// DNS record types that will be considered for management
|
||||
ManagedRecordTypes []string
|
||||
}
|
||||
|
||||
// RunOnce runs a single iteration of a reconciliation loop.
|
||||
@ -147,6 +149,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
|
||||
Desired: endpoints,
|
||||
DomainFilter: c.DomainFilter,
|
||||
PropertyComparator: c.Registry.PropertyValuesEqual,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
plan = plan.Calculate()
|
||||
@ -165,7 +168,7 @@ func (c *Controller) RunOnce(ctx context.Context) error {
|
||||
// MinInterval is used as window for batching events
|
||||
const MinInterval = 5 * time.Second
|
||||
|
||||
// RunOnceThrottled makes sure execution happens at most once per interval.
|
||||
// ScheduleRunOnce makes sure execution happens at most once per interval.
|
||||
func (c *Controller) ScheduleRunOnce(now time.Time) {
|
||||
c.nextRunAtMux.Lock()
|
||||
defer c.nextRunAtMux.Unlock()
|
||||
|
251
docs/tutorials/akamai-edgedns.md
Normal file
251
docs/tutorials/akamai-edgedns.md
Normal file
@ -0,0 +1,251 @@
|
||||
# Setting up External-DNS for Services on Akamai Edge DNS
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Akamai Edge DNS (formally known as Fast DNS) provider support was first released in External-DNS v0.5.18
|
||||
|
||||
### Zones
|
||||
|
||||
External-DNS manages service endpoints in existing DNS zones. The Akamai provider does not add, remove or configure new zones in anyway. Edge DNS zones can be created and managed thru the [Akamai Control Center](https://control.akamai.com) or [Akamai DevOps Tools](https://developer.akamai.com/devops), [Akamai CLI](https://developer.akamai.com/cli) and [Akamai Terraform Provider](https://developer.akamai.com/tools/integrations/terraform)
|
||||
|
||||
### Akamai Edge DNS Authentication
|
||||
|
||||
The Akamai Edge DNS provider requires valid Akamai Edgegrid API authentication credentials to access zones and manage associated DNS records.
|
||||
|
||||
Credentials can be provided to the provider either directly by key or indirectly via a file. The Akamai credential keys and mappings to the Akamai provider utilizing different presentation methods are:
|
||||
|
||||
| Edgegrid Auth Key | External-DNS Cmd Line Key | Environment/ConfigMap Key | Description |
|
||||
| ----------------- | ------------------------- | ------------------------- | ----------- |
|
||||
| host | akamai-serviceconsumerdomain | EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN | Akamai Edgegrid API server |
|
||||
| access_token | akamai-access-token | EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN | Akamai Edgegrid API access token |
|
||||
| client_token | akamai-client-token | EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN |Akamai Edgegrid API client token |
|
||||
| client-secret | akamai-client-secret | EXTERNAL_DNS_AKAMAI_CLIENT_SECRET |Akamai Edgegrid API client secret |
|
||||
|
||||
In addition to specifying auth credentials individually, the credentials may be referenced indirectly by using the Akamai Edgegrid .edgerc file convention.
|
||||
|
||||
| External-DNS Cmd Line | Environment/ConfigMap | Description |
|
||||
| --------------------- | --------------------- | ----------- |
|
||||
| akamai-edgerc-path | EXTERNAL_DNS_AKAMAI_EDGERC_PATH | Accessible path to Edgegrid credentials file, e.g /home/test/.edgerc |
|
||||
| akamai-edgerc-section | EXTERNAL_DNS_AKAMAI_EDGERC_SECTION | Section in Edgegrid credentials file containing credentials |
|
||||
|
||||
Note: akamai-edgerc-path and akamai-edgerc-section are present in External-DNS versions after v0.7.5
|
||||
|
||||
[Akamai API Authentication](https://developer.akamai.com/getting-started/edgegrid) provides an overview and further information pertaining to the generation of auth credentials for API base applications and tools.
|
||||
|
||||
The following example defines and references a Kubernetes ConfigMap secret, applied by referencing the secret and its keys in the env section of the deployment.
|
||||
|
||||
|
||||
## Deploy External-DNS
|
||||
|
||||
An operational External-DNS deployment consists of an External-DNS container and service. The following sections demonstrate the ConfigMap objects that would make up an example functional external DNS kubernetes configuration utilizing NGINX as the exposed service.
|
||||
|
||||
Connect your `kubectl` client to the cluster with which you want to test External-DNS, and then apply one of the following manifest files for deployment:
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=akamai
|
||||
- --domain-filter=example.com
|
||||
# zone-id-filter may be specified as well to filter on contract ID
|
||||
- --registry=txt
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
env:
|
||||
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["watch", "list"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.5
|
||||
args:
|
||||
- --source=service # or ingress or both
|
||||
- --provider=akamai
|
||||
- --domain-filter=example.com
|
||||
# zone-id-filter may be specified as well to filter on contract ID
|
||||
- --registry=txt
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
env:
|
||||
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
```
|
||||
|
||||
Create the deployment for External-DNS:
|
||||
|
||||
```
|
||||
$ kubectl create -f externaldns.yaml
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: nginx.example.com
|
||||
external-dns.alpha.kubernetes.io/ttl: "600" #optional
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
Create the deployment, service and ingress object:
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
## Verify Akamai Edge DNS Records
|
||||
|
||||
It is recommended to wait 3-5 minutes before validating the records to allow the record changes to propagate to all the Akamai name servers worldwide.
|
||||
|
||||
The records can be validated using the [Akamai Control Center](http://control.akamai.com) or by executing a dig, nslookup or similar DNS command.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you successfully configure and verify record management via External-DNS, you can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx.yaml
|
||||
$ kubectl delete -f externaldns.yaml
|
||||
```
|
||||
|
||||
## Additional Information
|
||||
|
||||
* The Akamai provider allows the administrative user to filter zones by both name (domain-filter) and contract Id (zone-id-filter). The Edge DNS API will return a '500 Internal Error' if an invalid contract Id is provided.
|
||||
* The provider will substitute any embedded quotes in TXT records with `` ` `` (back tick) when writing the records to the API.
|
||||
|
@ -1,192 +0,0 @@
|
||||
# Setting up Akamai FastDNS
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Akamai FastDNS provider support was added via [this PR](https://github.com/kubernetes-sigs/external-dns/pull/1384), thus you need to use a release where this pr is included. This should be at least v0.5.18
|
||||
|
||||
The Akamai FastDNS provider expects that your zones, you wish to add records to, already exists
|
||||
and are configured correctly. It does not add, remove or configure new zones in anyway.
|
||||
|
||||
To do this please refer to the [FastDNS documentation](https://developer.akamai.com/legacy/cli/packages/fast-dns.html).
|
||||
|
||||
Additional data you will have to provide:
|
||||
|
||||
* Service Consumer Domain
|
||||
* Access token
|
||||
* Client token
|
||||
* Client Secret
|
||||
|
||||
Make these available to external DNS somehow. In the following example a secret is used by referencing the secret and its keys in the env section of the deployment.
|
||||
|
||||
If you happen to have questions regarding authentication, please refer to the [API Client Authentication documentation](https://developer.akamai.com/legacy/introduction/Client_Auth.html)
|
||||
|
||||
## Deployment
|
||||
|
||||
Deploying external DNS for Akamai is actually nearly identical to deploying
|
||||
it for other providers. This is what a sample `deployment.yaml` looks like:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
app.kubernetes.io/version: v0.6.0
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: external-dns
|
||||
app.kubernetes.io/version: v0.6.0
|
||||
spec:
|
||||
# Only use if you're also using RBAC
|
||||
# serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
args:
|
||||
- --source=ingress # or service or both
|
||||
- --provider=akamai
|
||||
- --registry=txt
|
||||
- --txt-owner-id={{ owner-id-for-this-external-dns }}
|
||||
env:
|
||||
- name: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_SERVICECONSUMERDOMAIN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN
|
||||
- name: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_CLIENT_SECRET
|
||||
- name: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: external-dns
|
||||
key: EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN
|
||||
```
|
||||
|
||||
## RBAC
|
||||
|
||||
If your cluster is RBAC enabled, you also need to setup the following, before you can run external-dns:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
namespace: default
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services","endpoints","pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
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
|
||||
```
|
||||
## Verify ExternalDNS works (Ingress example)
|
||||
|
||||
Create an ingress resource manifest file.
|
||||
|
||||
> For ingress objects ExternalDNS will create a DNS record based on the host specified for the ingress object.
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: foo
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx" # use the one that corresponds to your ingress controller.
|
||||
spec:
|
||||
rules:
|
||||
- host: foo.bar.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: foo
|
||||
servicePort: 80
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
> If you want to give multiple names to service, you can set it to external-dns.alpha.kubernetes.io/hostname with a comma separator.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- port: 80
|
||||
name: http
|
||||
targetPort: 80
|
||||
selector:
|
||||
app: nginx
|
||||
|
||||
---
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
name: http
|
||||
```
|
||||
|
||||
|
||||
**Important!**: Don't run dig, nslookup or similar immediately. You'll get hit by [negative DNS caching](https://tools.ietf.org/html/rfc2308), which is hard to flush.
|
||||
Wait about 30s-1m (interval for external-dns to kick in)
|
@ -2,7 +2,7 @@
|
||||
|
||||
This tutorial describes how to use ExternalDNS with the [aws-alb-ingress-controller][1].
|
||||
|
||||
[1]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/
|
||||
[1]: https://kubernetes-sigs.github.io/aws-load-balancer-controller
|
||||
|
||||
## Setting up ExternalDNS and aws-alb-ingress-controller
|
||||
|
||||
@ -14,12 +14,12 @@ this is not required.
|
||||
|
||||
For help setting up the ALB Ingress Controller, follow the [Setup Guide][2].
|
||||
|
||||
[2]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/controller/setup/
|
||||
[2]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/installation/
|
||||
|
||||
Note that the ALB ingress controller uses the same tags for [subnet auto-discovery][3]
|
||||
as Kubernetes does with the AWS cloud provider.
|
||||
|
||||
[3]: https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/controller/config/#subnet-auto-discovery
|
||||
[3]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/subnet_discovery/
|
||||
|
||||
In the examples that follow, it is assumed that you configured the ALB Ingress
|
||||
Controller with the `ingress-class=alb` argument (not to be confused with the
|
||||
|
@ -414,6 +414,39 @@ You can configure Route53 to associate DNS records with healthchecks for automat
|
||||
|
||||
Note: ExternalDNS does not support creating healthchecks, and assumes that `<health-check-id>` already exists.
|
||||
|
||||
## Govcloud caveats
|
||||
|
||||
Due to the special nature with how Route53 runs in Govcloud, there are a few tweaks in the deployment settings.
|
||||
|
||||
* An Environment variable with name of AWS_REGION set to either us-gov-west-1 or us-gov-east-1 is required. Otherwise it tries to lookup a region that does not exist in Govcloud and it errors out.
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: AWS_REGION
|
||||
value: us-gov-west-1
|
||||
```
|
||||
|
||||
* Route53 in Govcloud does not allow aliases. Therefore, container args must be set so that it uses CNAMES and a txt-prefix must be set to something. Otherwise, it will try to create a TXT record with the same value than the CNAME itself, which is not allowed.
|
||||
|
||||
```yaml
|
||||
args:
|
||||
- --aws-prefer-cname
|
||||
- --txt-prefix={{ YOUR_PREFIX }}
|
||||
```
|
||||
|
||||
* The first two changes are needed if you use Route53 in Govcloud, which only supports private zones. There are also no cross account IAM whatsoever between Govcloud and commerical AWS accounts. If services and ingresses need to make Route 53 entries to an public zone in a commerical account, you will have set env variables of AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY with a key and secret to the commerical account that has the sufficient rights.
|
||||
|
||||
```yaml
|
||||
env:
|
||||
- name: AWS_ACCESS_KEY_ID
|
||||
value: XXXXXXXXX
|
||||
- name: AWS_SECRET_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ YOUR_SECRET_NAME }}
|
||||
key: {{ YOUR_SECRET_KEY }}
|
||||
```
|
||||
|
||||
## Clean up
|
||||
|
||||
Make sure to delete all Service objects before terminating the cluster so all load balancers get cleaned up correctly.
|
||||
|
@ -34,17 +34,19 @@ This is crucial as ExternalDNS reads those endpoints records when creating DNS-R
|
||||
In the subsequent parameter we will make use of this. If you don't want to work with ingress-resources in your later use, you can leave the parameter out.
|
||||
|
||||
Verify the correct propagation of the loadbalancer's ip by listing the ingresses.
|
||||
|
||||
```
|
||||
$ kubectl get ingress
|
||||
```
|
||||
|
||||
The address column should contain the ip for each ingress. ExternalDNS will pick up exactly this piece of information.
|
||||
|
||||
```
|
||||
NAME HOSTS ADDRESS PORTS AGE
|
||||
nginx1 sample1.aks.com 52.167.195.110 80 6d22h
|
||||
nginx2 sample2.aks.com 52.167.195.110 80 6d21h
|
||||
```
|
||||
|
||||
|
||||
If you do not want to deploy the ingress controller with Helm, ensure to pass the following cmdline-flags to it through the mechanism of your choice:
|
||||
|
||||
```
|
||||
@ -144,6 +146,8 @@ This is per default done through the file `~/.kube/config`.
|
||||
For general background information on this see [kubernetes-docs](https://kubernetes.io/docs/tasks/access-application-cluster/access-cluster/).
|
||||
Azure-CLI features functionality for automatically maintaining this file for AKS-Clusters. See [Azure-Docs](https://docs.microsoft.com/de-de/cli/azure/aks?view=azure-cli-latest#az-aks-get-credentials).
|
||||
|
||||
Follow the steps for [azure-dns provider](./azure.md#creating-configuration-file) to create a configuration file.
|
||||
|
||||
Then apply one of the following manifests depending on whether you use RBAC or not.
|
||||
|
||||
The credentials of the service principal are provided to ExternalDNS as environment-variables.
|
||||
@ -175,13 +179,14 @@ spec:
|
||||
- --provider=azure-private-dns
|
||||
- --azure-resource-group=externaldns
|
||||
- --azure-subscription-id=<use the id of your subscription>
|
||||
env:
|
||||
- name: AZURE_TENANT_ID
|
||||
value: "<use the tenantId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: "<use the aadClientId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_SECRET
|
||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
||||
volumeMounts:
|
||||
- name: azure-config-file
|
||||
mountPath: /etc/kubernetes
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled, cluster access)
|
||||
@ -245,13 +250,14 @@ spec:
|
||||
- --provider=azure-private-dns
|
||||
- --azure-resource-group=externaldns
|
||||
- --azure-subscription-id=<use the id of your subscription>
|
||||
env:
|
||||
- name: AZURE_TENANT_ID
|
||||
value: "<use the tenantId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: "<use the aadClientId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_SECRET
|
||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
||||
volumeMounts:
|
||||
- name: azure-config-file
|
||||
mountPath: /etc/kubernetes
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled, namespace access)
|
||||
@ -315,13 +321,14 @@ spec:
|
||||
- --provider=azure-private-dns
|
||||
- --azure-resource-group=externaldns
|
||||
- --azure-subscription-id=<use the id of your subscription>
|
||||
env:
|
||||
- name: AZURE_TENANT_ID
|
||||
value: "<use the tenantId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_ID
|
||||
value: "<use the aadClientId discovered during creation of service principal>"
|
||||
- name: AZURE_CLIENT_SECRET
|
||||
value: "<use the aadClientSecret discovered during creation of service principal>"
|
||||
volumeMounts:
|
||||
- name: azure-config-file
|
||||
mountPath: /etc/kubernetes
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: azure-config-file
|
||||
secret:
|
||||
secretName: azure-config-file
|
||||
```
|
||||
|
||||
Create the deployment for ExternalDNS:
|
||||
|
197
docs/tutorials/godaddy.md
Normal file
197
docs/tutorials/godaddy.md
Normal file
@ -0,0 +1,197 @@
|
||||
# Setting up ExternalDNS for Services on GoDaddy
|
||||
|
||||
This tutorial describes how to setup ExternalDNS for use within a
|
||||
Kubernetes cluster using GoDaddy DNS.
|
||||
|
||||
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
||||
|
||||
## Creating a zone with GoDaddy DNS
|
||||
|
||||
If you are new to GoDaddy, we recommend you first read the following
|
||||
instructions for creating a zone.
|
||||
|
||||
[Creating a zone using the GoDaddy web console](https://www.godaddy.com/)
|
||||
|
||||
[Creating a zone using the GoDaddy API](https://developer.godaddy.com/)
|
||||
|
||||
## Creating GoDaddy API key
|
||||
|
||||
You first need to create an API Key.
|
||||
|
||||
Using the [GoDaddy documentation](https://developer.godaddy.com/getstarted) you will have your `API key` and `API secret`
|
||||
|
||||
## Deploy ExternalDNS
|
||||
|
||||
Connect your `kubectl` client to the cluster with which you want to test ExternalDNS, and then apply one of the following manifest files for deployment:
|
||||
|
||||
### Manifest (for clusters without RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=godaddy
|
||||
- --txt-prefix=external-dns. # In case of multiple k8s cluster
|
||||
- --txt-owner-id=owner-id # In case of multiple k8s cluster
|
||||
- --godaddy-api-key=<Your API Key>
|
||||
- --godaddy-api-secret=<Your API secret>
|
||||
```
|
||||
|
||||
### Manifest (for clusters with RBAC enabled)
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: external-dns
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1beta1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: external-dns
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["services"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: ["extensions","networking.k8s.io"]
|
||||
resources: ["ingresses"]
|
||||
verbs: ["get","watch","list"]
|
||||
- apiGroups: [""]
|
||||
resources: ["nodes"]
|
||||
verbs: ["list","watch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["endpoints"]
|
||||
verbs: ["get","watch","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: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: external-dns
|
||||
spec:
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: external-dns
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: external-dns
|
||||
spec:
|
||||
serviceAccountName: external-dns
|
||||
containers:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.7
|
||||
args:
|
||||
- --source=service # ingress is also possible
|
||||
- --domain-filter=example.com # (optional) limit to only example.com domains; change to match the zone created above.
|
||||
- --provider=godaddy
|
||||
- --txt-prefix=external-dns. # In case of multiple k8s cluster
|
||||
- --txt-owner-id=owner-id # In case of multiple k8s cluster
|
||||
- --godaddy-api-key=<Your API Key>
|
||||
- --godaddy-api-secret=<Your API secret>
|
||||
```
|
||||
|
||||
## Deploying an Nginx Service
|
||||
|
||||
Create a service file called 'nginx.yaml' with the following contents:
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
annotations:
|
||||
external-dns.alpha.kubernetes.io/hostname: example.com
|
||||
external-dns.alpha.kubernetes.io/ttl: "120" #optional
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
**A note about annotations**
|
||||
|
||||
Verify that the annotation on the service uses the same hostname as the GoDaddy DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
|
||||
|
||||
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
|
||||
|
||||
ExternalDNS uses the hostname annotation to determine which services should be registered with DNS. Removing the hostname annotation will cause ExternalDNS to remove the corresponding DNS records.
|
||||
|
||||
### Create the deployment and service
|
||||
|
||||
```
|
||||
$ kubectl create -f nginx.yaml
|
||||
```
|
||||
|
||||
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the GoDaddy DNS records.
|
||||
|
||||
## Verifying GoDaddy DNS records
|
||||
|
||||
Use the GoDaddy web console or API to verify that the A record for your domain shows the external IP address of the services.
|
||||
|
||||
## Cleanup
|
||||
|
||||
Once you successfully configure and verify record management via ExternalDNS, you can delete the tutorial's example:
|
||||
|
||||
```
|
||||
$ kubectl delete -f nginx.yaml
|
||||
$ kubectl delete -f externaldns.yaml
|
||||
```
|
@ -78,7 +78,7 @@ rules:
|
||||
See also current RBAC yaml files:
|
||||
- [kube-ingress-aws-controller](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/ingress-controller/01-rbac.yaml)
|
||||
- [skipper](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/skipper/rbac.yaml)
|
||||
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/rbac.yaml)
|
||||
- [external-dns](https://github.com/zalando-incubator/kubernetes-on-aws/blob/dev/cluster/manifests/external-dns/01-rbac.yaml)
|
||||
|
||||
[3]: https://opensource.zalando.com/skipper/kubernetes/routegroups/#routegroups
|
||||
[4]: https://opensource.zalando.com/skipper
|
||||
|
@ -220,6 +220,8 @@ spec:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
args:
|
||||
- --registry=txt
|
||||
- --txt-prefix=external-dns-
|
||||
- --txt-owner-id=k8s
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-host=192.168.0.1
|
||||
@ -260,6 +262,8 @@ spec:
|
||||
- name: external-dns
|
||||
image: k8s.gcr.io/external-dns/external-dns:v0.7.3
|
||||
args:
|
||||
- --registry=txt
|
||||
- --txt-prefix=external-dns-
|
||||
- --txt-owner-id=k8s
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-host=192.168.0.1
|
||||
@ -273,17 +277,19 @@ spec:
|
||||
- --domain-filter=k8s.example.org
|
||||
```
|
||||
|
||||
## Microsoft DNS
|
||||
## Microsoft DNS (Insecure Updates)
|
||||
|
||||
While `external-dns` was not developed or tested against Microsoft DNS, it can be configured to work against it. YMMV.
|
||||
|
||||
### DNS-side configuration
|
||||
### Insecure Updates
|
||||
|
||||
#### DNS-side configuration
|
||||
|
||||
1. Create a DNS zone
|
||||
2. Enable insecure dynamic updates for the zone
|
||||
3. Enable Zone Transfers from all servers
|
||||
|
||||
### `external-dns` configuration
|
||||
#### `external-dns` configuration
|
||||
|
||||
You'll want to configure `external-dns` similarly to the following:
|
||||
|
||||
@ -298,4 +304,84 @@ You'll want to configure `external-dns` similarly to the following:
|
||||
...
|
||||
```
|
||||
|
||||
Since Microsoft DNS does not support secure updates via TSIG, this will let `external-dns` make insecure updates. Do this at your own risk.
|
||||
### Secure Updates Using RFC3645 (GSS-TSIG)
|
||||
|
||||
### DNS-side configuration
|
||||
|
||||
1. Create a DNS zone
|
||||
2. Enable secure dynamic updates for the zone
|
||||
3. Enable Zone Transfers from all servers
|
||||
|
||||
|
||||
#### Kerberos Configuration
|
||||
|
||||
DNS with secure updates relies upon a valid Kerberos configuration running within the `external-dns` container. At this time, you will need to create a ConfigMap for the `external-dns` container to use and mount it in your deployment. Below is an example of a working Kerberos configuration inside a ConfigMap definition. This may be different depending on many factors in your environment:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: krb5.conf
|
||||
data:
|
||||
krb5.conf: |
|
||||
[logging]
|
||||
default = FILE:/var/log/krb5libs.log
|
||||
kdc = FILE:/var/log/krb5kdc.log
|
||||
admin_server = FILE:/var/log/kadmind.log
|
||||
|
||||
[libdefaults]
|
||||
dns_lookup_realm = false
|
||||
ticket_lifetime = 24h
|
||||
renew_lifetime = 7d
|
||||
forwardable = true
|
||||
rdns = false
|
||||
pkinit_anchors = /etc/pki/tls/certs/ca-bundle.crt
|
||||
default_ccache_name = KEYRING:persistent:%{uid}
|
||||
|
||||
default_realm = YOURDOMAIN.COM
|
||||
|
||||
[realms]
|
||||
YOURDOMAIN.COM = {
|
||||
kdc = dc1.yourdomain.com
|
||||
admin_server = dc1.yourdomain.com
|
||||
}
|
||||
|
||||
[domain_realm]
|
||||
yourdomain.com = YOURDOMAIN.COM
|
||||
.yourdomain.com = YOURDOMAIN.COM
|
||||
```
|
||||
|
||||
Once the ConfigMap is created, the container `external-dns` container needs to be told to mount that ConfigMap as a volume at the default Kerberos configuration location. The pod spec should include a similar configuration to the following:
|
||||
|
||||
```yaml
|
||||
...
|
||||
volumeMounts:
|
||||
- mountPath: /etc/krb5.conf
|
||||
name: kerberos-config-volume
|
||||
subPath: krb5.conf
|
||||
...
|
||||
volumes:
|
||||
- configMap:
|
||||
defaultMode: 420
|
||||
name: krb5.conf
|
||||
name: kerberos-config-volume
|
||||
...
|
||||
```
|
||||
|
||||
#### `external-dns` configuration
|
||||
|
||||
You'll want to configure `external-dns` similarly to the following:
|
||||
|
||||
```text
|
||||
...
|
||||
- --provider=rfc2136
|
||||
- --rfc2136-gss-tsig
|
||||
- --rfc2136-host=123.123.123.123
|
||||
- --rfc2136-port=53
|
||||
- --rfc2136-zone=your-domain.com
|
||||
- --rfc2136-kerberos-username=your-domain-account
|
||||
- --rfc2136-kerberos-password=your-domain-password
|
||||
- --rfc2136-tsig-axfr # needed to enable zone transfers, which is required for deletion of records.
|
||||
...
|
||||
```
|
18
go.mod
18
go.mod
@ -8,17 +8,18 @@ require (
|
||||
github.com/Azure/azure-sdk-for-go v45.1.0+incompatible
|
||||
github.com/Azure/go-autorest/autorest v0.11.10
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.5
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v0.9.11
|
||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.0.0
|
||||
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
|
||||
github.com/alecthomas/colour v0.1.0 // indirect
|
||||
github.com/alecthomas/kingpin v2.2.5+incompatible
|
||||
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357
|
||||
github.com/aws/aws-sdk-go v1.31.4
|
||||
github.com/bodgit/tsig v0.0.2
|
||||
github.com/cloudflare/cloudflare-go v0.10.1
|
||||
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
|
||||
github.com/datawire/ambassador v1.6.0
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba
|
||||
github.com/digitalocean/godo v1.36.0
|
||||
github.com/dnsimple/dnsimple-go v0.60.0
|
||||
@ -33,7 +34,7 @@ require (
|
||||
github.com/linki/instrumented_http v0.2.0
|
||||
github.com/linode/linodego v0.19.0
|
||||
github.com/maxatome/go-testdeep v1.4.0
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/miekg/dns v1.1.36-0.20210109083720-731b191cabd1
|
||||
github.com/nesv/go-dynect v0.6.0
|
||||
github.com/nic-at/rc0go v1.1.1
|
||||
github.com/openshift/api v0.0.0-20200605231317-fb2a6ca106ae
|
||||
@ -46,7 +47,8 @@ require (
|
||||
github.com/sanyu/dynectsoap v0.0.0-20181203081243-b83de5edc4e0
|
||||
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.6.0.20200623155123-84df6c4b5301
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/smartystreets/gunit v1.3.4 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/terra-farm/udnssdk v1.3.5 // indirect
|
||||
github.com/transip/gotransip v5.8.2+incompatible
|
||||
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
|
||||
@ -54,16 +56,20 @@ require (
|
||||
github.com/vultr/govultr v0.4.2
|
||||
go.etcd.io/etcd v0.5.0-alpha.5.0.20200401174654-e694b7bb0875
|
||||
go.uber.org/ratelimit v0.1.0
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/tools v0.0.0-20200708003708-134513de8882 // indirect
|
||||
google.golang.org/api v0.15.0
|
||||
gopkg.in/ns1/ns1-go.v2 v2.0.0-20190322154155-0dafb5275fd1
|
||||
gopkg.in/yaml.v2 v2.2.8
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
|
||||
istio.io/api v0.0.0-20200529165953-72dad51d4ffc
|
||||
istio.io/client-go v0.0.0-20200529172309-31c16ea3f751
|
||||
k8s.io/api v0.18.8
|
||||
k8s.io/apimachinery v0.18.8
|
||||
k8s.io/client-go v0.18.8
|
||||
k8s.io/kubernetes v1.13.0
|
||||
)
|
||||
|
||||
replace (
|
||||
|
@ -3,7 +3,7 @@ kind: Kustomization
|
||||
|
||||
images:
|
||||
- name: k8s.gcr.io/external-dns/external-dns
|
||||
newTag: v0.7.5
|
||||
newTag: v0.7.6
|
||||
|
||||
resources:
|
||||
- ./external-dns-deployment.yaml
|
||||
|
15
main.go
15
main.go
@ -46,6 +46,7 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider/dnsimple"
|
||||
"sigs.k8s.io/external-dns/provider/dyn"
|
||||
"sigs.k8s.io/external-dns/provider/exoscale"
|
||||
"sigs.k8s.io/external-dns/provider/godaddy"
|
||||
"sigs.k8s.io/external-dns/provider/google"
|
||||
"sigs.k8s.io/external-dns/provider/hetzner"
|
||||
"sigs.k8s.io/external-dns/provider/infoblox"
|
||||
@ -151,7 +152,7 @@ func main() {
|
||||
var p provider.Provider
|
||||
switch cfg.Provider {
|
||||
case "akamai":
|
||||
p = akamai.NewAkamaiProvider(
|
||||
p, err = akamai.NewAkamaiProvider(
|
||||
akamai.AkamaiConfig{
|
||||
DomainFilter: domainFilter,
|
||||
ZoneIDFilter: zoneIDFilter,
|
||||
@ -159,9 +160,10 @@ func main() {
|
||||
ClientToken: cfg.AkamaiClientToken,
|
||||
ClientSecret: cfg.AkamaiClientSecret,
|
||||
AccessToken: cfg.AkamaiAccessToken,
|
||||
EdgercPath: cfg.AkamaiEdgercPath,
|
||||
EdgercSection: cfg.AkamaiEdgercSection,
|
||||
DryRun: cfg.DryRun,
|
||||
},
|
||||
)
|
||||
}, nil)
|
||||
case "alibabacloud":
|
||||
p, err = alibabacloud.NewAlibabaCloudProvider(cfg.AlibabaCloudConfigFile, domainFilter, zoneIDFilter, cfg.AlibabaCloudZoneType, cfg.DryRun)
|
||||
case "aws":
|
||||
@ -191,7 +193,7 @@ func main() {
|
||||
case "azure-dns", "azure":
|
||||
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
|
||||
case "azure-private-dns":
|
||||
p, err = azure.NewAzurePrivateDNSProvider(domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureSubscriptionID, cfg.DryRun)
|
||||
p, err = azure.NewAzurePrivateDNSProvider(cfg.AzureConfigFile, domainFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
|
||||
case "vinyldns":
|
||||
p, err = vinyldns.NewVinylDNSProvider(domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
case "vultr":
|
||||
@ -281,7 +283,7 @@ func main() {
|
||||
p, err = oci.NewOCIProvider(*config, domainFilter, zoneIDFilter, cfg.DryRun)
|
||||
}
|
||||
case "rfc2136":
|
||||
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, nil)
|
||||
p, err = rfc2136.NewRfc2136Provider(cfg.RFC2136Host, cfg.RFC2136Port, cfg.RFC2136Zone, cfg.RFC2136Insecure, cfg.RFC2136TSIGKeyName, cfg.RFC2136TSIGSecret, cfg.RFC2136TSIGSecretAlg, cfg.RFC2136TAXFR, domainFilter, cfg.DryRun, cfg.RFC2136MinTTL, cfg.RFC2136GSSTSIG, cfg.RFC2136KerberosUsername, cfg.RFC2136KerberosPassword, nil)
|
||||
case "ns1":
|
||||
p, err = ns1.NewNS1Provider(
|
||||
ns1.NS1Config{
|
||||
@ -297,6 +299,8 @@ func main() {
|
||||
p, err = transip.NewTransIPProvider(cfg.TransIPAccountName, cfg.TransIPPrivateKeyFile, domainFilter, cfg.DryRun)
|
||||
case "scaleway":
|
||||
p, err = scaleway.NewScalewayProvider(ctx, domainFilter, cfg.DryRun)
|
||||
case "godaddy":
|
||||
p, err = godaddy.NewGoDaddyProvider(ctx, domainFilter, cfg.GoDaddyTTL, cfg.GoDaddyAPIKey, cfg.GoDaddySecretKey, cfg.GoDaddyOTE, cfg.DryRun)
|
||||
default:
|
||||
log.Fatalf("unknown dns provider: %s", cfg.Provider)
|
||||
}
|
||||
@ -331,6 +335,7 @@ func main() {
|
||||
Policy: policy,
|
||||
Interval: cfg.Interval,
|
||||
DomainFilter: domainFilter,
|
||||
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
|
||||
}
|
||||
|
||||
if cfg.Once {
|
||||
|
@ -22,6 +22,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
|
||||
"github.com/alecthomas/kingpin"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
@ -88,6 +90,8 @@ type Config struct {
|
||||
AkamaiClientToken string
|
||||
AkamaiClientSecret string
|
||||
AkamaiAccessToken string
|
||||
AkamaiEdgercPath string
|
||||
AkamaiEdgercSection string
|
||||
InfobloxGridHost string
|
||||
InfobloxWapiPort int
|
||||
InfobloxWapiUsername string
|
||||
@ -137,6 +141,9 @@ type Config struct {
|
||||
RFC2136Port int
|
||||
RFC2136Zone string
|
||||
RFC2136Insecure bool
|
||||
RFC2136GSSTSIG bool
|
||||
RFC2136KerberosUsername string
|
||||
RFC2136KerberosPassword string
|
||||
RFC2136TSIGKeyName string
|
||||
RFC2136TSIGSecret string `secure:"yes"`
|
||||
RFC2136TSIGSecretAlg string
|
||||
@ -148,6 +155,11 @@ type Config struct {
|
||||
TransIPAccountName string
|
||||
TransIPPrivateKeyFile string
|
||||
DigitalOceanAPIPageSize int
|
||||
ManagedDNSRecordTypes []string
|
||||
GoDaddyAPIKey string `secure:"yes"`
|
||||
GoDaddySecretKey string `secure:"yes"`
|
||||
GoDaddyTTL int64
|
||||
GoDaddyOTE bool
|
||||
}
|
||||
|
||||
var defaultConfig = &Config{
|
||||
@ -195,6 +207,8 @@ var defaultConfig = &Config{
|
||||
AkamaiClientToken: "",
|
||||
AkamaiClientSecret: "",
|
||||
AkamaiAccessToken: "",
|
||||
AkamaiEdgercSection: "",
|
||||
AkamaiEdgercPath: "",
|
||||
InfobloxGridHost: "",
|
||||
InfobloxWapiPort: 443,
|
||||
InfobloxWapiUsername: "admin",
|
||||
@ -240,6 +254,9 @@ var defaultConfig = &Config{
|
||||
RFC2136Port: 0,
|
||||
RFC2136Zone: "",
|
||||
RFC2136Insecure: false,
|
||||
RFC2136GSSTSIG: false,
|
||||
RFC2136KerberosUsername: "",
|
||||
RFC2136KerberosPassword: "",
|
||||
RFC2136TSIGKeyName: "",
|
||||
RFC2136TSIGSecret: "",
|
||||
RFC2136TSIGSecretAlg: "",
|
||||
@ -250,6 +267,11 @@ var defaultConfig = &Config{
|
||||
TransIPAccountName: "",
|
||||
TransIPPrivateKeyFile: "",
|
||||
DigitalOceanAPIPageSize: 50,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
GoDaddyAPIKey: "",
|
||||
GoDaddySecretKey: "",
|
||||
GoDaddyTTL: 600,
|
||||
GoDaddyOTE: false,
|
||||
}
|
||||
|
||||
// NewConfig returns new Config object
|
||||
@ -310,7 +332,7 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
|
||||
|
||||
// Flags related to processing sources
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup,openshift-route)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route")
|
||||
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host")
|
||||
|
||||
app.Flag("namespace", "Limit sources of endpoints to a specific namespace (default: all namespaces)").Default(defaultConfig.Namespace).StringVar(&cfg.Namespace)
|
||||
app.Flag("annotation-filter", "Filter sources managed by external-dns via annotation using label selector semantics (default: all sources)").Default(defaultConfig.AnnotationFilter).StringVar(&cfg.AnnotationFilter)
|
||||
@ -327,9 +349,10 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("crd-source-apiversion", "API version of the CRD for crd source, e.g. `externaldns.k8s.io/v1alpha1`, valid only when using crd source").Default(defaultConfig.CRDSourceAPIVersion).StringVar(&cfg.CRDSourceAPIVersion)
|
||||
app.Flag("crd-source-kind", "Kind of the CRD for the crd source in API group and version specified by crd-source-apiversion").Default(defaultConfig.CRDSourceKind).StringVar(&cfg.CRDSourceKind)
|
||||
app.Flag("service-type-filter", "The service types to take care about (default: all, expected: ClusterIP, NodePort, LoadBalancer or ExternalName)").StringsVar(&cfg.ServiceTypeFilter)
|
||||
app.Flag("managed-record-types", "Comma separated list of record types to manage (default: A, CNAME) (supported records: CNAME, A, NS").Default("A", "CNAME").StringsVar(&cfg.ManagedDNSRecordTypes)
|
||||
|
||||
// Flags related to providers
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns")
|
||||
app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: aws, aws-sd, godaddy, google, azure, azure-dns, azure-private-dns, cloudflare, rcodezero, digitalocean, hetzner, dnsimple, akamai, infoblox, dyn, designate, coredns, skydns, inmemory, ovh, pdns, oci, exoscale, linode, rfc2136, ns1, transip, vinyldns, rdns, scaleway, vultr, ultradns)").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, "aws", "aws-sd", "google", "azure", "azure-dns", "hetzner", "azure-private-dns", "alibabacloud", "cloudflare", "rcodezero", "digitalocean", "dnsimple", "akamai", "infoblox", "dyn", "designate", "coredns", "skydns", "inmemory", "ovh", "pdns", "oci", "exoscale", "linode", "rfc2136", "ns1", "transip", "vinyldns", "rdns", "scaleway", "vultr", "ultradns", "godaddy")
|
||||
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("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains)
|
||||
app.Flag("zone-name-filter", "Filter target zones by zone domain (For now, only AzureDNS provider is using this flag); specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.ZoneNameFilter)
|
||||
@ -355,10 +378,12 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("cloudflare-proxied", "When using the Cloudflare provider, specify if the proxy mode must be enabled (default: disabled)").BoolVar(&cfg.CloudflareProxied)
|
||||
app.Flag("cloudflare-zones-per-page", "When using the Cloudflare provider, specify how many zones per page listed, max. possible 50 (default: 50)").Default(strconv.Itoa(defaultConfig.CloudflareZonesPerPage)).IntVar(&cfg.CloudflareZonesPerPage)
|
||||
app.Flag("coredns-prefix", "When using the CoreDNS provider, specify the prefix name").Default(defaultConfig.CoreDNSPrefix).StringVar(&cfg.CoreDNSPrefix)
|
||||
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
|
||||
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
|
||||
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
|
||||
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
|
||||
app.Flag("akamai-serviceconsumerdomain", "When using the Akamai provider, specify the base URL (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiServiceConsumerDomain).StringVar(&cfg.AkamaiServiceConsumerDomain)
|
||||
app.Flag("akamai-client-token", "When using the Akamai provider, specify the client token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientToken).StringVar(&cfg.AkamaiClientToken)
|
||||
app.Flag("akamai-client-secret", "When using the Akamai provider, specify the client secret (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiClientSecret).StringVar(&cfg.AkamaiClientSecret)
|
||||
app.Flag("akamai-access-token", "When using the Akamai provider, specify the access token (required when --provider=akamai and edgerc-path not specified)").Default(defaultConfig.AkamaiAccessToken).StringVar(&cfg.AkamaiAccessToken)
|
||||
app.Flag("akamai-edgerc-path", "When using the Akamai provider, specify the .edgerc file path. Path must be reachable form invocation environment. (required when --provider=akamai and *-token, secret serviceconsumerdomain not specified)").Default(defaultConfig.AkamaiEdgercPath).StringVar(&cfg.AkamaiEdgercPath)
|
||||
app.Flag("akamai-edgerc-section", "When using the Akamai provider, specify the .edgerc file path (Optional when edgerc-path is specified)").Default(defaultConfig.AkamaiEdgercSection).StringVar(&cfg.AkamaiEdgercSection)
|
||||
app.Flag("infoblox-grid-host", "When using the Infoblox provider, specify the Grid Manager host (required when --provider=infoblox)").Default(defaultConfig.InfobloxGridHost).StringVar(&cfg.InfobloxGridHost)
|
||||
app.Flag("infoblox-wapi-port", "When using the Infoblox provider, specify the WAPI port (default: 443)").Default(strconv.Itoa(defaultConfig.InfobloxWapiPort)).IntVar(&cfg.InfobloxWapiPort)
|
||||
app.Flag("infoblox-wapi-username", "When using the Infoblox provider, specify the WAPI username (default: admin)").Default(defaultConfig.InfobloxWapiUsername).StringVar(&cfg.InfobloxWapiUsername)
|
||||
@ -383,6 +408,11 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("ns1-ignoressl", "When using the NS1 provider, specify whether to verify the SSL certificate (default: false)").Default(strconv.FormatBool(defaultConfig.NS1IgnoreSSL)).BoolVar(&cfg.NS1IgnoreSSL)
|
||||
app.Flag("ns1-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.NS1MinTTLSeconds)
|
||||
app.Flag("digitalocean-api-page-size", "Configure the page size used when querying the DigitalOcean API.").Default(strconv.Itoa(defaultConfig.DigitalOceanAPIPageSize)).IntVar(&cfg.DigitalOceanAPIPageSize)
|
||||
// GoDaddy flags
|
||||
app.Flag("godaddy-api-key", "When using the GoDaddy provider, specify the API Key (required when --provider=godaddy)").Default(defaultConfig.GoDaddyAPIKey).StringVar(&cfg.GoDaddyAPIKey)
|
||||
app.Flag("godaddy-api-secret", "When using the GoDaddy provider, specify the API secret (required when --provider=godaddy)").Default(defaultConfig.GoDaddySecretKey).StringVar(&cfg.GoDaddySecretKey)
|
||||
app.Flag("godaddy-api-ttl", "TTL (in seconds) for records. This value will be used if the provided TTL for a service/ingress is not provided.").Int64Var(&cfg.GoDaddyTTL)
|
||||
app.Flag("godaddy-api-ote", "When using the GoDaddy provider, use OTE api (optional, default: false, when --provider=godaddy)").BoolVar(&cfg.GoDaddyOTE)
|
||||
|
||||
// Flags related to TLS communication
|
||||
app.Flag("tls-ca", "When using TLS communication, the path to the certificate authority to verify server communications (optionally specify --tls-client-cert for two-way TLS)").Default(defaultConfig.TLSCA).StringVar(&cfg.TLSCA)
|
||||
@ -403,6 +433,9 @@ func (cfg *Config) ParseFlags(args []string) error {
|
||||
app.Flag("rfc2136-tsig-secret-alg", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").Default(defaultConfig.RFC2136TSIGSecretAlg).StringVar(&cfg.RFC2136TSIGSecretAlg)
|
||||
app.Flag("rfc2136-tsig-axfr", "When using the RFC2136 provider, specify the TSIG (base64) value to attached to DNS messages (required when --rfc2136-insecure=false)").BoolVar(&cfg.RFC2136TAXFR)
|
||||
app.Flag("rfc2136-min-ttl", "When using the RFC2136 provider, specify minimal TTL (in duration format) for records. This value will be used if the provided TTL for a service/ingress is lower than this").Default(defaultConfig.RFC2136MinTTL.String()).DurationVar(&cfg.RFC2136MinTTL)
|
||||
app.Flag("rfc2136-gss-tsig", "When using the RFC2136 provider, specify whether to use secure updates with GSS-TSIG using Kerberos (default: false, requires --rfc2136-kerberos-username and rfc2136-kerberos-password)").Default(strconv.FormatBool(defaultConfig.RFC2136GSSTSIG)).BoolVar(&cfg.RFC2136GSSTSIG)
|
||||
app.Flag("rfc2136-kerberos-username", "When using the RFC2136 provider with GSS-TSIG, specify the username of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosUsername).StringVar(&cfg.RFC2136KerberosUsername)
|
||||
app.Flag("rfc2136-kerberos-password", "When using the RFC2136 provider with GSS-TSIG, specify the password of the user with permissions to update DNS records (required when --rfc2136-gss-tsig=true)").Default(defaultConfig.RFC2136KerberosPassword).StringVar(&cfg.RFC2136KerberosPassword)
|
||||
|
||||
// Flags related to TransIP provider
|
||||
app.Flag("transip-account", "When using the TransIP provider, specify the account name (required when --provider=transip)").Default(defaultConfig.TransIPAccountName).StringVar(&cfg.TransIPAccountName)
|
||||
|
@ -22,6 +22,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@ -66,6 +68,8 @@ var (
|
||||
AkamaiClientToken: "",
|
||||
AkamaiClientSecret: "",
|
||||
AkamaiAccessToken: "",
|
||||
AkamaiEdgercPath: "",
|
||||
AkamaiEdgercSection: "",
|
||||
InfobloxGridHost: "",
|
||||
InfobloxWapiPort: 443,
|
||||
InfobloxWapiUsername: "admin",
|
||||
@ -102,6 +106,7 @@ var (
|
||||
TransIPAccountName: "",
|
||||
TransIPPrivateKeyFile: "",
|
||||
DigitalOceanAPIPageSize: 50,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
overriddenConfig = &Config{
|
||||
@ -144,6 +149,8 @@ var (
|
||||
AkamaiClientToken: "o184671d5307a388180fbf7f11dbdf46",
|
||||
AkamaiClientSecret: "o184671d5307a388180fbf7f11dbdf46",
|
||||
AkamaiAccessToken: "o184671d5307a388180fbf7f11dbdf46",
|
||||
AkamaiEdgercPath: "/home/test/.edgerc",
|
||||
AkamaiEdgercSection: "default",
|
||||
InfobloxGridHost: "127.0.0.1",
|
||||
InfobloxWapiPort: 8443,
|
||||
InfobloxWapiUsername: "infoblox",
|
||||
@ -186,6 +193,7 @@ var (
|
||||
TransIPAccountName: "transip",
|
||||
TransIPPrivateKeyFile: "/path/to/transip.key",
|
||||
DigitalOceanAPIPageSize: 100,
|
||||
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
)
|
||||
|
||||
@ -235,6 +243,8 @@ func TestParseFlags(t *testing.T) {
|
||||
"--akamai-client-token=o184671d5307a388180fbf7f11dbdf46",
|
||||
"--akamai-client-secret=o184671d5307a388180fbf7f11dbdf46",
|
||||
"--akamai-access-token=o184671d5307a388180fbf7f11dbdf46",
|
||||
"--akamai-edgerc-path=/home/test/.edgerc",
|
||||
"--akamai-edgerc-section=default",
|
||||
"--infoblox-grid-host=127.0.0.1",
|
||||
"--infoblox-wapi-port=8443",
|
||||
"--infoblox-wapi-username=infoblox",
|
||||
@ -328,6 +338,8 @@ func TestParseFlags(t *testing.T) {
|
||||
"EXTERNAL_DNS_AKAMAI_CLIENT_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
|
||||
"EXTERNAL_DNS_AKAMAI_CLIENT_SECRET": "o184671d5307a388180fbf7f11dbdf46",
|
||||
"EXTERNAL_DNS_AKAMAI_ACCESS_TOKEN": "o184671d5307a388180fbf7f11dbdf46",
|
||||
"EXTERNAL_DNS_AKAMAI_EDGERC_PATH": "/home/test/.edgerc",
|
||||
"EXTERNAL_DNS_AKAMAI_EDGERC_SECTION": "default",
|
||||
"EXTERNAL_DNS_INFOBLOX_GRID_HOST": "127.0.0.1",
|
||||
"EXTERNAL_DNS_INFOBLOX_WAPI_PORT": "8443",
|
||||
"EXTERNAL_DNS_INFOBLOX_WAPI_USERNAME": "infoblox",
|
||||
|
@ -45,16 +45,16 @@ func ValidateConfig(cfg *externaldns.Config) error {
|
||||
|
||||
// Akamai provider specific validations
|
||||
if cfg.Provider == "akamai" {
|
||||
if cfg.AkamaiServiceConsumerDomain == "" {
|
||||
if cfg.AkamaiServiceConsumerDomain == "" && cfg.AkamaiEdgercPath != "" {
|
||||
return errors.New("no Akamai ServiceConsumerDomain specified")
|
||||
}
|
||||
if cfg.AkamaiClientToken == "" {
|
||||
if cfg.AkamaiClientToken == "" && cfg.AkamaiEdgercPath != "" {
|
||||
return errors.New("no Akamai client token specified")
|
||||
}
|
||||
if cfg.AkamaiClientSecret == "" {
|
||||
if cfg.AkamaiClientSecret == "" && cfg.AkamaiEdgercPath != "" {
|
||||
return errors.New("no Akamai client secret specified")
|
||||
}
|
||||
if cfg.AkamaiAccessToken == "" {
|
||||
if cfg.AkamaiAccessToken == "" && cfg.AkamaiEdgercPath != "" {
|
||||
return errors.New("no Akamai access token specified")
|
||||
}
|
||||
}
|
||||
@ -86,6 +86,16 @@ func ValidateConfig(cfg *externaldns.Config) error {
|
||||
if cfg.RFC2136MinTTL < 0 {
|
||||
return errors.New("TTL specified for rfc2136 is negative")
|
||||
}
|
||||
|
||||
if cfg.RFC2136Insecure && cfg.RFC2136GSSTSIG {
|
||||
return errors.New("--rfc2136-insecure and --rfc2136-gss-tsig are mutually exclusive arguments")
|
||||
}
|
||||
|
||||
if cfg.RFC2136GSSTSIG {
|
||||
if cfg.RFC2136KerberosPassword == "" || cfg.RFC2136KerberosUsername == "" {
|
||||
return errors.New("--rfc2136-kerberos-username and --rfc2136-kerberos-password both required when specifying --rfc2136-gss-tsig option")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.IgnoreHostnameAnnotation && cfg.FQDNTemplate == "" {
|
||||
|
@ -150,3 +150,63 @@ func TestValidateGoodRfc2136Config(t *testing.T) {
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestValidateBadRfc2136GssTsigConfig(t *testing.T) {
|
||||
var invalidRfc2136GssTsigConfigs = []*externaldns.Config{
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "",
|
||||
RFC2136MinTTL: 3600,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136KerberosUsername: "",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
},
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136Insecure: true,
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range invalidRfc2136GssTsigConfigs {
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGoodRfc2136GssTsigConfig(t *testing.T) {
|
||||
var validRfc2136GssTsigConfigs = []*externaldns.Config{
|
||||
{
|
||||
LogFormat: "json",
|
||||
Sources: []string{"test-source"},
|
||||
Provider: "rfc2136",
|
||||
RFC2136GSSTSIG: true,
|
||||
RFC2136Insecure: false,
|
||||
RFC2136KerberosUsername: "test-user",
|
||||
RFC2136KerberosPassword: "test-pass",
|
||||
RFC2136MinTTL: 3600,
|
||||
},
|
||||
}
|
||||
|
||||
for _, cfg := range validRfc2136GssTsigConfigs {
|
||||
err := ValidateConfig(cfg)
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
}
|
||||
|
26
plan/plan.go
26
plan/plan.go
@ -43,6 +43,8 @@ type Plan struct {
|
||||
DomainFilter endpoint.DomainFilter
|
||||
// Property comparator compares custom properties of providers
|
||||
PropertyComparator PropertyComparator
|
||||
// DNS record types that will be considered for management
|
||||
ManagedRecords []string
|
||||
}
|
||||
|
||||
// Changes holds lists of actions to be executed by dns providers
|
||||
@ -119,10 +121,10 @@ func (t planTable) addCandidate(e *endpoint.Endpoint) {
|
||||
func (p *Plan) Calculate() *Plan {
|
||||
t := newPlanTable()
|
||||
|
||||
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter) {
|
||||
for _, current := range filterRecordsForPlan(p.Current, p.DomainFilter, p.ManagedRecords) {
|
||||
t.addCurrent(current)
|
||||
}
|
||||
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter) {
|
||||
for _, desired := range filterRecordsForPlan(p.Desired, p.DomainFilter, p.ManagedRecords) {
|
||||
t.addCandidate(desired)
|
||||
}
|
||||
|
||||
@ -158,6 +160,7 @@ func (p *Plan) Calculate() *Plan {
|
||||
Current: p.Current,
|
||||
Desired: p.Desired,
|
||||
Changes: changes,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
return plan
|
||||
@ -224,7 +227,7 @@ func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint)
|
||||
// Per RFC 1034, CNAME records conflict with all other records - it is the
|
||||
// only record with this property. The behavior of the planner may need to be
|
||||
// made more sophisticated to codify this.
|
||||
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter) []*endpoint.Endpoint {
|
||||
func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.DomainFilter, managedRecords []string) []*endpoint.Endpoint {
|
||||
filtered := []*endpoint.Endpoint{}
|
||||
|
||||
for _, record := range records {
|
||||
@ -232,14 +235,8 @@ func filterRecordsForPlan(records []*endpoint.Endpoint, domainFilter endpoint.Do
|
||||
if !domainFilter.Match(record.DNSName) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Explicitly specify which records we want to use for planning.
|
||||
// TODO: Add AAAA records as well when they are supported.
|
||||
switch record.RecordType {
|
||||
case endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS:
|
||||
if isManagedRecord(record.RecordType, managedRecords) {
|
||||
filtered = append(filtered, record)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@ -280,3 +277,12 @@ func CompareBoolean(defaultValue bool, name, current, previous string) bool {
|
||||
|
||||
return v1 == v2
|
||||
}
|
||||
|
||||
func isManagedRecord(record string, managedRecords []string) bool {
|
||||
for _, r := range managedRecords {
|
||||
if record == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -217,6 +217,7 @@ func (suite *PlanTestSuite) TestSyncFirstRound() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -238,6 +239,7 @@ func (suite *PlanTestSuite) TestSyncSecondRound() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -259,6 +261,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -280,6 +283,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -301,6 +305,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificChange() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -325,6 +330,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithProviderSpecificDefaultFalse(
|
||||
PropertyComparator: func(name, previous, current string) bool {
|
||||
return CompareBoolean(false, name, previous, current)
|
||||
},
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -380,6 +386,7 @@ func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -422,6 +429,7 @@ func (suite *PlanTestSuite) TestDifferentTypes() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -443,6 +451,7 @@ func (suite *PlanTestSuite) TestIgnoreTXT() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -485,6 +494,7 @@ func (suite *PlanTestSuite) TestRemoveEndpoint() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -506,6 +516,7 @@ func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
|
||||
Policies: []Policy{&UpsertOnlyPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -528,6 +539,7 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -551,6 +563,7 @@ func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -573,6 +586,7 @@ func (suite *PlanTestSuite) TestMultipleRecordsSameNameDifferentSetIdentifier()
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -595,6 +609,7 @@ func (suite *PlanTestSuite) TestSetIdentifierUpdateCreatesAndDeletes() {
|
||||
Policies: []Policy{&SyncPolicy{}},
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -618,6 +633,7 @@ func (suite *PlanTestSuite) TestDomainFiltersInitial() {
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -641,6 +657,7 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
DomainFilter: endpoint.NewDomainFilterWithExclusions([]string{"domain.tld"}, []string{"ex.domain.tld"}),
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
changes := p.Calculate().Changes
|
||||
@ -838,6 +855,7 @@ func TestShouldUpdateProviderSpecific(tt *testing.T) {
|
||||
Current: []*endpoint.Endpoint{test.current},
|
||||
Desired: []*endpoint.Endpoint{test.desired},
|
||||
PropertyComparator: test.propertyComparator,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
b := plan.shouldUpdateProviderSpecific(test.desired, test.current)
|
||||
assert.Equal(t, test.shouldUpdate, b)
|
||||
|
@ -17,15 +17,13 @@ limitations under the License.
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
c "github.com/akamai/AkamaiOPEN-edgegrid-golang/client-v1"
|
||||
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
|
||||
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@ -34,22 +32,23 @@ import (
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
type akamaiClient interface {
|
||||
NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error)
|
||||
Do(config edgegrid.Config, req *http.Request) (*http.Response, error)
|
||||
const (
|
||||
// Default Record TTL
|
||||
edgeDNSRecordTTL = 600
|
||||
maxUint = ^uint(0)
|
||||
maxInt = int(maxUint >> 1)
|
||||
)
|
||||
|
||||
// edgeDNSClient is a proxy interface of the Akamai edgegrid configdns-v2 package that can be stubbed for testing.
|
||||
type AkamaiDNSService interface {
|
||||
ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error)
|
||||
GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error)
|
||||
GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error)
|
||||
DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error
|
||||
UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error
|
||||
CreateRecordsets(recordsets *dns.Recordsets, zone string, recLock bool) error
|
||||
}
|
||||
|
||||
type akamaiOpenClient struct{}
|
||||
|
||||
func (*akamaiOpenClient) NewRequest(config edgegrid.Config, method, path string, body io.Reader) (*http.Request, error) {
|
||||
return c.NewRequest(config, method, path, body)
|
||||
}
|
||||
|
||||
func (*akamaiOpenClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
|
||||
return c.Do(config, req)
|
||||
}
|
||||
|
||||
// AkamaiConfig clarifies the method signature
|
||||
type AkamaiConfig struct {
|
||||
DomainFilter endpoint.DomainFilter
|
||||
ZoneIDFilter provider.ZoneIDFilter
|
||||
@ -57,17 +56,25 @@ type AkamaiConfig struct {
|
||||
ClientToken string
|
||||
ClientSecret string
|
||||
AccessToken string
|
||||
EdgercPath string
|
||||
EdgercSection string
|
||||
MaxBody int
|
||||
AccountKey string
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
// AkamaiProvider implements the DNS provider for Akamai.
|
||||
type AkamaiProvider struct {
|
||||
provider.BaseProvider
|
||||
// Edgedns zones to filter on
|
||||
domainFilter endpoint.DomainFilter
|
||||
// Contract Ids to filter on
|
||||
zoneIDFilter provider.ZoneIDFilter
|
||||
config edgegrid.Config
|
||||
// Edgegrid library configuration
|
||||
config *edgegrid.Config
|
||||
dryRun bool
|
||||
client akamaiClient
|
||||
// Defines client. Allows for mocking.
|
||||
client AkamaiDNSService
|
||||
}
|
||||
|
||||
type akamaiZones struct {
|
||||
@ -79,84 +86,124 @@ type akamaiZone struct {
|
||||
Zone string `json:"zone"`
|
||||
}
|
||||
|
||||
type akamaiRecordsets struct {
|
||||
Recordsets []akamaiRecord `json:"recordsets"`
|
||||
}
|
||||
|
||||
type akamaiRecord struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Rdata []interface{} `json:"rdata"`
|
||||
}
|
||||
|
||||
// NewAkamaiProvider initializes a new Akamai DNS based Provider.
|
||||
func NewAkamaiProvider(akamaiConfig AkamaiConfig) *AkamaiProvider {
|
||||
edgeGridConfig := edgegrid.Config{
|
||||
func NewAkamaiProvider(akamaiConfig AkamaiConfig, akaService AkamaiDNSService) (provider.Provider, error) {
|
||||
var edgeGridConfig edgegrid.Config
|
||||
|
||||
/*
|
||||
log.Debugf("Host: %s", akamaiConfig.ServiceConsumerDomain)
|
||||
log.Debugf("ClientToken: %s", akamaiConfig.ClientToken)
|
||||
log.Debugf("ClientSecret: %s", akamaiConfig.ClientSecret)
|
||||
log.Debugf("AccessToken: %s", akamaiConfig.AccessToken)
|
||||
log.Debugf("EdgePath: %s", akamaiConfig.EdgercPath)
|
||||
log.Debugf("EdgeSection: %s", akamaiConfig.EdgercSection)
|
||||
*/
|
||||
// environment overrides edgerc file but config needs to be complete
|
||||
if akamaiConfig.ServiceConsumerDomain == "" || akamaiConfig.ClientToken == "" || akamaiConfig.ClientSecret == "" || akamaiConfig.AccessToken == "" {
|
||||
// Kubernetes config incomplete or non existent. Can't mix and match.
|
||||
// Look for Akamai environment or .edgerd creds
|
||||
var err error
|
||||
edgeGridConfig, err = edgegrid.Init(akamaiConfig.EdgercPath, akamaiConfig.EdgercSection) // use default .edgerc location and section
|
||||
if err != nil {
|
||||
log.Errorf("Edgegrid Init Failed")
|
||||
return &AkamaiProvider{}, err // return empty provider for backward compatibility
|
||||
}
|
||||
edgeGridConfig.HeaderToSign = append(edgeGridConfig.HeaderToSign, "X-External-DNS")
|
||||
} else {
|
||||
// Use external-dns config
|
||||
edgeGridConfig = edgegrid.Config{
|
||||
Host: akamaiConfig.ServiceConsumerDomain,
|
||||
ClientToken: akamaiConfig.ClientToken,
|
||||
ClientSecret: akamaiConfig.ClientSecret,
|
||||
AccessToken: akamaiConfig.AccessToken,
|
||||
MaxBody: 1024,
|
||||
MaxBody: 131072, // same default val as used by Edgegrid
|
||||
HeaderToSign: []string{
|
||||
"X-External-DNS",
|
||||
},
|
||||
Debug: false,
|
||||
}
|
||||
// Check for edgegrid overrides
|
||||
if envval, ok := os.LookupEnv("AKAMAI_MAX_BODY"); ok {
|
||||
if i, err := strconv.Atoi(envval); err == nil {
|
||||
edgeGridConfig.MaxBody = i
|
||||
log.Debugf("Edgegrid maxbody set to %s", envval)
|
||||
}
|
||||
}
|
||||
if envval, ok := os.LookupEnv("AKAMAI_ACCOUNT_KEY"); ok {
|
||||
edgeGridConfig.AccountKey = envval
|
||||
log.Debugf("Edgegrid applying account key %s", envval)
|
||||
}
|
||||
if envval, ok := os.LookupEnv("AKAMAI_DEBUG"); ok {
|
||||
if dbgval, err := strconv.ParseBool(envval); err == nil {
|
||||
edgeGridConfig.Debug = dbgval
|
||||
log.Debugf("Edgegrid debug set to %s", envval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider := &AkamaiProvider{
|
||||
domainFilter: akamaiConfig.DomainFilter,
|
||||
zoneIDFilter: akamaiConfig.ZoneIDFilter,
|
||||
config: edgeGridConfig,
|
||||
config: &edgeGridConfig,
|
||||
dryRun: akamaiConfig.DryRun,
|
||||
client: &akamaiOpenClient{},
|
||||
}
|
||||
return provider
|
||||
if akaService != nil {
|
||||
log.Debugf("Using STUB")
|
||||
provider.client = akaService
|
||||
} else {
|
||||
provider.client = provider
|
||||
}
|
||||
|
||||
// Init library for direct endpoint calls
|
||||
dns.Init(edgeGridConfig)
|
||||
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
func (p *AkamaiProvider) request(method, path string, body io.Reader) (*http.Response, error) {
|
||||
req, err := p.client.NewRequest(p.config, method, fmt.Sprintf("https://%s/%s", p.config.Host, path), body)
|
||||
if err != nil {
|
||||
log.Errorf("Akamai client failed to prepare the request")
|
||||
return nil, err
|
||||
}
|
||||
resp, err := p.client.Do(p.config, req)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Akamai client failed to do the request")
|
||||
return nil, err
|
||||
}
|
||||
if !c.IsSuccess(resp) {
|
||||
return nil, c.NewAPIError(resp)
|
||||
}
|
||||
|
||||
return resp, err
|
||||
func (p AkamaiProvider) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
|
||||
return dns.ListZones(queryArgs)
|
||||
}
|
||||
|
||||
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzones
|
||||
func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
|
||||
log.Debugf("Trying to fetch zones from Akamai")
|
||||
resp, err := p.request("GET", "config-dns/v2/zones?showAll=true&types=primary%2Csecondary", nil)
|
||||
func (p AkamaiProvider) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
|
||||
return dns.GetRecordsets(zone, queryArgs)
|
||||
}
|
||||
|
||||
func (p AkamaiProvider) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
|
||||
return recordsets.Save(zone, reclock)
|
||||
}
|
||||
|
||||
func (p AkamaiProvider) GetRecord(zone string, name string, recordtype string) (*dns.RecordBody, error) {
|
||||
return dns.GetRecord(zone, name, recordtype)
|
||||
}
|
||||
|
||||
func (p AkamaiProvider) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
|
||||
return record.Delete(zone, recLock)
|
||||
}
|
||||
|
||||
func (p AkamaiProvider) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
|
||||
return record.Update(zone, recLock)
|
||||
}
|
||||
|
||||
// Fetch zones using Edgegrid DNS v2 API
|
||||
func (p AkamaiProvider) fetchZones() (akamaiZones, error) {
|
||||
filteredZones := akamaiZones{Zones: make([]akamaiZone, 0)}
|
||||
queryArgs := dns.ZoneListQueryArgs{Types: "primary", ShowAll: true}
|
||||
// filter based on contractIds
|
||||
if len(p.zoneIDFilter.ZoneIDs) > 0 {
|
||||
queryArgs.ContractIds = strings.Join(p.zoneIDFilter.ZoneIDs, ",")
|
||||
}
|
||||
resp, err := p.client.ListZones(queryArgs) // retrieve all primary zones filtered by contract ids
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to fetch zones from Akamai")
|
||||
return zones, err
|
||||
return filteredZones, err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&zones)
|
||||
if err != nil {
|
||||
log.Errorf("Could not decode json response from Akamai on zone request")
|
||||
return zones, err
|
||||
for _, zone := range resp.Zones {
|
||||
if p.domainFilter.Match(zone.Zone) || !p.domainFilter.IsConfigured() {
|
||||
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractId, Zone: zone.Zone})
|
||||
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractId)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
filteredZones := akamaiZones{}
|
||||
for _, zone := range zones.Zones {
|
||||
if !p.zoneIDFilter.Match(zone.ContractID) {
|
||||
log.Debugf("Skipping zone: '%s' with ZoneID: '%s', it does not match against ZoneID filters", zone.Zone, zone.ContractID)
|
||||
continue
|
||||
}
|
||||
filteredZones.Zones = append(filteredZones.Zones, akamaiZone{ContractID: zone.ContractID, Zone: zone.Zone})
|
||||
log.Debugf("Fetched zone: '%s' (ZoneID: %s)", zone.Zone, zone.ContractID)
|
||||
}
|
||||
lenFilteredZones := len(filteredZones.Zones)
|
||||
if lenFilteredZones == 0 {
|
||||
@ -168,53 +215,39 @@ func (p *AkamaiProvider) fetchZones() (zones akamaiZones, err error) {
|
||||
return filteredZones, nil
|
||||
}
|
||||
|
||||
//Look here for endpoint documentation -> https://developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html#getzonerecordsets
|
||||
func (p *AkamaiProvider) fetchRecordSet(zone string) (recordSet akamaiRecordsets, err error) {
|
||||
log.Debugf("Trying to fetch endpoints for zone: '%s' from Akamai", zone)
|
||||
resp, err := p.request("GET", "config-dns/v2/zones/"+zone+"/recordsets?showAll=true&types=A%2CTXT%2CCNAME", nil)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to fetch records from Akamai for zone: '%s'", zone)
|
||||
return recordSet, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&recordSet)
|
||||
if err != nil {
|
||||
log.Errorf("Could not decode json response from Akamai for zone: '%s' on request", zone)
|
||||
return recordSet, err
|
||||
}
|
||||
|
||||
return recordSet, nil
|
||||
}
|
||||
|
||||
//Records returns the list of records in a given zone.
|
||||
func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
||||
zones, err := p.fetchZones()
|
||||
func (p AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoint, err error) {
|
||||
zones, err := p.fetchZones() // returns a filtered set of zones
|
||||
if err != nil {
|
||||
log.Warnf("No zones to fetch endpoints from!")
|
||||
log.Warnf("Failed to identify target zones! Error: %s", err.Error())
|
||||
return endpoints, err
|
||||
}
|
||||
for _, zone := range zones.Zones {
|
||||
records, err := p.fetchRecordSet(zone.Zone)
|
||||
recordsets, err := p.client.GetRecordsets(zone.Zone, dns.RecordsetQueryArgs{})
|
||||
if err != nil {
|
||||
log.Warnf("No recordsets could be fetched for zone: '%s'!", zone.Zone)
|
||||
log.Errorf("Recordsets retrieval for zone: '%s' failed! %s", zone.Zone, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range records.Recordsets {
|
||||
rdata := make([]string, len(record.Rdata))
|
||||
|
||||
for i, v := range record.Rdata {
|
||||
rdata[i] = v.(string)
|
||||
if len(recordsets.Recordsets) == 0 {
|
||||
log.Warnf("Zone %s contains no recordsets", zone.Zone)
|
||||
}
|
||||
|
||||
if !p.domainFilter.Match(record.Name) {
|
||||
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", record.Name, record.Type)
|
||||
for _, recordset := range recordsets.Recordsets {
|
||||
if !provider.SupportedRecordType(recordset.Type) {
|
||||
log.Debugf("Skipping endpoint DNSName: '%s' RecordType: '%s'. Record type not supported.", recordset.Name, recordset.Type)
|
||||
continue
|
||||
}
|
||||
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint(record.Name, record.Type, rdata...))
|
||||
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", record.Name, record.Type, rdata)
|
||||
if !p.domainFilter.Match(recordset.Name) {
|
||||
log.Debugf("Skipping endpoint. Record name %s doesn't match containing zone %s.", recordset.Name, zone)
|
||||
continue
|
||||
}
|
||||
var temp interface{} = int64(recordset.TTL)
|
||||
var ttl endpoint.TTL = endpoint.TTL(temp.(int64))
|
||||
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(recordset.Name,
|
||||
recordset.Type,
|
||||
ttl,
|
||||
trimTxtRdata(recordset.Rdata, recordset.Type)...))
|
||||
log.Debugf("Fetched endpoint DNSName: '%s' RecordType: '%s' Rdata: '%s')", recordset.Name, recordset.Type, recordset.Rdata)
|
||||
}
|
||||
}
|
||||
lenEndpoints := len(endpoints)
|
||||
@ -222,161 +255,237 @@ func (p *AkamaiProvider) Records(context.Context) (endpoints []*endpoint.Endpoin
|
||||
log.Warnf("No endpoints could be fetched")
|
||||
} else {
|
||||
log.Debugf("Fetched '%d' endpoints from Akamai", lenEndpoints)
|
||||
log.Debugf("Endpoints [%v]", endpoints)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
func (p AkamaiProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
zoneNameIDMapper := provider.ZoneIDName{}
|
||||
zones, err := p.fetchZones()
|
||||
if err != nil {
|
||||
log.Warnf("No zones to fetch endpoints from!")
|
||||
return nil
|
||||
log.Errorf("Failed to fetch zones from Akamai")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, z := range zones.Zones {
|
||||
zoneNameIDMapper[z.Zone] = z.Zone
|
||||
}
|
||||
log.Debugf("Processing zones: [%v]", zoneNameIDMapper)
|
||||
|
||||
_, cf := p.createRecords(zoneNameIDMapper, changes.Create)
|
||||
if !p.dryRun {
|
||||
if len(cf) > 0 {
|
||||
log.Warnf("Not all desired endpoints could be created, retrying next iteration")
|
||||
for _, f := range cf {
|
||||
log.Warnf("Not created was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
|
||||
// Create recordsets
|
||||
log.Debugf("Create Changes requested [%v]", changes.Create)
|
||||
if err := p.createRecordsets(zoneNameIDMapper, changes.Create); err != nil {
|
||||
return err
|
||||
}
|
||||
// Delete recordsets
|
||||
log.Debugf("Delete Changes requested [%v]", changes.Delete)
|
||||
if err := p.deleteRecordsets(zoneNameIDMapper, changes.Delete); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update recordsets
|
||||
log.Debugf("Update Changes requested [%v]", changes.UpdateNew)
|
||||
if err := p.updateNewRecordsets(zoneNameIDMapper, changes.UpdateNew); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that all old endpoints were accounted for
|
||||
revRecs := changes.Delete
|
||||
revRecs = append(revRecs, changes.UpdateNew...)
|
||||
for _, rec := range changes.UpdateOld {
|
||||
found := false
|
||||
for _, r := range revRecs {
|
||||
if rec.DNSName == r.DNSName {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, df := p.deleteRecords(zoneNameIDMapper, changes.Delete)
|
||||
if !p.dryRun {
|
||||
if len(df) > 0 {
|
||||
log.Warnf("Not all endpoints that require deletion could be deleted, retrying next iteration")
|
||||
for _, f := range df {
|
||||
log.Warnf("Not deleted was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, uf := p.updateNewRecords(zoneNameIDMapper, changes.UpdateNew)
|
||||
if !p.dryRun {
|
||||
if len(uf) > 0 {
|
||||
log.Warnf("Not all endpoints that require updating could be updated, retrying next iteration")
|
||||
for _, f := range uf {
|
||||
log.Warnf("Not updated was DNSName: '%s' RecordType: '%s'", f.DNSName, f.RecordType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, uold := range changes.UpdateOld {
|
||||
if !p.dryRun {
|
||||
log.Debugf("UpdateOld (ignored) for DNSName: '%s' RecordType: '%s'", uold.DNSName, uold.RecordType)
|
||||
if !found {
|
||||
log.Warnf("UpdateOld endpoint '%s' is not accounted for in UpdateNew|Delete endpoint list", rec.DNSName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AkamaiProvider) newAkamaiRecord(dnsName, recordType string, targets ...string) *akamaiRecord {
|
||||
cleanTargets := make([]interface{}, len(targets))
|
||||
for idx, target := range targets {
|
||||
cleanTargets[idx] = strings.TrimSuffix(target, ".")
|
||||
}
|
||||
return &akamaiRecord{
|
||||
// Create DNS Recordset
|
||||
func newAkamaiRecordset(dnsName, recordType string, ttl int, targets []string) dns.Recordset {
|
||||
return dns.Recordset{
|
||||
Name: strings.TrimSuffix(dnsName, "."),
|
||||
Rdata: cleanTargets,
|
||||
Rdata: targets,
|
||||
Type: recordType,
|
||||
TTL: 300,
|
||||
TTL: ttl,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *AkamaiProvider) createRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (created []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
|
||||
for _, endpoint := range endpoints {
|
||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
||||
log.Debugf("Skipping creation at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||
continue
|
||||
// cleanTargets preps recordset rdata if necessary for EdgeDNS
|
||||
func cleanTargets(rtype string, targets ...string) []string {
|
||||
log.Debugf("Targets to clean: [%v]", targets)
|
||||
if rtype == "CNAME" || rtype == "SRV" {
|
||||
for idx, target := range targets {
|
||||
targets[idx] = strings.TrimSuffix(target, ".")
|
||||
}
|
||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
||||
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
|
||||
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
|
||||
} else if rtype == "TXT" {
|
||||
for idx, target := range targets {
|
||||
log.Debugf("TXT data to clean: [%s]", target)
|
||||
// need to embed text data in quotes. Make sure not piling on
|
||||
target = strings.Trim(target, "\"")
|
||||
// bug in DNS API with embedded quotes.
|
||||
if strings.Contains(target, "owner") && strings.Contains(target, "\"") {
|
||||
target = strings.ReplaceAll(target, "\"", "`")
|
||||
}
|
||||
targets[idx] = "\"" + target + "\""
|
||||
}
|
||||
}
|
||||
log.Debugf("Clean targets: [%v]", targets)
|
||||
|
||||
log.Infof("Create new Endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||
return targets
|
||||
}
|
||||
|
||||
// trimTxtRdata removes surrounding quotes for received TXT rdata
|
||||
func trimTxtRdata(rdata []string, rtype string) []string {
|
||||
if rtype == "TXT" {
|
||||
for idx, d := range rdata {
|
||||
if strings.Contains(d, "`") {
|
||||
rdata[idx] = strings.ReplaceAll(d, "`", "\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("Trimmed data: [%v]", rdata)
|
||||
|
||||
return rdata
|
||||
}
|
||||
|
||||
func ttlAsInt(src endpoint.TTL) int {
|
||||
var temp interface{} = int64(src)
|
||||
var temp64 = temp.(int64)
|
||||
var ttl int = edgeDNSRecordTTL
|
||||
if temp64 > 0 && temp64 <= int64(maxInt) {
|
||||
ttl = int(temp64)
|
||||
}
|
||||
|
||||
return ttl
|
||||
}
|
||||
|
||||
// Create Endpoint Recordsets
|
||||
func (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
|
||||
if len(endpoints) == 0 {
|
||||
log.Info("No endpoints to create")
|
||||
return nil
|
||||
}
|
||||
|
||||
endpointsByZone := edgeChangesByZone(zoneNameIDMapper, endpoints)
|
||||
|
||||
// create all recordsets by zone
|
||||
for zone, endpoints := range endpointsByZone {
|
||||
recordsets := &dns.Recordsets{Recordsets: make([]dns.Recordset, 0)}
|
||||
for _, endpoint := range endpoints {
|
||||
newrec := newAkamaiRecordset(endpoint.DNSName,
|
||||
endpoint.RecordType,
|
||||
ttlAsInt(endpoint.RecordTTL),
|
||||
cleanTargets(endpoint.RecordType, endpoint.Targets...))
|
||||
logfields := log.Fields{
|
||||
"record": newrec.Name,
|
||||
"type": newrec.Type,
|
||||
"ttl": newrec.TTL,
|
||||
"target": fmt.Sprintf("%v", newrec.Rdata),
|
||||
"zone": zone,
|
||||
}
|
||||
log.WithFields(logfields).Info("Creating recordsets")
|
||||
recordsets.Recordsets = append(recordsets.Recordsets, newrec)
|
||||
}
|
||||
|
||||
if p.dryRun {
|
||||
continue
|
||||
}
|
||||
_, err := p.request("POST", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
|
||||
// Create recordsets all at once
|
||||
err := p.client.CreateRecordsets(recordsets, zone, true)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create Akamai endpoint DNSName: '%s' RecordType: '%s' for zone: '%s'", endpoint.DNSName, endpoint.RecordType, zoneName)
|
||||
failed = append(failed, endpoint)
|
||||
continue
|
||||
}
|
||||
created = append(created, endpoint)
|
||||
} else {
|
||||
log.Warnf("No matching zone for endpoint addition DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
|
||||
failed = append(failed, endpoint)
|
||||
log.Errorf("Failed to create endpoints for DNS zone %s. Error: %s", zone, err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return created, failed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AkamaiProvider) deleteRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (deleted []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
|
||||
func (p AkamaiProvider) deleteRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
|
||||
for _, endpoint := range endpoints {
|
||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
||||
log.Debugf("Skipping deletion at Akamai of endpoint: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
|
||||
if zoneName == "" {
|
||||
log.Debugf("Skipping Akamai Edge DNS endpoint deletion: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||
continue
|
||||
}
|
||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
||||
log.Infof("Deletion at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||
log.Infof("Akamai Edge DNS recordset deletion- Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||
|
||||
if p.dryRun {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := p.request("DELETE", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, nil)
|
||||
recName := strings.TrimSuffix(endpoint.DNSName, ".")
|
||||
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to delete Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
|
||||
failed = append(failed, endpoint)
|
||||
if _, ok := err.(*dns.RecordError); !ok {
|
||||
return fmt.Errorf("endpoint deletion. record validation failed. error: %s", err.Error())
|
||||
}
|
||||
log.Infof("Endpoint deletion. Record doesn't exist. Name: %s, Type: %s", recName, endpoint.RecordType)
|
||||
continue
|
||||
}
|
||||
deleted = append(deleted, endpoint)
|
||||
} else {
|
||||
log.Warnf("No matching zone for endpoint deletion DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
|
||||
failed = append(failed, endpoint)
|
||||
if err := p.client.DeleteRecord(rec, zoneName, true); err != nil {
|
||||
log.Errorf("edge dns recordset deletion failed. error: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
return deleted, failed
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *AkamaiProvider) updateNewRecords(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) (updated []*endpoint.Endpoint, failed []*endpoint.Endpoint) {
|
||||
// Update endpoint recordsets
|
||||
func (p AkamaiProvider) updateNewRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error {
|
||||
for _, endpoint := range endpoints {
|
||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
||||
log.Debugf("Skipping update at Akamai of endpoint DNSName: '%s' RecordType: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||
zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName)
|
||||
if zoneName == "" {
|
||||
log.Debugf("Skipping Akamai Edge DNS endpoint update: '%s' type: '%s', it does not match against Domain filters", endpoint.DNSName, endpoint.RecordType)
|
||||
continue
|
||||
}
|
||||
if zoneName, _ := zoneNameIDMapper.FindZone(endpoint.DNSName); zoneName != "" {
|
||||
akamaiRecord := p.newAkamaiRecord(endpoint.DNSName, endpoint.RecordType, endpoint.Targets...)
|
||||
body, _ := json.MarshalIndent(akamaiRecord, "", " ")
|
||||
|
||||
log.Infof("Updating endpoint at Akamai FastDNS - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||
log.Infof("Akamai Edge DNS recordset update - Zone: '%s', DNSName: '%s', RecordType: '%s', Targets: '%+v'", zoneName, endpoint.DNSName, endpoint.RecordType, endpoint.Targets)
|
||||
|
||||
if p.dryRun {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := p.request("PUT", "config-dns/v2/zones/"+zoneName+"/names/"+endpoint.DNSName+"/types/"+endpoint.RecordType, bytes.NewReader(body))
|
||||
recName := strings.TrimSuffix(endpoint.DNSName, ".")
|
||||
rec, err := p.client.GetRecord(zoneName, recName, endpoint.RecordType)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to update Akamai endpoint DNSName: '%s' for zone: '%s'", endpoint.DNSName, zoneName)
|
||||
failed = append(failed, endpoint)
|
||||
log.Errorf("Endpoint update. Record validation failed. Error: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
rec.TTL = ttlAsInt(endpoint.RecordTTL)
|
||||
rec.Target = cleanTargets(endpoint.RecordType, endpoint.Targets...)
|
||||
if err := p.client.UpdateRecord(rec, zoneName, true); err != nil {
|
||||
log.Errorf("Akamai Edge DNS recordset update failed. Error: %s", err.Error())
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// edgeChangesByZone separates a multi-zone change into a single change per zone.
|
||||
func edgeChangesByZone(zoneMap provider.ZoneIDName, endpoints []*endpoint.Endpoint) map[string][]*endpoint.Endpoint {
|
||||
createsByZone := make(map[string][]*endpoint.Endpoint, len(zoneMap))
|
||||
for _, z := range zoneMap {
|
||||
createsByZone[z] = make([]*endpoint.Endpoint, 0)
|
||||
}
|
||||
for _, ep := range endpoints {
|
||||
zone, _ := zoneMap.FindZone(ep.DNSName)
|
||||
if zone != "" {
|
||||
createsByZone[zone] = append(createsByZone[zone], ep)
|
||||
continue
|
||||
}
|
||||
updated = append(updated, endpoint)
|
||||
} else {
|
||||
log.Warnf("No matching zone for endpoint update DNSName: '%s' RecordType: '%s'", endpoint.DNSName, endpoint.RecordType)
|
||||
failed = append(failed, endpoint)
|
||||
log.Debugf("Skipping Akamai Edge DNS creation of endpoint: '%s' type: '%s', it does not match against Domain filters", ep.DNSName, ep.RecordType)
|
||||
}
|
||||
}
|
||||
return updated, failed
|
||||
|
||||
return createsByZone
|
||||
}
|
||||
|
@ -17,148 +17,206 @@ limitations under the License.
|
||||
package akamai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"testing"
|
||||
|
||||
"github.com/akamai/AkamaiOPEN-edgegrid-golang/edgegrid"
|
||||
dns "github.com/akamai/AkamaiOPEN-edgegrid-golang/configdns-v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
type mockAkamaiClient struct {
|
||||
mock.Mock
|
||||
type edgednsStubData struct {
|
||||
objType string // zone, record, recordsets
|
||||
output []interface{}
|
||||
updateRecords []interface{}
|
||||
createRecords []interface{}
|
||||
}
|
||||
|
||||
func (m *mockAkamaiClient) NewRequest(config edgegrid.Config, met, p string, b io.Reader) (*http.Request, error) {
|
||||
switch {
|
||||
case met == "GET":
|
||||
switch {
|
||||
case strings.HasPrefix(p, "https:///config-dns/v2/zones?"):
|
||||
b = bytes.NewReader([]byte("{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"},{\"contractId\":\"Exclude-Me\",\"zone\":\"exclude.me\"}]}"))
|
||||
case strings.HasPrefix(p, "https:///config-dns/v2/zones/example.com/"):
|
||||
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}"))
|
||||
case strings.HasPrefix(p, "https:///config-dns/v2/zones/exclude.me/"):
|
||||
b = bytes.NewReader([]byte("{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}"))
|
||||
}
|
||||
case met == "DELETE":
|
||||
b = bytes.NewReader([]byte("{\"title\": \"Success\", \"status\": 200, \"detail\": \"Record deleted\", \"requestId\": \"4321\"}"))
|
||||
case met == "ERROR":
|
||||
b = bytes.NewReader([]byte("{\"status\": 404 }"))
|
||||
}
|
||||
req := httptest.NewRequest(met, p, b)
|
||||
return req, nil
|
||||
type edgednsStub struct {
|
||||
stubData map[string]edgednsStubData
|
||||
}
|
||||
|
||||
func (m *mockAkamaiClient) Do(config edgegrid.Config, req *http.Request) (*http.Response, error) {
|
||||
handler := func(w http.ResponseWriter, r *http.Request) (isError bool) {
|
||||
b, _ := ioutil.ReadAll(r.Body)
|
||||
io.WriteString(w, string(b))
|
||||
return string(b) == "{\"status\": 404 }"
|
||||
func newStub() *edgednsStub {
|
||||
return &edgednsStub{
|
||||
stubData: make(map[string]edgednsStubData),
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
err := handler(w, req)
|
||||
resp := w.Result()
|
||||
}
|
||||
|
||||
if err == true {
|
||||
resp.StatusCode = 400
|
||||
func createAkamaiStubProvider(stub *edgednsStub, domfilter endpoint.DomainFilter, idfilter provider.ZoneIDFilter) (*AkamaiProvider, error) {
|
||||
|
||||
akamaiConfig := AkamaiConfig{
|
||||
DomainFilter: domfilter,
|
||||
ZoneIDFilter: idfilter,
|
||||
ServiceConsumerDomain: "testzone.com",
|
||||
ClientToken: "test_token",
|
||||
ClientSecret: "test_client_secret",
|
||||
AccessToken: "test_access_token",
|
||||
}
|
||||
|
||||
prov, err := NewAkamaiProvider(akamaiConfig, stub)
|
||||
aprov := prov.(*AkamaiProvider)
|
||||
return aprov, err
|
||||
}
|
||||
|
||||
func (r *edgednsStub) createStubDataEntry(objtype string) {
|
||||
|
||||
log.Debugf("Creating stub data entry")
|
||||
if _, exists := r.stubData[objtype]; !exists {
|
||||
r.stubData[objtype] = edgednsStubData{objType: objtype}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *edgednsStub) setOutput(objtype string, output []interface{}) {
|
||||
|
||||
log.Debugf("Setting output to %v", output)
|
||||
r.createStubDataEntry(objtype)
|
||||
stubdata := r.stubData[objtype]
|
||||
stubdata.output = output
|
||||
r.stubData[objtype] = stubdata
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *edgednsStub) setUpdateRecords(objtype string, records []interface{}) {
|
||||
|
||||
log.Debugf("Setting updaterecords to %v", records)
|
||||
r.createStubDataEntry(objtype)
|
||||
stubdata := r.stubData[objtype]
|
||||
stubdata.updateRecords = records
|
||||
r.stubData[objtype] = stubdata
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *edgednsStub) setCreateRecords(objtype string, records []interface{}) {
|
||||
|
||||
log.Debugf("Setting createrecords to %v", records)
|
||||
r.createStubDataEntry(objtype)
|
||||
stubdata := r.stubData[objtype]
|
||||
stubdata.createRecords = records
|
||||
r.stubData[objtype] = stubdata
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *edgednsStub) ListZones(queryArgs dns.ZoneListQueryArgs) (*dns.ZoneListResponse, error) {
|
||||
|
||||
log.Debugf("Entering ListZones")
|
||||
// Ignore Metadata`
|
||||
resp := &dns.ZoneListResponse{}
|
||||
zones := make([]*dns.ZoneResponse, 0)
|
||||
for _, zname := range r.stubData["zone"].output {
|
||||
log.Debugf("Processing output: %v", zname)
|
||||
zn := &dns.ZoneResponse{Zone: zname.(string), ContractId: "contract"}
|
||||
log.Debugf("Created Zone Object: %v", zn)
|
||||
zones = append(zones, zn)
|
||||
}
|
||||
resp.Zones = zones
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (r *edgednsStub) GetRecordsets(zone string, queryArgs dns.RecordsetQueryArgs) (*dns.RecordSetResponse, error) {
|
||||
|
||||
log.Debugf("Entering GetRecordsets")
|
||||
// Ignore Metadata`
|
||||
resp := &dns.RecordSetResponse{}
|
||||
sets := make([]dns.Recordset, 0)
|
||||
for _, rec := range r.stubData["recordset"].output {
|
||||
rset := rec.(dns.Recordset)
|
||||
sets = append(sets, rset)
|
||||
}
|
||||
resp.Recordsets = sets
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func TestRequestError(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
func (r *edgednsStub) CreateRecordsets(recordsets *dns.Recordsets, zone string, reclock bool) error {
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
|
||||
m := "ERROR"
|
||||
p := ""
|
||||
b := ""
|
||||
x, err := c.request(m, p, bytes.NewReader([]byte(b)))
|
||||
assert.Nil(t, x)
|
||||
assert.NotNil(t, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestFetchZonesZoneIDFilter(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Test"}),
|
||||
}
|
||||
func (r *edgednsStub) GetRecord(zone string, name string, record_type string) (*dns.RecordBody, error) {
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
resp := &dns.RecordBody{}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (r *edgednsStub) DeleteRecord(record *dns.RecordBody, zone string, recLock bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *edgednsStub) UpdateRecord(record *dns.RecordBody, zone string, recLock bool) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test FetchZones
|
||||
func TestFetchZonesZoneIDFilter(t *testing.T) {
|
||||
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.NewZoneIDFilter([]string{"Test"})
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
stub.setOutput("zone", []interface{}{"test1.testzone.com", "test2.testzone.com"})
|
||||
|
||||
x, _ := c.fetchZones()
|
||||
y, _ := json.Marshal(x)
|
||||
if assert.NotNil(t, y) {
|
||||
assert.Equal(t, "{\"zones\":[{\"contractId\":\"Test\",\"zone\":\"example.com\"}]}", string(y))
|
||||
assert.Equal(t, "{\"zones\":[{\"contractId\":\"contract\",\"zone\":\"test1.testzone.com\"},{\"contractId\":\"contract\",\"zone\":\"test2.testzone.com\"}]}", string(y))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchZonesEmpty(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"Nonexistent"}),
|
||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.NewDomainFilter([]string{"Nonexistent"})
|
||||
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
stub.setOutput("zone", []interface{}{})
|
||||
|
||||
x, _ := c.fetchZones()
|
||||
y, _ := json.Marshal(x)
|
||||
if assert.NotNil(t, y) {
|
||||
assert.Equal(t, "{\"zones\":null}", string(y))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRecordset1(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
|
||||
x, _ := c.fetchRecordSet("example.com")
|
||||
y, _ := json.Marshal(x)
|
||||
if assert.NotNil(t, y) {
|
||||
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.example.com\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"10.0.0.2\",\"10.0.0.3\"]},{\"name\":\"www.example.com\",\"type\":\"TXT\",\"ttl\":300,\"rdata\":[\"heritage=external-dns,external-dns/owner=default\"]}]}", string(y))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRecordset2(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
|
||||
x, _ := c.fetchRecordSet("exclude.me")
|
||||
y, _ := json.Marshal(x)
|
||||
if assert.NotNil(t, y) {
|
||||
assert.Equal(t, "{\"recordsets\":[{\"name\":\"www.exclude.me\",\"type\":\"A\",\"ttl\":300,\"rdata\":[\"192.168.0.1\",\"192.168.0.2\"]}]}", string(y))
|
||||
assert.Equal(t, "{\"zones\":[]}", string(y))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAkamaiRecords tests record endpoint
|
||||
func TestAkamaiRecords(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
|
||||
recordsets := make([]interface{}, 0)
|
||||
recordsets = append(recordsets, dns.Recordset{
|
||||
Name: "www.example.com",
|
||||
Type: endpoint.RecordTypeA,
|
||||
Rdata: []string{"10.0.0.2", "10.0.0.3"},
|
||||
})
|
||||
recordsets = append(recordsets, dns.Recordset{
|
||||
Name: "www.example.com",
|
||||
Type: endpoint.RecordTypeTXT,
|
||||
Rdata: []string{"heritage=external-dns,external-dns/owner=default"},
|
||||
})
|
||||
recordsets = append(recordsets, dns.Recordset{
|
||||
Name: "www.exclude.me",
|
||||
Type: endpoint.RecordTypeA,
|
||||
Rdata: []string{"192.168.0.1", "192.168.0.2"},
|
||||
})
|
||||
stub.setOutput("recordset", recordsets)
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
@ -171,28 +229,40 @@ func TestAkamaiRecords(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAkamaiRecordsEmpty(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Nonexistent"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.NewZoneIDFilter([]string{"Nonexistent"})
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
stub.setOutput("zone", []interface{}{"test1.testzone.com"})
|
||||
recordsets := make([]interface{}, 0)
|
||||
stub.setOutput("recordset", recordsets)
|
||||
|
||||
x, _ := c.Records(context.Background())
|
||||
assert.Nil(t, x)
|
||||
}
|
||||
|
||||
func TestAkamaiRecordsFilters(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"www.exclude.me"}),
|
||||
ZoneIDFilter: provider.NewZoneIDFilter([]string{"Exclude-Me"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
|
||||
stub := newStub()
|
||||
domfilter := endpoint.NewDomainFilter([]string{"www.exclude.me"})
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
stub.setOutput("zone", []interface{}{"www.exclude.me"})
|
||||
recordsets := make([]interface{}, 0)
|
||||
recordsets = append(recordsets, dns.Recordset{
|
||||
Name: "www.example.com",
|
||||
Type: endpoint.RecordTypeA,
|
||||
Rdata: []string{"10.0.0.2", "10.0.0.3"},
|
||||
})
|
||||
recordsets = append(recordsets, dns.Recordset{
|
||||
Name: "www.exclude.me",
|
||||
Type: endpoint.RecordTypeA,
|
||||
Rdata: []string{"192.168.0.1", "192.168.0.2"},
|
||||
})
|
||||
stub.setOutput("recordset", recordsets)
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "192.168.0.1", "192.168.0.2"))
|
||||
|
||||
@ -202,32 +272,32 @@ func TestAkamaiRecordsFilters(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateRecords tests create function
|
||||
// (p AkamaiProvider) createRecordsets(zoneNameIDMapper provider.ZoneIDName, endpoints []*endpoint.Endpoint) error
|
||||
func TestCreateRecords(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
|
||||
x, _ := c.createRecords(zoneNameIDMapper, endpoints)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.createRecordsets(zoneNameIDMapper, endpoints)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestCreateRecordsDomainFilter(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
@ -235,38 +305,36 @@ func TestCreateRecordsDomainFilter(t *testing.T) {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
|
||||
x, _ := c.createRecords(zoneNameIDMapper, exclude)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.createRecordsets(zoneNameIDMapper, exclude)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
// TestDeleteRecords validate delete
|
||||
func TestDeleteRecords(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
|
||||
x, _ := c.deleteRecords(zoneNameIDMapper, endpoints)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.deleteRecordsets(zoneNameIDMapper, endpoints)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
//
|
||||
func TestDeleteRecordsDomainFilter(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
@ -274,38 +342,36 @@ func TestDeleteRecordsDomainFilter(t *testing.T) {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
|
||||
x, _ := c.deleteRecords(zoneNameIDMapper, exclude)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.deleteRecordsets(zoneNameIDMapper, exclude)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
// Test record update func
|
||||
func TestUpdateRecords(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.DomainFilter{}
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
|
||||
x, _ := c.updateNewRecords(zoneNameIDMapper, endpoints)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.updateNewRecordsets(zoneNameIDMapper, endpoints)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
//
|
||||
func TestUpdateRecordsDomainFilter(t *testing.T) {
|
||||
config := AkamaiConfig{
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"example.com"}),
|
||||
}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
zoneNameIDMapper := provider.ZoneIDName{"example.com": "example.com"}
|
||||
endpoints := make([]*endpoint.Endpoint, 0)
|
||||
@ -313,19 +379,19 @@ func TestUpdateRecordsDomainFilter(t *testing.T) {
|
||||
endpoints = append(endpoints, endpoint.NewEndpoint("www.example.com", endpoint.RecordTypeTXT, "heritage=external-dns,external-dns/owner=default"))
|
||||
exclude := append(endpoints, endpoint.NewEndpoint("www.exclude.me", endpoint.RecordTypeA, "10.0.0.2", "10.0.0.3"))
|
||||
|
||||
x, _ := c.updateNewRecords(zoneNameIDMapper, exclude)
|
||||
if assert.NotNil(t, x) {
|
||||
assert.Equal(t, endpoints, x)
|
||||
}
|
||||
err = c.updateNewRecordsets(zoneNameIDMapper, exclude)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestAkamaiApplyChanges(t *testing.T) {
|
||||
config := AkamaiConfig{}
|
||||
|
||||
client := &mockAkamaiClient{}
|
||||
c := NewAkamaiProvider(config)
|
||||
c.client = client
|
||||
stub := newStub()
|
||||
domfilter := endpoint.NewDomainFilter([]string{"example.com"})
|
||||
idfilter := provider.ZoneIDFilter{}
|
||||
c, err := createAkamaiStubProvider(stub, domfilter, idfilter)
|
||||
assert.Nil(t, err)
|
||||
|
||||
stub.setOutput("zone", []interface{}{"example.com"})
|
||||
changes := &plan.Changes{}
|
||||
changes.Create = []*endpoint.Endpoint{
|
||||
{DNSName: "www.example.com", RecordType: "A", Targets: endpoint.Targets{"target"}, RecordTTL: 300},
|
||||
|
@ -1152,6 +1152,7 @@ func TestAWSHealthTargetAnnotation(tt *testing.T) {
|
||||
Current: []*endpoint.Endpoint{test.current},
|
||||
Desired: []*endpoint.Endpoint{test.desired},
|
||||
PropertyComparator: provider.PropertyValuesEqual,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
plan = plan.Calculate()
|
||||
assert.Equal(t, test.shouldUpdate, len(plan.Changes.UpdateNew) == 1)
|
||||
|
@ -19,17 +19,12 @@ package azure
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2018-05-01/dns"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
@ -41,18 +36,6 @@ const (
|
||||
azureRecordTTL = 300
|
||||
)
|
||||
|
||||
type config struct {
|
||||
Cloud string `json:"cloud" yaml:"cloud"`
|
||||
TenantID string `json:"tenantId" yaml:"tenantId"`
|
||||
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
|
||||
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
|
||||
Location string `json:"location" yaml:"location"`
|
||||
ClientID string `json:"aadClientId" yaml:"aadClientId"`
|
||||
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
|
||||
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
|
||||
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
|
||||
}
|
||||
|
||||
// ZonesClient is an interface of dns.ZoneClient that can be stubbed for testing.
|
||||
type ZonesClient interface {
|
||||
ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result dns.ZoneListResultIterator, err error)
|
||||
@ -82,46 +65,22 @@ type AzureProvider struct {
|
||||
//
|
||||
// Returns the provider or an error if a provider could not be created.
|
||||
func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zoneNameFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, userAssignedIdentityClientID string, dryRun bool) (*AzureProvider, error) {
|
||||
contents, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||
}
|
||||
cfg := config{}
|
||||
err = yaml.Unmarshal(contents, &cfg)
|
||||
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||
}
|
||||
|
||||
// If a resource group was given, override what was present in the config file
|
||||
if resourceGroup != "" {
|
||||
cfg.ResourceGroup = resourceGroup
|
||||
}
|
||||
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
|
||||
if userAssignedIdentityClientID != "" {
|
||||
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
|
||||
}
|
||||
|
||||
var environment azure.Environment
|
||||
if cfg.Cloud == "" {
|
||||
environment = azure.PublicCloud
|
||||
} else {
|
||||
environment, err = azure.EnvironmentFromName(cfg.Cloud)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
|
||||
}
|
||||
}
|
||||
|
||||
token, err := getAccessToken(cfg, environment)
|
||||
token, err := getAccessToken(*cfg, cfg.Environment)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token: %v", err)
|
||||
}
|
||||
|
||||
zonesClient := dns.NewZonesClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
zonesClient := dns.NewZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
recordSetsClient := dns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||
|
||||
provider := &AzureProvider{
|
||||
return &AzureProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneNameFilter: zoneNameFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
@ -130,61 +89,7 @@ func NewAzureProvider(configFile string, domainFilter endpoint.DomainFilter, zon
|
||||
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
|
||||
zonesClient: zonesClient,
|
||||
recordSetsClient: recordSetsClient,
|
||||
}
|
||||
return provider, nil
|
||||
}
|
||||
|
||||
// getAccessToken retrieves Azure API access token.
|
||||
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
|
||||
// Try to retrieve token with service principal credentials.
|
||||
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
|
||||
// and service principal exists. In this case, we still want to use service principal to authenticate.
|
||||
if len(cfg.ClientID) > 0 &&
|
||||
len(cfg.ClientSecret) > 0 &&
|
||||
// due to some historical reason, for pure MSI cluster,
|
||||
// they will use "msi" as placeholder in azure.json.
|
||||
// In this case, we shouldn't try to use SPN to authenticate.
|
||||
!strings.EqualFold(cfg.ClientID, "msi") &&
|
||||
!strings.EqualFold(cfg.ClientSecret, "msi") {
|
||||
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
|
||||
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
|
||||
}
|
||||
|
||||
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service principal token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Try to retrieve token with MSI.
|
||||
if cfg.UseManagedIdentityExtension {
|
||||
log.Info("Using managed identity extension to retrieve access token for Azure API.")
|
||||
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
|
||||
}
|
||||
|
||||
if cfg.UserAssignedIdentityID != "" {
|
||||
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
|
||||
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
log.Info("Resolving to system assigned identity.")
|
||||
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no credentials provided for Azure API")
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Records gets the current records.
|
||||
@ -352,20 +257,20 @@ func (p *AzureProvider) mapChanges(zones []dns.Zone, changes *plan.Changes) (azu
|
||||
func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMap) {
|
||||
// Delete records first
|
||||
for zone, endpoints := range deleted {
|
||||
for _, endpoint := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, endpoint)
|
||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
||||
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName)
|
||||
for _, ep := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, ep)
|
||||
if !p.domainFilter.Match(ep.DNSName) {
|
||||
log.Debugf("Skipping deletion of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
|
||||
continue
|
||||
}
|
||||
if p.dryRun {
|
||||
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
|
||||
log.Infof("Would delete %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
|
||||
} else {
|
||||
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", endpoint.RecordType, name, zone)
|
||||
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(endpoint.RecordType), ""); err != nil {
|
||||
log.Infof("Deleting %s record named '%s' for Azure DNS zone '%s'.", ep.RecordType, name, zone)
|
||||
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, name, dns.RecordType(ep.RecordType), ""); err != nil {
|
||||
log.Errorf(
|
||||
"Failed to delete %s record named '%s' for Azure DNS zone '%s': %v",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
zone,
|
||||
err,
|
||||
@ -378,18 +283,18 @@ func (p *AzureProvider) deleteRecords(ctx context.Context, deleted azureChangeMa
|
||||
|
||||
func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMap) {
|
||||
for zone, endpoints := range updated {
|
||||
for _, endpoint := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, endpoint)
|
||||
if !p.domainFilter.Match(endpoint.DNSName) {
|
||||
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", endpoint.DNSName)
|
||||
for _, ep := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, ep)
|
||||
if !p.domainFilter.Match(ep.DNSName) {
|
||||
log.Debugf("Skipping update of record %s because it was filtered out by the specified --domain-filter", ep.DNSName)
|
||||
continue
|
||||
}
|
||||
if p.dryRun {
|
||||
log.Infof(
|
||||
"Would update %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
@ -397,20 +302,20 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
|
||||
|
||||
log.Infof(
|
||||
"Updating %s record named '%s' to '%s' for Azure DNS zone '%s'.",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.newRecordSet(endpoint)
|
||||
recordSet, err := p.newRecordSet(ep)
|
||||
if err == nil {
|
||||
_, err = p.recordSetsClient.CreateOrUpdate(
|
||||
ctx,
|
||||
p.resourceGroup,
|
||||
zone,
|
||||
name,
|
||||
dns.RecordType(endpoint.RecordType),
|
||||
dns.RecordType(ep.RecordType),
|
||||
recordSet,
|
||||
"",
|
||||
"",
|
||||
@ -419,9 +324,9 @@ func (p *AzureProvider) updateRecords(ctx context.Context, updated azureChangeMa
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to update %s record named '%s' to '%s' for DNS zone '%s': %v",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
|
@ -21,9 +21,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/profiles/latest/privatedns/mgmt/privatedns"
|
||||
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@ -50,8 +49,8 @@ type AzurePrivateDNSProvider struct {
|
||||
domainFilter endpoint.DomainFilter
|
||||
zoneIDFilter provider.ZoneIDFilter
|
||||
dryRun bool
|
||||
subscriptionID string
|
||||
resourceGroup string
|
||||
userAssignedIdentityClientID string
|
||||
zonesClient PrivateZonesClient
|
||||
recordSetsClient PrivateRecordSetsClient
|
||||
}
|
||||
@ -59,32 +58,31 @@ type AzurePrivateDNSProvider struct {
|
||||
// NewAzurePrivateDNSProvider creates a new Azure Private DNS provider.
|
||||
//
|
||||
// Returns the provider or an error if a provider could not be created.
|
||||
func NewAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup string, subscriptionID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
|
||||
authorizer, err := auth.NewAuthorizerFromEnvironment()
|
||||
func NewAzurePrivateDNSProvider(configFile string, domainFilter endpoint.DomainFilter, zoneIDFilter provider.ZoneIDFilter, resourceGroup, userAssignedIdentityClientID string, dryRun bool) (*AzurePrivateDNSProvider, error) {
|
||||
cfg, err := getConfig(configFile, resourceGroup, userAssignedIdentityClientID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||
}
|
||||
|
||||
settings, err := auth.GetSettingsFromEnvironment()
|
||||
token, err := getAccessToken(*cfg, cfg.Environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("failed to get token: %v", err)
|
||||
}
|
||||
|
||||
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
|
||||
zonesClient.Authorizer = authorizer
|
||||
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(settings.Environment.ResourceManagerEndpoint, subscriptionID)
|
||||
recordSetsClient.Authorizer = authorizer
|
||||
zonesClient := privatedns.NewPrivateZonesClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
zonesClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||
recordSetsClient := privatedns.NewRecordSetsClientWithBaseURI(cfg.Environment.ResourceManagerEndpoint, cfg.SubscriptionID)
|
||||
recordSetsClient.Authorizer = autorest.NewBearerAuthorizer(token)
|
||||
|
||||
provider := &AzurePrivateDNSProvider{
|
||||
return &AzurePrivateDNSProvider{
|
||||
domainFilter: domainFilter,
|
||||
zoneIDFilter: zoneIDFilter,
|
||||
dryRun: dryRun,
|
||||
subscriptionID: subscriptionID,
|
||||
resourceGroup: resourceGroup,
|
||||
resourceGroup: cfg.ResourceGroup,
|
||||
userAssignedIdentityClientID: cfg.UserAssignedIdentityID,
|
||||
zonesClient: zonesClient,
|
||||
recordSetsClient: recordSetsClient,
|
||||
}
|
||||
return provider, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Records gets the current records.
|
||||
@ -256,16 +254,16 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
|
||||
log.Debugf("Records to be deleted: %d", len(deleted))
|
||||
// Delete records first
|
||||
for zone, endpoints := range deleted {
|
||||
for _, endpoint := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, endpoint)
|
||||
for _, ep := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, ep)
|
||||
if p.dryRun {
|
||||
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
|
||||
log.Infof("Would delete %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
|
||||
} else {
|
||||
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", endpoint.RecordType, name, zone)
|
||||
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(endpoint.RecordType), name, ""); err != nil {
|
||||
log.Infof("Deleting %s record named '%s' for Azure Private DNS zone '%s'.", ep.RecordType, name, zone)
|
||||
if _, err := p.recordSetsClient.Delete(ctx, p.resourceGroup, zone, privatedns.RecordType(ep.RecordType), name, ""); err != nil {
|
||||
log.Errorf(
|
||||
"Failed to delete %s record named '%s' for Azure Private DNS zone '%s': %v",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
zone,
|
||||
err,
|
||||
@ -279,14 +277,14 @@ func (p *AzurePrivateDNSProvider) deleteRecords(ctx context.Context, deleted azu
|
||||
func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azurePrivateDNSChangeMap) {
|
||||
log.Debugf("Records to be updated: %d", len(updated))
|
||||
for zone, endpoints := range updated {
|
||||
for _, endpoint := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, endpoint)
|
||||
for _, ep := range endpoints {
|
||||
name := p.recordSetNameForZone(zone, ep)
|
||||
if p.dryRun {
|
||||
log.Infof(
|
||||
"Would update %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
continue
|
||||
@ -294,19 +292,19 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
|
||||
|
||||
log.Infof(
|
||||
"Updating %s record named '%s' to '%s' for Azure Private DNS zone '%s'.",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
)
|
||||
|
||||
recordSet, err := p.newRecordSet(endpoint)
|
||||
recordSet, err := p.newRecordSet(ep)
|
||||
if err == nil {
|
||||
_, err = p.recordSetsClient.CreateOrUpdate(
|
||||
ctx,
|
||||
p.resourceGroup,
|
||||
zone,
|
||||
privatedns.RecordType(endpoint.RecordType),
|
||||
privatedns.RecordType(ep.RecordType),
|
||||
name,
|
||||
recordSet,
|
||||
"",
|
||||
@ -316,9 +314,9 @@ func (p *AzurePrivateDNSProvider) updateRecords(ctx context.Context, updated azu
|
||||
if err != nil {
|
||||
log.Errorf(
|
||||
"Failed to update %s record named '%s' to '%s' for Azure Private DNS zone '%s': %v",
|
||||
endpoint.RecordType,
|
||||
ep.RecordType,
|
||||
name,
|
||||
endpoint.Targets,
|
||||
ep.Targets,
|
||||
zone,
|
||||
err,
|
||||
)
|
||||
|
@ -18,16 +18,11 @@ package azure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/services/privatedns/mgmt/2018-09-01/privatedns"
|
||||
"github.com/Azure/go-autorest/autorest"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"github.com/Azure/go-autorest/autorest/azure/auth"
|
||||
"github.com/Azure/go-autorest/autorest/to"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
@ -255,36 +250,6 @@ func newAzurePrivateDNSProvider(domainFilter endpoint.DomainFilter, zoneIDFilter
|
||||
}
|
||||
}
|
||||
|
||||
func validateAzurePrivateDNSClientsResourceManager(t *testing.T, environmentName string, expectedResourceManagerEndpoint string) {
|
||||
err := os.Setenv(auth.EnvironmentName, environmentName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
azurePrivateDNSProvider, err := NewAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), "k8s", "sub", true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
zonesClientBaseURI := azurePrivateDNSProvider.zonesClient.(privatedns.PrivateZonesClient).BaseURI
|
||||
recordSetsClientBaseURI := azurePrivateDNSProvider.recordSetsClient.(privatedns.RecordSetsClient).BaseURI
|
||||
|
||||
assert.Equal(t, zonesClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, zonesClientBaseURI)
|
||||
assert.Equal(t, recordSetsClientBaseURI, expectedResourceManagerEndpoint, "expected and actual resource manager endpoints don't match. expected: %s, got: %s", expectedResourceManagerEndpoint, recordSetsClientBaseURI)
|
||||
}
|
||||
|
||||
func TestNewAzurePrivateDNSProvider(t *testing.T) {
|
||||
// make sure to reset the environment variables at the end again
|
||||
originalEnv := os.Getenv(auth.EnvironmentName)
|
||||
defer os.Setenv(auth.EnvironmentName, originalEnv)
|
||||
|
||||
validateAzurePrivateDNSClientsResourceManager(t, "", azure.PublicCloud.ResourceManagerEndpoint)
|
||||
validateAzurePrivateDNSClientsResourceManager(t, "AZURECHINACLOUD", azure.ChinaCloud.ResourceManagerEndpoint)
|
||||
validateAzurePrivateDNSClientsResourceManager(t, "AZUREGERMANCLOUD", azure.GermanCloud.ResourceManagerEndpoint)
|
||||
validateAzurePrivateDNSClientsResourceManager(t, "AZUREUSGOVERNMENTCLOUD", azure.USGovernmentCloud.ResourceManagerEndpoint)
|
||||
}
|
||||
|
||||
func TestAzurePrivateDNSRecord(t *testing.T) {
|
||||
provider, err := newMockedAzurePrivateDNSProvider(endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s",
|
||||
&[]privatedns.PrivateZone{
|
||||
|
129
provider/azure/config.go
Normal file
129
provider/azure/config.go
Normal file
@ -0,0 +1,129 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/go-autorest/autorest/adal"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// config represents common config items for Azure DNS and Azure Private DNS
|
||||
type config struct {
|
||||
Cloud string `json:"cloud" yaml:"cloud"`
|
||||
Environment azure.Environment `json:"-" yaml:"-"`
|
||||
TenantID string `json:"tenantId" yaml:"tenantId"`
|
||||
SubscriptionID string `json:"subscriptionId" yaml:"subscriptionId"`
|
||||
ResourceGroup string `json:"resourceGroup" yaml:"resourceGroup"`
|
||||
Location string `json:"location" yaml:"location"`
|
||||
ClientID string `json:"aadClientId" yaml:"aadClientId"`
|
||||
ClientSecret string `json:"aadClientSecret" yaml:"aadClientSecret"`
|
||||
UseManagedIdentityExtension bool `json:"useManagedIdentityExtension" yaml:"useManagedIdentityExtension"`
|
||||
UserAssignedIdentityID string `json:"userAssignedIdentityID" yaml:"userAssignedIdentityID"`
|
||||
}
|
||||
|
||||
func getConfig(configFile, resourceGroup, userAssignedIdentityClientID string) (*config, error) {
|
||||
contents, err := ioutil.ReadFile(configFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||
}
|
||||
cfg := &config{}
|
||||
err = yaml.Unmarshal(contents, &cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read Azure config file '%s': %v", configFile, err)
|
||||
}
|
||||
|
||||
// If a resource group was given, override what was present in the config file
|
||||
if resourceGroup != "" {
|
||||
cfg.ResourceGroup = resourceGroup
|
||||
}
|
||||
// If userAssignedIdentityClientID is provided explicitly, override existing one in config file
|
||||
if userAssignedIdentityClientID != "" {
|
||||
cfg.UserAssignedIdentityID = userAssignedIdentityClientID
|
||||
}
|
||||
|
||||
var environment azure.Environment
|
||||
if cfg.Cloud == "" {
|
||||
environment = azure.PublicCloud
|
||||
} else {
|
||||
environment, err = azure.EnvironmentFromName(cfg.Cloud)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid cloud value '%s': %v", cfg.Cloud, err)
|
||||
}
|
||||
}
|
||||
cfg.Environment = environment
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// getAccessToken retrieves Azure API access token.
|
||||
func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePrincipalToken, error) {
|
||||
// Try to retrieve token with service principal credentials.
|
||||
// Try to use service principal first, some AKS clusters are in an intermediate state that `UseManagedIdentityExtension` is `true`
|
||||
// and service principal exists. In this case, we still want to use service principal to authenticate.
|
||||
if len(cfg.ClientID) > 0 &&
|
||||
len(cfg.ClientSecret) > 0 &&
|
||||
// due to some historical reason, for pure MSI cluster,
|
||||
// they will use "msi" as placeholder in azure.json.
|
||||
// In this case, we shouldn't try to use SPN to authenticate.
|
||||
!strings.EqualFold(cfg.ClientID, "msi") &&
|
||||
!strings.EqualFold(cfg.ClientSecret, "msi") {
|
||||
log.Info("Using client_id+client_secret to retrieve access token for Azure API.")
|
||||
oauthConfig, err := adal.NewOAuthConfig(environment.ActiveDirectoryEndpoint, cfg.TenantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve OAuth config: %v", err)
|
||||
}
|
||||
|
||||
token, err := adal.NewServicePrincipalToken(*oauthConfig, cfg.ClientID, cfg.ClientSecret, environment.ResourceManagerEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create service principal token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Try to retrieve token with MSI.
|
||||
if cfg.UseManagedIdentityExtension {
|
||||
log.Info("Using managed identity extension to retrieve access token for Azure API.")
|
||||
msiEndpoint, err := adal.GetMSIVMEndpoint()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get the managed service identity endpoint: %v", err)
|
||||
}
|
||||
|
||||
if cfg.UserAssignedIdentityID != "" {
|
||||
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)
|
||||
token, err := adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, environment.ServiceManagementEndpoint, cfg.UserAssignedIdentityID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
log.Info("Resolving to system assigned identity.")
|
||||
token, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, environment.ServiceManagementEndpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create the managed service identity token: %v", err)
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("no credentials provided for Azure API")
|
||||
}
|
67
provider/azure/config_test.go
Normal file
67
provider/azure/config_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package azure
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Azure/go-autorest/autorest/azure"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetAzureEnvironmentConfig(t *testing.T) {
|
||||
tmp, err := ioutil.TempFile("", "azureconf")
|
||||
if err != nil {
|
||||
t.Errorf("couldn't write temp file %v", err)
|
||||
}
|
||||
defer os.Remove(tmp.Name())
|
||||
|
||||
tests := map[string]struct {
|
||||
cloud string
|
||||
err error
|
||||
}{
|
||||
"AzureChinaCloud": {"AzureChinaCloud", nil},
|
||||
"AzureGermanCloud": {"AzureGermanCloud", nil},
|
||||
"AzurePublicCloud": {"", nil},
|
||||
"AzureUSGovernment": {"AzureUSGovernmentCloud", nil},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, _ = tmp.Seek(0, 0)
|
||||
_, _ = tmp.Write([]byte(fmt.Sprintf(`{"cloud": "%s"}`, test.cloud)))
|
||||
got, err := getConfig(tmp.Name(), "", "")
|
||||
if err != nil {
|
||||
t.Errorf("got unexpected err %v", err)
|
||||
}
|
||||
|
||||
if test.cloud == "" {
|
||||
test.cloud = "AzurePublicCloud"
|
||||
}
|
||||
want, err := azure.EnvironmentFromName(test.cloud)
|
||||
if err != nil {
|
||||
t.Errorf("couldn't get azure environment from provided name %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(want, got.Environment) {
|
||||
t.Errorf("got %v, want %v", got.Environment, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -229,7 +229,7 @@ func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, erro
|
||||
return cloudflare.Zone{}, errors.New("Unknown zoneID: " + zoneID)
|
||||
}
|
||||
|
||||
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, args ...interface{}) {
|
||||
func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endpoint.Endpoint, actions []MockAction, managedRecords []string, args ...interface{}) {
|
||||
t.Helper()
|
||||
|
||||
var client *mockCloudFlareClient
|
||||
@ -253,6 +253,7 @@ func AssertActions(t *testing.T, provider *CloudFlareProvider, endpoints []*endp
|
||||
Current: records,
|
||||
Desired: endpoints,
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
||||
ManagedRecords: managedRecords,
|
||||
}
|
||||
|
||||
changes := plan.Calculate().Changes
|
||||
@ -305,7 +306,10 @@ func TestCloudflareA(t *testing.T) {
|
||||
Proxied: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestCloudflareCname(t *testing.T) {
|
||||
@ -340,7 +344,9 @@ func TestCloudflareCname(t *testing.T) {
|
||||
Proxied: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareCustomTTL(t *testing.T) {
|
||||
@ -365,7 +371,9 @@ func TestCloudflareCustomTTL(t *testing.T) {
|
||||
Proxied: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareProxiedDefault(t *testing.T) {
|
||||
@ -389,7 +397,9 @@ func TestCloudflareProxiedDefault(t *testing.T) {
|
||||
Proxied: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareProxiedOverrideTrue(t *testing.T) {
|
||||
@ -419,7 +429,9 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
|
||||
Proxied: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareProxiedOverrideFalse(t *testing.T) {
|
||||
@ -449,7 +461,9 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
|
||||
Proxied: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
|
||||
@ -479,7 +493,9 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
|
||||
Proxied: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
[]string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
)
|
||||
}
|
||||
|
||||
func TestCloudflareSetProxied(t *testing.T) {
|
||||
@ -525,7 +541,7 @@ func TestCloudflareSetProxied(t *testing.T) {
|
||||
Proxied: testCase.proxiable,
|
||||
},
|
||||
},
|
||||
}, testCase.recordType+" record on "+testCase.domain)
|
||||
}, []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME, endpoint.RecordTypeNS}, testCase.recordType+" record on "+testCase.domain)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1084,6 +1100,7 @@ func TestProviderPropertiesIdempotency(t *testing.T) {
|
||||
Current: current,
|
||||
Desired: desired,
|
||||
PropertyComparator: provider.PropertyValuesEqual,
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
plan = *plan.Calculate()
|
||||
@ -1138,6 +1155,7 @@ func TestCloudflareComplexUpdate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
planned := plan.Calculate()
|
||||
@ -1227,6 +1245,7 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
|
||||
Current: records,
|
||||
Desired: endpoints,
|
||||
DomainFilter: endpoint.NewDomainFilter([]string{"bar.com"}),
|
||||
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
|
||||
}
|
||||
|
||||
planned := plan.Calculate()
|
||||
|
300
provider/godaddy/client.go
Normal file
300
provider/godaddy/client.go
Normal file
@ -0,0 +1,300 @@
|
||||
/*
|
||||
Copyright 2020 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 godaddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
|
||||
)
|
||||
|
||||
// DefaultTimeout api requests after 180s
|
||||
const DefaultTimeout = 180 * time.Second
|
||||
|
||||
// Errors
|
||||
var (
|
||||
ErrAPIDown = errors.New("godaddy: the GoDaddy API is down")
|
||||
)
|
||||
|
||||
// APIError error
|
||||
type APIError struct {
|
||||
Code string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (err *APIError) Error() string {
|
||||
return fmt.Sprintf("Error %s: %q", err.Code, err.Message)
|
||||
}
|
||||
|
||||
// Logger is the interface that should be implemented for loggers that wish to
|
||||
// log HTTP requests and HTTP responses.
|
||||
type Logger interface {
|
||||
// LogRequest logs an HTTP request.
|
||||
LogRequest(*http.Request)
|
||||
|
||||
// LogResponse logs an HTTP response.
|
||||
LogResponse(*http.Response)
|
||||
}
|
||||
|
||||
// Client represents a client to call the GoDaddy API
|
||||
type Client struct {
|
||||
// APIKey holds the Application key
|
||||
APIKey string
|
||||
|
||||
// APISecret holds the Application secret key
|
||||
APISecret string
|
||||
|
||||
// API endpoint
|
||||
APIEndPoint string
|
||||
|
||||
// Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default.
|
||||
Client *http.Client
|
||||
|
||||
// Logger is used to log HTTP requests and responses.
|
||||
Logger Logger
|
||||
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewClient represents a new client to call the API
|
||||
func NewClient(useOTE bool, apiKey, apiSecret string) (*Client, error) {
|
||||
var endpoint string
|
||||
|
||||
if useOTE {
|
||||
endpoint = " https://api.ote-godaddy.com"
|
||||
} else {
|
||||
endpoint = "https://api.godaddy.com"
|
||||
}
|
||||
|
||||
client := Client{
|
||||
APIKey: apiKey,
|
||||
APISecret: apiSecret,
|
||||
APIEndPoint: endpoint,
|
||||
Client: &http.Client{},
|
||||
Timeout: DefaultTimeout,
|
||||
}
|
||||
|
||||
// Get and check the configuration
|
||||
if err := client.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
//
|
||||
// Common request wrappers
|
||||
//
|
||||
|
||||
// Get is a wrapper for the GET method
|
||||
func (c *Client) Get(url string, resType interface{}) error {
|
||||
return c.CallAPI("GET", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// Patch is a wrapper for the POST method
|
||||
func (c *Client) Patch(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("PATCH", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Post is a wrapper for the POST method
|
||||
func (c *Client) Post(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("POST", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Put is a wrapper for the PUT method
|
||||
func (c *Client) Put(url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPI("PUT", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// Delete is a wrapper for the DELETE method
|
||||
func (c *Client) Delete(url string, resType interface{}) error {
|
||||
return c.CallAPI("DELETE", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// GetWithContext is a wrapper for the GET method
|
||||
func (c *Client) GetWithContext(ctx context.Context, url string, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "GET", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// PatchWithContext is a wrapper for the POST method
|
||||
func (c *Client) PatchWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "PATCH", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// PostWithContext is a wrapper for the POST method
|
||||
func (c *Client) PostWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "POST", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// PutWithContext is a wrapper for the PUT method
|
||||
func (c *Client) PutWithContext(ctx context.Context, url string, reqBody, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "PUT", url, reqBody, resType, true)
|
||||
}
|
||||
|
||||
// DeleteWithContext is a wrapper for the DELETE method
|
||||
func (c *Client) DeleteWithContext(ctx context.Context, url string, resType interface{}) error {
|
||||
return c.CallAPIWithContext(ctx, "DELETE", url, nil, resType, true)
|
||||
}
|
||||
|
||||
// NewRequest returns a new HTTP request
|
||||
func (c *Client) NewRequest(method, path string, reqBody interface{}, needAuth bool) (*http.Request, error) {
|
||||
var body []byte
|
||||
var err error
|
||||
|
||||
if reqBody != nil {
|
||||
body, err = json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
target := fmt.Sprintf("%s%s", c.APIEndPoint, path)
|
||||
req, err := http.NewRequest(method, target, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Inject headers
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("sso-key %s:%s", c.APIKey, c.APISecret))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", "ExternalDNS/"+externaldns.Version)
|
||||
|
||||
// Send the request with requested timeout
|
||||
c.Client.Timeout = c.Timeout
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Do sends an HTTP request and returns an HTTP response
|
||||
func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
if c.Logger != nil {
|
||||
c.Logger.LogRequest(req)
|
||||
}
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.Logger != nil {
|
||||
c.Logger.LogResponse(resp)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// CallAPI is the lowest level call helper. If needAuth is true,
|
||||
// inject authentication headers and sign the request.
|
||||
//
|
||||
// Request signature is a sha1 hash on following fields, joined by '+':
|
||||
// - applicationSecret (from Client instance)
|
||||
// - consumerKey (from Client instance)
|
||||
// - capitalized method (from arguments)
|
||||
// - full request url, including any query string argument
|
||||
// - full serialized request body
|
||||
// - server current time (takes time delta into account)
|
||||
//
|
||||
// Call will automatically assemble the target url from the endpoint
|
||||
// configured in the client instance and the path argument. If the reqBody
|
||||
// argument is not nil, it will also serialize it as json and inject
|
||||
// the required Content-Type header.
|
||||
//
|
||||
// If everything went fine, unmarshall response into resType and return nil
|
||||
// otherwise, return the error
|
||||
func (c *Client) CallAPI(method, path string, reqBody, resType interface{}, needAuth bool) error {
|
||||
return c.CallAPIWithContext(context.Background(), method, path, reqBody, resType, needAuth)
|
||||
}
|
||||
|
||||
// CallAPIWithContext is the lowest level call helper. If needAuth is true,
|
||||
// inject authentication headers and sign the request.
|
||||
//
|
||||
// Request signature is a sha1 hash on following fields, joined by '+':
|
||||
// - applicationSecret (from Client instance)
|
||||
// - consumerKey (from Client instance)
|
||||
// - capitalized method (from arguments)
|
||||
// - full request url, including any query string argument
|
||||
// - full serialized request body
|
||||
// - server current time (takes time delta into account)
|
||||
//
|
||||
// Context is used by http.Client to handle context cancelation
|
||||
//
|
||||
// Call will automatically assemble the target url from the endpoint
|
||||
// configured in the client instance and the path argument. If the reqBody
|
||||
// argument is not nil, it will also serialize it as json and inject
|
||||
// the required Content-Type header.
|
||||
//
|
||||
// If everything went fine, unmarshall response into resType and return nil
|
||||
// otherwise, return the error
|
||||
func (c *Client) CallAPIWithContext(ctx context.Context, method, path string, reqBody, resType interface{}, needAuth bool) error {
|
||||
req, err := c.NewRequest(method, path, reqBody, needAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
response, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.UnmarshalResponse(response, resType)
|
||||
}
|
||||
|
||||
// UnmarshalResponse checks the response and unmarshals it into the response
|
||||
// type if needed Helper function, called from CallAPI
|
||||
func (c *Client) UnmarshalResponse(response *http.Response, resType interface{}) error {
|
||||
// Read all the response body
|
||||
defer response.Body.Close()
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// < 200 && >= 300 : API error
|
||||
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
||||
apiError := &APIError{
|
||||
Code: fmt.Sprintf("HTTPStatus: %d", response.StatusCode),
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(body, apiError); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return apiError
|
||||
}
|
||||
|
||||
// Nothing to unmarshal
|
||||
if len(body) == 0 || resType == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return json.Unmarshal(body, &resType)
|
||||
}
|
||||
|
||||
func (c *Client) validate() error {
|
||||
var response interface{}
|
||||
|
||||
if err := c.Get("/v1/domains?statuses=ACTIVE", response); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
508
provider/godaddy/godaddy.go
Normal file
508
provider/godaddy/godaddy.go
Normal file
@ -0,0 +1,508 @@
|
||||
/*
|
||||
Copyright 2020 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 godaddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
"sigs.k8s.io/external-dns/provider"
|
||||
)
|
||||
|
||||
const (
|
||||
gdMinimalTTL = 600
|
||||
gdCreate = iota
|
||||
gdUpdate
|
||||
gdDelete
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
||||
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
||||
// ErrNoDryRun No dry run support for the moment
|
||||
ErrNoDryRun = errors.New("dry run not supported")
|
||||
)
|
||||
|
||||
type gdClient interface {
|
||||
Patch(string, interface{}, interface{}) error
|
||||
Post(string, interface{}, interface{}) error
|
||||
Put(string, interface{}, interface{}) error
|
||||
Get(string, interface{}) error
|
||||
Delete(string, interface{}) error
|
||||
}
|
||||
|
||||
// GDProvider declare GoDaddy provider
|
||||
type GDProvider struct {
|
||||
provider.BaseProvider
|
||||
|
||||
domainFilter endpoint.DomainFilter
|
||||
client gdClient
|
||||
ttl int64
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type gdEndpoint struct {
|
||||
endpoint *endpoint.Endpoint
|
||||
action int
|
||||
}
|
||||
|
||||
type gdRecordField struct {
|
||||
Data string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
TTL int64 `json:"ttl"`
|
||||
Type string `json:"type"`
|
||||
Port *int `json:"port,omitempty"`
|
||||
Priority *int `json:"priority,omitempty"`
|
||||
Weight *int64 `json:"weight,omitempty"`
|
||||
Protocol *string `json:"protocol,omitempty"`
|
||||
Service *string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
type gdRecords struct {
|
||||
records []gdRecordField
|
||||
changed bool
|
||||
zone string
|
||||
}
|
||||
|
||||
type gdZone struct {
|
||||
CreatedAt string
|
||||
Domain string
|
||||
DomainID int64
|
||||
ExpirationProtected bool
|
||||
Expires string
|
||||
ExposeWhois bool
|
||||
HoldRegistrar bool
|
||||
Locked bool
|
||||
NameServers *[]string
|
||||
Privacy bool
|
||||
RenewAuto bool
|
||||
RenewDeadline string
|
||||
Renewable bool
|
||||
Status string
|
||||
TransferProtected bool
|
||||
}
|
||||
|
||||
type gdZoneIDName map[string]*gdRecords
|
||||
|
||||
func (z gdZoneIDName) add(zoneID string, zoneRecord *gdRecords) {
|
||||
z[zoneID] = zoneRecord
|
||||
}
|
||||
|
||||
func (z gdZoneIDName) findZoneRecord(hostname string) (suitableZoneID string, suitableZoneRecord *gdRecords) {
|
||||
for zoneID, zoneRecord := range z {
|
||||
if hostname == zoneRecord.zone || strings.HasSuffix(hostname, "."+zoneRecord.zone) {
|
||||
if suitableZoneRecord == nil || len(zoneRecord.zone) > len(suitableZoneRecord.zone) {
|
||||
suitableZoneID = zoneID
|
||||
suitableZoneRecord = zoneRecord
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// NewGoDaddyProvider initializes a new GoDaddy DNS based Provider.
|
||||
func NewGoDaddyProvider(ctx context.Context, domainFilter endpoint.DomainFilter, ttl int64, apiKey, apiSecret string, useOTE, dryRun bool) (*GDProvider, error) {
|
||||
client, err := NewClient(useOTE, apiKey, apiSecret)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Add Dry Run support
|
||||
if dryRun {
|
||||
return nil, ErrNoDryRun
|
||||
}
|
||||
|
||||
return &GDProvider{
|
||||
client: client,
|
||||
domainFilter: domainFilter,
|
||||
ttl: maxOf(gdMinimalTTL, ttl),
|
||||
DryRun: dryRun,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) zones() ([]string, error) {
|
||||
zones := []gdZone{}
|
||||
filteredZones := []string{}
|
||||
|
||||
if err := p.client.Get("/v1/domains?statuses=ACTIVE", &zones); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, zone := range zones {
|
||||
if p.domainFilter.Match(zone.Domain) {
|
||||
filteredZones = append(filteredZones, zone.Domain)
|
||||
log.Debugf("GoDaddy: %s zone found", zone.Domain)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("GoDaddy: %d zones found", len(filteredZones))
|
||||
|
||||
return filteredZones, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) zonesRecords(ctx context.Context, all bool) ([]string, []gdRecords, error) {
|
||||
var allRecords []gdRecords
|
||||
zones, err := p.zones()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if len(zones) == 0 {
|
||||
allRecords = []gdRecords{}
|
||||
} else if len(zones) == 1 {
|
||||
record, err := p.records(&ctx, zones[0], all)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
allRecords = append(allRecords, *record)
|
||||
} else {
|
||||
chRecords := make(chan gdRecords, len(zones))
|
||||
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
for _, zoneName := range zones {
|
||||
zone := zoneName
|
||||
eg.Go(func() error {
|
||||
record, err := p.records(&ctx, zone, all)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chRecords <- *record
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
close(chRecords)
|
||||
|
||||
for records := range chRecords {
|
||||
allRecords = append(allRecords, records)
|
||||
}
|
||||
}
|
||||
|
||||
return zones, allRecords, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRecords, error) {
|
||||
var recordsIds []gdRecordField
|
||||
|
||||
log.Debugf("GoDaddy: Getting records for %s", zone)
|
||||
|
||||
if err := p.client.Get(fmt.Sprintf("/v1/domains/%s/records", zone), &recordsIds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if all {
|
||||
return &gdRecords{
|
||||
zone: zone,
|
||||
records: recordsIds,
|
||||
}, nil
|
||||
}
|
||||
|
||||
results := &gdRecords{
|
||||
zone: zone,
|
||||
records: make([]gdRecordField, 0, len(recordsIds)),
|
||||
}
|
||||
|
||||
for _, rec := range recordsIds {
|
||||
if provider.SupportedRecordType(rec.Type) {
|
||||
log.Debugf("GoDaddy: Record %s for %s is %+v", rec.Name, zone, rec)
|
||||
|
||||
results.records = append(results.records, rec)
|
||||
} else {
|
||||
log.Infof("GoDaddy: Discard record %s for %s is %+v", rec.Name, zone, rec)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) groupByNameAndType(zoneRecords []gdRecords) []*endpoint.Endpoint {
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
|
||||
// group supported records by name and type
|
||||
groupsByZone := map[string]map[string][]gdRecordField{}
|
||||
|
||||
for _, zone := range zoneRecords {
|
||||
groups := map[string][]gdRecordField{}
|
||||
|
||||
groupsByZone[zone.zone] = groups
|
||||
|
||||
for _, r := range zone.records {
|
||||
groupBy := fmt.Sprintf("%s - %s", r.Type, r.Name)
|
||||
|
||||
if _, ok := groups[groupBy]; !ok {
|
||||
groups[groupBy] = []gdRecordField{}
|
||||
}
|
||||
|
||||
groups[groupBy] = append(groups[groupBy], r)
|
||||
}
|
||||
}
|
||||
|
||||
// create single endpoint with all the targets for each name/type
|
||||
for zoneName, groups := range groupsByZone {
|
||||
for _, records := range groups {
|
||||
targets := []string{}
|
||||
|
||||
for _, record := range records {
|
||||
targets = append(targets, record.Data)
|
||||
}
|
||||
|
||||
var recordName string
|
||||
|
||||
if records[0].Name == "@" {
|
||||
recordName = strings.TrimPrefix(zoneName, ".")
|
||||
} else {
|
||||
recordName = strings.TrimPrefix(fmt.Sprintf("%s.%s", records[0].Name, zoneName), ".")
|
||||
}
|
||||
|
||||
endpoint := endpoint.NewEndpointWithTTL(
|
||||
recordName,
|
||||
records[0].Type,
|
||||
endpoint.TTL(records[0].TTL),
|
||||
targets...,
|
||||
)
|
||||
|
||||
endpoints = append(endpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// Records returns the list of records in all relevant zones.
|
||||
func (p *GDProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
_, records, err := p.zonesRecords(ctx, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints := p.groupByNameAndType(records)
|
||||
|
||||
log.Infof("GoDaddy: %d endpoints have been found", len(endpoints))
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (p *GDProvider) flushRecords(patch bool, zoneRecord *gdRecords) error {
|
||||
if patch {
|
||||
return p.client.Patch(fmt.Sprintf("/v1/domains/%s/records", zoneRecord.zone), zoneRecord.records, nil)
|
||||
}
|
||||
|
||||
return p.client.Put(fmt.Sprintf("/v1/domains/%s/records", zoneRecord.zone), zoneRecord.records, nil)
|
||||
}
|
||||
|
||||
func (p *GDProvider) appendChange(action int, endpoints []*endpoint.Endpoint, allChanges []gdEndpoint) []gdEndpoint {
|
||||
for _, e := range endpoints {
|
||||
allChanges = append(allChanges, gdEndpoint{
|
||||
action: action,
|
||||
endpoint: e,
|
||||
})
|
||||
}
|
||||
|
||||
return allChanges
|
||||
}
|
||||
|
||||
func (p *GDProvider) changeAllRecords(patch bool, endpoints []gdEndpoint, zoneRecords []*gdRecords) error {
|
||||
zoneNameIDMapper := gdZoneIDName{}
|
||||
|
||||
for _, zoneRecord := range zoneRecords {
|
||||
if patch {
|
||||
zoneRecord.changed = false
|
||||
zoneRecord.records = nil
|
||||
}
|
||||
|
||||
zoneNameIDMapper.add(zoneRecord.zone, zoneRecord)
|
||||
}
|
||||
|
||||
for _, e := range endpoints {
|
||||
dnsName := e.endpoint.DNSName
|
||||
zone, zoneRecord := zoneNameIDMapper.findZoneRecord(dnsName)
|
||||
|
||||
if zone == "" {
|
||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName)
|
||||
} else {
|
||||
dnsName = strings.TrimSuffix(dnsName, "."+zone)
|
||||
|
||||
if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) {
|
||||
dnsName = "@"
|
||||
}
|
||||
|
||||
for _, target := range e.endpoint.Targets {
|
||||
change := gdRecordField{
|
||||
Type: e.endpoint.RecordType,
|
||||
Name: dnsName,
|
||||
TTL: p.ttl,
|
||||
Data: target,
|
||||
}
|
||||
|
||||
if e.endpoint.RecordTTL.IsConfigured() {
|
||||
change.TTL = maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL))
|
||||
}
|
||||
|
||||
zoneRecord.applyChange(e.action, change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyChanges applies a given set of changes in a given zone.
|
||||
func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
|
||||
if countTargets(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, records, err := p.zonesRecords(ctx, true)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
changedZoneRecords := make([]*gdRecords, len(records))
|
||||
|
||||
for i := range records {
|
||||
changedZoneRecords[i] = &records[i]
|
||||
}
|
||||
|
||||
allChanges := make([]gdEndpoint, 0, countTargets(changes))
|
||||
|
||||
allChanges = p.appendChange(gdCreate, changes.Create, allChanges)
|
||||
allChanges = p.appendChange(gdCreate, changes.UpdateNew, allChanges)
|
||||
allChanges = p.appendChange(gdDelete, changes.UpdateOld, allChanges)
|
||||
allChanges = p.appendChange(gdDelete, changes.Delete, allChanges)
|
||||
|
||||
log.Infof("GoDaddy: %d changes will be done", len(allChanges))
|
||||
|
||||
patch := len(changes.UpdateOld)+len(changes.Delete) == 0
|
||||
|
||||
if err = p.changeAllRecords(patch, allChanges, changedZoneRecords); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range changedZoneRecords {
|
||||
if record.changed {
|
||||
if err = p.flushRecords(patch, record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *gdRecords) addRecord(change gdRecordField) {
|
||||
log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
p.records = append(p.records, change)
|
||||
p.changed = true
|
||||
}
|
||||
|
||||
func (p *gdRecords) updateRecord(change gdRecordField) {
|
||||
log.Debugf("GoDaddy: Update an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
for index, record := range p.records {
|
||||
if record.Type == change.Type && record.Name == change.Name {
|
||||
p.records[index] = change
|
||||
p.changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove one record from the record list
|
||||
func (p *gdRecords) deleteRecord(change gdRecordField) {
|
||||
log.Debugf("GoDaddy: Delete an entry %s to zone %s", change.String(), p.zone)
|
||||
|
||||
deleteIndex := -1
|
||||
|
||||
for index, record := range p.records {
|
||||
if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data {
|
||||
deleteIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if deleteIndex >= 0 {
|
||||
p.records[deleteIndex] = p.records[len(p.records)-1]
|
||||
|
||||
p.records = p.records[:len(p.records)-1]
|
||||
p.changed = true
|
||||
} else {
|
||||
log.Warnf("GoDaddy: record in zone %s not found %s to delete", p.zone, change.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *gdRecords) applyChange(action int, change gdRecordField) {
|
||||
switch action {
|
||||
case gdCreate:
|
||||
p.addRecord(change)
|
||||
case gdUpdate:
|
||||
p.updateRecord(change)
|
||||
case gdDelete:
|
||||
p.deleteRecord(change)
|
||||
}
|
||||
}
|
||||
|
||||
func (c gdRecordField) String() string {
|
||||
return fmt.Sprintf("%s %d IN %s %s", c.Name, c.TTL, c.Type, c.Data)
|
||||
}
|
||||
|
||||
func countTargets(p *plan.Changes) int {
|
||||
changes := [][]*endpoint.Endpoint{p.Create, p.UpdateNew, p.UpdateOld, p.Delete}
|
||||
count := 0
|
||||
|
||||
for _, endpoints := range changes {
|
||||
for _, endpoint := range endpoints {
|
||||
count += len(endpoint.Targets)
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func maxOf(vars ...int64) int64 {
|
||||
max := vars[0]
|
||||
|
||||
for _, i := range vars {
|
||||
if max < i {
|
||||
max = i
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
376
provider/godaddy/godaddy_test.go
Normal file
376
provider/godaddy/godaddy_test.go
Normal file
@ -0,0 +1,376 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package godaddy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
"sigs.k8s.io/external-dns/plan"
|
||||
)
|
||||
|
||||
type mockGoDaddyClient struct {
|
||||
mock.Mock
|
||||
currentTest *testing.T
|
||||
}
|
||||
|
||||
func newMockGoDaddyClient(t *testing.T) *mockGoDaddyClient {
|
||||
return &mockGoDaddyClient{
|
||||
currentTest: t,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
zoneNameExampleOrg string = "example.org"
|
||||
zoneNameExampleNet string = "example.net"
|
||||
)
|
||||
|
||||
func (c *mockGoDaddyClient) Post(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("POST: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Patch(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("PATCH: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Put(endpoint string, input interface{}, output interface{}) error {
|
||||
log.Infof("PUT: %s - %v", endpoint, input)
|
||||
stub := c.Called(endpoint, input)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Get(endpoint string, output interface{}) error {
|
||||
log.Infof("GET: %s", endpoint)
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func (c *mockGoDaddyClient) Delete(endpoint string, output interface{}) error {
|
||||
log.Infof("DELETE: %s", endpoint)
|
||||
stub := c.Called(endpoint)
|
||||
data, _ := json.Marshal(stub.Get(0))
|
||||
json.Unmarshal(data, output)
|
||||
return stub.Error(1)
|
||||
}
|
||||
|
||||
func TestGoDaddyZones(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
domainFilter: endpoint.NewDomainFilter([]string{"com"}),
|
||||
}
|
||||
|
||||
// Basic zones
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: "example.com",
|
||||
},
|
||||
{
|
||||
Domain: "example.net",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
domains, err := provider.zones()
|
||||
|
||||
assert.NoError(err)
|
||||
assert.Contains(domains, "example.com")
|
||||
assert.NotContains(domains, "example.net")
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
domains, err = provider.zones()
|
||||
assert.Error(err)
|
||||
assert.Nil(domains)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyZoneRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "NS",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
zones, records, err := provider.zonesRecords(context.TODO(), true)
|
||||
|
||||
assert.NoError(err)
|
||||
|
||||
assert.ElementsMatch(zones, []string{
|
||||
zoneNameExampleNet,
|
||||
})
|
||||
|
||||
assert.ElementsMatch(records, []gdRecords{
|
||||
{
|
||||
zone: zoneNameExampleNet,
|
||||
records: []gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "NS",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zones list
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return(nil, ErrAPIDown).Once()
|
||||
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error on getting zone record detail
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return(nil, ErrAPIDown).Once()
|
||||
|
||||
zones, records, err = provider.zonesRecords(context.TODO(), false)
|
||||
assert.Error(err)
|
||||
assert.Nil(zones)
|
||||
assert.Nil(records)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyRecords(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
// Basic zones records
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleOrg,
|
||||
},
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.org/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "@",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "www",
|
||||
Type: "CNAME",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "example.org",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.43",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
endpoints, err := provider.Records(context.TODO())
|
||||
assert.NoError(err)
|
||||
|
||||
// Little fix for multi targets endpoint
|
||||
for _, endpoint := range endpoints {
|
||||
sort.Strings(endpoint.Targets)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(endpoints, []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "godaddy.example.net",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
"203.0.113.43",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "example.org",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
},
|
||||
},
|
||||
{
|
||||
DNSName: "www.example.org",
|
||||
RecordType: "CNAME",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Labels: endpoint.NewLabels(),
|
||||
Targets: []string{
|
||||
"example.org",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
client.AssertExpectations(t)
|
||||
|
||||
// Error getting zone
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return(nil, ErrAPIDown).Once()
|
||||
endpoints, err = provider.Records(context.TODO())
|
||||
assert.Error(err)
|
||||
assert.Nil(endpoints)
|
||||
client.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGoDaddyChange(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
client := newMockGoDaddyClient(t)
|
||||
provider := &GDProvider{
|
||||
client: client,
|
||||
}
|
||||
|
||||
changes := plan.Changes{
|
||||
Create: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: ".example.net",
|
||||
RecordType: "A",
|
||||
RecordTTL: gdMinimalTTL,
|
||||
Targets: []string{
|
||||
"203.0.113.42",
|
||||
},
|
||||
},
|
||||
},
|
||||
Delete: []*endpoint.Endpoint{
|
||||
{
|
||||
DNSName: "godaddy.example.net",
|
||||
RecordType: "A",
|
||||
Targets: []string{
|
||||
"203.0.113.43",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Fetch domains
|
||||
client.On("Get", "/v1/domains?statuses=ACTIVE").Return([]gdZone{
|
||||
{
|
||||
Domain: zoneNameExampleNet,
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Fetch record
|
||||
client.On("Get", "/v1/domains/example.net/records").Return([]gdRecordField{
|
||||
{
|
||||
Name: "godaddy",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.43",
|
||||
},
|
||||
}, nil).Once()
|
||||
|
||||
// Update domain
|
||||
client.On("Put", "/v1/domains/example.net/records", []gdRecordField{
|
||||
{
|
||||
Name: "@",
|
||||
Type: "A",
|
||||
TTL: gdMinimalTTL,
|
||||
Data: "203.0.113.42",
|
||||
},
|
||||
}).Return(nil, nil).Once()
|
||||
|
||||
assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
|
||||
|
||||
client.AssertExpectations(t)
|
||||
}
|
@ -24,7 +24,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bodgit/tsig"
|
||||
extendedClient "github.com/bodgit/tsig/client"
|
||||
"github.com/bodgit/tsig/gss"
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
@ -36,6 +40,9 @@ import (
|
||||
const (
|
||||
// maximum size of a UDP transport message in DNS protocol
|
||||
udpMaxMsgSize = 512
|
||||
|
||||
// maximum time DNS client can be off from server for an update to succeed
|
||||
clockSkew = 300
|
||||
)
|
||||
|
||||
// rfc2136 provider type
|
||||
@ -50,6 +57,12 @@ type rfc2136Provider struct {
|
||||
axfr bool
|
||||
minTTL time.Duration
|
||||
|
||||
// options specific to rfc3645 gss-tsig support
|
||||
gssTsig bool
|
||||
krb5Username string
|
||||
krb5Password string
|
||||
krb5Realm string
|
||||
|
||||
// only consider hosted zones managing domains ending in this suffix
|
||||
domainFilter endpoint.DomainFilter
|
||||
dryRun bool
|
||||
@ -72,9 +85,9 @@ type rfc2136Actions interface {
|
||||
}
|
||||
|
||||
// NewRfc2136Provider is a factory function for OpenStack rfc2136 providers
|
||||
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, actions rfc2136Actions) (provider.Provider, error) {
|
||||
func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, keyName string, secret string, secretAlg string, axfr bool, domainFilter endpoint.DomainFilter, dryRun bool, minTTL time.Duration, gssTsig bool, krb5Username string, krb5Password string, actions rfc2136Actions) (provider.Provider, error) {
|
||||
secretAlgChecked, ok := tsigAlgs[secretAlg]
|
||||
if !ok && !insecure {
|
||||
if !ok && !insecure && !gssTsig {
|
||||
return nil, errors.Errorf("%s is not supported TSIG algorithm", secretAlg)
|
||||
}
|
||||
|
||||
@ -82,6 +95,10 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k
|
||||
nameserver: net.JoinHostPort(host, strconv.Itoa(port)),
|
||||
zoneName: dns.Fqdn(zoneName),
|
||||
insecure: insecure,
|
||||
gssTsig: gssTsig,
|
||||
krb5Username: krb5Username,
|
||||
krb5Password: krb5Password,
|
||||
krb5Realm: strings.ToUpper(zoneName),
|
||||
domainFilter: domainFilter,
|
||||
dryRun: dryRun,
|
||||
axfr: axfr,
|
||||
@ -103,6 +120,22 @@ func NewRfc2136Provider(host string, port int, zoneName string, insecure bool, k
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// KeyName will return TKEY name and TSIG handle to use for followon actions with a secure connection
|
||||
func (r rfc2136Provider) KeyData() (keyName *string, handle *gss.GSS, err error) {
|
||||
handle, err = gss.New()
|
||||
if err != nil {
|
||||
return keyName, handle, err
|
||||
}
|
||||
|
||||
rawHost, _, err := net.SplitHostPort(r.nameserver)
|
||||
if err != nil {
|
||||
return keyName, handle, err
|
||||
}
|
||||
|
||||
keyName, _, err = handle.NegotiateContextWithCredentials(rawHost, r.krb5Realm, r.krb5Username, r.krb5Password)
|
||||
return keyName, handle, err
|
||||
}
|
||||
|
||||
// Records returns the list of records.
|
||||
func (r rfc2136Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
rrs, err := r.List()
|
||||
@ -163,7 +196,7 @@ OuterLoop:
|
||||
|
||||
func (r rfc2136Provider) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelope, err error) {
|
||||
t := new(dns.Transfer)
|
||||
if !r.insecure {
|
||||
if !r.insecure && !r.gssTsig {
|
||||
t.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
|
||||
}
|
||||
|
||||
@ -180,8 +213,8 @@ func (r rfc2136Provider) List() ([]dns.RR, error) {
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetAxfr(r.zoneName)
|
||||
if !r.insecure {
|
||||
m.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix())
|
||||
if !r.insecure && !r.gssTsig {
|
||||
m.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
|
||||
}
|
||||
|
||||
env, err := r.actions.IncomeTransfer(m, r.nameserver)
|
||||
@ -304,12 +337,31 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
|
||||
}
|
||||
log.Debugf("SendMessage")
|
||||
|
||||
c := new(dns.Client)
|
||||
c := new(extendedClient.Client)
|
||||
c.SingleInflight = true
|
||||
|
||||
if !r.insecure {
|
||||
if r.gssTsig {
|
||||
keyName, handle, err := r.KeyData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer handle.Close()
|
||||
defer handle.DeleteContext(keyName)
|
||||
|
||||
c.TsigAlgorithm = map[string]*extendedClient.TsigAlgorithm{
|
||||
tsig.GSS: {
|
||||
Generate: handle.GenerateGSS,
|
||||
Verify: handle.VerifyGSS,
|
||||
},
|
||||
}
|
||||
c.TsigSecret = map[string]string{*keyName: ""}
|
||||
|
||||
msg.SetTsig(*keyName, tsig.GSS, clockSkew, time.Now().Unix())
|
||||
} else {
|
||||
c.TsigSecret = map[string]string{r.tsigKeyName: r.tsigSecret}
|
||||
msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, 300, time.Now().Unix())
|
||||
msg.SetTsig(r.tsigKeyName, r.tsigSecretAlg, clockSkew, time.Now().Unix())
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Len() > udpMaxMsgSize {
|
||||
@ -318,9 +370,12 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
|
||||
|
||||
resp, _, err := c.Exchange(msg, r.nameserver)
|
||||
if err != nil {
|
||||
if resp != nil && resp.Rcode != dns.RcodeSuccess {
|
||||
log.Infof("error in dns.Client.Exchange: %s", err)
|
||||
return err
|
||||
}
|
||||
log.Warnf("warn in dns.Client.Exchange: %s", err)
|
||||
}
|
||||
if resp != nil && resp.Rcode != dns.RcodeSuccess {
|
||||
log.Infof("Bad dns.Client.Exchange response: %s", resp)
|
||||
return fmt.Errorf("bad return code: %s", dns.RcodeToString[resp.Rcode])
|
||||
|
@ -95,7 +95,7 @@ func (r *rfc2136Stub) IncomeTransfer(m *dns.Msg, a string) (env chan *dns.Envelo
|
||||
}
|
||||
|
||||
func createRfc2136StubProvider(stub *rfc2136Stub) (provider.Provider, error) {
|
||||
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, stub)
|
||||
return NewRfc2136Provider("", 0, "", false, "key", "secret", "hmac-sha512", true, endpoint.DomainFilter{}, false, 300*time.Second, false, "", "", stub)
|
||||
}
|
||||
|
||||
func extractAuthoritySectionFromMessage(msg fmt.Stringer) []string {
|
||||
|
@ -269,8 +269,13 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
|
||||
records := []*domain.Record{}
|
||||
|
||||
for _, target := range ep.Targets {
|
||||
finalTargetName := target
|
||||
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
|
||||
finalTargetName = provider.EnsureTrailingDot(target)
|
||||
}
|
||||
|
||||
records = append(records, &domain.Record{
|
||||
Data: target,
|
||||
Data: finalTargetName,
|
||||
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
||||
Priority: priority,
|
||||
TTL: ttl,
|
||||
@ -285,9 +290,14 @@ func endpointToScalewayRecordsChangeDelete(zoneName string, ep *endpoint.Endpoin
|
||||
records := []*domain.RecordChange{}
|
||||
|
||||
for _, target := range ep.Targets {
|
||||
finalTargetName := target
|
||||
if domain.RecordType(ep.RecordType) == domain.RecordTypeCNAME {
|
||||
finalTargetName = provider.EnsureTrailingDot(target)
|
||||
}
|
||||
|
||||
records = append(records, &domain.RecordChange{
|
||||
Delete: &domain.RecordChangeDelete{
|
||||
Data: target,
|
||||
Data: finalTargetName,
|
||||
Name: strings.Trim(strings.TrimSuffix(ep.DNSName, zoneName), ". "),
|
||||
Type: domain.RecordType(ep.RecordType),
|
||||
},
|
||||
|
@ -93,7 +93,7 @@ func (m *mockScalewayDomain) ListDNSZoneRecords(req *domain.ListDNSZoneRecordsRe
|
||||
Type: domain.RecordTypeA,
|
||||
},
|
||||
{
|
||||
Data: "test.example.com",
|
||||
Data: "test.example.com.",
|
||||
Name: "two",
|
||||
TTL: 600,
|
||||
Priority: 30,
|
||||
@ -330,7 +330,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) {
|
||||
Add: &domain.RecordChangeAdd{
|
||||
Records: []*domain.Record{
|
||||
{
|
||||
Data: "example.com",
|
||||
Data: "example.com.",
|
||||
Name: "",
|
||||
TTL: 600,
|
||||
Type: domain.RecordTypeCNAME,
|
||||
|
@ -20,6 +20,14 @@ function generate_changelog {
|
||||
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
|
||||
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
|
||||
done
|
||||
|
||||
git log "$previous_tag".. --reverse --oneline --grep='(#' | \
|
||||
while read -r sha title; do
|
||||
pr_num="$(grep -o '#[[:digit:]]\+' <<<"$title")"
|
||||
pr_desc="$(git show -s --format=%s "$sha")"
|
||||
pr_author="$(gh pr view "$pr_num" | grep author | awk '{ print $2 }' | tr $'\n' ' ')"
|
||||
printf "* %s (%s) @%s\n\n" "$pr_desc" "$pr_num" "$pr_author"
|
||||
done
|
||||
}
|
||||
|
||||
function create_release {
|
||||
|
283
source/ambassador_host.go
Normal file
283
source/ambassador_host.go
Normal file
@ -0,0 +1,283 @@
|
||||
/*
|
||||
Copyright 2020 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 source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ambassador "github.com/datawire/ambassador/pkg/api/getambassador.io/v2"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
api "k8s.io/kubernetes/pkg/apis/core"
|
||||
|
||||
"sigs.k8s.io/external-dns/endpoint"
|
||||
)
|
||||
|
||||
// ambHostAnnotation is the annotation in the Host that maps to a Service
|
||||
const ambHostAnnotation = "external-dns.ambassador-service"
|
||||
|
||||
// groupName is the group name for the Ambassador API
|
||||
const groupName = "getambassador.io"
|
||||
|
||||
var schemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v2"}
|
||||
|
||||
var ambHostGVR = schemeGroupVersion.WithResource("hosts")
|
||||
|
||||
// ambassadorHostSource is an implementation of Source for Ambassador Host objects.
|
||||
// The IngressRoute implementation uses the spec.virtualHost.fqdn value for the hostname.
|
||||
// Use targetAnnotationKey to explicitly set Endpoint.
|
||||
type ambassadorHostSource struct {
|
||||
dynamicKubeClient dynamic.Interface
|
||||
kubeClient kubernetes.Interface
|
||||
namespace string
|
||||
ambassadorHostInformer informers.GenericInformer
|
||||
unstructuredConverter *unstructuredConverter
|
||||
}
|
||||
|
||||
// NewAmbassadorHostSource creates a new ambassadorHostSource with the given config.
|
||||
func NewAmbassadorHostSource(
|
||||
dynamicKubeClient dynamic.Interface,
|
||||
kubeClient kubernetes.Interface,
|
||||
namespace string) (Source, error) {
|
||||
var err error
|
||||
|
||||
// Use shared informer to listen for add/update/delete of Host in the specified namespace.
|
||||
// Set resync period to 0, to prevent processing when nothing has changed.
|
||||
informerFactory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dynamicKubeClient, 0, namespace, nil)
|
||||
ambassadorHostInformer := informerFactory.ForResource(ambHostGVR)
|
||||
|
||||
// Add default resource event handlers to properly initialize informer.
|
||||
ambassadorHostInformer.Informer().AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// TODO informer is not explicitly stopped since controller is not passing in its channel.
|
||||
informerFactory.Start(wait.NeverStop)
|
||||
|
||||
// wait for the local cache to be populated.
|
||||
err = poll(time.Second, 60*time.Second, func() (bool, error) {
|
||||
return ambassadorHostInformer.Informer().HasSynced(), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to sync cache")
|
||||
}
|
||||
|
||||
uc, err := newUnstructuredConverter()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to setup Unstructured Converter")
|
||||
}
|
||||
|
||||
return &ambassadorHostSource{
|
||||
dynamicKubeClient: dynamicKubeClient,
|
||||
kubeClient: kubeClient,
|
||||
namespace: namespace,
|
||||
ambassadorHostInformer: ambassadorHostInformer,
|
||||
unstructuredConverter: uc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Endpoints returns endpoint objects for each host-target combination that should be processed.
|
||||
// Retrieves all Hosts in the source's namespace(s).
|
||||
func (sc *ambassadorHostSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||
hosts, err := sc.ambassadorHostInformer.Lister().ByNamespace(sc.namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpoints := []*endpoint.Endpoint{}
|
||||
for _, hostObj := range hosts {
|
||||
unstructuredHost, ok := hostObj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert")
|
||||
}
|
||||
|
||||
host := &ambassador.Host{}
|
||||
err := sc.unstructuredConverter.scheme.Convert(unstructuredHost, host, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fullname := fmt.Sprintf("%s/%s", host.Namespace, host.Name)
|
||||
|
||||
// look for the "exernal-dns.ambassador-service" annotation. If it is not there then just ignore this `Host`
|
||||
service, found := host.Annotations[ambHostAnnotation]
|
||||
if !found {
|
||||
log.Debugf("Host %s ignored: no annotation %q found", fullname, ambHostAnnotation)
|
||||
continue
|
||||
}
|
||||
|
||||
targets, err := sc.targetsFromAmbassadorLoadBalancer(ctx, service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hostEndpoints, err := sc.endpointsFromHost(ctx, host, targets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hostEndpoints) == 0 {
|
||||
log.Debugf("No endpoints could be generated from Host %s", fullname)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Endpoints generated from Host: %s: %v", fullname, hostEndpoints)
|
||||
endpoints = append(endpoints, hostEndpoints...)
|
||||
}
|
||||
|
||||
for _, ep := range endpoints {
|
||||
sort.Sort(ep.Targets)
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
// endpointsFromHost extracts the endpoints from a Host object
|
||||
func (sc *ambassadorHostSource) endpointsFromHost(ctx context.Context, host *ambassador.Host, targets endpoint.Targets) ([]*endpoint.Endpoint, error) {
|
||||
var endpoints []*endpoint.Endpoint
|
||||
|
||||
providerSpecific := endpoint.ProviderSpecific{}
|
||||
setIdentifier := ""
|
||||
|
||||
annotations := host.Annotations
|
||||
ttl, err := getTTLFromAnnotations(annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if host.Spec != nil {
|
||||
hostname := host.Spec.Hostname
|
||||
if hostname != "" {
|
||||
endpoints = append(endpoints, endpointsForHostname(hostname, targets, ttl, providerSpecific, setIdentifier)...)
|
||||
}
|
||||
}
|
||||
|
||||
return endpoints, nil
|
||||
}
|
||||
|
||||
func (sc *ambassadorHostSource) targetsFromAmbassadorLoadBalancer(ctx context.Context, service string) (targets endpoint.Targets, err error) {
|
||||
lbNamespace, lbName, err := parseAmbLoadBalancerService(service)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
svc, err := sc.kubeClient.CoreV1().Services(lbNamespace).Get(ctx, lbName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, lb := range svc.Status.LoadBalancer.Ingress {
|
||||
if lb.IP != "" {
|
||||
targets = append(targets, lb.IP)
|
||||
}
|
||||
if lb.Hostname != "" {
|
||||
targets = append(targets, lb.Hostname)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// parseAmbLoadBalancerService returns a name/namespace tuple from the annotation in
|
||||
// an Ambassador Host CRD
|
||||
//
|
||||
// This is a thing because Ambassador has historically supported cross-namespace
|
||||
// references using a name.namespace syntax, but here we want to also support
|
||||
// namespace/name.
|
||||
//
|
||||
// Returns namespace, name, error.
|
||||
|
||||
func parseAmbLoadBalancerService(service string) (namespace, name string, err error) {
|
||||
// Start by assuming that we have namespace/name.
|
||||
parts := strings.Split(service, "/")
|
||||
|
||||
if len(parts) == 1 {
|
||||
// No "/" at all, so let's try for name.namespace. To be consistent with the
|
||||
// rest of Ambassador, use SplitN to limit this to one split, so that e.g.
|
||||
// svc.foo.bar uses service "svc" in namespace "foo.bar".
|
||||
parts = strings.SplitN(service, ".", 2)
|
||||
|
||||
if len(parts) == 2 {
|
||||
// We got a namespace, great.
|
||||
name := parts[0]
|
||||
namespace := parts[1]
|
||||
|
||||
return namespace, name, nil
|
||||
}
|
||||
|
||||
// If here, we have no separator, so the whole string is the service, and
|
||||
// we can assume the default namespace.
|
||||
name := service
|
||||
namespace := api.NamespaceDefault
|
||||
|
||||
return namespace, name, nil
|
||||
} else if len(parts) == 2 {
|
||||
// This is "namespace/name". Note that the name could be qualified,
|
||||
// which is fine.
|
||||
namespace := parts[0]
|
||||
name := parts[1]
|
||||
|
||||
return namespace, name, nil
|
||||
}
|
||||
|
||||
// If we got here, this string is simply ill-formatted. Return an error.
|
||||
return "", "", errors.New(fmt.Sprintf("invalid external-dns service: %s", service))
|
||||
}
|
||||
|
||||
func (sc *ambassadorHostSource) AddEventHandler(ctx context.Context, handler func()) {
|
||||
}
|
||||
|
||||
// unstructuredConverter handles conversions between unstructured.Unstructured and Ambassador types
|
||||
type unstructuredConverter struct {
|
||||
// scheme holds an initializer for converting Unstructured to a type
|
||||
scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// newUnstructuredConverter returns a new unstructuredConverter initialized
|
||||
func newUnstructuredConverter() (*unstructuredConverter, error) {
|
||||
uc := &unstructuredConverter{
|
||||
scheme: runtime.NewScheme(),
|
||||
}
|
||||
|
||||
// Setup converter to understand custom CRD types
|
||||
ambassador.AddToScheme(uc.scheme)
|
||||
|
||||
// Add the core types we need
|
||||
if err := scheme.AddToScheme(uc.scheme); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return uc, nil
|
||||
}
|
78
source/ambassador_host_test.go
Normal file
78
source/ambassador_host_test.go
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2019 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type AmbassadorSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestAmbassadorSource(t *testing.T) {
|
||||
suite.Run(t, new(AmbassadorSuite))
|
||||
t.Run("Interface", testAmbassadorSourceImplementsSource)
|
||||
}
|
||||
|
||||
// testAmbassadorSourceImplementsSource tests that ambassadorHostSource is a valid Source.
|
||||
func testAmbassadorSourceImplementsSource(t *testing.T) {
|
||||
require.Implements(t, (*Source)(nil), new(ambassadorHostSource))
|
||||
}
|
||||
|
||||
// TestParseAmbLoadBalancerService tests our parsing of Ambassador service info.
|
||||
func TestParseAmbLoadBalancerService(t *testing.T) {
|
||||
vectors := []struct {
|
||||
input string
|
||||
ns string
|
||||
svc string
|
||||
errstr string
|
||||
}{
|
||||
{"svc", "default", "svc", ""},
|
||||
{"ns/svc", "ns", "svc", ""},
|
||||
{"svc.ns", "ns", "svc", ""},
|
||||
{"svc.ns.foo.bar", "ns.foo.bar", "svc", ""},
|
||||
{"ns/svc/foo/bar", "", "", "invalid external-dns service: ns/svc/foo/bar"},
|
||||
{"ns/svc/foo.bar", "", "", "invalid external-dns service: ns/svc/foo.bar"},
|
||||
{"ns.foo/svc/bar", "", "", "invalid external-dns service: ns.foo/svc/bar"},
|
||||
}
|
||||
|
||||
for _, v := range vectors {
|
||||
ns, svc, err := parseAmbLoadBalancerService(v.input)
|
||||
|
||||
errstr := ""
|
||||
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
|
||||
if v.ns != ns {
|
||||
t.Errorf("%s: got ns \"%s\", wanted \"%s\"", v.input, ns, v.ns)
|
||||
}
|
||||
|
||||
if v.svc != svc {
|
||||
t.Errorf("%s: got svc \"%s\", wanted \"%s\"", v.input, svc, v.svc)
|
||||
}
|
||||
|
||||
if v.errstr != errstr {
|
||||
t.Errorf("%s: got err \"%s\", wanted \"%s\"", v.input, errstr, v.errstr)
|
||||
}
|
||||
}
|
||||
}
|
@ -628,14 +628,17 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
|
||||
|
||||
for _, port := range svc.Spec.Ports {
|
||||
if port.NodePort > 0 {
|
||||
// following the RFC 2782, SRV record must have a following format
|
||||
// _service._proto.name. TTL class SRV priority weight port
|
||||
// see https://en.wikipedia.org/wiki/SRV_record
|
||||
|
||||
// build a target with a priority of 0, weight of 0, and pointing the given port on the given host
|
||||
target := fmt.Sprintf("0 50 %d %s", port.NodePort, hostname)
|
||||
|
||||
// figure out the portname
|
||||
portName := port.Name
|
||||
if portName == "" {
|
||||
portName = fmt.Sprintf("%d", port.NodePort)
|
||||
}
|
||||
// take the service name from the K8s Service object
|
||||
// it is safe to use since it is DNS compatible
|
||||
// see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-label-names
|
||||
serviceName := svc.ObjectMeta.Name
|
||||
|
||||
// figure out the protocol
|
||||
protocol := strings.ToLower(string(port.Protocol))
|
||||
@ -643,7 +646,7 @@ func (sc *serviceSource) extractNodePortEndpoints(svc *v1.Service, nodeTargets e
|
||||
protocol = "tcp"
|
||||
}
|
||||
|
||||
recordName := fmt.Sprintf("_%s._%s.%s", portName, protocol, hostname)
|
||||
recordName := fmt.Sprintf("_%s._%s.%s", serviceName, protocol, hostname)
|
||||
|
||||
var ep *endpoint.Endpoint
|
||||
if ttl.IsConfigured() {
|
||||
|
@ -1642,7 +1642,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1729,7 +1729,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
map[string]string{},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.bar.example.com", Targets: endpoint.Targets{"0 50 30192 foo.bar.example.com"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.bar.example.com", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1775,7 +1775,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1819,7 +1819,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1865,7 +1865,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1912,7 +1912,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"10.0.1.1", "10.0.1.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
@ -1959,7 +1959,7 @@ func TestNodePortServices(t *testing.T) {
|
||||
},
|
||||
nil,
|
||||
[]*endpoint.Endpoint{
|
||||
{DNSName: "_30192._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "_foo._tcp.foo.example.org", Targets: endpoint.Targets{"0 50 30192 foo.example.org"}, RecordType: endpoint.RecordTypeSRV},
|
||||
{DNSName: "foo.example.org", Targets: endpoint.Targets{"54.10.11.1", "54.10.11.2"}, RecordType: endpoint.RecordTypeA},
|
||||
},
|
||||
false,
|
||||
|
@ -83,12 +83,12 @@ type SingletonClientGenerator struct {
|
||||
kubeClient kubernetes.Interface
|
||||
istioClient *istioclient.Clientset
|
||||
cfClient *cfclient.Client
|
||||
contourClient dynamic.Interface
|
||||
dynKubeClient dynamic.Interface
|
||||
openshiftClient openshift.Interface
|
||||
kubeOnce sync.Once
|
||||
istioOnce sync.Once
|
||||
cfOnce sync.Once
|
||||
contourOnce sync.Once
|
||||
dynCliOnce sync.Once
|
||||
openshiftOnce sync.Once
|
||||
}
|
||||
|
||||
@ -134,13 +134,13 @@ func NewCFClient(cfAPIEndpoint string, cfUsername string, cfPassword string) (*c
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// DynamicKubernetesClient generates a contour client if it was not created before
|
||||
// DynamicKubernetesClient generates a dynamic client if it was not created before
|
||||
func (p *SingletonClientGenerator) DynamicKubernetesClient() (dynamic.Interface, error) {
|
||||
var err error
|
||||
p.contourOnce.Do(func() {
|
||||
p.contourClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
|
||||
p.dynCliOnce.Do(func() {
|
||||
p.dynKubeClient, err = NewDynamicKubernetesClient(p.KubeConfig, p.APIServerURL, p.RequestTimeout)
|
||||
})
|
||||
return p.contourClient, err
|
||||
return p.dynKubeClient, err
|
||||
}
|
||||
|
||||
// OpenShiftClient generates an openshift client if it was not created before
|
||||
@ -213,6 +213,16 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
|
||||
return nil, err
|
||||
}
|
||||
return NewCloudFoundrySource(cfClient)
|
||||
case "ambassador-host":
|
||||
kubernetesClient, err := p.KubeClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dynamicClient, err := p.DynamicKubernetesClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewAmbassadorHostSource(dynamicClient, kubernetesClient, cfg.Namespace)
|
||||
case "contour-ingressroute":
|
||||
kubernetesClient, err := p.KubeClient()
|
||||
if err != nil {
|
||||
|
Loading…
Reference in New Issue
Block a user