Merge remote-tracking branch 'origin/master' into ingress-class-filtering

This commit is contained in:
Dave Salisbury 2021-12-28 14:27:30 +11:00
commit 097df5c458
43 changed files with 981 additions and 406 deletions

View File

@ -7,6 +7,7 @@ on:
jobs:
lint-test:
if: github.repository == 'kubernetes-sigs/external-dns'
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -36,7 +37,7 @@ jobs:
fi
- name: Run chart-testing (lint)
run: ct lint
run: ct lint --check-version-increment=false
- name: Create Kind cluster
uses: helm/kind-action@v1.2.0

View File

@ -5,10 +5,11 @@ on:
branches:
- master
paths:
- "charts/external-dns/**"
- "charts/external-dns/Chart.yaml"
jobs:
release:
if: github.repository == 'kubernetes-sigs/external-dns'
runs-on: ubuntu-latest
steps:
- name: Checkout

View File

@ -1,8 +1,6 @@
name: trivy vulnerability scanner
on:
push:
branches:
- master
jobs:
build:
name: Build
@ -10,18 +8,10 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build an image from Dockerfile
run: |
make build.docker
- uses: cachix/install-nix-action@v13
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: workflow/nix-shell-action@v1
with:
packages: trivy
script: |
make build.docker
./scripts/run-trivy.sh
- name: Run trivy
run: |
./scripts/run-trivy.sh

1
OWNERS
View File

@ -4,6 +4,7 @@
approvers:
- raffo
- njuettner
- seanmalloy
reviewers:
- njuettner

View File

@ -110,6 +110,13 @@ The following table clarifies the current status of the providers according to t
| GoDaddy | Alpha | |
| Gandi | Alpha | @packi |
## Kubernetes version compatibility
| ExternalDNS | <= 0.9.x | >= 0.10.0 |
| ------------------ | :----------------: | :----------------: |
| Kubernetes <= 1.18 | :white_check_mark: | :x: |
| Kubernetes >= 1.19 | :x: | :white_check_mark: |
## Running ExternalDNS:
The are two ways of running ExternalDNS:

6
charts/OWNERS Normal file
View File

@ -0,0 +1,6 @@
labels:
- chart
approvers:
- stevehipwell
reviewers:
- stevehipwell

View File

@ -2,8 +2,8 @@ apiVersion: v2
name: external-dns
description: ExternalDNS synchronizes exposed Kubernetes Services and Ingresses with DNS providers.
type: application
version: 1.4.0
appVersion: 0.10.1
version: 1.7.0
appVersion: 0.10.2
keywords:
- kubernetes
- external-dns
@ -17,5 +17,9 @@ maintainers:
email: steve.hipwell@gmail.com
annotations:
artifacthub.io/changes: |
- kind: added
description: "Allow custom ClusterRole rules to be specified for sources without defaults."
- kind: changed
description: "Update image to v0.10.1"
description: "Update ExternalDNS version to v0.10.2."
- kind: changed
description: "Set ClusterRole rules based more enabled sources."

View File

@ -21,7 +21,7 @@ helm upgrade --install external-dns/external-dns
The following table lists the configurable parameters of the _ExternalDNS_ chart and their default values.
| Parameter | Description | Default |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------|
| `image.repository` | Image repository. | `k8s.gcr.io/external-dns/external-dns` |
| `image.tag` | Image tag, will override the default tag derived from the chart app version. | `""` |
| `image.pullPolicy` | Image pull policy. | `IfNotPresent` |
@ -32,6 +32,7 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart
| `serviceAccount.annotations` | Annotations to add to the service account. | `{}` |
| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the full name template. | `""` |
| `rbac.create` | If `true`, create the RBAC resources. | `true` |
| `rbac.additionalPermissions` | Additional permissions to be added to the cluster role. | `{}` |
| `podLabels` | Labels to add to the pod. | `{}` |
| `podAnnotations` | Annotations to add to the pod. | `{}` |
| `podSecurityContext` | Security context for the pod, this supports the full [PodSecurityContext](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#podsecuritycontext-v1-core) API. | _see values.yaml_ |
@ -45,6 +46,7 @@ The following table lists the configurable parameters of the _ExternalDNS_ chart
| `env` | [Environment variables](https://kubernetes.io/docs/tasks/inject-data-application/define-environment-variable-container/) for the _external-dns_ container, this supports the full [EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#envvar-v1-core) API including secrets and configmaps. | `[]` |
| `livenessProbe` | [Liveness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) for the _external-dns_ container, this supports the full [Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#probe-v1-core) API. | See _values.yaml_ |
| `readinessProbe` | [Readiness probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) for the _external-dns_ container, this supports the full [Probe](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#probe-v1-core) API. | See _values.yaml_ |
| `service.annotations` | Annotations to add to the service. | `{}` |
| `service.port` | Port to expose via the service. | `7979` |
| `extraVolumes` | Additional volumes for the pod, this supports the full [VolumeDevice](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volumedevice-v1-core) API. | `[]` |
| `extraVolumeMounts` | Additional volume mounts for the _external-dns_ container, this supports the full [VolumeMount](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#volumemount-v1-core) API. | `[]` |

View File

@ -6,13 +6,98 @@ metadata:
labels:
{{- include "external-dns.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
{{- if or (has "node" .Values.sources) (has "pod" .Values.sources) (has "service" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "gloo-proxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }}
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
{{- end }}
{{- if or (has "pod" .Values.sources) (has "service" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "gloo-proxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }}
- apiGroups: [""]
resources: ["pods"]
verbs: ["get","watch","list"]
{{- end }}
{{- if or (has "service" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "gloo-proxy" .Values.sources) (has "istio-gateway" .Values.sources) (has "istio-virtualservice" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }}
- apiGroups: [""]
resources: ["services","endpoints"]
verbs: ["get","watch","list"]
{{- end }}
{{- if or (has "ingress" .Values.sources) (has "contour-httpproxy" .Values.sources) (has "openshift-route" .Values.sources) (has "skipper-routegroup" .Values.sources) }}
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "istio-gateway" .Values.sources }}
- apiGroups: ["networking.istio.io"]
resources: ["gateways"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "istio-virtualservice" .Values.sources }}
- apiGroups: ["networking.istio.io"]
resources: ["virtualservices"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "ambassador-host" .Values.sources }}
- apiGroups: ["getambassador.io"]
resources: ["hosts","ingresses"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "contour-httpproxy" .Values.sources }}
- apiGroups: ["projectcontour.io"]
resources: ["httpproxies"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "contour-ingressroute" .Values.sources }}
- apiGroups: ["contour.heptio.com"]
resources: ["ingressroutes"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "crd" .Values.sources }}
- apiGroups: ["externaldns.k8s.io"]
resources: ["dnsendpoints"]
verbs: ["get","watch","list"]
- apiGroups: ["externaldns.k8s.io"]
resources: ["dnsendpoints/status"]
verbs: ["*"]
{{- end }}
{{- if has "gloo-proxy" .Values.sources }}
- apiGroups: ["gloo.solo.io","gateway.solo.io"]
resources: ["proxies","virtualservices"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "kong-tcpingress" .Values.sources }}
- apiGroups: ["configuration.konghq.com"]
resources: ["tcpingresses"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "openshift-route" .Values.sources }}
- apiGroups: ["route.openshift.io"]
resources: ["routes"]
verbs: ["get","watch","list"]
{{- end }}
{{- if has "skipper-routegroup" .Values.sources }}
- apiGroups: ["zalando.org"]
resources: ["routegroups"]
verbs: ["get","watch","list"]
- apiGroups: ["zalando.org"]
resources: ["routegroups/status"]
verbs: ["patch","update"]
{{- end }}
{{- with .Values.rbac.additionalPermissions }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}

View File

@ -4,6 +4,10 @@ metadata:
name: {{ include "external-dns.fullname" . }}
labels:
{{- include "external-dns.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: ClusterIP
selector:

View File

@ -24,6 +24,7 @@ serviceAccount:
rbac:
# Specifies whether RBAC resources should be created
create: true
additionalPermissions: {}
podLabels: {}
@ -73,6 +74,7 @@ readinessProbe:
service:
port: 7979
annotations: {}
extraVolumes: []

View File

@ -3,7 +3,7 @@ timeout: 5000s
options:
substitution_option: ALLOW_LOOSE
steps:
- name: "gcr.io/k8s-testimages/gcb-docker-gcloud:v20200824-5d057db"
- name: "gcr.io/k8s-staging-test-infra/gcb-docker-gcloud:v20211118-2f2d816b90"
entrypoint: make
env:
- DOCKER_CLI_EXPERIMENTAL=enabled

View File

@ -94,6 +94,30 @@ var (
Help: "Number of Source errors.",
},
)
registryARecords = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "registry",
Name: "a_records",
Help: "Number of Registry A records.",
},
)
sourceARecords = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "source",
Name: "a_records",
Help: "Number of Source A records.",
},
)
verifiedARecords = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: "external_dns",
Subsystem: "controller",
Name: "verified_a_records",
Help: "Number of DNS A-records that exists both in source and registry.",
},
)
)
func init() {
@ -105,6 +129,9 @@ func init() {
prometheus.MustRegister(deprecatedRegistryErrors)
prometheus.MustRegister(deprecatedSourceErrors)
prometheus.MustRegister(controllerNoChangesTotal)
prometheus.MustRegister(registryARecords)
prometheus.MustRegister(sourceARecords)
prometheus.MustRegister(verifiedARecords)
}
// Controller is responsible for orchestrating the different components.
@ -141,7 +168,8 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return err
}
registryEndpointsTotal.Set(float64(len(records)))
regARecords := filterARecords(records)
registryARecords.Set(float64(len(regARecords)))
ctx = context.WithValue(ctx, provider.RecordsContextKey, records)
endpoints, err := c.Source.Endpoints(ctx)
@ -151,7 +179,10 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return err
}
sourceEndpointsTotal.Set(float64(len(endpoints)))
srcARecords := filterARecords(endpoints)
sourceARecords.Set(float64(len(srcARecords)))
vRecords := fetchMatchingARecords(endpoints, records)
verifiedARecords.Set(float64(len(vRecords)))
endpoints = c.Registry.AdjustEndpoints(endpoints)
plan := &plan.Plan{
@ -181,6 +212,32 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return nil
}
// Checks and returns the intersection of A records in endpoint and registry.
func fetchMatchingARecords(endpoints []*endpoint.Endpoint, registryRecords []*endpoint.Endpoint) []string {
aRecords := filterARecords(endpoints)
recordsMap := make(map[string]struct{})
for _, regRecord := range registryRecords {
recordsMap[regRecord.DNSName] = struct{}{}
}
var cm []string
for _, sourceRecord := range aRecords {
if _, found := recordsMap[sourceRecord]; found {
cm = append(cm, sourceRecord)
}
}
return cm
}
func filterARecords(endpoints []*endpoint.Endpoint) []string {
var aRecords []string
for _, endPoint := range endpoints {
if endPoint.RecordType == endpoint.RecordTypeA {
aRecords = append(aRecords, endPoint.DNSName)
}
}
return aRecords
}
// ScheduleRunOnce makes sure execution happens at most once per interval.
func (c *Controller) ScheduleRunOnce(now time.Time) {
c.nextRunAtMux.Lock()

View File

@ -19,6 +19,8 @@ package controller
import (
"context"
"errors"
"github.com/prometheus/client_golang/prometheus"
"math"
"reflect"
"testing"
"time"
@ -49,6 +51,10 @@ type filteredMockProvider struct {
ApplyChangesCalls []*plan.Changes
}
type errorMockProvider struct {
mockProvider
}
func (p *filteredMockProvider) GetDomainFilter() endpoint.DomainFilterInterface {
return p.domainFilter
}
@ -70,6 +76,10 @@ func (p *mockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error
return p.RecordsStore, nil
}
func (p *errorMockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
return nil, errors.New("error for testing")
}
// ApplyChanges validates that the passed in changes satisfy the assumptions.
func (p *mockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
if len(changes.Create) != len(p.ExpectChanges.Create) {
@ -180,6 +190,13 @@ func TestRunOnce(t *testing.T) {
// Validate that the mock source was called.
source.AssertExpectations(t)
// check the verified records
assert.Equal(t, math.Float64bits(1), valueFromMetric(verifiedARecords))
}
func valueFromMetric(metric prometheus.Gauge) uint64 {
ref := reflect.ValueOf(metric)
return reflect.Indirect(ref).FieldByName("valBits").Uint()
}
func TestShouldRunOnce(t *testing.T) {
@ -376,3 +393,127 @@ func TestWhenMultipleControllerConsidersAllFilteredComain(t *testing.T) {
},
)
}
func TestVerifyARecords(t *testing.T) {
testControllerFiltersDomains(
t,
[]*endpoint.Endpoint{
{
DNSName: "create-record.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "some-record.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
},
endpoint.NewDomainFilter([]string{"used.tld"}),
[]*endpoint.Endpoint{
{
DNSName: "some-record.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "create-record.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
},
[]*plan.Changes{},
)
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords))
testControllerFiltersDomains(
t,
[]*endpoint.Endpoint{
{
DNSName: "some-record.1.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "some-record.2.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "some-record.3.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"24.24.24.24"},
},
},
endpoint.NewDomainFilter([]string{"used.tld"}),
[]*endpoint.Endpoint{
{
DNSName: "some-record.1.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "some-record.2.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
},
[]*plan.Changes{{
Create: []*endpoint.Endpoint{
{
DNSName: "some-record.3.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"24.24.24.24"},
},
},
}},
)
assert.Equal(t, math.Float64bits(2), valueFromMetric(verifiedARecords))
}
func TestARecords(t *testing.T) {
testControllerFiltersDomains(
t,
[]*endpoint.Endpoint{
{
DNSName: "record1.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "record2.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
{
DNSName: "_mysql-svc._tcp.mysql.used.tld",
RecordType: endpoint.RecordTypeSRV,
Targets: endpoint.Targets{"0 50 30007 mysql.used.tld"},
},
},
endpoint.NewDomainFilter([]string{"used.tld"}),
[]*endpoint.Endpoint{
{
DNSName: "record1.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
{
DNSName: "_mysql-svc._tcp.mysql.used.tld",
RecordType: endpoint.RecordTypeSRV,
Targets: endpoint.Targets{"0 50 30007 mysql.used.tld"},
},
},
[]*plan.Changes{{
Create: []*endpoint.Endpoint{
{
DNSName: "record2.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
},
}},
)
assert.Equal(t, math.Float64bits(2), valueFromMetric(sourceARecords))
assert.Equal(t, math.Float64bits(1), valueFromMetric(registryARecords))
}

View File

@ -0,0 +1,5 @@
# Helm Chart
## Chart Changes
When contributing chart changes please follow the same process as when contributing other content but also please **DON'T** modify _Chart.yaml_ in the PR as this would result in a chart release when merged and will mean that your PR will need modifying before it can be accepted. The chart version will be updated as part of the PR to release the chart.

View File

@ -185,6 +185,10 @@ Here is the full list of available metrics provided by ExternalDNS:
| external_dns_registry_errors_total | Number of Registry errors | Counter |
| external_dns_source_endpoints_total | Number of Endpoints in the registry | Gauge |
| external_dns_source_errors_total | Number of Source errors | Counter |
| external_dns_controller_verified_records | Number of DNS A-records that exists both in | Gauge |
| | source & registry | |
| external_dns_registry_a_records | Number of A records in registry | Gauge |
| external_dns_source_a_records | Number of A records in source | Gauge |
### How can I run ExternalDNS under a specific GCP Service Account, e.g. to access DNS records in other projects?

View File

@ -24,10 +24,21 @@ You must be an official maintainer of the project to be able to do a release.
### Steps
- Run `scripts/releaser.sh` to create a new GitHub release.
- Run `scripts/releaser.sh` to create a new GitHub release. Alternatively you can create a release in the GitHub UI making sure to click on the autogenerate release node feature.
- The step above will trigger the Kubernetes based CI/CD system [Prow](https://prow.k8s.io/?repo=kubernetes-sigs%2Fexternal-dns). Verify that a new image was built and uploaded to `gcr.io/k8s-staging-external-dns/external-dns`.
- Create a PR in the [k8s.io repo](https://github.com/kubernetes/k8s.io) (see https://github.com/kubernetes/k8s.io/pull/540 for reference) by taking the current staging image using the sha256 digest. Once the PR is merged, the image will be live with the corresponding tag specified in the PR.
- Verify that the image is pullable with the given tag (i.e. `v0.7.5`).
- Branch out from the default branch and run `scripts/kustomize-version-udapter.sh` to update the image tag used in the kustomization.yaml.
- Create an issue to release the corresponding Helm chart via the chart release process (below) assigned to a chart maintainer
- Create a PR with the kustomize change.
- Once the PR is merged, all is done :-)
## How to release a new chart version
The chart needs to be released in response to an ExternalDNS image release or on an as-needed basis; this should be triggered by an issue to release the chart.
### Steps
- Create a PR to update _Chart.yaml_ with the ExternalDNS version in `appVersion`, agreed on chart release version in `version` and `annotations` showing the changes
- Validate that the chart linting is successful
- Merge the PR to trigger a GitHub action to release the chart

View File

@ -83,14 +83,18 @@ metadata:
kubernetes.io/ingress.class: alb
name: echoserver
spec:
ingressClassName: alb
rules:
- host: echoserver.mycluster.example.org
http: &echoserver_root
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
- host: echoserver.example.org
http: *echoserver_root
```
@ -119,13 +123,17 @@ metadata:
kubernetes.io/ingress.class: alb
name: echoserver
spec:
ingressClassName: alb
rules:
- http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```
In the above example we create a default path that works for any hostname, and
@ -154,14 +162,18 @@ metadata:
kubernetes.io/ingress.class: alb
name: echoserver
spec:
ingressClassName: alb
rules:
- host: echoserver.example.org
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```
The above Ingress object will result in the creation of an ALB with a dualstack

View File

@ -55,7 +55,7 @@ If your EKS-managed cluster is >= 1.13 and was created after 2019-09-04, refer
to the [Amazon EKS
documentation](https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html)
for instructions on how to create the IAM Role. Otherwise, you will need to use
kiam or kube2iam.
kiam or kube2iam or set the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY on the deployment.
### kiam
@ -464,6 +464,58 @@ $ aws route53 delete-hosted-zone --id /hostedzone/ZEWFWZ4R16P7IB
## Throttling
Route53 has a [5 API requests per second per account hard quota](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests-route-53).
Running several fast polling ExternalDNS instances in a given account can easily hit that limit. Some ways to circumvent that issue includes:
* Augment the synchronization interval (`--interval`), at the cost of slower changes propagation.
* If the ExternalDNS managed zones list doesn't change frequently, set `--aws-zones-cache-duration` (zones list cache time-to-live) to a larger value. Note that zones list cache can be disabled with `--aws-zones-cache-duration=0s`.
Running several fast polling ExternalDNS instances in a given account can easily hit that limit. Some ways to reduce the request rate include:
* Reduce the polling loop's synchronization interval at the possible cost of slower change propagation (but see `--events` below to reduce the impact).
* `--interval=5m` (default `1m`)
* Trigger the polling loop on changes to K8s objects, rather than only at `interval`, to have responsive updates with long poll intervals
* `--events`
* Limit the [sources watched](https://github.com/kubernetes-sigs/external-dns/blob/master/pkg/apis/externaldns/types.go#L364) when the `--events` flag is specified to specific types, namespaces, labels, or annotations
* `--source=ingress --source=service` - specify multiple times for multiple sources
* `--namespace=my-app`
* `--label-filter=app in (my-app)`
* `--annotation-filter=kubernetes.io/ingress.class in (nginx-external)` - note that this filter would apply to services too..
* Limit services watched by type (not applicable to ingress or other types)
* `--service-type-filter=LoadBalancer` default `all`
* Limit the hosted zones considered
* `--zone-id-filter=ABCDEF12345678` - specify multiple times if needed
* `--domain-filter=example.com` by domain suffix - specify multiple times if needed
* `--regex-domain-filter=example*` by domain suffix but as a regex - overrides domain-filter
* `--exclude-domains=ignore.this.example.com` to exclude a domain or subdomain
* `--regex-domain-exclusion=ignore*` subtracts it's matches from `regex-domain-filter`'s matches
* `--aws-zone-type=public` only sync zones of this type `[public|private]`
* `--aws-zone-tags=owner=k8s` only sync zones with this tag
* If the list of zones managed by ExternalDNS doesn't change frequently, cache it by setting a TTL.
* `--aws-zones-cache-duration=3h` (default `0` - disabled)
* Increase the number of changes applied to Route53 in each batch
* `--aws-batch-change-size=4000` (default `1000`)
* Increase the interval between changes
* `--aws-batch-change-interval=10s` (default `1s`)
* Introducing some jitter to the pod initialization, so that when multiple instances of ExternalDNS are updated at the same time they do not make their requests on the same second.
A simple way to implement randomised startup is with an init container:
```
...
spec:
initContainers:
- name: init-jitter
image: k8s.gcr.io/external-dns/external-dns:v0.7.6
command:
- /bin/sh
- -c
- 'FOR=$((RANDOM % 10))s;echo "Sleeping for $FOR";sleep $FOR'
containers:
...
```
### EKS
An effective starting point for EKS with an ingress controller might look like:
```bash
--interval=5m
--events
--source=ingress
--domain-filter=example.com
--aws-zones-cache-duration=1h
```

View File

@ -20,6 +20,10 @@ BlueCat Gateway username and password can be supplied using the configuration fi
| rootZone | Yes |
| skipTLSVerify | No (default false) |
### HTTP proxy
BlueCat provider supports getting the proxy URL from the environment variables. The format is the one specified by golang's [http.ProxyFromEnvironment](https://pkg.go.dev/net/http#ProxyFromEnvironment).
## Deploy
Setup configuration file as k8s `Secret`.
```

View File

@ -145,14 +145,18 @@ metadata:
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.mycluster.example.org
http: &echoserver_root
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
- host: echoserver.example.org
http: *echoserver_root
```
@ -180,13 +184,17 @@ metadata:
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
ingressClassName: skipper
rules:
- http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```
In the above example we create a default path that works for any hostname, and
@ -213,14 +221,18 @@ metadata:
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.example.org
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```
The above Ingress object will result in the creation of an ALB with a dualstack
@ -247,14 +259,18 @@ metadata:
kubernetes.io/ingress.class: skipper
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.example.org
http:
paths:
- backend:
serviceName: echoserver
servicePort: 80
path: /
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```
The above Ingress object will result in the creation of an NLB. A

View File

@ -297,13 +297,18 @@ metadata:
annotations:
kubernetes.io/ingress.class: nginx
spec:
ingressClassName: nginx
rules:
- host: via-ingress.external-dns-test.gcp.zalan.do
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
- path: /
backend:
service:
name: nginx
port:
number: 80
pathType: Prefix
---
@ -593,13 +598,18 @@ metadata:
annotations:
kubernetes.io/ingress.class: nginx
spec:
ingressClassName: nginx
rules:
- host: via-ingress.external-dns-test.gcp.zalan.do
http:
paths:
- backend:
serviceName: nginx
servicePort: 80
- path: /
backend:
service:
name: nginx
port:
number: 80
pathType: Prefix
---
apiVersion: v1
kind: Service

View File

@ -2,6 +2,60 @@
This tutorial describes how to configure ExternalDNS to use the OpenShift Route source.
It is meant to supplement the other provider-specific setup tutorials.
### For OCP 4.x
In OCP 4.x, if you have multiple ingress controllers then you must specify an ingress controller name or a router name(you can get it from the route's Status.Ingress.RouterName field).
If you don't specify an ingress controller's or router name when you have multiple ingresscontrollers in your environment then the route gets populated with multiple entries of router canonical hostnames which causes external dns to create a CNAME record with multiple router canonical hostnames pointing to the route host which is a violation of RFC 1912 and is not allowed by Cloud Providers which leads to failure of record creation.
Once you specify the ingresscontroller or router name then that will be matched by the external-dns and the router canonical hostname corresponding to this routerName(which is present in route's Status.Ingress.RouterName field) is selected and a CNAME record of this route host pointing to this router canonical hostname is created.
Your externaldns CR shall be created as per the following example.
Replace names in the domain section and zone ID as per your environment.
This is example is for AWS environment.
```yaml
apiVersion: externaldns.olm.openshift.io/v1alpha1
kind: ExternalDNS
metadata:
name: sample1
spec:
domains:
- filterType: Include
matchType: Exact
names: apps.miheer.externaldns
provider:
type: AWS
source:
hostnameAnnotation: Allow
openshiftRouteOptions:
routerName: default
type: OpenShiftRoute
zones:
- Z05387772BD5723IZFRX3
```
This will create an externaldns pod with the following container args under spec in the external-dns namespace where `- --source=openshift-route` and `- --openshift-router-name=default` is added by the external-dns-operator.
```
spec:
containers:
- args:
- --domain-filter=apps.misalunk.externaldns
- --metrics-address=127.0.0.1:7979
- --txt-owner-id=external-dns-sample1
- --provider=aws
- --source=openshift-route
- --policy=sync
- --registry=txt
- --log-level=debug
- --zone-id-filter=Z05387772BD5723IZFRX3
- --openshift-router-name=default
- --txt-prefix=external-dns-
```
### For OCP 3.11 environment
### Prepare ROUTER_CANONICAL_HOSTNAME in default/router deployment
Read and go through [Finding the Host Name of the Router](https://docs.openshift.com/container-platform/3.11/install_config/router/default_haproxy_router.html#finding-router-hostname).
If no ROUTER_CANONICAL_HOSTNAME is set, you must annotate each route with external-dns.alpha.kubernetes.io/target!

8
go.mod
View File

@ -18,19 +18,19 @@ require (
github.com/aliyun/alibaba-cloud-sdk-go v1.61.357
github.com/aws/aws-sdk-go v1.40.53
github.com/bodgit/tsig v0.0.2
github.com/cloudflare/cloudflare-go v0.13.2
github.com/cloudflare/cloudflare-go v0.25.0
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.69.1
github.com/dnsimple/dnsimple-go v0.60.0
github.com/exoscale/egoscale v0.73.2
github.com/exoscale/egoscale v0.80.1
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/go-gandi/go-gandi v0.0.0-20200921091836-0d8a64b9cc09
github.com/go-logr/logr v1.1.0 // indirect
github.com/golang/sync v0.0.0-20180314180146-1d60e4601c6f
github.com/google/go-cmp v0.5.6
github.com/gophercloud/gophercloud v0.21.0
github.com/gophercloud/gophercloud v0.22.0
github.com/hooklift/gowsdl v0.5.0
github.com/infobloxopen/infoblox-go-client v1.1.1
github.com/json-iterator/go v1.1.12 // indirect
@ -48,7 +48,7 @@ require (
github.com/oracle/oci-go-sdk v21.4.0+incompatible
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
github.com/pkg/errors v0.9.1
github.com/projectcontour/contour v1.18.1
github.com/projectcontour/contour v1.18.2
github.com/prometheus/client_golang v1.11.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f
github.com/sirupsen/logrus v1.8.1

26
go.sum
View File

@ -126,9 +126,9 @@ github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a/go.mod h1:AsBYmtPY5rg
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhprfxlpofQ=
github.com/StackExchange/dnscontrol v0.2.8 h1:7jviqDH9cIqRSRpH0UxgmpT7a8CwEhs9mLHBhoYhXo8=
github.com/StackExchange/dnscontrol v0.2.8/go.mod h1:BH+5nX50JxHDdb3+AD/z/UfYMCc7iaqEkRtQ+NjcFGE=
github.com/Venafi/vcert/v4 v4.13.1/go.mod h1:Z3sJFoAurFNXPpoSUSHq46aIeHLiGQEMDhprfxlpofQ=
github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
@ -212,8 +212,13 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cloudflare-go v0.13.2 h1:bhMGoNhAg21DuqJjU9jQepRRft6vYfo6pejT3NN4V6A=
github.com/cloudflare/cloudflare-go v0.10.1 h1:d2CL6F9k2O0Ux0w27LgogJ5UOzZRj6a/hDPFqPP68d8=
github.com/cloudflare/cloudflare-go v0.10.1/go.mod h1:C0Y6eWnTJPMK2ceuOxx2pjh78UUHihcXeTTHb8r7QjU=
github.com/cloudflare/cloudflare-go v0.13.2/go.mod h1:27kfc1apuifUmJhp069y0+hwlKDg4bd8LWlu7oKeZvM=
github.com/cloudflare/cloudflare-go v0.22.0 h1:3TYbkyz/IBbM6XozrTKWiNpv7RjJGamALy9yu2SBqTo=
github.com/cloudflare/cloudflare-go v0.22.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI=
github.com/cloudflare/cloudflare-go v0.25.0 h1:GwyKwGq8ciGNjKiTpjj6RvU3+uJNuPBNjlUkeQRx0yU=
github.com/cloudflare/cloudflare-go v0.25.0/go.mod h1:sPWL/lIC6biLEdyGZwBQ1rGQKF1FhM7N60fuNiFdYTI=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/cncf/udpa v0.0.0-20200324003616-bae28a880fdb/go.mod h1:HNVadOiXCy7Jk3R2knJ+qm++zkncJxxBMpjdGgJ+UJc=
@ -328,8 +333,8 @@ github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs=
github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/exoscale/egoscale v0.73.2 h1:YGy0YufwuaRduaqTM6ptyZQpBxr2L9eA6kKfmGW8jKg=
github.com/exoscale/egoscale v0.73.2/go.mod h1:Fpy/cIVjiUkI0DntkoFl4fZpaeqRFQ6SVcX1A0APg0E=
github.com/exoscale/egoscale v0.80.1 h1:+JR0RhGKjkgHIluWeRWTTnveSGIRE1ifbpnjB/Fkwlk=
github.com/exoscale/egoscale v0.80.1/go.mod h1:wi0myUxPsV8SdEtdJHQJxFLL/wEw9fiw9Gs1PWRkvkM=
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -571,8 +576,8 @@ github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9
github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=
github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gophercloud/gophercloud v0.21.0 h1:21rxoQM7cSaZCPgfP45h71Vt1amZa942l7AtUWLOI2I=
github.com/gophercloud/gophercloud v0.21.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4=
github.com/gophercloud/gophercloud v0.22.0 h1:9lFISNLafZcecT0xUveIMt3IafexC6DIV9ek1SZdSMw=
github.com/gophercloud/gophercloud v0.22.0/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -732,7 +737,6 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d h1:JV46OtdhH2vVt8mJ1EWUE94k99vbN9fZs1WQ8kcEapU=
github.com/kubermatic/glog-logrus v0.0.0-20180829085450-3fa5b9870d1d/go.mod h1:CHQ3o5KBH1PIS2Fb1mRLTIWO5YzP9kSUB3KoCICwlvA=
github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
@ -782,6 +786,7 @@ github.com/mattn/go-oci8 v0.0.7/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mN
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.12.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
@ -858,6 +863,7 @@ github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:v
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.4.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -938,8 +944,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/projectcontour/contour v1.18.1 h1:J1MmqchDFuou066Gkr13eYtPaBnfb5j7h5ETH9OfIfQ=
github.com/projectcontour/contour v1.18.1/go.mod h1:prL5IyPK2ek6MUWwm8C5S1pHsWOE/2Y8Ym7ak3feVpo=
github.com/projectcontour/contour v1.18.2 h1:q16Q0f8mb14DATpCus/qBONawBy1Zn4vrvBj1bXh3hc=
github.com/projectcontour/contour v1.18.2/go.mod h1:prL5IyPK2ek6MUWwm8C5S1pHsWOE/2Y8Ym7ak3feVpo=
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
@ -1084,6 +1090,7 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
@ -1277,6 +1284,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b h1:eB48h3HiRycXNy8E0Gf5e0hv7YT6Kt14L/D73G1fuwo=

View File

@ -3,7 +3,7 @@ kind: Kustomization
images:
- name: k8s.gcr.io/external-dns/external-dns
newTag: v0.10.0
newTag: v0.10.2
resources:
- ./external-dns-deployment.yaml

View File

@ -132,6 +132,7 @@ func main() {
SkipperRouteGroupVersion: cfg.SkipperRouteGroupVersion,
RequestTimeout: cfg.RequestTimeout,
DefaultTargets: cfg.DefaultTargets,
OCPRouterName: cfg.OCPRouterName,
}
// Lookup all the selected sources by names and pass them the desired configuration.

View File

@ -176,6 +176,7 @@ type Config struct {
GoDaddySecretKey string `secure:"yes"`
GoDaddyTTL int64
GoDaddyOTE bool
OCPRouterName string
}
var defaultConfig = &Config{
@ -363,8 +364,9 @@ func (cfg *Config) ParseFlags(args []string) error {
// Flags related to Skipper RouteGroup
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing sources
// Flags related to processing source
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, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress")
app.Flag("openshift-router-name", "if source is openshift-route then you can pass the ingress controller name. Based on this name external-dns will select the respective router from the route status and map that routerCanonicalHostname to the route host while creating a CNAME record.").StringVar(&cfg.OCPRouterName)
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)
app.Flag("label-filter", "Filter sources managed by external-dns via label selector when listing all resources; currently supported by source types CRD, ingress, service and openshift-route").Default(defaultConfig.LabelFilter).StringVar(&cfg.LabelFilter)

View File

@ -115,6 +115,7 @@ var (
DigitalOceanAPIPageSize: 50,
ManagedDNSRecordTypes: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
RFC2136BatchChangeSize: 50,
OCPRouterName: "default",
}
overriddenConfig = &Config{
@ -225,6 +226,7 @@ func TestParseFlags(t *testing.T) {
args: []string{
"--source=service",
"--provider=google",
"--openshift-router-name=default",
},
envVars: map[string]string{},
expected: minimalConfig,

View File

@ -353,6 +353,7 @@ func TestAWSRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"),
endpoint.NewEndpoint("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
})
@ -376,6 +377,7 @@ func TestAWSRecords(t *testing.T) {
endpoint.NewEndpointWithTTL("multi-value-answer-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set").WithProviderSpecific(providerSpecificMultiValueAnswer, ""),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationContinentCode, "EU"),
endpoint.NewEndpointWithTTL("geolocation-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificGeolocationCountryCode, "DE"),
endpoint.NewEndpointWithTTL("geolocation-subdivision-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificGeolocationSubdivisionCode, "NY"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id"),
endpoint.NewEndpointWithTTL("healthcheck-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "4.3.2.1").WithSetIdentifier("test-set-2").WithProviderSpecific(providerSpecificWeight, "20").WithProviderSpecific(providerSpecificHealthCheckID, "abc-def-healthcheck-id"),
})

View File

@ -18,12 +18,10 @@ package azure
import (
"context"
"strings"
"testing"
"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/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/stretchr/testify/assert"
@ -446,59 +444,6 @@ func testAzureApplyChangesInternal(t *testing.T, dryRun bool, client RecordSetsC
}
}
func TestAzureGetAccessToken(t *testing.T) {
env := azure.PublicCloud
cfg := config{
ClientID: "",
ClientSecret: "",
TenantID: "",
UseManagedIdentityExtension: false,
}
_, err := getAccessToken(cfg, env)
if err == nil {
t.Fatalf("expected to fail, but got no error")
}
// Expect to use managed identity in this case
cfg = config{
ClientID: "msi",
ClientSecret: "msi",
TenantID: "cefe8aef-5127-4d65-a299-012053f81f60",
UserAssignedIdentityID: "userAssignedIdentityClientID",
UseManagedIdentityExtension: true,
}
token, err := getAccessToken(cfg, env)
if err != nil {
t.Fatalf("expected to construct a token successfully, but got error %v", err)
}
_, err = token.MarshalJSON()
if err == nil ||
!strings.Contains(err.Error(), "marshalling ServicePrincipalMSISecret is not supported") {
t.Fatalf("expected to fail to marshal token, but got %v", err)
}
// Expect to use SPN in this case
cfg = config{
ClientID: "SPNClientID",
ClientSecret: "SPNSecret",
TenantID: "cefe8aef-5127-4d65-a299-012053f81f60",
UserAssignedIdentityID: "userAssignedIdentityClientID",
UseManagedIdentityExtension: true,
}
token, err = getAccessToken(cfg, env)
if err != nil {
t.Fatalf("expected to construct a token successfully, but got error %v", err)
}
innerToken, err := token.MarshalJSON()
if err != nil {
t.Fatalf("expected to marshal token successfully, but got error %v", err)
}
if !strings.Contains(string(innerToken), "SPNClientID") {
t.Fatalf("expect the clientID of the token is SPNClientID, but got token %s", string(innerToken))
}
}
func TestAzureNameFilter(t *testing.T) {
provider, err := newMockedAzureProvider(endpoint.NewDomainFilter([]string{"nginx.example.com"}), endpoint.NewDomainFilter([]string{"example.com"}), provider.NewZoneIDFilter([]string{""}), true, "k8s", "",
&[]dns.Zone{

View File

@ -19,7 +19,6 @@ package azure
import (
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
@ -104,10 +103,6 @@ func getAccessToken(cfg config, environment azure.Environment) (*adal.ServicePri
// Try to retrieve token with MSI.
if cfg.UseManagedIdentityExtension {
log.Info("Using managed identity extension to retrieve access token for Azure API.")
os.Setenv("MSI_ENDPOINT", "http://dummy")
defer func() {
os.Unsetenv("MSI_ENDPOINT")
}()
if cfg.UserAssignedIdentityID != "" {
log.Infof("Resolving to user assigned identity, client id is %s.", cfg.UserAssignedIdentityID)

View File

@ -587,10 +587,7 @@ func getBluecatGatewayToken(cfg bluecatConfig) (string, http.Cookie, error) {
return "", http.Cookie{}, errors.Wrap(err, "could not unmarshal credentials for bluecat gateway config")
}
c := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: cfg.SkipTLSVerify},
}}
c := newHTTPClient(cfg.SkipTLSVerify)
resp, err := c.Post(cfg.GatewayHost+"/rest_login", "application/json", bytes.NewBuffer(body))
if err != nil {
@ -622,12 +619,8 @@ func getBluecatGatewayToken(cfg bluecatConfig) (string, http.Cookie, error) {
}
func (c GatewayClientConfig) getBluecatZones(zoneName string) ([]BluecatZone, error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zoneName)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration + "/views/" + c.View + "/" + zonePath
req, err := c.buildHTTPRequest("GET", url, nil)
@ -660,12 +653,7 @@ func (c GatewayClientConfig) getBluecatZones(zoneName string) ([]BluecatZone, er
}
func (c GatewayClientConfig) getHostRecords(zone string, records *[]BluecatHostRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
@ -692,12 +680,7 @@ func (c GatewayClientConfig) getHostRecords(zone string, records *[]BluecatHostR
}
func (c GatewayClientConfig) getCNAMERecords(zone string, records *[]BluecatCNAMERecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
@ -724,12 +707,7 @@ func (c GatewayClientConfig) getCNAMERecords(zone string, records *[]BluecatCNAM
}
func (c GatewayClientConfig) getTXTRecords(zone string, records *[]BluecatTXTRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
@ -757,12 +735,7 @@ func (c GatewayClientConfig) getTXTRecords(zone string, records *[]BluecatTXTRec
}
func (c GatewayClientConfig) getHostRecord(name string, record *BluecatHostRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -785,12 +758,7 @@ func (c GatewayClientConfig) getHostRecord(name string, record *BluecatHostRecor
}
func (c GatewayClientConfig) getCNAMERecord(name string, record *BluecatCNAMERecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -813,12 +781,7 @@ func (c GatewayClientConfig) getCNAMERecord(name string, record *BluecatCNAMERec
}
func (c GatewayClientConfig) getTXTRecord(name string, record *BluecatTXTRecord) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -842,12 +805,7 @@ func (c GatewayClientConfig) getTXTRecord(name string, record *BluecatTXTRecord)
}
func (c GatewayClientConfig) createHostRecord(zone string, req *bluecatCreateHostRecordRequest) (res interface{}, err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
@ -866,12 +824,7 @@ func (c GatewayClientConfig) createHostRecord(zone string, req *bluecatCreateHos
}
func (c GatewayClientConfig) createCNAMERecord(zone string, req *bluecatCreateCNAMERecordRequest) (res interface{}, err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
@ -892,12 +845,7 @@ func (c GatewayClientConfig) createCNAMERecord(zone string, req *bluecatCreateCN
}
func (c GatewayClientConfig) createTXTRecord(zone string, req *bluecatCreateTXTRecordRequest) (interface{}, error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
zonePath := expandZone(zone)
// Remove the trailing 'zones/'
@ -917,12 +865,7 @@ func (c GatewayClientConfig) createTXTRecord(zone string, req *bluecatCreateTXTR
}
func (c GatewayClientConfig) deleteHostRecord(name string, zone string) (err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -941,12 +884,7 @@ func (c GatewayClientConfig) deleteHostRecord(name string, zone string) (err err
}
func (c GatewayClientConfig) deleteCNAMERecord(name string, zone string) (err error) {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -965,12 +903,7 @@ func (c GatewayClientConfig) deleteCNAMERecord(name string, zone string) (err er
}
func (c GatewayClientConfig) deleteTXTRecord(name string, zone string) error {
transportCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.SkipTLSVerify},
}
client := &http.Client{
Transport: transportCfg,
}
client := newHTTPClient(c.SkipTLSVerify)
url := c.Host + "/api/v1/configurations/" + c.DNSConfiguration +
"/views/" + c.View + "/" +
@ -1042,3 +975,15 @@ func extractOwnerfromTXTRecord(propString string) (string, error) {
}
return strings.Split(match[0], "=")[1], nil
}
// newHTTPClient returns an instance of http client
func newHTTPClient(skipTLSVerify bool) *http.Client {
return &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: skipTLSVerify,
},
},
}
}

View File

@ -43,6 +43,15 @@ const (
defaultCloudFlareRecordTTL = 1
)
// We have to use pointers to bools now, as the upstream cloudflare-go library requires them
// see: https://github.com/cloudflare/cloudflare-go/pull/595
// proxyEnabled is a pointer to a bool true showing the record should be proxied through cloudflare
var proxyEnabled *bool = boolPtr(true)
// proxyDisabled is a pointer to a bool false showing the record should not be proxied through cloudflare
var proxyDisabled *bool = boolPtr(false)
var cloudFlareTypeNotSupported = map[string]bool{
"LOC": true,
"MX": true,
@ -54,53 +63,53 @@ var cloudFlareTypeNotSupported = map[string]bool{
// cloudFlareDNS is the subset of the CloudFlare API that we actually use. Add methods as required. Signatures must match exactly.
type cloudFlareDNS interface {
UserDetails() (cloudflare.User, error)
UserDetails(ctx context.Context) (cloudflare.User, error)
ZoneIDByName(zoneName string) (string, error)
ListZones(zoneID ...string) ([]cloudflare.Zone, error)
ListZones(ctx context.Context, zoneID ...string) ([]cloudflare.Zone, error)
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
ZoneDetails(zoneID string) (cloudflare.Zone, error)
DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error)
DeleteDNSRecord(zoneID, recordID string) error
UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error
ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error)
DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error)
CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error)
DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error
UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr cloudflare.DNSRecord) error
}
type zoneService struct {
service *cloudflare.API
}
func (z zoneService) UserDetails() (cloudflare.User, error) {
return z.service.UserDetails()
func (z zoneService) UserDetails(ctx context.Context) (cloudflare.User, error) {
return z.service.UserDetails(ctx)
}
func (z zoneService) ListZones(zoneID ...string) ([]cloudflare.Zone, error) {
return z.service.ListZones(zoneID...)
func (z zoneService) ListZones(ctx context.Context, zoneID ...string) ([]cloudflare.Zone, error) {
return z.service.ListZones(ctx, zoneID...)
}
func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
return z.service.ZoneIDByName(zoneName)
}
func (z zoneService) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
return z.service.CreateDNSRecord(zoneID, rr)
func (z zoneService) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
return z.service.CreateDNSRecord(ctx, zoneID, rr)
}
func (z zoneService) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) {
return z.service.DNSRecords(zoneID, rr)
func (z zoneService) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) {
return z.service.DNSRecords(ctx, zoneID, rr)
}
func (z zoneService) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error {
return z.service.UpdateDNSRecord(zoneID, recordID, rr)
func (z zoneService) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr cloudflare.DNSRecord) error {
return z.service.UpdateDNSRecord(ctx, zoneID, recordID, rr)
}
func (z zoneService) DeleteDNSRecord(zoneID, recordID string) error {
return z.service.DeleteDNSRecord(zoneID, recordID)
func (z zoneService) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error {
return z.service.DeleteDNSRecord(ctx, zoneID, recordID)
}
func (z zoneService) ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error) {
return z.service.ListZonesContext(ctx, opts...)
}
func (z zoneService) ZoneDetails(zoneID string) (cloudflare.Zone, error) {
return z.service.ZoneDetails(zoneID)
func (z zoneService) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
return z.service.ZoneDetails(ctx, zoneID)
}
// CloudFlareProvider is an implementation of Provider for CloudFlare DNS.
@ -162,7 +171,7 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
log.Debugln("zoneIDFilter configured. only looking up zone IDs defined")
for _, zoneID := range p.zoneIDFilter.ZoneIDs {
log.Debugf("looking up zone %s", zoneID)
detailResponse, err := p.Client.ZoneDetails(zoneID)
detailResponse, err := p.Client.ZoneDetails(ctx, zoneID)
if err != nil {
log.Errorf("zone %s lookup failed, %v", zoneID, err)
continue
@ -177,24 +186,20 @@ func (p *CloudFlareProvider) Zones(ctx context.Context) ([]cloudflare.Zone, erro
}
log.Debugln("no zoneIDFilter configured, looking at all zones")
for {
zonesResponse, err := p.Client.ListZonesContext(ctx, cloudflare.WithPagination(p.PaginationOptions))
if err != nil {
return nil, err
}
for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) {
log.Debugf("zone %s not in domain filter", zone.Name)
continue
}
result = append(result, zone)
}
if p.PaginationOptions.Page == zonesResponse.ResultInfo.TotalPages {
break
}
p.PaginationOptions.Page++
zonesResponse, err := p.Client.ListZonesContext(ctx)
if err != nil {
return nil, err
}
for _, zone := range zonesResponse.Result {
if !p.domainFilter.Match(zone.Name) {
log.Debugf("zone %s not in domain filter", zone.Name)
continue
}
result = append(result, zone)
}
return result, nil
}
@ -207,7 +212,7 @@ func (p *CloudFlareProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
endpoints := []*endpoint.Endpoint{}
for _, zone := range zones {
records, err := p.Client.DNSRecords(zone.ID, cloudflare.DNSRecord{})
records, err := p.Client.DNSRecords(ctx, zone.ID, cloudflare.DNSRecord{})
if err != nil {
return nil, err
}
@ -281,7 +286,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
changesByZone := p.changesByZone(zones, changes)
for zoneID, changes := range changesByZone {
records, err := p.Client.DNSRecords(zoneID, cloudflare.DNSRecord{})
records, err := p.Client.DNSRecords(ctx, zoneID, cloudflare.DNSRecord{})
if err != nil {
return fmt.Errorf("could not fetch records from zone, %v", err)
}
@ -306,7 +311,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
err := p.Client.UpdateDNSRecord(zoneID, recordID, change.ResourceRecord)
err := p.Client.UpdateDNSRecord(ctx, zoneID, recordID, change.ResourceRecord)
if err != nil {
log.WithFields(logFields).Errorf("failed to update record: %v", err)
}
@ -316,12 +321,12 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
err := p.Client.DeleteDNSRecord(zoneID, recordID)
err := p.Client.DeleteDNSRecord(ctx, zoneID, recordID)
if err != nil {
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
} else if change.Action == cloudFlareCreate {
_, err := p.Client.CreateDNSRecord(zoneID, change.ResourceRecord)
_, err := p.Client.CreateDNSRecord(ctx, zoneID, change.ResourceRecord)
if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err)
}
@ -382,16 +387,12 @@ func (p *CloudFlareProvider) newCloudFlareChange(action string, endpoint *endpoi
ttl = int(endpoint.RecordTTL)
}
if len(endpoint.Targets) > 1 {
log.Errorf("Updates should have just one target")
}
return &cloudFlareChange{
Action: action,
ResourceRecord: cloudflare.DNSRecord{
Name: endpoint.DNSName,
TTL: ttl,
Proxied: proxied,
Proxied: &proxied,
Type: endpoint.RecordType,
Content: target,
},
@ -450,8 +451,15 @@ func groupByNameAndType(records []cloudflare.DNSRecord) []*endpoint.Endpoint {
records[0].Type,
endpoint.TTL(records[0].TTL),
targets...).
WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(records[0].Proxied)))
WithProviderSpecific(source.CloudflareProxiedKey, strconv.FormatBool(*records[0].Proxied)),
)
}
return endpoints
}
// boolPtr is used as a helper function to return a pointer to a boolean
// Needed because some parameters require a pointer.
func boolPtr(b bool) *bool {
return &b
}

View File

@ -55,7 +55,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
Type: endpoint.RecordTypeA,
TTL: 120,
Content: "1.2.3.4",
Proxied: false,
Proxied: proxyDisabled,
},
{
ID: "2345678901",
@ -64,7 +64,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
Type: endpoint.RecordTypeA,
TTL: 120,
Content: "3.4.5.6",
Proxied: false,
Proxied: proxyDisabled,
},
{
ID: "1231231233",
@ -73,7 +73,7 @@ var ExampleDomain = []cloudflare.DNSRecord{
Type: endpoint.RecordTypeA,
TTL: 1,
Content: "2.3.4.5",
Proxied: false,
Proxied: proxyDisabled,
},
}
@ -105,7 +105,7 @@ func NewMockCloudFlareClientWithRecords(records map[string][]cloudflare.DNSRecor
return m
}
func (m *mockCloudFlareClient) CreateDNSRecord(zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) (*cloudflare.DNSRecordResponse, error) {
m.Actions = append(m.Actions, MockAction{
Name: "Create",
ZoneId: zoneID,
@ -118,7 +118,7 @@ func (m *mockCloudFlareClient) CreateDNSRecord(zoneID string, rr cloudflare.DNSR
return nil, nil
}
func (m *mockCloudFlareClient) DNSRecords(zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) {
func (m *mockCloudFlareClient) DNSRecords(ctx context.Context, zoneID string, rr cloudflare.DNSRecord) ([]cloudflare.DNSRecord, error) {
if m.dnsRecordsError != nil {
return nil, m.dnsRecordsError
}
@ -132,7 +132,7 @@ func (m *mockCloudFlareClient) DNSRecords(zoneID string, rr cloudflare.DNSRecord
return result, nil
}
func (m *mockCloudFlareClient) UpdateDNSRecord(zoneID, recordID string, rr cloudflare.DNSRecord) error {
func (m *mockCloudFlareClient) UpdateDNSRecord(ctx context.Context, zoneID, recordID string, rr cloudflare.DNSRecord) error {
m.Actions = append(m.Actions, MockAction{
Name: "Update",
ZoneId: zoneID,
@ -147,7 +147,7 @@ func (m *mockCloudFlareClient) UpdateDNSRecord(zoneID, recordID string, rr cloud
return nil
}
func (m *mockCloudFlareClient) DeleteDNSRecord(zoneID, recordID string) error {
func (m *mockCloudFlareClient) DeleteDNSRecord(ctx context.Context, zoneID, recordID string) error {
m.Actions = append(m.Actions, MockAction{
Name: "Delete",
ZoneId: zoneID,
@ -162,7 +162,7 @@ func (m *mockCloudFlareClient) DeleteDNSRecord(zoneID, recordID string) error {
return nil
}
func (m *mockCloudFlareClient) UserDetails() (cloudflare.User, error) {
func (m *mockCloudFlareClient) UserDetails(ctx context.Context) (cloudflare.User, error) {
return m.User, nil
}
@ -176,7 +176,7 @@ func (m *mockCloudFlareClient) ZoneIDByName(zoneName string) (string, error) {
return "", errors.New("Unknown zone: " + zoneName)
}
func (m *mockCloudFlareClient) ListZones(zoneID ...string) ([]cloudflare.Zone, error) {
func (m *mockCloudFlareClient) ListZones(ctx context.Context, zoneID ...string) ([]cloudflare.Zone, error) {
if m.listZonesError != nil {
return nil, m.listZonesError
}
@ -216,7 +216,7 @@ func (m *mockCloudFlareClient) ListZonesContext(ctx context.Context, opts ...clo
}, nil
}
func (m *mockCloudFlareClient) ZoneDetails(zoneID string) (cloudflare.Zone, error) {
func (m *mockCloudFlareClient) ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error) {
for id, zoneName := range m.Zones {
if zoneID == id {
return cloudflare.Zone{
@ -292,7 +292,7 @@ func TestCloudflareA(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: false,
Proxied: proxyDisabled,
},
},
{
@ -303,7 +303,7 @@ func TestCloudflareA(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.2",
TTL: 1,
Proxied: false,
Proxied: proxyDisabled,
},
},
},
@ -330,7 +330,7 @@ func TestCloudflareCname(t *testing.T) {
Name: "cname.bar.com",
Content: "google.com",
TTL: 1,
Proxied: false,
Proxied: proxyDisabled,
},
},
{
@ -341,7 +341,7 @@ func TestCloudflareCname(t *testing.T) {
Name: "cname.bar.com",
Content: "facebook.com",
TTL: 1,
Proxied: false,
Proxied: proxyDisabled,
},
},
},
@ -368,7 +368,7 @@ func TestCloudflareCustomTTL(t *testing.T) {
Name: "ttl.bar.com",
Content: "127.0.0.1",
TTL: 120,
Proxied: false,
Proxied: proxyDisabled,
},
},
},
@ -394,7 +394,7 @@ func TestCloudflareProxiedDefault(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: true,
Proxied: proxyEnabled,
},
},
},
@ -426,7 +426,7 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: true,
Proxied: proxyEnabled,
},
},
},
@ -458,7 +458,7 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: false,
Proxied: proxyDisabled,
},
},
},
@ -490,7 +490,7 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
Name: "bar.com",
Content: "127.0.0.1",
TTL: 1,
Proxied: true,
Proxied: proxyEnabled,
},
},
},
@ -499,19 +499,21 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) {
}
func TestCloudflareSetProxied(t *testing.T) {
var proxied *bool = proxyEnabled
var notProxied *bool = proxyDisabled
var testCases = []struct {
recordType string
domain string
proxiable bool
proxiable *bool
}{
{"A", "bar.com", true},
{"CNAME", "bar.com", true},
{"TXT", "bar.com", false},
{"MX", "bar.com", false},
{"NS", "bar.com", false},
{"SPF", "bar.com", false},
{"SRV", "bar.com", false},
{"A", "*.bar.com", false},
{"A", "bar.com", proxied},
{"CNAME", "bar.com", proxied},
{"TXT", "bar.com", notProxied},
{"MX", "bar.com", notProxied},
{"NS", "bar.com", notProxied},
{"SPF", "bar.com", notProxied},
{"SRV", "bar.com", notProxied},
{"A", "*.bar.com", notProxied},
}
for _, testCase := range testCases {
@ -684,6 +686,7 @@ func TestCloudflareApplyChanges(t *testing.T) {
Name: "new.bar.com",
Content: "target",
TTL: 1,
Proxied: proxyDisabled,
},
},
{
@ -693,6 +696,7 @@ func TestCloudflareApplyChanges(t *testing.T) {
Name: "foobar.bar.com",
Content: "target-new",
TTL: 1,
Proxied: proxyDisabled,
},
},
})
@ -779,6 +783,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
},
ExpectedEndpoints: []*endpoint.Endpoint{
@ -805,12 +810,14 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "foo.com",
Type: endpoint.RecordTypeA,
Content: "10.10.10.2",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
},
ExpectedEndpoints: []*endpoint.Endpoint{
@ -837,24 +844,28 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "foo.com",
Type: endpoint.RecordTypeA,
Content: "10.10.10.2",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "10.10.10.2",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
},
ExpectedEndpoints: []*endpoint.Endpoint{
@ -894,18 +905,21 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "foo.com",
Type: endpoint.RecordTypeA,
Content: "10.10.10.2",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "bar.de",
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
},
ExpectedEndpoints: []*endpoint.Endpoint{
@ -945,18 +959,21 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
Type: endpoint.RecordTypeA,
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "foo.com",
Type: endpoint.RecordTypeA,
Content: "10.10.10.2",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
{
Name: "bar.de",
Type: "NOT SUPPORTED",
Content: "10.10.10.1",
TTL: defaultCloudFlareRecordTTL,
Proxied: proxyDisabled,
},
},
ExpectedEndpoints: []*endpoint.Endpoint{
@ -984,94 +1001,101 @@ func TestCloudflareGroupByNameAndType(t *testing.T) {
func TestProviderPropertiesIdempotency(t *testing.T) {
testCases := []struct {
Name string
ProviderProxiedByDefault bool
RecordsAreProxied bool
RecordsAreProxied *bool
ShouldBeUpdated bool
}{
{
Name: "ProxyDefault: false, ShouldBeProxied: false, ExpectUpdates: false",
ProviderProxiedByDefault: false,
RecordsAreProxied: false,
RecordsAreProxied: proxyDisabled,
ShouldBeUpdated: false,
},
{
Name: "ProxyDefault: true, ShouldBeProxied: true, ExpectUpdates: false",
ProviderProxiedByDefault: true,
RecordsAreProxied: true,
RecordsAreProxied: proxyEnabled,
ShouldBeUpdated: false,
},
{
Name: "ProxyDefault: true, ShouldBeProxied: false, ExpectUpdates: true",
ProviderProxiedByDefault: true,
RecordsAreProxied: false,
RecordsAreProxied: proxyDisabled,
ShouldBeUpdated: true,
},
{
Name: "ProxyDefault: false, ShouldBeProxied: true, ExpectUpdates: true",
ProviderProxiedByDefault: false,
RecordsAreProxied: true,
RecordsAreProxied: proxyEnabled,
ShouldBeUpdated: true,
},
}
for _, test := range testCases {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": {
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
Content: "1.2.3.4",
Proxied: test.RecordsAreProxied,
t.Run(test.Name, func(t *testing.T) {
client := NewMockCloudFlareClientWithRecords(map[string][]cloudflare.DNSRecord{
"001": {
{
ID: "1234567890",
ZoneID: "001",
Name: "foobar.bar.com",
Type: endpoint.RecordTypeA,
TTL: 120,
Content: "1.2.3.4",
Proxied: test.RecordsAreProxied,
},
},
},
})
provider := &CloudFlareProvider{
Client: client,
proxiedByDefault: test.ProviderProxiedByDefault,
}
ctx := context.Background()
current, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
assert.Equal(t, 1, len(current))
desired := []*endpoint.Endpoint{}
for _, c := range current {
// Copy all except ProviderSpecific fields
desired = append(desired, &endpoint.Endpoint{
DNSName: c.DNSName,
Targets: c.Targets,
RecordType: c.RecordType,
SetIdentifier: c.SetIdentifier,
RecordTTL: c.RecordTTL,
Labels: c.Labels,
})
}
plan := plan.Plan{
Current: current,
Desired: desired,
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
provider := &CloudFlareProvider{
Client: client,
proxiedByDefault: test.ProviderProxiedByDefault,
}
ctx := context.Background()
plan = *plan.Calculate()
assert.NotNil(t, plan.Changes, "should have plan")
if plan.Changes == nil {
return
}
assert.Equal(t, 0, len(plan.Changes.Create), "should not have creates")
assert.Equal(t, 0, len(plan.Changes.Delete), "should not have deletes")
current, err := provider.Records(ctx)
if err != nil {
t.Errorf("should not fail, %s", err)
}
assert.Equal(t, 1, len(current))
if test.ShouldBeUpdated {
assert.Equal(t, 1, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 1, len(plan.Changes.UpdateOld), "should not have old updates")
} else {
assert.Equal(t, 0, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 0, len(plan.Changes.UpdateOld), "should not have old updates")
}
desired := []*endpoint.Endpoint{}
for _, c := range current {
// Copy all except ProviderSpecific fields
desired = append(desired, &endpoint.Endpoint{
DNSName: c.DNSName,
Targets: c.Targets,
RecordType: c.RecordType,
SetIdentifier: c.SetIdentifier,
RecordTTL: c.RecordTTL,
Labels: c.Labels,
})
}
plan := plan.Plan{
Current: current,
Desired: desired,
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = *plan.Calculate()
assert.NotNil(t, plan.Changes, "should have plan")
if plan.Changes == nil {
return
}
assert.Equal(t, 0, len(plan.Changes.Create), "should not have creates")
assert.Equal(t, 0, len(plan.Changes.Delete), "should not have deletes")
if test.ShouldBeUpdated {
assert.Equal(t, 1, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 1, len(plan.Changes.UpdateOld), "should not have old updates")
} else {
assert.Equal(t, 0, len(plan.Changes.UpdateNew), "should not have new updates")
assert.Equal(t, 0, len(plan.Changes.UpdateOld), "should not have old updates")
}
})
}
}
@ -1129,7 +1153,7 @@ func TestCloudflareComplexUpdate(t *testing.T) {
Type: "A",
Content: "2.3.4.5",
TTL: 1,
Proxied: true,
Proxied: proxyEnabled,
},
},
MockAction{
@ -1141,7 +1165,7 @@ func TestCloudflareComplexUpdate(t *testing.T) {
Type: "A",
Content: "1.2.3.4",
TTL: 1,
Proxied: true,
Proxied: proxyEnabled,
},
},
MockAction{
@ -1162,7 +1186,7 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) {
Type: endpoint.RecordTypeA,
TTL: 1,
Content: "1.2.3.4",
Proxied: true,
Proxied: proxyEnabled,
},
},
})

View File

@ -176,6 +176,9 @@ OuterLoop:
case dns.TypeTXT:
rrValues = (rr.(*dns.TXT).Txt)
rrType = "TXT"
case dns.TypeNS:
rrValues = []string{rr.(*dns.NS).Ns}
rrType = "NS"
default:
continue // Unhandled record type
}

View File

@ -172,6 +172,11 @@ func TestRfc2136ApplyChanges(t *testing.T) {
RecordType: "TXT",
Targets: []string{"boom"},
},
{
DNSName: "ns.foobar.com",
RecordType: "NS",
Targets: []string{"boom"},
},
},
Delete: []*endpoint.Endpoint{
{
@ -190,13 +195,16 @@ func TestRfc2136ApplyChanges(t *testing.T) {
err = provider.ApplyChanges(context.Background(), p)
assert.NoError(t, err)
assert.Equal(t, 2, len(stub.createMsgs))
assert.Equal(t, 3, len(stub.createMsgs))
assert.True(t, strings.Contains(stub.createMsgs[0].String(), "v1.foo.com"))
assert.True(t, strings.Contains(stub.createMsgs[0].String(), "1.2.3.4"))
assert.True(t, strings.Contains(stub.createMsgs[1].String(), "v1.foobar.com"))
assert.True(t, strings.Contains(stub.createMsgs[1].String(), "boom"))
assert.True(t, strings.Contains(stub.createMsgs[2].String(), "ns.foobar.com"))
assert.True(t, strings.Contains(stub.createMsgs[2].String(), "boom"))
assert.Equal(t, 2, len(stub.updateMsgs))
assert.True(t, strings.Contains(stub.updateMsgs[0].String(), "v2.foo.com"))
assert.True(t, strings.Contains(stub.updateMsgs[1].String(), "v2.foobar.com"))

View File

@ -1,3 +1,13 @@
#! /bin/bash
set -e
trivy image --exit-code 1 us.gcr.io/k8s-artifacts-prod/external-dns/external-dns:$(git describe --tags --always --dirty)
# install trivy
cd /tmp
curl -LO https://github.com/aquasecurity/trivy/releases/download/v0.20.2/trivy_0.20.2_Linux-64bit.tar.gz
echo "38a6de48e21a34e0fa0d2cf63439c0afcbbae0e78fb3feada7a84a9cf6e7f60c trivy_0.20.2_Linux-64bit.tar.gz" | sha256sum -c
tar -xvf trivy_0.20.2_Linux-64bit.tar.gz
chmod +x trivy
# run trivy
cd -
/tmp/trivy image --exit-code 1 us.gcr.io/k8s-artifacts-prod/external-dns/external-dns:$(git describe --tags --always --dirty)

View File

@ -49,6 +49,7 @@ type ocpRouteSource struct {
ignoreHostnameAnnotation bool
routeInformer routeInformer.RouteInformer
labelSelector labels.Selector
ocpRouterName string
}
// NewOcpRouteSource creates a new ocpRouteSource with the given config.
@ -60,6 +61,7 @@ func NewOcpRouteSource(
combineFQDNAnnotation bool,
ignoreHostnameAnnotation bool,
labelSelector labels.Selector,
ocpRouterName string,
) (Source, error) {
tmpl, err := parseTemplate(fqdnTemplate)
if err != nil {
@ -96,11 +98,16 @@ func NewOcpRouteSource(
ignoreHostnameAnnotation: ignoreHostnameAnnotation,
routeInformer: informer,
labelSelector: labelSelector,
ocpRouterName: ocpRouterName,
}, nil
}
// TODO add a meaningful EventHandler
func (ors *ocpRouteSource) AddEventHandler(ctx context.Context, handler func()) {
log.Debug("Adding event handler for openshift route")
// Right now there is no way to remove event handler from informer, see:
// https://github.com/kubernetes/kubernetes/issues/79610
ors.routeInformer.Informer().AddEventHandler(eventHandlerFunc(handler))
}
// Endpoints returns endpoint objects for each host-target combination that should be processed.
@ -128,7 +135,7 @@ func (ors *ocpRouteSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint,
continue
}
orEndpoints := endpointsFromOcpRoute(ocpRoute, ors.ignoreHostnameAnnotation)
orEndpoints := ors.endpointsFromOcpRoute(ocpRoute, ors.ignoreHostnameAnnotation)
// apply template if host is missing on OpenShift Route
if (ors.combineFQDNAnnotation || len(orEndpoints) == 0) && ors.fqdnTemplate != nil {
@ -174,7 +181,7 @@ func (ors *ocpRouteSource) endpointsFromTemplate(ocpRoute *routev1.Route) ([]*en
targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
if len(targets) == 0 {
targets = targetsFromOcpRouteStatus(ocpRoute.Status)
targets = ors.targetsFromOcpRouteStatus(ocpRoute.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
@ -223,7 +230,7 @@ func (ors *ocpRouteSource) setResourceLabel(ocpRoute *routev1.Route, endpoints [
}
// endpointsFromOcpRoute extracts the endpoints from a OpenShift Route object
func endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation bool) []*endpoint.Endpoint {
func (ors *ocpRouteSource) endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation bool) []*endpoint.Endpoint {
var endpoints []*endpoint.Endpoint
ttl, err := getTTLFromAnnotations(ocpRoute.Annotations)
@ -234,7 +241,7 @@ func endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation boo
targets := getTargetsFromTargetAnnotation(ocpRoute.Annotations)
if len(targets) == 0 {
targets = targetsFromOcpRouteStatus(ocpRoute.Status)
targets = ors.targetsFromOcpRouteStatus(ocpRoute.Status)
}
providerSpecific, setIdentifier := getProviderSpecificAnnotations(ocpRoute.Annotations)
@ -253,14 +260,18 @@ func endpointsFromOcpRoute(ocpRoute *routev1.Route, ignoreHostnameAnnotation boo
return endpoints
}
func targetsFromOcpRouteStatus(status routev1.RouteStatus) endpoint.Targets {
func (ors *ocpRouteSource) targetsFromOcpRouteStatus(status routev1.RouteStatus) endpoint.Targets {
var targets endpoint.Targets
for _, ing := range status.Ingress {
if ing.RouterCanonicalHostname != "" {
if len(ors.ocpRouterName) != 0 {
if ing.RouterName == ors.ocpRouterName {
targets = append(targets, ing.RouterCanonicalHostname)
return targets
}
} else if ing.RouterCanonicalHostname != "" {
targets = append(targets, ing.RouterCanonicalHostname)
return targets
}
}
return targets
}

View File

@ -50,6 +50,7 @@ func (suite *OCPRouteSuite) SetupTest() {
false,
false,
labels.Everything(),
"",
)
suite.routeWithTargets = &routev1.Route{
@ -147,6 +148,7 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
false,
false,
labelSelector,
"",
)
if ti.expectError {
@ -160,8 +162,6 @@ func testOcpRouteSourceNewOcpRouteSource(t *testing.T) {
// testOcpRouteSourceEndpoints tests that various OCP routes generate the correct endpoints.
func testOcpRouteSourceEndpoints(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
title string
targetNamespace string
@ -172,6 +172,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
expected []*endpoint.Endpoint
expectError bool
labelFilter string
ocpRouterName string
}{
{
title: "route with basic hostname and route status target",
@ -196,6 +197,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
},
},
ocpRouterName: "",
expected: []*endpoint.Endpoint{
{
DNSName: "my-domain.com",
@ -206,6 +208,119 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
expectError: false,
},
{
title: "route with basic hostname and route status target with one RouterCanonicalHostname and one ocpRouterNames defined",
targetNamespace: "",
annotationFilter: "",
fqdnTemplate: "",
ignoreHostnameAnnotation: false,
ocpRoute: &routev1.Route{
Spec: routev1.RouteSpec{
Host: "my-domain.com",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "route-with-target",
Annotations: map[string]string{},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
RouterName: "default",
RouterCanonicalHostname: "router-default.my-domain.com",
},
},
},
},
ocpRouterName: "default",
expected: []*endpoint.Endpoint{
{
DNSName: "my-domain.com",
Targets: []string{
"router-default.my-domain.com",
},
},
},
expectError: false,
},
{
title: "route with basic hostname and route status target with one RouterCanonicalHostname and one ocpRouterNames defined and two router canonical names",
targetNamespace: "",
annotationFilter: "",
fqdnTemplate: "",
ignoreHostnameAnnotation: false,
ocpRoute: &routev1.Route{
Spec: routev1.RouteSpec{
Host: "my-domain.com",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "route-with-target",
Annotations: map[string]string{},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
RouterName: "default",
RouterCanonicalHostname: "router-default.my-domain.com",
},
{
RouterName: "test",
RouterCanonicalHostname: "router-test.my-domain.com",
},
},
},
},
ocpRouterName: "default",
expected: []*endpoint.Endpoint{
{
DNSName: "my-domain.com",
Targets: []string{
"router-default.my-domain.com",
},
},
},
expectError: false,
},
{
title: "route with basic hostname and route status target with one RouterCanonicalHostname and one ocpRouterName defined and two router canonical names",
targetNamespace: "",
annotationFilter: "",
fqdnTemplate: "",
ignoreHostnameAnnotation: false,
ocpRoute: &routev1.Route{
Spec: routev1.RouteSpec{
Host: "my-domain.com",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "route-with-target",
Annotations: map[string]string{},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
RouterName: "default",
RouterCanonicalHostname: "router-default.my-domain.com",
},
{
RouterName: "test",
RouterCanonicalHostname: "router-test.my-domain.com",
},
},
},
},
ocpRouterName: "default",
expected: []*endpoint.Endpoint{
{
DNSName: "my-domain.com",
Targets: []string{
"router-default.my-domain.com",
},
},
},
expectError: false,
},
{
title: "route with incorrect externalDNS controller annotation",
targetNamespace: "",
@ -221,8 +336,9 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
},
},
expected: []*endpoint.Endpoint{},
expectError: false,
ocpRouterName: "",
expected: []*endpoint.Endpoint{},
expectError: false,
},
{
title: "route with basic hostname and annotation target",
@ -242,6 +358,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
},
},
ocpRouterName: "",
expected: []*endpoint.Endpoint{
{
DNSName: "my-annotation-domain.com",
@ -273,6 +390,7 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
},
},
ocpRouterName: "",
expected: []*endpoint.Endpoint{
{
DNSName: "my-annotation-domain.com",
@ -304,17 +422,16 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
},
},
},
expected: []*endpoint.Endpoint{},
expectError: false,
ocpRouterName: "",
expected: []*endpoint.Endpoint{},
expectError: false,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()
// Create a Kubernetes testing client
fakeClient := fake.NewSimpleClientset()
_, err := fakeClient.RouteV1().Routes(tc.ocpRoute.Namespace).Create(context.Background(), tc.ocpRoute, metav1.CreateOptions{})
require.NoError(t, err)
@ -329,7 +446,9 @@ func testOcpRouteSourceEndpoints(t *testing.T) {
false,
false,
labelSelector,
tc.ocpRouterName,
)
require.NoError(t, err)
res, err := source.Endpoints(context.Background())

View File

@ -223,6 +223,7 @@ func (sc *serviceSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
lastMergedEndpoint := len(mergedEndpoints) - 1
if mergedEndpoints[lastMergedEndpoint].DNSName == endpoints[i].DNSName &&
mergedEndpoints[lastMergedEndpoint].RecordType == endpoints[i].RecordType &&
mergedEndpoints[lastMergedEndpoint].SetIdentifier == endpoints[i].SetIdentifier &&
mergedEndpoints[lastMergedEndpoint].RecordTTL == endpoints[i].RecordTTL {
mergedEndpoints[lastMergedEndpoint].Targets = append(mergedEndpoints[lastMergedEndpoint].Targets, endpoints[i].Targets[0])
} else {

View File

@ -1085,7 +1085,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
ignoreHostnameAnnotation bool
labels map[string]string
clusterIP string
hostnames map[string]string
services map[string]map[string]string
serviceTypesFilter []string
expected []*endpoint.Endpoint
expectError bool
@ -1103,8 +1103,8 @@ func testMultipleServicesEndpoints(t *testing.T) {
false,
map[string]string{},
"",
map[string]string{
"1.2.3.4": "foo.example.org",
map[string]map[string]string{
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
@ -1125,10 +1125,10 @@ func testMultipleServicesEndpoints(t *testing.T) {
false,
map[string]string{},
"",
map[string]string{
"1.2.3.4": "foo.example.org",
"1.2.3.5": "foo.example.org",
"1.2.3.6": "foo.example.org",
map[string]map[string]string{
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org"},
"1.2.3.6": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
@ -1149,14 +1149,14 @@ func testMultipleServicesEndpoints(t *testing.T) {
false,
map[string]string{},
"",
map[string]string{
"1.2.3.5": "foo.example.org",
"10.1.1.3": "bar.example.org",
"10.1.1.1": "bar.example.org",
"1.2.3.4": "foo.example.org",
"10.1.1.2": "bar.example.org",
"20.1.1.1": "foobar.example.org",
"1.2.3.6": "foo.example.org",
map[string]map[string]string{
"1.2.3.5": {hostnameAnnotationKey: "foo.example.org"},
"10.1.1.3": {hostnameAnnotationKey: "bar.example.org"},
"10.1.1.1": {hostnameAnnotationKey: "bar.example.org"},
"1.2.3.4": {hostnameAnnotationKey: "foo.example.org"},
"10.1.1.2": {hostnameAnnotationKey: "bar.example.org"},
"20.1.1.1": {hostnameAnnotationKey: "foobar.example.org"},
"1.2.3.6": {hostnameAnnotationKey: "foo.example.org"},
},
[]string{},
[]*endpoint.Endpoint{
@ -1166,6 +1166,30 @@ func testMultipleServicesEndpoints(t *testing.T) {
},
false,
},
{
"test that services with different set-identifier do not get merged together",
"",
"",
"testing",
"foo",
v1.ServiceTypeLoadBalancer,
"",
"",
false,
false,
map[string]string{},
"",
map[string]map[string]string{
"a.elb.com": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "a"},
"b.elb.com": {hostnameAnnotationKey: "foo.example.org", SetIdentifierKey: "b"},
},
[]string{},
[]*endpoint.Endpoint{
{DNSName: "foo.example.org", Targets: endpoint.Targets{"a.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/fooa.elb.com"}, SetIdentifier: "a"},
{DNSName: "foo.example.org", Targets: endpoint.Targets{"b.elb.com"}, Labels: map[string]string{endpoint.ResourceLabelKey: "service/testing/foob.elb.com"}, SetIdentifier: "b"},
},
false,
},
} {
tc := tc
t.Run(tc.title, func(t *testing.T) {
@ -1175,12 +1199,9 @@ func testMultipleServicesEndpoints(t *testing.T) {
kubernetes := fake.NewSimpleClientset()
// Create services to test against
for serviceip, hostname := range tc.hostnames {
for lb, annotations := range tc.services {
ingresses := []v1.LoadBalancerIngress{}
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: serviceip})
annotations := make(map[string]string)
annotations[hostnameAnnotationKey] = hostname
ingresses = append(ingresses, v1.LoadBalancerIngress{IP: lb})
service := &v1.Service{
Spec: v1.ServiceSpec{
@ -1189,7 +1210,7 @@ func testMultipleServicesEndpoints(t *testing.T) {
},
ObjectMeta: metav1.ObjectMeta{
Namespace: tc.svcNamespace,
Name: tc.svcName + serviceip,
Name: tc.svcName + lb,
Labels: tc.labels,
Annotations: annotations,
},

View File

@ -68,6 +68,7 @@ type Config struct {
SkipperRouteGroupVersion string
RequestTimeout time.Duration
DefaultTargets []string
OCPRouterName string
}
// ClientGenerator provides clients
@ -255,7 +256,7 @@ func BuildWithConfig(source string, p ClientGenerator, cfg *Config) (Source, err
if err != nil {
return nil, err
}
return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter)
return NewOcpRouteSource(ocpClient, cfg.Namespace, cfg.AnnotationFilter, cfg.FQDNTemplate, cfg.CombineFQDNAndAnnotation, cfg.IgnoreHostnameAnnotation, cfg.LabelFilter, cfg.OCPRouterName)
case "fake":
return NewFakeSource(cfg.FQDNTemplate)
case "connector":