Merge branch 'master' into master

This commit is contained in:
Alejandro J. Nuñez Madrazo 2023-06-26 07:03:30 +01:00 committed by GitHub
commit 27f9f5286c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 5449 additions and 1565 deletions

View File

@ -28,7 +28,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install go version
uses: actions/setup-go@v3
uses: actions/setup-go@v4
with:
go-version: '^1.20'

View File

@ -21,7 +21,7 @@ jobs:
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
debug: ${{ secrets.ACTIONS_RUNNER_DEBUG }}
debug: ${{ secrets.ACTIONS_RUNNER_DEBUG == 'true' }}
script: |
const result = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,

View File

@ -45,7 +45,7 @@ jobs:
run: |
changed=$(ct list-changed)
if [[ -n "$changed" ]]; then
echo "::set-output name=changed::true"
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Run chart-testing (lint)

View File

@ -23,7 +23,7 @@ jobs:
- name: Set up Go 1.x
uses: actions/setup-go@v4
with:
go-version: 1.19
go-version: '1.20'
id: go
- name: Check out code into the Go module directory
@ -31,5 +31,5 @@ jobs:
- name: Lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.2
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3
make lint

View File

@ -29,7 +29,7 @@ jobs:
run: |
set -euo pipefail
chart_version="$(grep -Po "(?<=^version: ).+" charts/external-dns/Chart.yaml)"
echo "::set-output name=version::${chart_version}"
echo "version=${chart_version}" >> $GITHUB_OUTPUT
- name: Get changelog entry
id: changelog_reader

View File

@ -60,6 +60,14 @@ issues:
- unused
- varcheck
- whitespace
- path: source/ambassador_host.go
linters: [ typecheck ]
- path: source/contour_httpproxy.go
linters: [ typecheck ]
- path: source/f5_virtualserver.go
linters: [ typecheck ]
- path: source/kong_tcpingress.go
linters: [ typecheck ]
run:
skip-files:

View File

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### All Changes
- Disallowed privilege escalation in container security context and set the seccomp profile type to `RuntimeDefault`. ([#3689](https://github.com/kubernetes-sigs/external-dns/pull/3689)) [@nrvnrvn](https://github.com/nrvnrvn)
## [v1.13.0] - 2023-03-30
### All Changes

View File

@ -0,0 +1 @@
provider: inmemory

View File

@ -43,8 +43,11 @@ shareProcessNamespace: false
podSecurityContext:
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 65534
readOnlyRootFilesystem: true

View File

@ -195,8 +195,6 @@ func (c *Controller) RunOnce(ctx context.Context) error {
return err
}
missingRecords := c.Registry.MissingRecords()
registryEndpointsTotal.Set(float64(len(records)))
regARecords, regAAAARecords := countAddressRecords(records)
registryARecords.Set(float64(regARecords))
@ -218,29 +216,6 @@ func (c *Controller) RunOnce(ctx context.Context) error {
verifiedAAAARecords.Set(float64(vAAAARecords))
endpoints = c.Registry.AdjustEndpoints(endpoints)
if len(missingRecords) > 0 {
// Add missing records before the actual plan is applied.
// This prevents the problems when the missing TXT record needs to be
// created and deleted/upserted in the same batch.
missingRecordsPlan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
Missing: missingRecords,
DomainFilter: endpoint.MatchAllDomainFilters{c.DomainFilter, c.Registry.GetDomainFilter()},
PropertyComparator: c.Registry.PropertyValuesEqual,
ManagedRecords: c.ManagedRecordTypes,
}
missingRecordsPlan = missingRecordsPlan.Calculate()
if missingRecordsPlan.Changes.HasChanges() {
err = c.Registry.ApplyChanges(ctx, missingRecordsPlan.Changes)
if err != nil {
registryErrorsTotal.Inc()
deprecatedRegistryErrors.Inc()
return err
}
log.Info("All missing records are created")
}
}
plan := &plan.Plan{
Policies: []plan.Policy{c.Policy},
Current: records,

View File

@ -311,51 +311,6 @@ func testControllerFiltersDomains(t *testing.T, configuredEndpoints []*endpoint.
}
}
type noopRegistryWithMissing struct {
*registry.NoopRegistry
missingRecords []*endpoint.Endpoint
}
func (r *noopRegistryWithMissing) MissingRecords() []*endpoint.Endpoint {
return r.missingRecords
}
func testControllerFiltersDomainsWithMissing(t *testing.T, configuredEndpoints []*endpoint.Endpoint, domainFilter endpoint.DomainFilterInterface, providerEndpoints, missingEndpoints []*endpoint.Endpoint, expectedChanges []*plan.Changes) {
t.Helper()
cfg := externaldns.NewConfig()
cfg.ManagedDNSRecordTypes = []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME}
source := new(testutils.MockSource)
source.On("Endpoints").Return(configuredEndpoints, nil)
// Fake some existing records in our DNS provider and validate some desired changes.
provider := &filteredMockProvider{
RecordsStore: providerEndpoints,
}
noop, err := registry.NewNoopRegistry(provider)
require.NoError(t, err)
r := &noopRegistryWithMissing{
NoopRegistry: noop,
missingRecords: missingEndpoints,
}
ctrl := &Controller{
Source: source,
Registry: r,
Policy: &plan.SyncPolicy{},
DomainFilter: domainFilter,
ManagedRecordTypes: cfg.ManagedDNSRecordTypes,
}
assert.NoError(t, ctrl.RunOnce(context.Background()))
assert.Equal(t, 1, provider.RecordsCallCount)
require.Len(t, provider.ApplyChangesCalls, len(expectedChanges))
for i, change := range expectedChanges {
assert.Equal(t, *change, *provider.ApplyChangesCalls[i])
}
}
func TestControllerSkipsEmptyChanges(t *testing.T) {
testControllerFiltersDomains(
t,
@ -683,60 +638,6 @@ func TestARecords(t *testing.T) {
assert.Equal(t, math.Float64bits(1), valueFromMetric(registryARecords))
}
// TestMissingRecordsApply validates that the missing records result in the dedicated plan apply.
func TestMissingRecordsApply(t *testing.T) {
testControllerFiltersDomainsWithMissing(
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"},
},
},
endpoint.NewDomainFilter([]string{"used.tld"}),
[]*endpoint.Endpoint{
{
DNSName: "record1.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"1.2.3.4"},
},
},
[]*endpoint.Endpoint{
{
DNSName: "a-record1.used.tld",
RecordType: endpoint.RecordTypeTXT,
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
},
},
[]*plan.Changes{
// Missing record had its own plan applied.
{
Create: []*endpoint.Endpoint{
{
DNSName: "a-record1.used.tld",
RecordType: endpoint.RecordTypeTXT,
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
},
},
},
{
Create: []*endpoint.Endpoint{
{
DNSName: "record2.used.tld",
RecordType: endpoint.RecordTypeA,
Targets: endpoint.Targets{"8.8.8.8"},
},
},
},
})
}
func TestAAAARecords(t *testing.T) {
testControllerFiltersDomains(
t,

41
docs/registry/dynamodb.md Normal file
View File

@ -0,0 +1,41 @@
# The DynamoDB registry
The DynamoDB registry stores DNS record metadata in an AWS DynamoDB table.
## The DynamoDB Table
By default, the DynamoDB registry stores data in the table named `external-dns`.
A different table may be specified using the `--dynamodb-table` flag.
A different region may be specified using the `--dynamodb-region` flag.
The table must have a partition (hash) key named `k` and string type.
The table must not have a sort (range) key.
## IAM permissions
The ExternalDNS Role must be granted the following permissions:
```json
{
"Effect": "Allow",
"Action": [
"DynamoDB:DescribeTable",
"DynamoDB:PartiQLDelete",
"DynamoDB:PartiQLInsert",
"DynamoDB:PartiQLUpdate",
"DynamoDB:Scan"
],
"Resource": [
"arn:aws:dynamodb:*:*:table/external-dns"
]
}
```
The region and account ID may be specified explicitly specified instead of using wildcards.
## Caching
The DynamoDB registry can optionally cache DNS records read from the provider. This can mitigate
rate limits imposed by the provider.
Caching is enabled by specifying a cache duration with the `--txt-cache-interval` flag.

View File

@ -11,6 +11,7 @@ The registry implementation is specified using the `--registry` flag.
## Supported registries
* [txt](txt.md) (default) - Stores in TXT records in the same provider
* [txt](txt.md) (default) - Stores metadata in TXT records in the same provider.
* [dynamodb](dynamodb.md) - Stores metadata in an AWS DynamoDB table.
* noop - Passes metadata directly to the provider. For most providers, this means the metadata is not persisted.
* aws-sd - Stores metadata in AWS Service Discovery. Only usable with the `aws-sd` provider.

View File

@ -0,0 +1,96 @@
# Configuring ExternalDNS to use the Traefik Proxy Source
This tutorial describes how to configure ExternalDNS to use the Traefik Proxy source.
It is meant to supplement the other provider-specific setup tutorials.
## Manifest (for clusters without RBAC enabled)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
containers:
- name: external-dns
# update this to the desired external-dns version
image: registry.k8s.io/external-dns/external-dns:v0.13.3
args:
- --source=traefik-proxy
- --provider=aws
- --registry=txt
- --txt-owner-id=my-identifier
```
## Manifest (for clusters with RBAC enabled)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","pods"]
verbs: ["get","watch","list"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list","watch"]
- apiGroups: ["traefik.containo.us","traefik.io"]
resources: ["ingressroutes", "ingressroutetcps", "ingressrouteudps"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-dns
template:
metadata:
labels:
app: external-dns
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
# update this to the desired external-dns version
image: registry.k8s.io/external-dns/external-dns:v0.13.3
args:
- --source=traefik-proxy
- --provider=aws
- --registry=txt
- --txt-owner-id=my-identifier
```

View File

@ -162,6 +162,13 @@ type ProviderSpecificProperty struct {
// ProviderSpecific holds configuration which is specific to individual DNS providers
type ProviderSpecific []ProviderSpecificProperty
// EndpointKey is the type of a map key for separating endpoints or targets.
type EndpointKey struct {
DNSName string
RecordType string
SetIdentifier string
}
// Endpoint is a high-level way of a connection between a service and an IP
type Endpoint struct {
// The hostname of the DNS record
@ -222,22 +229,52 @@ func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
// warrant its own field on the Endpoint object itself. It differs from Labels in the fact that it's
// not persisted in the Registry but only kept in memory during a single record synchronization.
func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint {
if e.ProviderSpecific == nil {
e.ProviderSpecific = ProviderSpecific{}
}
e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value})
e.SetProviderSpecificProperty(key, value)
return e
}
// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists.
func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) {
// GetProviderSpecificProperty returns the value of a ProviderSpecificProperty if the property exists.
func (e *Endpoint) GetProviderSpecificProperty(key string) (string, bool) {
for _, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
return providerSpecific, true
return providerSpecific.Value, true
}
}
return ProviderSpecificProperty{}, false
return "", false
}
// SetProviderSpecificProperty sets the value of a ProviderSpecificProperty.
func (e *Endpoint) SetProviderSpecificProperty(key string, value string) {
for i, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
e.ProviderSpecific[i] = ProviderSpecificProperty{
Name: key,
Value: value,
}
return
}
}
e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value})
}
// DeleteProviderSpecificProperty deletes any ProviderSpecificProperty of the specified name.
func (e *Endpoint) DeleteProviderSpecificProperty(key string) {
for i, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
e.ProviderSpecific = append(e.ProviderSpecific[:i], e.ProviderSpecific[i+1:]...)
return
}
}
}
// Key returns the EndpointKey of the Endpoint.
func (e *Endpoint) Key() EndpointKey {
return EndpointKey{
DNSName: e.DNSName,
RecordType: e.RecordType,
SetIdentifier: e.SetIdentifier,
}
}
func (e *Endpoint) String() string {

200
go.mod
View File

@ -4,79 +4,80 @@ go 1.20
require (
cloud.google.com/go/compute/metadata v0.2.3
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.28
github.com/Azure/go-autorest/autorest/adal v0.9.21
github.com/Azure/azure-sdk-for-go v68.0.0+incompatible
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/adal v0.9.23
github.com/Azure/go-autorest/autorest/to v0.4.0
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.11.1
github.com/F5Networks/k8s-bigip-ctlr/v2 v2.13.0
github.com/IBM-Cloud/ibm-cloud-cli-sdk v1.1.0
github.com/IBM/go-sdk-core/v5 v5.13.4
github.com/IBM/networking-go-sdk v0.36.0
github.com/StackExchange/dnscontrol/v3 v3.27.1
github.com/IBM/networking-go-sdk v0.42.0
github.com/StackExchange/dnscontrol/v3 v3.31.6
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2
github.com/alecthomas/kingpin v2.2.5+incompatible
github.com/aliyun/alibaba-cloud-sdk-go v1.62.4
github.com/ans-group/sdk-go v1.10.4
github.com/aws/aws-sdk-go v1.44.136
github.com/alecthomas/kingpin v2.2.6+incompatible
github.com/aliyun/alibaba-cloud-sdk-go v1.62.380
github.com/ans-group/sdk-go v1.16.5
github.com/aws/aws-sdk-go v1.44.285
github.com/bodgit/tsig v1.2.2
github.com/civo/civogo v0.3.33
github.com/cloudflare/cloudflare-go v0.58.1
github.com/civo/civogo v0.3.14
github.com/cloudflare/cloudflare-go v0.69.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.97.0
github.com/dnsimple/dnsimple-go v1.0.1
github.com/exoscale/egoscale v0.97.0
github.com/digitalocean/godo v1.99.0
github.com/dnsimple/dnsimple-go v1.2.0
github.com/exoscale/egoscale v1.19.0
github.com/ffledgling/pdns-go v0.0.0-20180219074714-524e7daccd99
github.com/go-gandi/go-gandi v0.6.0
github.com/google/go-cmp v0.5.9
github.com/gophercloud/gophercloud v1.4.0
github.com/hooklift/gowsdl v0.5.0
github.com/infobloxopen/infoblox-go-client/v2 v2.1.2-0.20220407114022-6f4c71443168
github.com/infobloxopen/infoblox-go-client/v2 v2.3.0
github.com/linki/instrumented_http v0.3.0
github.com/linode/linodego v1.9.1
github.com/maxatome/go-testdeep v1.12.0
github.com/miekg/dns v1.1.51
github.com/linode/linodego v1.17.0
github.com/maxatome/go-testdeep v1.13.0
github.com/miekg/dns v1.1.55
github.com/nesv/go-dynect v0.6.0
github.com/nic-at/rc0go v1.1.1
github.com/onsi/ginkgo v1.16.5
github.com/openshift/api v0.0.0-20210315202829-4b79815405ec
github.com/openshift/client-go v0.0.0-20210112165513-ebc401615f47
github.com/oracle/oci-go-sdk/v65 v65.35.0
github.com/ovh/go-ovh v1.1.0
github.com/openshift/api v0.0.0-20230607130528-611114dca681
github.com/openshift/client-go v0.0.0-20230607134213-3cd0021bbee3
github.com/oracle/oci-go-sdk/v65 v65.41.0
github.com/ovh/go-ovh v1.4.1
github.com/pkg/errors v0.9.1
github.com/pluralsh/gqlclient v1.1.6
github.com/projectcontour/contour v1.23.2
github.com/prometheus/client_golang v1.14.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.2
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.599
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.344
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.599
github.com/transip/gotransip/v6 v6.19.0
github.com/ultradns/ultradns-sdk-go v0.0.0-20200616202852-e62052662f60
github.com/vinyldns/go-vinyldns v0.0.0-20200211145900-fe8a3d82e556
github.com/pluralsh/gqlclient v1.3.17
github.com/projectcontour/contour v1.25.0
github.com/prometheus/client_golang v1.16.0
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.17
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.8.4
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.684
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.684
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/privatedns v1.0.684
github.com/transip/gotransip/v6 v6.20.0
github.com/ultradns/ultradns-sdk-go v1.3.7
github.com/vinyldns/go-vinyldns v0.9.16
github.com/vultr/govultr/v2 v2.17.2
go.etcd.io/etcd/api/v3 v3.5.8
go.etcd.io/etcd/client/v3 v3.5.8
go.etcd.io/etcd/api/v3 v3.5.9
go.etcd.io/etcd/client/v3 v3.5.9
go.uber.org/ratelimit v0.2.0
golang.org/x/net v0.8.0
golang.org/x/oauth2 v0.5.0
golang.org/x/sync v0.1.0
google.golang.org/api v0.110.0
gopkg.in/ns1/ns1-go.v2 v2.7.4
golang.org/x/net v0.11.0
golang.org/x/oauth2 v0.9.0
golang.org/x/sync v0.3.0
golang.org/x/time v0.3.0
google.golang.org/api v0.128.0
gopkg.in/ns1/ns1-go.v2 v2.7.6
gopkg.in/yaml.v2 v2.4.0
istio.io/api v0.0.0-20210128181506-0c4b8e54850f
istio.io/client-go v0.0.0-20210128182905-ee2edd059e02
k8s.io/api v0.26.0
k8s.io/apimachinery v0.26.0
k8s.io/client-go v0.26.0
sigs.k8s.io/gateway-api v0.6.0
istio.io/api v0.0.0-20230524015941-fa6c5f7916bf
istio.io/client-go v1.18.0
k8s.io/api v0.27.3
k8s.io/apimachinery v0.27.3
k8s.io/client-go v0.27.3
sigs.k8s.io/gateway-api v0.7.1
)
require (
cloud.google.com/go/compute v1.18.0 // indirect
cloud.google.com/go/compute v1.19.3 // indirect
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
@ -84,11 +85,8 @@ require (
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Masterminds/semver v1.4.2 // indirect
github.com/Yamashou/gqlgenc v0.11.0 // indirect
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 // indirect
github.com/alecthomas/colour v0.1.0 // indirect
github.com/alecthomas/repr v0.0.0-20200325044227-4184120f674c // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 // indirect
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
github.com/ans-group/go-durationstring v1.2.0 // indirect
@ -96,41 +94,44 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deepmap/oapi-codegen v1.9.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/emicklei/go-restful/v3 v3.10.2 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/frankban/quicktest v1.14.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/strfmt v0.21.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.13.0 // indirect
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.3 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.15 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
@ -142,9 +143,10 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.3 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
@ -156,52 +158,56 @@ require (
github.com/openshift/gssapi v0.0.0-20161010215902-5fb4217df13b // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/peterhellberg/link v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.43.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/schollz/progressbar/v3 v3.8.6 // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/smartystreets/gunit v1.3.4 // indirect
github.com/sony/gobreaker v0.5.0 // indirect
github.com/spf13/afero v1.9.3 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.15.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/terra-farm/udnssdk v1.3.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.8 // indirect
github.com/vektah/gqlparser/v2 v2.5.1 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.9 // indirect
go.mongodb.org/mongo-driver v1.11.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.6.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.10.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
golang.org/x/tools v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.55.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/go-playground/validator.v9 v9.31.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/resty.v1 v1.12.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
istio.io/gogo-genproto v0.0.0-20190930162913-45029607206a // indirect
k8s.io/klog/v2 v2.80.1 // indirect
k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 // indirect
k8s.io/utils v0.0.0-20221107191617-1a15be271d1d // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect
moul.io/http2curl v1.0.0 // indirect
sigs.k8s.io/controller-runtime v0.12.1 // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/controller-runtime v0.14.6 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
replace k8s.io/klog/v2 => github.com/Raffo/knolog v0.0.0-20211016155154-e4d5e0cc970a

729
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -19,11 +19,12 @@ package testutils
import (
"fmt"
"sort"
"testing"
"sigs.k8s.io/external-dns/endpoint"
)
func ExampleSameEndpoints() {
func TestExampleSameEndpoints(t *testing.T) {
eps := []*endpoint.Endpoint{
{
DNSName: "example.org",

31
main.go
View File

@ -25,6 +25,11 @@ import (
"syscall"
"time"
awsSDK "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/route53"
sd "github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/labels"
@ -180,6 +185,20 @@ func main() {
zoneTypeFilter := provider.NewZoneTypeFilter(cfg.AWSZoneType)
zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter)
var awsSession *session.Session
if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" {
awsSession, err = aws.NewSession(
aws.AWSSessionConfig{
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
APIRetries: cfg.AWSAPIRetries,
},
)
if err != nil {
log.Fatal(err)
}
}
var p provider.Provider
switch cfg.Provider {
case "akamai":
@ -207,13 +226,11 @@ func main() {
BatchChangeSize: cfg.AWSBatchChangeSize,
BatchChangeInterval: cfg.AWSBatchChangeInterval,
EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth,
AssumeRole: cfg.AWSAssumeRole,
AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID,
APIRetries: cfg.AWSAPIRetries,
PreferCNAME: cfg.AWSPreferCNAME,
DryRun: cfg.DryRun,
ZoneCacheDuration: cfg.AWSZoneCacheDuration,
},
route53.New(awsSession),
)
case "aws-sd":
// Check that only compatible Registry is used with AWS-SD
@ -221,7 +238,7 @@ func main() {
log.Infof("Registry \"%s\" cannot be used with AWS Cloud Map. Switching to \"aws-sd\".", cfg.Registry)
cfg.Registry = "aws-sd"
}
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.AWSAssumeRole, cfg.AWSAssumeRoleExternalID, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID)
p, err = awssd.NewAWSSDProvider(domainFilter, cfg.AWSZoneType, cfg.DryRun, cfg.AWSSDServiceCleanup, cfg.TXTOwnerID, sd.New(awsSession))
case "azure-dns", "azure":
p, err = azure.NewAzureProvider(cfg.AzureConfigFile, domainFilter, zoneNameFilter, zoneIDFilter, cfg.AzureResourceGroup, cfg.AzureUserAssignedIdentityClientID, cfg.DryRun)
case "azure-private-dns":
@ -380,6 +397,12 @@ func main() {
var r registry.Registry
switch cfg.Registry {
case "dynamodb":
config := awsSDK.NewConfig()
if cfg.AWSDynamoDBRegion != "" {
config = config.WithRegion(cfg.AWSDynamoDBRegion)
}
r, err = registry.NewDynamoDBRegistry(p, cfg.TXTOwnerID, dynamodb.New(awsSession, config), cfg.AWSDynamoDBTable, cfg.TXTCacheInterval)
case "noop":
r, err = registry.NewNoopRegistry(p)
case "txt":

View File

@ -15,6 +15,7 @@ nav:
- Registries:
- About: registry/registry.md
- TXT: registry/txt.md
- DynamoDB: registry/dynamodb.md
- Advanced Topics:
- Initial Design: initial-design.md
- TTL: ttl.md

View File

@ -93,6 +93,8 @@ type Config struct {
AWSPreferCNAME bool
AWSZoneCacheDuration time.Duration
AWSSDServiceCleanup bool
AWSDynamoDBRegion string
AWSDynamoDBTable string
AzureConfigFile string
AzureResourceGroup string
AzureSubscriptionID string
@ -255,6 +257,8 @@ var defaultConfig = &Config{
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSDynamoDBRegion: "",
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
@ -414,7 +418,7 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("skipper-routegroup-groupversion", "The resource version for skipper routegroup").Default(source.DefaultRoutegroupVersion).StringVar(&cfg.SkipperRouteGroupVersion)
// Flags related to processing 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, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver")
app.Flag("source", "The resource types that are queried for endpoints; specify multiple times for multiple sources (required, options: service, ingress, node, fake, connector, gateway-httproute, gateway-grpcroute, gateway-tlsroute, gateway-tcproute, gateway-udproute, istio-gateway, istio-virtualservice, cloudfoundry, contour-ingressroute, contour-httpproxy, gloo-proxy, crd, empty, skipper-routegroup, openshift-route, ambassador-host, kong-tcpingress, f5-virtualserver, traefik-proxy)").Required().PlaceHolder("source").EnumsVar(&cfg.Sources, "service", "ingress", "node", "pod", "gateway-httproute", "gateway-grpcroute", "gateway-tlsroute", "gateway-tcproute", "gateway-udproute", "istio-gateway", "istio-virtualservice", "cloudfoundry", "contour-ingressroute", "contour-httpproxy", "gloo-proxy", "fake", "connector", "crd", "empty", "skipper-routegroup", "openshift-route", "ambassador-host", "kong-tcpingress", "f5-virtualserver", "traefik-proxy")
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)
@ -457,12 +461,12 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("alibaba-cloud-zone-type", "When using the Alibaba Cloud provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AlibabaCloudZoneType).EnumVar(&cfg.AlibabaCloudZoneType, "", "public", "private")
app.Flag("aws-zone-type", "When using the AWS provider, filter for zones of this type (optional, options: public, private)").Default(defaultConfig.AWSZoneType).EnumVar(&cfg.AWSZoneType, "", "public", "private")
app.Flag("aws-zone-tags", "When using the AWS provider, filter for zones with these tags").Default("").StringsVar(&cfg.AWSZoneTagFilter)
app.Flag("aws-assume-role", "When using the AWS provider, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("aws-assume-role-external-id", "When using the AWS provider and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID)
app.Flag("aws-assume-role", "When using the AWS API, assume this IAM role. Useful for hosted zones in another AWS account. Specify the full ARN, e.g. `arn:aws:iam::123455567:role/external-dns` (optional)").Default(defaultConfig.AWSAssumeRole).StringVar(&cfg.AWSAssumeRole)
app.Flag("aws-assume-role-external-id", "When using the AWS API and assuming a role then specify this external ID` (optional)").Default(defaultConfig.AWSAssumeRoleExternalID).StringVar(&cfg.AWSAssumeRoleExternalID)
app.Flag("aws-batch-change-size", "When using the AWS provider, set the maximum number of changes that will be applied in each batch.").Default(strconv.Itoa(defaultConfig.AWSBatchChangeSize)).IntVar(&cfg.AWSBatchChangeSize)
app.Flag("aws-batch-change-interval", "When using the AWS provider, set the interval between batch changes.").Default(defaultConfig.AWSBatchChangeInterval.String()).DurationVar(&cfg.AWSBatchChangeInterval)
app.Flag("aws-evaluate-target-health", "When using the AWS provider, set whether to evaluate the health of a DNS target (default: enabled, disable with --no-aws-evaluate-target-health)").Default(strconv.FormatBool(defaultConfig.AWSEvaluateTargetHealth)).BoolVar(&cfg.AWSEvaluateTargetHealth)
app.Flag("aws-api-retries", "When using the AWS provider, set the maximum number of retries for API calls before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("aws-api-retries", "When using the AWS API, set the maximum number of retries before giving up.").Default(strconv.Itoa(defaultConfig.AWSAPIRetries)).IntVar(&cfg.AWSAPIRetries)
app.Flag("aws-prefer-cname", "When using the AWS provider, prefer using CNAME instead of ALIAS (default: disabled)").BoolVar(&cfg.AWSPreferCNAME)
app.Flag("aws-zones-cache-duration", "When using the AWS provider, set the zones list cache TTL (0s to disable).").Default(defaultConfig.AWSZoneCacheDuration.String()).DurationVar(&cfg.AWSZoneCacheDuration)
app.Flag("aws-sd-service-cleanup", "When using the AWS CloudMap provider, delete empty Services without endpoints (default: disabled)").BoolVar(&cfg.AWSSDServiceCleanup)
@ -572,13 +576,15 @@ func (cfg *Config) ParseFlags(args []string) error {
app.Flag("policy", "Modify how DNS records are synchronized between sources and providers (default: sync, options: sync, upsert-only, create-only)").Default(defaultConfig.Policy).EnumVar(&cfg.Policy, "sync", "upsert-only", "create-only")
// Flags related to the registry
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "aws-sd")
app.Flag("txt-owner-id", "When using the TXT registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("registry", "The registry implementation to use to keep track of DNS record ownership (default: txt, options: txt, noop, dynamodb, aws-sd)").Default(defaultConfig.Registry).EnumVar(&cfg.Registry, "txt", "noop", "dynamodb", "aws-sd")
app.Flag("txt-owner-id", "When using the TXT or DynamoDB registry, a name that identifies this instance of ExternalDNS (default: default)").Default(defaultConfig.TXTOwnerID).StringVar(&cfg.TXTOwnerID)
app.Flag("txt-prefix", "When using the TXT registry, a custom string that's prefixed to each ownership DNS record (optional). Could contain record type template like '%{record_type}-prefix-'. Mutual exclusive with txt-suffix!").Default(defaultConfig.TXTPrefix).StringVar(&cfg.TXTPrefix)
app.Flag("txt-suffix", "When using the TXT registry, a custom string that's suffixed to the host portion of each ownership DNS record (optional). Could contain record type template like '-%{record_type}-suffix'. Mutual exclusive with txt-prefix!").Default(defaultConfig.TXTSuffix).StringVar(&cfg.TXTSuffix)
app.Flag("txt-wildcard-replacement", "When using the TXT registry, a custom string that's used instead of an asterisk for TXT records corresponding to wildcard DNS records (optional)").Default(defaultConfig.TXTWildcardReplacement).StringVar(&cfg.TXTWildcardReplacement)
app.Flag("txt-encrypt-enabled", "When using the TXT registry, set if TXT records should be encrypted before stored (default: disabled)").BoolVar(&cfg.TXTEncryptEnabled)
app.Flag("txt-encrypt-aes-key", "When using the TXT registry, set TXT record decryption and encryption 32 byte aes key (required when --txt-encrypt=true)").Default(defaultConfig.TXTEncryptAESKey).StringVar(&cfg.TXTEncryptAESKey)
app.Flag("dynamodb-region", "When using the DynamoDB registry, the AWS region of the DynamoDB table (optional)").Default(cfg.AWSDynamoDBRegion).StringVar(&cfg.AWSDynamoDBRegion)
app.Flag("dynamodb-table", "When using the DynamoDB registry, the name of the DynamoDB table (default: \"external-dns\")").Default(defaultConfig.AWSDynamoDBTable).StringVar(&cfg.AWSDynamoDBTable)
// Flags related to the main control loop
app.Flag("txt-cache-interval", "The interval between cache synchronizations in duration format (default: disabled)").Default(defaultConfig.TXTCacheInterval.String()).DurationVar(&cfg.TXTCacheInterval)

View File

@ -65,6 +65,7 @@ var (
AWSPreferCNAME: false,
AWSZoneCacheDuration: 0 * time.Second,
AWSSDServiceCleanup: false,
AWSDynamoDBTable: "external-dns",
AzureConfigFile: "/etc/kubernetes/azure.json",
AzureResourceGroup: "",
AzureSubscriptionID: "",
@ -170,6 +171,7 @@ var (
AWSPreferCNAME: true,
AWSZoneCacheDuration: 10 * time.Second,
AWSSDServiceCleanup: true,
AWSDynamoDBTable: "custom-table",
AzureConfigFile: "azure.json",
AzureResourceGroup: "arg",
AzureSubscriptionID: "arg",
@ -351,6 +353,7 @@ func TestParseFlags(t *testing.T) {
"--txt-owner-id=owner-1",
"--txt-prefix=associated-txt-record",
"--txt-cache-interval=12h",
"--dynamodb-table=custom-table",
"--interval=10m",
"--min-event-sync-interval=50s",
"--once",
@ -464,6 +467,7 @@ func TestParseFlags(t *testing.T) {
"EXTERNAL_DNS_AWS_PREFER_CNAME": "true",
"EXTERNAL_DNS_AWS_ZONES_CACHE_DURATION": "10s",
"EXTERNAL_DNS_AWS_SD_SERVICE_CLEANUP": "true",
"EXTERNAL_DNS_DYNAMODB_TABLE": "custom-table",
"EXTERNAL_DNS_POLICY": "upsert-only",
"EXTERNAL_DNS_REGISTRY": "noop",
"EXTERNAL_DNS_TXT_OWNER_ID": "owner-1",

View File

@ -37,8 +37,6 @@ type Plan struct {
Current []*endpoint.Endpoint
// List of desired records
Desired []*endpoint.Endpoint
// List of missing records to be created, use for the migrations (e.g. old-new TXT format)
Missing []*endpoint.Endpoint
// Policies under which the desired changes are calculated
Policies []Policy
// List of changes necessary to move towards desired state
@ -177,11 +175,6 @@ func (p *Plan) Calculate() *Plan {
changes = pol.Apply(changes)
}
// Handle the migration of the TXT records created before the new format (introduced in v0.12.0)
if len(p.Missing) > 0 {
changes.Create = append(changes.Create, filterRecordsForPlan(p.Missing, p.DomainFilter, append(p.ManagedRecords, endpoint.RecordTypeTXT))...)
}
plan := &Plan{
Current: p.Current,
Desired: p.Desired,

View File

@ -51,9 +51,6 @@ type PlanTestSuite struct {
domainFilterFiltered2 *endpoint.Endpoint
domainFilterFiltered3 *endpoint.Endpoint
domainFilterExcluded *endpoint.Endpoint
domainFilterFilteredTXT1 *endpoint.Endpoint
domainFilterFilteredTXT2 *endpoint.Endpoint
domainFilterExcludedTXT *endpoint.Endpoint
}
func (suite *PlanTestSuite) SetupTest() {
@ -233,21 +230,6 @@ func (suite *PlanTestSuite) SetupTest() {
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: "A",
}
suite.domainFilterFilteredTXT1 = &endpoint.Endpoint{
DNSName: "a-foo.domain.tld",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: "TXT",
}
suite.domainFilterFilteredTXT2 = &endpoint.Endpoint{
DNSName: "cname-bar.domain.tld",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: "TXT",
}
suite.domainFilterExcludedTXT = &endpoint.Endpoint{
DNSName: "cname-bar.otherdomain.tld",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: "TXT",
}
}
func (suite *PlanTestSuite) TestSyncFirstRound() {
@ -661,21 +643,6 @@ func (suite *PlanTestSuite) TestDomainFiltersUpdate() {
validateEntries(suite.T(), changes.Delete, expectedDelete)
}
func (suite *PlanTestSuite) TestMissing() {
missing := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2, suite.domainFilterExcludedTXT}
expectedCreate := []*endpoint.Endpoint{suite.domainFilterFilteredTXT1, suite.domainFilterFilteredTXT2}
p := &Plan{
Policies: []Policy{&SyncPolicy{}},
Missing: missing,
DomainFilter: endpoint.NewDomainFilter([]string{"domain.tld"}),
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
changes := p.Calculate().Changes
validateEntries(suite.T(), changes.Create, expectedCreate)
}
func (suite *PlanTestSuite) TestAAAARecords() {
current := []*endpoint.Endpoint{}

View File

@ -25,11 +25,8 @@ import (
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/linki/instrumented_http"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
@ -47,10 +44,12 @@ const (
// As we are using the standard AWS client, this should already be compliant.
// Hence, ifever AWS decides to raise this limit, we will automatically reduce the pressure on rate limits
route53PageSize = "300"
// provider specific key that designates whether an AWS ALIAS record has the EvaluateTargetHealth
// field set to true.
providerSpecificAlias = "alias"
providerSpecificTargetHostedZone = "aws/target-hosted-zone"
// providerSpecificAlias specifies whether a CNAME endpoint maps to an AWS ALIAS record.
providerSpecificAlias = "alias"
providerSpecificTargetHostedZone = "aws/target-hosted-zone"
// providerSpecificEvaluateTargetHealth specifies whether an AWS ALIAS record
// has the EvaluateTargetHealth field set to true. Present iff the endpoint
// has a `providerSpecificAlias` value of `true`.
providerSpecificEvaluateTargetHealth = "aws/evaluate-target-health"
providerSpecificWeight = "aws/weight"
providerSpecificRegion = "aws/region"
@ -224,49 +223,15 @@ type AWSConfig struct {
BatchChangeSize int
BatchChangeInterval time.Duration
EvaluateTargetHealth bool
AssumeRole string
AssumeRoleExternalID string
APIRetries int
PreferCNAME bool
DryRun bool
ZoneCacheDuration time.Duration
}
// NewAWSProvider initializes a new AWS Route53 based Provider.
func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries)
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
},
}),
)
session, err := session.NewSessionWithOptions(session.Options{
Config: *config,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, errors.Wrap(err, "failed to instantiate AWS session")
}
if awsConfig.AssumeRole != "" {
if awsConfig.AssumeRoleExternalID != "" {
log.Infof("Assuming role: %s with external id %s", awsConfig.AssumeRole, awsConfig.AssumeRoleExternalID)
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole, func(p *stscreds.AssumeRoleProvider) {
p.ExternalID = &awsConfig.AssumeRoleExternalID
}))
} else {
log.Infof("Assuming role: %s", awsConfig.AssumeRole)
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole))
}
}
func NewAWSProvider(awsConfig AWSConfig, client Route53API) (*AWSProvider, error) {
provider := &AWSProvider{
client: route53.New(session),
client: client,
domainFilter: awsConfig.DomainFilter,
zoneIDFilter: awsConfig.ZoneIDFilter,
zoneTypeFilter: awsConfig.ZoneTypeFilter,
@ -283,13 +248,6 @@ func NewAWSProvider(awsConfig AWSConfig) (*AWSProvider, error) {
return provider, nil
}
func (p *AWSProvider) PropertyValuesEqual(name string, previous string, current string) bool {
if name == "aws/evaluate-target-health" {
return true
}
return p.BaseProvider.PropertyValuesEqual(name, previous, current)
}
// Zones returns the list of hosted zones.
func (p *AWSProvider) Zones(ctx context.Context) (map[string]*route53.HostedZone, error) {
if p.zonesCache.zones != nil && time.Since(p.zonesCache.age) < p.zonesCache.duration {
@ -390,7 +348,11 @@ func (p *AWSProvider) records(ctx context.Context, zones map[string]*route53.Hos
targets[idx] = aws.StringValue(rr.Value)
}
newEndpoints = append(newEndpoints, endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...))
ep := endpoint.NewEndpointWithTTL(wildcardUnescape(aws.StringValue(r.Name)), aws.StringValue(r.Type), ttl, targets...)
if aws.StringValue(r.Type) == endpoint.RecordTypeCNAME {
ep = ep.WithProviderSpecific(providerSpecificAlias, "false")
}
newEndpoints = append(newEndpoints, ep)
}
if r.AliasTarget != nil {
@ -466,8 +428,12 @@ func (p *AWSProvider) requiresDeleteCreate(old *endpoint.Endpoint, new *endpoint
}
// an ALIAS record change to/from a CNAME
if old.RecordType == endpoint.RecordTypeCNAME && useAlias(old, p.preferCNAME) != useAlias(new, p.preferCNAME) {
return true
if old.RecordType == endpoint.RecordTypeCNAME {
oldAlias, _ := old.GetProviderSpecificProperty(providerSpecificAlias)
newAlias, _ := new.GetProviderSpecificProperty(providerSpecificAlias)
if oldAlias != newAlias {
return true
}
}
// a set identifier change
@ -667,23 +633,29 @@ func (p *AWSProvider) newChanges(action string, endpoints []*endpoint.Endpoint)
func (p *AWSProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
for _, ep := range endpoints {
alias := false
if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok {
alias = aliasString.Value == "true"
} else if useAlias(ep, p.preferCNAME) {
alias = true
log.Debugf("Modifying endpoint: %v, setting %s=true", ep, providerSpecificAlias)
ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{
Name: providerSpecificAlias,
Value: "true",
})
if ep.RecordType != endpoint.RecordTypeCNAME {
ep.DeleteProviderSpecificProperty(providerSpecificAlias)
} else if aliasString, ok := ep.GetProviderSpecificProperty(providerSpecificAlias); ok {
alias = aliasString == "true"
if !alias && aliasString != "false" {
ep.SetProviderSpecificProperty(providerSpecificAlias, "false")
}
} else {
alias = useAlias(ep, p.preferCNAME)
log.Debugf("Modifying endpoint: %v, setting %s=%v", ep, providerSpecificAlias, alias)
ep.SetProviderSpecificProperty(providerSpecificAlias, strconv.FormatBool(alias))
}
if _, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); alias && !ok {
log.Debugf("Modifying endpoint: %v, setting %s=%t", ep, providerSpecificEvaluateTargetHealth, p.evaluateTargetHealth)
ep.ProviderSpecific = append(ep.ProviderSpecific, endpoint.ProviderSpecificProperty{
Name: providerSpecificEvaluateTargetHealth,
Value: fmt.Sprintf("%t", p.evaluateTargetHealth),
})
if alias {
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok {
if prop != "true" && prop != "false" {
ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, "false")
}
} else {
ep.SetProviderSpecificProperty(providerSpecificEvaluateTargetHealth, strconv.FormatBool(p.evaluateTargetHealth))
}
} else {
ep.DeleteProviderSpecificProperty(providerSpecificEvaluateTargetHealth)
}
}
return endpoints
@ -706,7 +678,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
if targetHostedZone := isAWSAlias(ep); targetHostedZone != "" {
evalTargetHealth := p.evaluateTargetHealth
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificEvaluateTargetHealth); ok {
evalTargetHealth = prop.Value == "true"
evalTargetHealth = prop == "true"
}
// If the endpoint has a Dualstack label, append a change for AAAA record as well.
if val, ok := ep.Labels[endpoint.DualstackLabelKey]; ok {
@ -737,18 +709,18 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
if setIdentifier != "" {
change.ResourceRecordSet.SetIdentifier = aws.String(setIdentifier)
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificWeight); ok {
weight, err := strconv.ParseInt(prop.Value, 10, 64)
weight, err := strconv.ParseInt(prop, 10, 64)
if err != nil {
log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop.Value, err)
log.Errorf("Failed parsing value of %s: %s: %v; using weight of 0", providerSpecificWeight, prop, err)
weight = 0
}
change.ResourceRecordSet.Weight = aws.Int64(weight)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificRegion); ok {
change.ResourceRecordSet.Region = aws.String(prop.Value)
change.ResourceRecordSet.Region = aws.String(prop)
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificFailover); ok {
change.ResourceRecordSet.Failover = aws.String(prop.Value)
change.ResourceRecordSet.Failover = aws.String(prop)
}
if _, ok := ep.GetProviderSpecificProperty(providerSpecificMultiValueAnswer); ok {
change.ResourceRecordSet.MultiValueAnswer = aws.Bool(true)
@ -757,15 +729,15 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
geolocation := &route53.GeoLocation{}
useGeolocation := false
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationContinentCode); ok {
geolocation.ContinentCode = aws.String(prop.Value)
geolocation.ContinentCode = aws.String(prop)
useGeolocation = true
} else {
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationCountryCode); ok {
geolocation.CountryCode = aws.String(prop.Value)
geolocation.CountryCode = aws.String(prop)
useGeolocation = true
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificGeolocationSubdivisionCode); ok {
geolocation.SubdivisionCode = aws.String(prop.Value)
geolocation.SubdivisionCode = aws.String(prop)
useGeolocation = true
}
}
@ -775,7 +747,7 @@ func (p *AWSProvider) newChange(action string, ep *endpoint.Endpoint) (*Route53C
}
if prop, ok := ep.GetProviderSpecificProperty(providerSpecificHealthCheckID); ok {
change.ResourceRecordSet.HealthCheckId = aws.String(prop.Value)
change.ResourceRecordSet.HealthCheckId = aws.String(prop)
}
if ownedRecord, ok := ep.Labels[endpoint.OwnedRecordLabelKey]; ok {
@ -989,13 +961,13 @@ func useAlias(ep *endpoint.Endpoint, preferCNAME bool) bool {
// isAWSAlias determines if a given endpoint is supposed to create an AWS Alias record
// and (if so) returns the target hosted zone ID
func isAWSAlias(ep *endpoint.Endpoint) string {
prop, exists := ep.GetProviderSpecificProperty(providerSpecificAlias)
if exists && prop.Value == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 {
isAlias, exists := ep.GetProviderSpecificProperty(providerSpecificAlias)
if exists && isAlias == "true" && ep.RecordType == endpoint.RecordTypeCNAME && len(ep.Targets) > 0 {
// alias records can only point to canonical hosted zones (e.g. to ELBs) or other records in the same zone
if hostedZoneID, ok := ep.GetProviderSpecificProperty(providerSpecificTargetHostedZone); ok {
// existing Endpoint where we got the target hosted zone from the Route53 data
return hostedZoneID.Value
return hostedZoneID
}
// check if the target is in a canonical hosted zone

View File

@ -494,7 +494,7 @@ func TestAWSRecords(t *testing.T) {
records, err := provider.Records(context.Background())
require.NoError(t, err)
validateEndpoints(t, records, []*endpoint.Endpoint{
validateEndpoints(t, provider, records, []*endpoint.Endpoint{
endpoint.NewEndpointWithTTL("list-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "1.2.3.4"),
endpoint.NewEndpointWithTTL("list-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
endpoint.NewEndpointWithTTL("*.wildcard-test.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8"),
@ -511,7 +511,7 @@ func TestAWSRecords(t *testing.T) {
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.RecordTypeCNAME, endpoint.TTL(recordTTL), "foo.example.com").WithSetIdentifier("test-set-1").WithProviderSpecific(providerSpecificWeight, "10").WithProviderSpecific(providerSpecificHealthCheckID, "foo-bar-healthcheck-id").WithProviderSpecific(providerSpecificAlias, "false"),
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"),
endpoint.NewEndpointWithTTL("mail.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeMX, endpoint.TTL(recordTTL), "10 mailhost1.example.com", "20 mailhost2.example.com"),
})
@ -529,11 +529,11 @@ func TestAWSAdjustEndpoints(t *testing.T) {
endpoint.NewEndpoint("cname-test-elb-no-eth.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "false"), // eth = evaluate target health
}
provider.AdjustEndpoints(records)
records = provider.AdjustEndpoints(records)
validateEndpoints(t, records, []*endpoint.Endpoint{
validateEndpoints(t, provider, records, []*endpoint.Endpoint{
endpoint.NewEndpoint("a-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeA, "8.8.8.8"),
endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com"),
endpoint.NewEndpoint("cname-test.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.example.com").WithProviderSpecific(providerSpecificAlias, "false"),
endpoint.NewEndpoint("cname-test-alias.zone-1.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "alias-target.zone-2.ext-dns-test-2.teapot.zalan.do").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpoint("cname-test-elb.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true").WithProviderSpecific(providerSpecificEvaluateTargetHealth, "true"),
endpoint.NewEndpoint("cname-test-elb-no-alias.zone-2.ext-dns-test-2.teapot.zalan.do", endpoint.RecordTypeCNAME, "foo.eu-central-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "false"),
@ -1426,7 +1426,7 @@ func TestAWSsubmitChanges(t *testing.T) {
records, err := provider.Records(ctx)
require.NoError(t, err)
validateEndpoints(t, records, endpoints)
validateEndpoints(t, provider, records, endpoints)
}
func TestAWSsubmitChangesError(t *testing.T) {
@ -1607,8 +1607,11 @@ func TestAWSBatchChangeSetExceedingNameChange(t *testing.T) {
require.Equal(t, 0, len(batchCs))
}
func validateEndpoints(t *testing.T, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
func validateEndpoints(t *testing.T, provider *AWSProvider, endpoints []*endpoint.Endpoint, expected []*endpoint.Endpoint) {
assert.True(t, testutils.SameEndpoints(endpoints, expected), "actual and expected endpoints don't match. %+v:%+v", endpoints, expected)
normalized := provider.AdjustEndpoints(endpoints)
assert.True(t, testutils.SameEndpoints(normalized, expected), "actual and normalized endpoints don't match. %+v:%+v", endpoints, normalized)
}
func validateAWSZones(t *testing.T, zones map[string]*route53.HostedZone, expected map[string]*route53.HostedZone) {
@ -1840,51 +1843,6 @@ func TestAWSSuitableZones(t *testing.T) {
}
}
func TestAWSHealthTargetAnnotation(tt *testing.T) {
comparator := func(name, previous, current string) bool {
return previous == current
}
for _, test := range []struct {
name string
current *endpoint.Endpoint
desired *endpoint.Endpoint
propertyComparator func(name, previous, current string) bool
shouldUpdate bool
}{
{
name: "skip AWS target health",
current: &endpoint.Endpoint{
RecordType: "A",
DNSName: "foo.com",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "true"},
},
},
desired: &endpoint.Endpoint{
DNSName: "foo.com",
RecordType: "A",
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{Name: "aws/evaluate-target-health", Value: "false"},
},
},
propertyComparator: comparator,
shouldUpdate: false,
},
} {
tt.Run(test.name, func(t *testing.T) {
provider := &AWSProvider{}
plan := &plan.Plan{
Current: []*endpoint.Endpoint{test.current},
Desired: []*endpoint.Endpoint{test.desired},
PropertyComparator: provider.PropertyValuesEqual,
ManagedRecords: []string{endpoint.RecordTypeA, endpoint.RecordTypeCNAME},
}
plan = plan.Calculate()
assert.Equal(t, test.shouldUpdate, len(plan.Changes.UpdateNew) == 1)
})
}
}
func createAWSZone(t *testing.T, provider *AWSProvider, zone *route53.HostedZone) {
params := &route53.CreateHostedZoneInput{
CallerReference: aws.String("external-dns.alpha.kubernetes.io/test-zone"),
@ -1908,7 +1866,7 @@ func setAWSRecords(t *testing.T, provider *AWSProvider, records []*route53.Resou
endpoints, err := provider.Records(ctx)
require.NoError(t, err)
validateEndpoints(t, endpoints, []*endpoint.Endpoint{})
validateEndpoints(t, provider, endpoints, []*endpoint.Endpoint{})
var changes Route53Changes
for _, record := range records {
@ -2071,13 +2029,13 @@ func TestRequiresDeleteCreate(t *testing.T) {
provider, _ := newAWSProvider(t, endpoint.NewDomainFilter([]string{"foo.bar."}), provider.NewZoneIDFilter([]string{}), provider.NewZoneTypeFilter(""), defaultEvaluateTargetHealth, false, nil)
oldRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeA, endpoint.TTL(recordTTL), "8.8.8.8")
newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar")
newRecordType := endpoint.NewEndpointWithTTL("recordType", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false")
assert.False(t, provider.requiresDeleteCreate(oldRecordType, oldRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, oldRecordType)
assert.True(t, provider.requiresDeleteCreate(oldRecordType, newRecordType), "actual and expected endpoints don't match. %+v:%+v", oldRecordType, newRecordType)
oldCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar")
newCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com")
oldCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar").WithProviderSpecific(providerSpecificAlias, "false")
newCNAMEAlias := endpoint.NewEndpointWithTTL("CNAMEAlias", endpoint.RecordTypeCNAME, endpoint.TTL(recordTTL), "bar.us-east-1.elb.amazonaws.com").WithProviderSpecific(providerSpecificAlias, "true")
assert.False(t, provider.requiresDeleteCreate(oldCNAMEAlias, oldCNAMEAlias), "actual and expected endpoints don't match. %+v:%+v", oldCNAMEAlias, oldCNAMEAlias.DNSName)
assert.True(t, provider.requiresDeleteCreate(oldCNAMEAlias, newCNAMEAlias), "actual and expected endpoints don't match. %+v:%+v", oldCNAMEAlias, newCNAMEAlias)

75
provider/aws/session.go Normal file
View File

@ -0,0 +1,75 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package aws
import (
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/linki/instrumented_http"
"github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
)
// AWSSessionConfig contains configuration to create a new AWS provider.
type AWSSessionConfig struct {
AssumeRole string
AssumeRoleExternalID string
APIRetries int
}
func NewSession(awsConfig AWSSessionConfig) (*session.Session, error) {
config := aws.NewConfig().WithMaxRetries(awsConfig.APIRetries)
config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
},
}),
)
session, err := session.NewSessionWithOptions(session.Options{
Config: *config,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, fmt.Errorf("instantiating AWS session: %w", err)
}
if awsConfig.AssumeRole != "" {
if awsConfig.AssumeRoleExternalID != "" {
logrus.Infof("Assuming role: %s with external id %s", awsConfig.AssumeRole, awsConfig.AssumeRoleExternalID)
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole, func(p *stscreds.AssumeRoleProvider) {
p.ExternalID = &awsConfig.AssumeRoleExternalID
}))
} else {
logrus.Infof("Assuming role: %s", awsConfig.AssumeRole)
session.Config.WithCredentials(stscreds.NewCredentials(session, awsConfig.AssumeRole))
}
}
session.Handlers.Build.PushBack(request.MakeAddToUserAgentHandler("ExternalDNS", externaldns.Version))
return session, nil
}

View File

@ -25,15 +25,10 @@ import (
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
sd "github.com/aws/aws-sdk-go/service/servicediscovery"
"github.com/linki/instrumented_http"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
@ -86,42 +81,9 @@ type AWSSDProvider struct {
}
// NewAWSSDProvider initializes a new AWS Cloud Map based Provider.
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, assumeRole string, assumeRoleExternalID string, dryRun, cleanEmptyService bool, ownerID string) (*AWSSDProvider, error) {
config := aws.NewConfig()
config = config.WithHTTPClient(
instrumented_http.NewClient(config.HTTPClient, &instrumented_http.Callbacks{
PathProcessor: func(path string) string {
parts := strings.Split(path, "/")
return parts[len(parts)-1]
},
}),
)
sess, err := session.NewSessionWithOptions(session.Options{
Config: *config,
SharedConfigState: session.SharedConfigEnable,
})
if err != nil {
return nil, err
}
if assumeRole != "" {
if assumeRoleExternalID != "" {
log.Infof("Assuming role %q with external ID %q", assumeRole, assumeRoleExternalID)
sess.Config.WithCredentials(stscreds.NewCredentials(sess, assumeRole, func(p *stscreds.AssumeRoleProvider) {
p.ExternalID = &assumeRoleExternalID
}))
} else {
log.Infof("Assuming role: %s", assumeRole)
sess.Config.WithCredentials(stscreds.NewCredentials(sess, assumeRole))
}
}
sess.Handlers.Build.PushBack(request.MakeAddToUserAgentHandler("ExternalDNS", externaldns.Version))
func NewAWSSDProvider(domainFilter endpoint.DomainFilter, namespaceType string, dryRun, cleanEmptyService bool, ownerID string, client AWSSDClient) (*AWSSDProvider, error) {
provider := &AWSSDProvider{
client: sd.New(sess),
client: client,
dryRun: dryRun,
namespaceFilter: domainFilter,
namespaceTypeFilter: newSdNamespaceFilter(namespaceType),

View File

@ -69,7 +69,7 @@ type cloudFlareDNS interface {
ListZonesContext(ctx context.Context, opts ...cloudflare.ReqOption) (cloudflare.ZonesResponse, error)
ZoneDetails(ctx context.Context, zoneID string) (cloudflare.Zone, error)
ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error)
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error)
CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error)
DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error
UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error
}
@ -90,7 +90,7 @@ func (z zoneService) ZoneIDByName(zoneName string) (string, error) {
return z.service.ZoneIDByName(zoneName)
}
func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error) {
func (z zoneService) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
return z.service.CreateDNSRecord(ctx, rc, rp)
}
@ -99,7 +99,8 @@ func (z zoneService) ListDNSRecords(ctx context.Context, rc *cloudflare.Resource
}
func (z zoneService) UpdateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.UpdateDNSRecordParams) error {
return z.service.UpdateDNSRecord(ctx, rc, rp)
_, err := z.service.UpdateDNSRecord(ctx, rc, rp)
return err
}
func (z zoneService) DeleteDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, recordID string) error {
@ -137,9 +138,20 @@ type RecordParamsTypes interface {
cloudflare.UpdateDNSRecordParams | cloudflare.CreateDNSRecordParams
}
// getRecordParam is a generic function that returns the appropriate Record Param based on the cloudFlareChange passed in
func getRecordParam[T RecordParamsTypes](cfc cloudFlareChange) T {
return T{
// getUpdateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
func getUpdateDNSRecordParam(cfc cloudFlareChange) cloudflare.UpdateDNSRecordParams {
return cloudflare.UpdateDNSRecordParams{
Name: cfc.ResourceRecord.Name,
TTL: cfc.ResourceRecord.TTL,
Proxied: cfc.ResourceRecord.Proxied,
Type: cfc.ResourceRecord.Type,
Content: cfc.ResourceRecord.Content,
}
}
// getCreateDNSRecordParam is a function that returns the appropriate Record Param based on the cloudFlareChange passed in
func getCreateDNSRecordParam(cfc cloudFlareChange) cloudflare.CreateDNSRecordParams {
return cloudflare.CreateDNSRecordParams{
Name: cfc.ResourceRecord.Name,
TTL: cfc.ResourceRecord.TTL,
Proxied: cfc.ResourceRecord.Proxied,
@ -334,7 +346,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
log.WithFields(logFields).Errorf("failed to find previous record: %v", change.ResourceRecord)
continue
}
recordParam := getRecordParam[cloudflare.UpdateDNSRecordParams](*change)
recordParam := getUpdateDNSRecordParam(*change)
recordParam.ID = recordID
err := p.Client.UpdateDNSRecord(ctx, resourceContainer, recordParam)
if err != nil {
@ -351,7 +363,7 @@ func (p *CloudFlareProvider) submitChanges(ctx context.Context, changes []*cloud
log.WithFields(logFields).Errorf("failed to delete record: %v", err)
}
} else if change.Action == cloudFlareCreate {
recordParam := getRecordParam[cloudflare.CreateDNSRecordParams](*change)
recordParam := getCreateDNSRecordParam(*change)
_, err := p.Client.CreateDNSRecord(ctx, resourceContainer, recordParam)
if err != nil {
log.WithFields(logFields).Errorf("failed to create record: %v", err)

View File

@ -130,7 +130,7 @@ func getDNSRecordFromRecordParams(rp any) cloudflare.DNSRecord {
}
}
func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (*cloudflare.DNSRecordResponse, error) {
func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.CreateDNSRecordParams) (cloudflare.DNSRecord, error) {
recordData := getDNSRecordFromRecordParams(rp)
m.Actions = append(m.Actions, MockAction{
Name: "Create",
@ -141,7 +141,7 @@ func (m *mockCloudFlareClient) CreateDNSRecord(ctx context.Context, rc *cloudfla
if zone, ok := m.Records[rc.Identifier]; ok {
zone[rp.ID] = recordData
}
return nil, nil
return cloudflare.DNSRecord{}, nil
}
func (m *mockCloudFlareClient) ListDNSRecords(ctx context.Context, rc *cloudflare.ResourceContainer, rp cloudflare.ListDNSRecordsParams) ([]cloudflare.DNSRecord, *cloudflare.ResultInfo, error) {
@ -680,7 +680,7 @@ func TestCloudflareProvider(t *testing.T) {
_ = os.Unsetenv("CF_API_TOKEN")
tokenFile := "/tmp/cf_api_token"
if err := os.WriteFile(tokenFile, []byte("abc123def"), 0644); err != nil {
if err := os.WriteFile(tokenFile, []byte("abc123def"), 0o644); err != nil {
t.Errorf("failed to write token file, %s", err)
}
_ = os.Setenv("CF_API_TOKEN", tokenFile)

View File

@ -23,9 +23,13 @@ import (
"errors"
"fmt"
"io"
"math/rand"
"net/http"
"strconv"
"time"
"golang.org/x/time/rate"
"sigs.k8s.io/external-dns/pkg/apis/externaldns"
)
@ -71,6 +75,9 @@ type Client struct {
// Client is the underlying HTTP client used to run the requests. It may be overloaded but a default one is instanciated in ``NewClient`` by default.
Client *http.Client
// GoDaddy limits to 60 requests per minute
Ratelimiter *rate.Limiter
// Logger is used to log HTTP requests and responses.
Logger Logger
@ -115,6 +122,7 @@ func NewClient(useOTE bool, apiKey, apiSecret string) (*Client, error) {
APISecret: apiSecret,
APIEndPoint: endpoint,
Client: &http.Client{},
Ratelimiter: rate.NewLimiter(rate.Every(60*time.Second), 60),
Timeout: DefaultTimeout,
}
@ -216,7 +224,22 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
if c.Logger != nil {
c.Logger.LogRequest(req)
}
c.Ratelimiter.Wait(req.Context())
resp, err := c.Client.Do(req)
// In case of several clients behind NAT we still can hit rate limit
for i := 1; i < 3 && err == nil && resp.StatusCode == 429; i++ {
retryAfter, _ := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 0)
jitter := rand.Int63n(retryAfter)
retryAfterSec := retryAfter + jitter/2
sleepTime := time.Duration(retryAfterSec) * time.Second
time.Sleep(sleepTime)
c.Ratelimiter.Wait(req.Context())
resp, err = c.Client.Do(req)
}
if err != nil {
return nil, err
}

View File

@ -5,7 +5,7 @@ Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
@ -34,13 +34,13 @@ import (
const (
gdMinimalTTL = 600
gdCreate = 0
gdUpdate = 1
gdReplace = 1
gdDelete = 2
)
var actionNames = []string{
"create",
"update",
"replace",
"delete",
}
@ -82,9 +82,8 @@ type gdRecordField struct {
Service *string `json:"service,omitempty"`
}
type gdUpdateRecordField struct {
type gdReplaceRecordField struct {
Data string `json:"data"`
Name string `json:"name"`
TTL int64 `json:"ttl"`
Port *int `json:"port,omitempty"`
Priority *int `json:"priority,omitempty"`
@ -247,7 +246,7 @@ func (p *GDProvider) records(ctx *context.Context, zone string, all bool) (*gdRe
results.records = append(results.records, rec)
} else {
log.Infof("GoDaddy: Discard record %s for %s is %+v", rec.Name, zone, rec)
log.Infof("GoDaddy: Ignore record %s for %s is %+v", rec.Name, zone, rec)
}
}
@ -347,28 +346,20 @@ func (p *GDProvider) changeAllRecords(endpoints []gdEndpoint, zoneRecords []*gdR
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", dnsName)
} else {
dnsName = strings.TrimSuffix(dnsName, "."+zone)
if dnsName == zone {
dnsName = ""
}
if e.endpoint.RecordType == endpoint.RecordTypeA && (len(dnsName) == 0) {
dnsName = "@"
}
for _, target := range e.endpoint.Targets {
change := gdRecordField{
Type: e.endpoint.RecordType,
Name: dnsName,
TTL: p.ttl,
Data: target,
}
e.endpoint.RecordTTL = endpoint.TTL(maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL)))
if e.endpoint.RecordTTL.IsConfigured() {
change.TTL = maxOf(gdMinimalTTL, int64(e.endpoint.RecordTTL))
}
if err := zoneRecord.applyEndpoint(e.action, p.client, *e.endpoint, dnsName, p.DryRun); err != nil {
log.Errorf("Unable to apply change %s on record %s type %s, %v", actionNames[e.action], dnsName, e.endpoint.RecordType, err)
if err := zoneRecord.applyChange(e.action, p.client, change, p.DryRun); err != nil {
log.Errorf("Unable to apply change %s on record %s, %v", actionNames[e.action], change, err)
return err
}
return err
}
}
}
@ -393,11 +384,49 @@ func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) er
changedZoneRecords[i] = &records[i]
}
allChanges := make([]gdEndpoint, 0, countTargets(changes))
var allChanges []gdEndpoint
allChanges = p.appendChange(gdDelete, changes.Delete, allChanges)
allChanges = p.appendChange(gdDelete, changes.UpdateOld, allChanges)
allChanges = p.appendChange(gdCreate, changes.UpdateNew, allChanges)
iOldSkip := make(map[int]bool)
iNewSkip := make(map[int]bool)
for iOld, recOld := range changes.UpdateOld {
for iNew, recNew := range changes.UpdateNew {
if recOld.DNSName == recNew.DNSName && recOld.RecordType == recNew.RecordType {
ReplaceEndpoints := []*endpoint.Endpoint{recNew}
allChanges = p.appendChange(gdReplace, ReplaceEndpoints, allChanges)
iOldSkip[iOld] = true
iNewSkip[iNew] = true
break
}
}
}
for iOld, recOld := range changes.UpdateOld {
_, found := iOldSkip[iOld]
if found {
continue
}
for iNew, recNew := range changes.UpdateNew {
_, found := iNewSkip[iNew]
if found {
continue
}
if recOld.DNSName != recNew.DNSName {
continue
}
DeleteEndpoints := []*endpoint.Endpoint{recOld}
CreateEndpoints := []*endpoint.Endpoint{recNew}
allChanges = p.appendChange(gdDelete, DeleteEndpoints, allChanges)
allChanges = p.appendChange(gdCreate, CreateEndpoints, allChanges)
break
}
}
allChanges = p.appendChange(gdCreate, changes.Create, allChanges)
log.Infof("GoDaddy: %d changes will be done", len(allChanges))
@ -409,18 +438,73 @@ func (p *GDProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) er
return nil
}
func (p *gdRecords) addRecord(client gdClient, change gdRecordField, dryRun bool) error {
func (p *gdRecords) addRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
var response GDErrorResponse
for _, target := range endpoint.Targets {
change := gdRecordField{
Type: endpoint.RecordType,
Name: dnsName,
TTL: int64(endpoint.RecordTTL),
Data: target,
}
p.records = append(p.records, change)
p.changed = true
log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
if dryRun {
log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil {
log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response)
return err
}
}
return nil
}
func (p *gdRecords) replaceRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
changed := []gdReplaceRecordField{}
records := []string{}
for _, target := range endpoint.Targets {
change := gdRecordField{
Type: endpoint.RecordType,
Name: dnsName,
TTL: int64(endpoint.RecordTTL),
Data: target,
}
for index, record := range p.records {
if record.Type == change.Type && record.Name == change.Name {
p.records[index] = change
p.changed = true
}
}
records = append(records, target)
changed = append(changed, gdReplaceRecordField{
Data: change.Data,
TTL: change.TTL,
Port: change.Port,
Priority: change.Priority,
Weight: change.Weight,
Protocol: change.Protocol,
Service: change.Service,
})
}
var response GDErrorResponse
log.Debugf("GoDaddy: Add an entry %s to zone %s", change.String(), p.zone)
p.records = append(p.records, change)
p.changed = true
if dryRun {
log.Infof("[DryRun] - Add record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records", p.zone), []gdRecordField{change}, &response); err != nil {
log.Errorf("Add record %s.%s of type %s failed: %s", change.Name, p.zone, change.Type, response)
log.Infof("[DryRun] - Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
return nil
}
log.Debugf("Replace record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
if err := client.Put(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), changed, &response); err != nil {
log.Errorf("Replace record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response)
return err
}
@ -428,83 +512,62 @@ func (p *gdRecords) addRecord(client gdClient, change gdRecordField, dryRun bool
return nil
}
func (p *gdRecords) updateRecord(client gdClient, change gdRecordField, dryRun bool) error {
log.Debugf("GoDaddy: Update an entry %s to zone %s", change.String(), p.zone)
// Remove one record from the record list
func (p *gdRecords) deleteRecord(client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
records := []string{}
for index, record := range p.records {
if record.Type == change.Type && record.Name == change.Name {
var response GDErrorResponse
for _, target := range endpoint.Targets {
change := gdRecordField{
Type: endpoint.RecordType,
Name: dnsName,
TTL: int64(endpoint.RecordTTL),
Data: target,
}
records = append(records, target)
p.records[index] = change
p.changed = true
log.Debugf("GoDaddy: Delete an entry %s from zone %s", change.String(), p.zone)
changed := []gdUpdateRecordField{{
Data: change.Data,
Name: change.Name,
TTL: change.TTL,
Port: change.Port,
Priority: change.Priority,
Weight: change.Weight,
Protocol: change.Protocol,
Service: change.Service,
}}
deleteIndex := -1
if dryRun {
log.Infof("[DryRun] - Update record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(changed))
} else if err := client.Patch(fmt.Sprintf("/v1/domains/%s/records/%s", p.zone, change.Type), changed, &response); err != nil {
log.Errorf("Update record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response)
return err
for index, record := range p.records {
if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data {
deleteIndex = index
break
}
}
if deleteIndex >= 0 {
p.records[deleteIndex] = p.records[len(p.records)-1]
p.records = p.records[:len(p.records)-1]
p.changed = true
}
}
if dryRun {
log.Infof("[DryRun] - Delete record %s.%s of type %s %s", dnsName, p.zone, endpoint.RecordType, records)
return nil
}
var response GDErrorResponse
if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, endpoint.RecordType, dnsName), &response); err != nil {
log.Errorf("Delete record %s.%s of type %s failed: %v", dnsName, p.zone, endpoint.RecordType, response)
return err
}
return nil
}
// Remove one record from the record list
func (p *gdRecords) deleteRecord(client gdClient, change gdRecordField, dryRun bool) error {
log.Debugf("GoDaddy: Delete an entry %s to zone %s", change.String(), p.zone)
deleteIndex := -1
for index, record := range p.records {
if record.Type == change.Type && record.Name == change.Name && record.Data == change.Data {
deleteIndex = index
break
}
}
if deleteIndex >= 0 {
var response GDErrorResponse
p.records[deleteIndex] = p.records[len(p.records)-1]
p.records = p.records[:len(p.records)-1]
p.changed = true
if dryRun {
log.Infof("[DryRun] - Delete record %s.%s of type %s %s", change.Name, p.zone, change.Type, toString(change))
} else if err := client.Delete(fmt.Sprintf("/v1/domains/%s/records/%s/%s", p.zone, change.Type, change.Name), &response); err != nil {
log.Errorf("Delete record %s.%s of type %s failed: %v", change.Name, p.zone, change.Type, response)
return err
}
} else {
log.Warnf("GoDaddy: record in zone %s not found %s to delete", p.zone, change.String())
}
return nil
}
func (p *gdRecords) applyChange(action int, client gdClient, change gdRecordField, dryRun bool) error {
func (p *gdRecords) applyEndpoint(action int, client gdClient, endpoint endpoint.Endpoint, dnsName string, dryRun bool) error {
switch action {
case gdCreate:
return p.addRecord(client, change, dryRun)
case gdUpdate:
return p.updateRecord(client, change, dryRun)
return p.addRecord(client, endpoint, dnsName, dryRun)
case gdReplace:
return p.replaceRecord(client, endpoint, dnsName, dryRun)
case gdDelete:
return p.deleteRecord(client, change, dryRun)
return p.deleteRecord(client, endpoint, dnsName, dryRun)
}
return nil

View File

@ -757,7 +757,7 @@ func (p *IBMCloudProvider) groupPrivateRecords(records []dnssvcsv1.ResourceRecor
for _, records := range groups {
targets := make([]string, len(records))
for i, record := range records {
data := record.Rdata.(map[string]interface{})
data := record.Rdata
log.Debugf("record data: %v", data)
switch *record.Type {
case "A":
@ -820,18 +820,18 @@ func (p *IBMCloudProvider) newIBMCloudChange(action string, endpoint *endpoint.E
}
if p.privateZone {
var rData interface{}
rData := make(map[string]interface{})
switch endpoint.RecordType {
case "A":
rData = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{
rData[dnssvcsv1.CreateResourceRecordOptions_Type_A] = &dnssvcsv1.ResourceRecordInputRdataRdataARecord{
Ip: core.StringPtr(target),
}
case "CNAME":
rData = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{
rData[dnssvcsv1.CreateResourceRecordOptions_Type_Cname] = &dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord{
Cname: core.StringPtr(target),
}
case "TXT":
rData = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{
rData[dnssvcsv1.CreateResourceRecordOptions_Type_Txt] = &dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord{
Text: core.StringPtr(target),
}
}
@ -869,15 +869,15 @@ func (p *IBMCloudProvider) createRecord(ctx context.Context, zoneID string, chan
}
switch *change.PrivateResourceRecord.Type {
case "A":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
aData, _ := p.Client.NewResourceRecordInputRdataRdataARecord(*data.Ip)
createResourceRecordOptions.SetRdata(aData)
case "CNAME":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
cnameData, _ := p.Client.NewResourceRecordInputRdataRdataCnameRecord(*data.Cname)
createResourceRecordOptions.SetRdata(cnameData)
case "TXT":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
txtData, _ := p.Client.NewResourceRecordInputRdataRdataTxtRecord(*data.Text)
createResourceRecordOptions.SetRdata(txtData)
}
@ -910,15 +910,15 @@ func (p *IBMCloudProvider) updateRecord(ctx context.Context, zoneID, recordID st
}
switch *change.PrivateResourceRecord.Type {
case "A":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_A].(*dnssvcsv1.ResourceRecordInputRdataRdataARecord)
aData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataARecord(*data.Ip)
updateResourceRecordOptions.SetRdata(aData)
case "CNAME":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Cname].(*dnssvcsv1.ResourceRecordInputRdataRdataCnameRecord)
cnameData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataCnameRecord(*data.Cname)
updateResourceRecordOptions.SetRdata(cnameData)
case "TXT":
data, _ := change.PrivateResourceRecord.Rdata.(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
data, _ := change.PrivateResourceRecord.Rdata[dnssvcsv1.CreateResourceRecordOptions_Type_Txt].(*dnssvcsv1.ResourceRecordInputRdataRdataTxtRecord)
txtData, _ := p.Client.NewResourceRecordUpdateInputRdataRdataTxtRecord(*data.Text)
updateResourceRecordOptions.SetRdata(txtData)
}

View File

@ -307,12 +307,9 @@ func (p *ProviderConfig) Records(ctx context.Context) (endpoints []*endpoint.End
}
var resT []ibclient.RecordTXT
objT := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Zone: zone.Fqdn,
View: p.view,
},
)
objT := ibclient.NewEmptyRecordTXT()
objT.Zone = zone.Fqdn
objT.View = p.view
err = p.client.GetObject(objT, "", searchParams, &resT)
if err != nil && !isNotFoundError(err) {
return nil, fmt.Errorf("could not fetch TXT records from zone '%s': %w", zone.Fqdn, err)
@ -603,13 +600,10 @@ func (p *ProviderConfig) recordSet(ep *endpoint.Endpoint, getObject bool, target
if target, err2 := strconv.Unquote(ep.Targets[0]); err2 == nil && !strings.Contains(ep.Targets[0], " ") {
ep.Targets = endpoint.Targets{target}
}
obj := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Name: ep.DNSName,
Text: ep.Targets[0],
View: p.view,
},
)
obj := ibclient.NewEmptyRecordTXT()
obj.Name = ep.DNSName
obj.Text = ep.Targets[0]
obj.View = p.view
if getObject {
queryParams := ibclient.NewQueryParams(false, map[string]string{"name": obj.Name})
err = p.client.GetObject(obj, "", queryParams, &res)

View File

@ -254,11 +254,8 @@ func (client *mockIBConnector) DeleteObject(ref string) (refRes string, err erro
}
case "record:txt":
var records []ibclient.RecordTXT
obj := ibclient.NewRecordTXT(
ibclient.RecordTXT{
Name: result[2],
},
)
obj := ibclient.NewEmptyRecordTXT()
obj.Name = result[2]
client.GetObject(obj, ref, nil, &records)
for _, record := range records {
client.deletedEndpoints = append(
@ -355,13 +352,11 @@ func createMockInfobloxObject(name, recordType, value string) ibclient.IBObject
obj.Canonical = value
return obj
case endpoint.RecordTypeTXT:
return ibclient.NewRecordTXT(
ibclient.RecordTXT{
Ref: ref,
Name: name,
Text: value,
},
)
obj := ibclient.NewEmptyRecordTXT()
obj.Name = name
obj.Ref = ref
obj.Text = value
return obj
case "HOST":
obj := ibclient.NewEmptyHostRecord()
obj.Name = name
@ -738,7 +733,6 @@ func TestExtendedRequestNameRegExBuilder(t *testing.T) {
assert.True(t, req.URL.Query().Get("name~") == "")
}
func TestExtendedRequestMaxResultsBuilder(t *testing.T) {
hostCfg := ibclient.HostConfig{
Host: "localhost",

View File

@ -22,6 +22,7 @@ import (
"strings"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
@ -132,11 +133,7 @@ func (im *InMemoryProvider) Records(ctx context.Context) ([]*endpoint.Endpoint,
return nil, err
}
for _, record := range records {
ep := endpoint.NewEndpoint(record.Name, record.Type, record.Target).WithSetIdentifier(record.SetIdentifier)
ep.Labels = record.Labels
endpoints = append(endpoints, ep)
}
endpoints = append(endpoints, copyEndpoints(records)...)
}
return endpoints, nil
@ -187,11 +184,11 @@ func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Chan
}
for zoneID := range perZoneChanges {
change := &inMemoryChange{
Create: convertToInMemoryRecord(perZoneChanges[zoneID].Create),
UpdateNew: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateNew),
UpdateOld: convertToInMemoryRecord(perZoneChanges[zoneID].UpdateOld),
Delete: convertToInMemoryRecord(perZoneChanges[zoneID].Delete),
change := &plan.Changes{
Create: perZoneChanges[zoneID].Create,
UpdateNew: perZoneChanges[zoneID].UpdateNew,
UpdateOld: perZoneChanges[zoneID].UpdateOld,
Delete: perZoneChanges[zoneID].Delete,
}
err := im.client.ApplyChanges(ctx, zoneID, change)
if err != nil {
@ -202,16 +199,15 @@ func (im *InMemoryProvider) ApplyChanges(ctx context.Context, changes *plan.Chan
return nil
}
func convertToInMemoryRecord(endpoints []*endpoint.Endpoint) []*inMemoryRecord {
records := []*inMemoryRecord{}
func copyEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
records := make([]*endpoint.Endpoint, 0, len(endpoints))
for _, ep := range endpoints {
records = append(records, &inMemoryRecord{
Type: ep.RecordType,
Name: ep.DNSName,
Target: ep.Targets[0],
SetIdentifier: ep.SetIdentifier,
Labels: ep.Labels,
})
newEp := endpoint.NewEndpointWithTTL(ep.DNSName, ep.RecordType, ep.RecordTTL, ep.Targets...).WithSetIdentifier(ep.SetIdentifier)
newEp.Labels = endpoint.NewLabels()
for k, v := range ep.Labels {
newEp.Labels[k] = v
}
records = append(records, newEp)
}
return records
}
@ -244,26 +240,7 @@ func (f *filter) EndpointZoneID(endpoint *endpoint.Endpoint, zones map[string]st
return matchZoneID
}
// inMemoryRecord - record stored in memory
// Type - type of record
// Name - DNS name assigned to the record
// Target - target of the record
type inMemoryRecord struct {
Type string
SetIdentifier string
Name string
Target string
Labels endpoint.Labels
}
type zone map[string][]*inMemoryRecord
type inMemoryChange struct {
Create []*inMemoryRecord
UpdateNew []*inMemoryRecord
UpdateOld []*inMemoryRecord
Delete []*inMemoryRecord
}
type zone map[endpoint.EndpointKey]*endpoint.Endpoint
type inMemoryClient struct {
zones map[string]zone
@ -273,14 +250,14 @@ func newInMemoryClient() *inMemoryClient {
return &inMemoryClient{map[string]zone{}}
}
func (c *inMemoryClient) Records(zone string) ([]*inMemoryRecord, error) {
func (c *inMemoryClient) Records(zone string) ([]*endpoint.Endpoint, error) {
if _, ok := c.zones[zone]; !ok {
return nil, ErrZoneNotFound
}
records := []*inMemoryRecord{}
var records []*endpoint.Endpoint
for _, rec := range c.zones[zone] {
records = append(records, rec...)
records = append(records, rec)
}
return records, nil
}
@ -297,66 +274,44 @@ func (c *inMemoryClient) CreateZone(zone string) error {
if _, ok := c.zones[zone]; ok {
return ErrZoneAlreadyExists
}
c.zones[zone] = map[string][]*inMemoryRecord{}
c.zones[zone] = map[endpoint.EndpointKey]*endpoint.Endpoint{}
return nil
}
func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *inMemoryChange) error {
func (c *inMemoryClient) ApplyChanges(ctx context.Context, zoneID string, changes *plan.Changes) error {
if err := c.validateChangeBatch(zoneID, changes); err != nil {
return err
}
for _, newEndpoint := range changes.Create {
if _, ok := c.zones[zoneID][newEndpoint.Name]; !ok {
c.zones[zoneID][newEndpoint.Name] = make([]*inMemoryRecord, 0)
}
c.zones[zoneID][newEndpoint.Name] = append(c.zones[zoneID][newEndpoint.Name], newEndpoint)
c.zones[zoneID][newEndpoint.Key()] = newEndpoint
}
for _, updateEndpoint := range changes.UpdateNew {
for _, rec := range c.zones[zoneID][updateEndpoint.Name] {
if rec.Type == updateEndpoint.Type {
rec.Target = updateEndpoint.Target
break
}
}
c.zones[zoneID][updateEndpoint.Key()] = updateEndpoint
}
for _, deleteEndpoint := range changes.Delete {
newSet := make([]*inMemoryRecord, 0)
for _, rec := range c.zones[zoneID][deleteEndpoint.Name] {
if rec.Type != deleteEndpoint.Type {
newSet = append(newSet, rec)
}
}
c.zones[zoneID][deleteEndpoint.Name] = newSet
delete(c.zones[zoneID], deleteEndpoint.Key())
}
return nil
}
func (c *inMemoryClient) updateMesh(mesh map[string]map[string]map[string]bool, record *inMemoryRecord) error {
if _, exists := mesh[record.Name]; exists {
if _, exists := mesh[record.Name][record.Type]; exists {
if mesh[record.Name][record.Type][record.SetIdentifier] {
return ErrDuplicateRecordFound
}
mesh[record.Name][record.Type][record.SetIdentifier] = true
return nil
}
mesh[record.Name][record.Type] = map[string]bool{record.SetIdentifier: true}
return nil
func (c *inMemoryClient) updateMesh(mesh sets.Set[endpoint.EndpointKey], record *endpoint.Endpoint) error {
if mesh.Has(record.Key()) {
return ErrDuplicateRecordFound
}
mesh[record.Name] = map[string]map[string]bool{record.Type: {record.SetIdentifier: true}}
mesh.Insert(record.Key())
return nil
}
// validateChangeBatch validates that the changes passed to InMemory DNS provider is valid
func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChange) error {
func (c *inMemoryClient) validateChangeBatch(zone string, changes *plan.Changes) error {
curZone, ok := c.zones[zone]
if !ok {
return ErrZoneNotFound
}
mesh := map[string]map[string]map[string]bool{}
mesh := sets.New[endpoint.EndpointKey]()
for _, newEndpoint := range changes.Create {
if c.findByTypeAndSetIdentifier(newEndpoint.Type, newEndpoint.SetIdentifier, curZone[newEndpoint.Name]) != nil {
if _, exists := curZone[newEndpoint.Key()]; exists {
return ErrRecordAlreadyExists
}
if err := c.updateMesh(mesh, newEndpoint); err != nil {
@ -364,7 +319,7 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
}
}
for _, updateEndpoint := range changes.UpdateNew {
if c.findByTypeAndSetIdentifier(updateEndpoint.Type, updateEndpoint.SetIdentifier, curZone[updateEndpoint.Name]) == nil {
if _, exists := curZone[updateEndpoint.Key()]; !exists {
return ErrRecordNotFound
}
if err := c.updateMesh(mesh, updateEndpoint); err != nil {
@ -372,12 +327,12 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
}
}
for _, updateOldEndpoint := range changes.UpdateOld {
if rec := c.findByTypeAndSetIdentifier(updateOldEndpoint.Type, updateOldEndpoint.SetIdentifier, curZone[updateOldEndpoint.Name]); rec == nil || rec.Target != updateOldEndpoint.Target {
if rec, exists := curZone[updateOldEndpoint.Key()]; !exists || rec.Targets[0] != updateOldEndpoint.Targets[0] {
return ErrRecordNotFound
}
}
for _, deleteEndpoint := range changes.Delete {
if rec := c.findByTypeAndSetIdentifier(deleteEndpoint.Type, deleteEndpoint.SetIdentifier, curZone[deleteEndpoint.Name]); rec == nil || rec.Target != deleteEndpoint.Target {
if rec, exists := curZone[deleteEndpoint.Key()]; !exists || rec.Targets[0] != deleteEndpoint.Targets[0] {
return ErrRecordNotFound
}
if err := c.updateMesh(mesh, deleteEndpoint); err != nil {
@ -386,12 +341,3 @@ func (c *inMemoryClient) validateChangeBatch(zone string, changes *inMemoryChang
}
return nil
}
func (c *inMemoryClient) findByTypeAndSetIdentifier(recordType, setIdentifier string, records []*inMemoryRecord) *inMemoryRecord {
for _, record := range records {
if record.Type == recordType && record.SetIdentifier == setIdentifier {
return record
}
}
return nil
}

View File

@ -22,7 +22,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
@ -32,7 +31,6 @@ import (
var _ provider.Provider = &InMemoryProvider{}
func TestInMemoryProvider(t *testing.T) {
t.Run("findByType", testInMemoryFindByType)
t.Run("Records", testInMemoryRecords)
t.Run("validateChangeBatch", testInMemoryValidateChangeBatch)
t.Run("ApplyChanges", testInMemoryApplyChanges)
@ -40,114 +38,6 @@ func TestInMemoryProvider(t *testing.T) {
t.Run("CreateZone", testInMemoryCreateZone)
}
func testInMemoryFindByType(t *testing.T) {
for _, ti := range []struct {
title string
findType string
findSetIdentifier string
records []*inMemoryRecord
expected *inMemoryRecord
expectedEmpty bool
}{
{
title: "no records, empty type",
findType: "",
records: nil,
expected: nil,
expectedEmpty: true,
},
{
title: "no records, non-empty type",
findType: endpoint.RecordTypeA,
records: nil,
expected: nil,
expectedEmpty: true,
},
{
title: "one record, empty type",
findType: "",
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
},
},
expected: nil,
expectedEmpty: true,
},
{
title: "one record, wrong type",
findType: endpoint.RecordTypeCNAME,
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
},
},
expected: nil,
expectedEmpty: true,
},
{
title: "one record, right type",
findType: endpoint.RecordTypeA,
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
},
},
expected: &inMemoryRecord{
Type: endpoint.RecordTypeA,
},
},
{
title: "multiple records, right type",
findType: endpoint.RecordTypeA,
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
},
{
Type: endpoint.RecordTypeTXT,
},
},
expected: &inMemoryRecord{
Type: endpoint.RecordTypeA,
},
},
{
title: "multiple records, right type and set identifier",
findType: endpoint.RecordTypeA,
findSetIdentifier: "test-set-1",
records: []*inMemoryRecord{
{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-1",
},
{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-2",
},
{
Type: endpoint.RecordTypeTXT,
},
},
expected: &inMemoryRecord{
Type: endpoint.RecordTypeA,
SetIdentifier: "test-set-1",
},
},
} {
t.Run(ti.title, func(t *testing.T) {
c := newInMemoryClient()
record := c.findByTypeAndSetIdentifier(ti.findType, ti.findSetIdentifier, ti.records)
if ti.expectedEmpty {
assert.Nil(t, record)
} else {
require.NotNil(t, record)
assert.Equal(t, *ti.expected, *record)
}
})
}
}
func testInMemoryRecords(t *testing.T) {
for _, ti := range []struct {
title string
@ -175,35 +65,12 @@ func testInMemoryRecords(t *testing.T) {
title: "records, zone with records",
zone: "org",
init: map[string]zone{
"org": {
"example.org": []*inMemoryRecord{
{
Name: "example.org",
Target: "8.8.8.8",
Type: endpoint.RecordTypeA,
},
{
Name: "example.org",
Type: endpoint.RecordTypeTXT,
},
},
"foo.org": []*inMemoryRecord{
{
Name: "foo.org",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
},
"com": {
"example.com": []*inMemoryRecord{
{
Name: "example.com",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
},
"org": makeZone(
"example.org", "8.8.8.8", endpoint.RecordTypeA,
"example.org", "", endpoint.RecordTypeTXT,
"foo.org", "4.4.4.4", endpoint.RecordTypeCNAME,
),
"com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME),
},
expectError: false,
expected: []*endpoint.Endpoint{
@ -246,41 +113,13 @@ func testInMemoryRecords(t *testing.T) {
func testInMemoryValidateChangeBatch(t *testing.T) {
init := map[string]zone{
"org": {
"example.org": []*inMemoryRecord{
{
Name: "example.org",
Target: "8.8.8.8",
Type: endpoint.RecordTypeA,
},
{
Name: "example.org",
},
},
"foo.org": []*inMemoryRecord{
{
Name: "foo.org",
Target: "bar.org",
Type: endpoint.RecordTypeCNAME,
},
},
"foo.bar.org": []*inMemoryRecord{
{
Name: "foo.bar.org",
Target: "5.5.5.5",
Type: endpoint.RecordTypeA,
},
},
},
"com": {
"example.com": []*inMemoryRecord{
{
Name: "example.com",
Target: "another-example.com",
Type: endpoint.RecordTypeCNAME,
},
},
},
"org": makeZone(
"example.org", "8.8.8.8", endpoint.RecordTypeA,
"example.org", "", endpoint.RecordTypeTXT,
"foo.org", "bar.org", endpoint.RecordTypeCNAME,
"foo.bar.org", "5.5.5.5", endpoint.RecordTypeA,
),
"com": makeZone("example.com", "another-example.com", endpoint.RecordTypeCNAME),
}
for _, ti := range []struct {
title string
@ -561,11 +400,11 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
t.Run(ti.title, func(t *testing.T) {
c := &inMemoryClient{}
c.zones = ti.init
ichanges := &inMemoryChange{
Create: convertToInMemoryRecord(ti.changes.Create),
UpdateNew: convertToInMemoryRecord(ti.changes.UpdateNew),
UpdateOld: convertToInMemoryRecord(ti.changes.UpdateOld),
Delete: convertToInMemoryRecord(ti.changes.Delete),
ichanges := &plan.Changes{
Create: ti.changes.Create,
UpdateNew: ti.changes.UpdateNew,
UpdateOld: ti.changes.UpdateOld,
Delete: ti.changes.Delete,
}
err := c.validateChangeBatch(ti.zone, ichanges)
if ti.expectError {
@ -579,42 +418,12 @@ func testInMemoryValidateChangeBatch(t *testing.T) {
func getInitData() map[string]zone {
return map[string]zone{
"org": {
"example.org": []*inMemoryRecord{
{
Name: "example.org",
Target: "8.8.8.8",
Type: endpoint.RecordTypeA,
},
{
Name: "example.org",
Type: endpoint.RecordTypeTXT,
},
},
"foo.org": []*inMemoryRecord{
{
Name: "foo.org",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
"foo.bar.org": []*inMemoryRecord{
{
Name: "foo.bar.org",
Target: "5.5.5.5",
Type: endpoint.RecordTypeA,
},
},
},
"com": {
"example.com": []*inMemoryRecord{
{
Name: "example.com",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
},
"org": makeZone("example.org", "8.8.8.8", endpoint.RecordTypeA,
"example.org", "", endpoint.RecordTypeTXT,
"foo.org", "4.4.4.4", endpoint.RecordTypeCNAME,
"foo.bar.org", "5.5.5.5", endpoint.RecordTypeA,
),
"com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME),
}
}
@ -679,36 +488,11 @@ func testInMemoryApplyChanges(t *testing.T) {
},
},
expectedZonesState: map[string]zone{
"org": {
"example.org": []*inMemoryRecord{
{
Name: "example.org",
Target: "8.8.8.8",
Type: endpoint.RecordTypeA,
},
{
Name: "example.org",
Type: endpoint.RecordTypeTXT,
},
},
"foo.org": []*inMemoryRecord{
{
Name: "foo.org",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
"foo.bar.org": []*inMemoryRecord{},
},
"com": {
"example.com": []*inMemoryRecord{
{
Name: "example.com",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
},
"org": makeZone("example.org", "8.8.8.8", endpoint.RecordTypeA,
"example.org", "", endpoint.RecordTypeTXT,
"foo.org", "4.4.4.4", endpoint.RecordTypeCNAME,
),
"com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME),
},
},
{
@ -720,6 +504,7 @@ func testInMemoryApplyChanges(t *testing.T) {
DNSName: "foo.bar.new.org",
Targets: endpoint.Targets{"4.8.8.9"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
},
},
UpdateNew: []*endpoint.Endpoint{
@ -727,6 +512,7 @@ func testInMemoryApplyChanges(t *testing.T) {
DNSName: "foo.bar.org",
Targets: endpoint.Targets{"4.8.8.4"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
},
},
UpdateOld: []*endpoint.Endpoint{
@ -734,6 +520,7 @@ func testInMemoryApplyChanges(t *testing.T) {
DNSName: "foo.bar.org",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
},
},
Delete: []*endpoint.Endpoint{
@ -741,48 +528,18 @@ func testInMemoryApplyChanges(t *testing.T) {
DNSName: "example.org",
Targets: endpoint.Targets{"8.8.8.8"},
RecordType: endpoint.RecordTypeA,
Labels: endpoint.NewLabels(),
},
},
},
expectedZonesState: map[string]zone{
"org": {
"example.org": []*inMemoryRecord{
{
Name: "example.org",
Type: endpoint.RecordTypeTXT,
},
},
"foo.org": []*inMemoryRecord{
{
Name: "foo.org",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
"foo.bar.org": []*inMemoryRecord{
{
Name: "foo.bar.org",
Target: "4.8.8.4",
Type: endpoint.RecordTypeA,
},
},
"foo.bar.new.org": []*inMemoryRecord{
{
Name: "foo.bar.new.org",
Target: "4.8.8.9",
Type: endpoint.RecordTypeA,
},
},
},
"com": {
"example.com": []*inMemoryRecord{
{
Name: "example.com",
Target: "4.4.4.4",
Type: endpoint.RecordTypeCNAME,
},
},
},
"org": makeZone(
"example.org", "", endpoint.RecordTypeTXT,
"foo.org", "4.4.4.4", endpoint.RecordTypeCNAME,
"foo.bar.org", "4.8.8.4", endpoint.RecordTypeA,
"foo.bar.new.org", "4.8.8.9", endpoint.RecordTypeA,
),
"com": makeZone("example.com", "4.4.4.4", endpoint.RecordTypeCNAME),
},
},
} {
@ -815,3 +572,17 @@ func testInMemoryCreateZone(t *testing.T) {
err = im.CreateZone("zone")
assert.EqualError(t, err, ErrZoneAlreadyExists.Error())
}
func makeZone(s ...string) map[endpoint.EndpointKey]*endpoint.Endpoint {
if len(s)%3 != 0 {
panic("makeZone arguments must be multiple of 3")
}
output := map[endpoint.EndpointKey]*endpoint.Endpoint{}
for i := 0; i < len(s); i += 3 {
ep := endpoint.NewEndpoint(s[i], s[i+2], s[i+1])
output[ep.Key()] = ep
}
return output
}

View File

@ -30,6 +30,13 @@ type Provider interface {
Records(ctx context.Context) ([]*endpoint.Endpoint, error)
ApplyChanges(ctx context.Context, changes *plan.Changes) error
PropertyValuesEqual(name string, previous string, current string) bool
// AdjustEndpoints canonicalizes a set of candidate endpoints.
// It is called with a set of candidate endpoints obtained from the various sources.
// It returns a set modified as required by the provider. The provider is responsible for
// adding, removing, and modifying the ProviderSpecific properties to match
// the endpoints that the provider returns in `Records` so that the change plan will not have
// unnecessary (potentially failing) changes. It may also modify other fields, add, or remove
// Endpoints. It is permitted to modify the supplied endpoints.
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
GetDomainFilter() endpoint.DomainFilterInterface
}

View File

@ -389,7 +389,6 @@ func (r rfc2136Provider) SendMessage(msg *dns.Msg) error {
log.Debugf("SendMessage")
c := new(dns.Client)
c.SingleInflight = true
if !r.insecure {
if r.gssTsig {

View File

@ -19,6 +19,7 @@ package scaleway
import (
"context"
"fmt"
"os"
"strconv"
"strings"
@ -55,9 +56,19 @@ type ScalewayChange struct {
// NewScalewayProvider initializes a new Scaleway DNS provider
func NewScalewayProvider(ctx context.Context, domainFilter endpoint.DomainFilter, dryRun bool) (*ScalewayProvider, error) {
var err error
defaultPageSize := uint64(1000)
if envPageSize, ok := os.LookupEnv("SCW_DEFAULT_PAGE_SIZE"); ok {
defaultPageSize, err = strconv.ParseUint(envPageSize, 10, 32)
if err != nil {
log.Infof("Ignoring default page size %s, defaulting to 1000", envPageSize)
defaultPageSize = 1000
}
}
scwClient, err := scw.NewClient(
scw.WithEnv(),
scw.WithUserAgent("ExternalDNS/"+externaldns.Version),
scw.WithDefaultPageSize(uint32(defaultPageSize)),
)
if err != nil {
return nil, err
@ -256,6 +267,10 @@ func (p *ScalewayProvider) generateApplyRequests(ctx context.Context, changes *p
req.Changes = append(req.Changes, &domain.RecordChange{
Add: recordsToAdd[zoneName],
})
// ignore sending empty update requests
if len(req.Changes) == 1 && len(req.Changes[0].Add.Records) == 0 {
continue
}
returnedRequests = append(returnedRequests, req)
}
@ -278,9 +293,9 @@ func endpointToScalewayRecords(zoneName string, ep *endpoint.Endpoint) []*domain
}
priority := scalewayDefaultPriority
if prop, ok := ep.GetProviderSpecificProperty(scalewayPriorityKey); ok {
prio, err := strconv.ParseUint(prop.Value, 10, 32)
prio, err := strconv.ParseUint(prop, 10, 32)
if err != nil {
log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop.Value, err, scalewayDefaultPriority)
log.Errorf("Failed parsing value of %s: %s: %v; using priority of %d", scalewayPriorityKey, prop, err, scalewayDefaultPriority)
} else {
priority = uint32(prio)
}

View File

@ -135,11 +135,12 @@ func (api *mockAPIService) DescribePrivateZoneRecordList(request *privatedns.Des
func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainListRequest) (response *dnspod.DescribeDomainListResponse, err error) {
response = dnspod.NewDescribeDomainListResponse()
response.Response = &struct {
DomainCountInfo *dnspod.DomainCountInfo `json:"DomainCountInfo,omitempty" name:"DomainCountInfo"`
DomainList []*dnspod.DomainListItem `json:"DomainList,omitempty" name:"DomainList"`
RequestId *string `json:"RequestId,omitempty" name:"RequestId"`
}{}
response.Response = &dnspod.DescribeDomainListResponseParams{
DomainCountInfo: &dnspod.DomainCountInfo{
AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))),
},
DomainList: api.dnspodDomains,
}
response.Response.DomainList = api.dnspodDomains
response.Response.DomainCountInfo = &dnspod.DomainCountInfo{
AllTotal: common.Uint64Ptr(uint64(len(api.dnspodDomains))),
@ -149,11 +150,7 @@ func (api *mockAPIService) DescribeDomainList(request *dnspod.DescribeDomainList
func (api *mockAPIService) DescribeRecordList(request *dnspod.DescribeRecordListRequest) (response *dnspod.DescribeRecordListResponse, err error) {
response = dnspod.NewDescribeRecordListResponse()
response.Response = &struct {
RecordCountInfo *dnspod.RecordCountInfo `json:"RecordCountInfo,omitempty" name:"RecordCountInfo"`
RecordList []*dnspod.RecordListItem `json:"RecordList,omitempty" name:"RecordList"`
RequestId *string `json:"RequestId,omitempty" name:"RequestId"`
}{}
response.Response = &dnspod.DescribeRecordListResponseParams{}
if _, exist := api.dnspodRecords[*request.Domain]; !exist {
response.Response.RecordList = make([]*dnspod.RecordListItem, 0)
response.Response.RecordCountInfo = &dnspod.RecordCountInfo{

View File

@ -67,11 +67,6 @@ func (sdr *AWSSDRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, er
return records, nil
}
// MissingRecords returns nil because there is no missing records for AWSSD registry
func (sdr *AWSSDRegistry) MissingRecords() []*endpoint.Endpoint {
return nil
}
// ApplyChanges filters out records not owned the External-DNS, additionally it adds the required label
// inserted in the AWS SD instance as a CreateID field
func (sdr *AWSSDRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {

437
registry/dynamodb.go Normal file
View File

@ -0,0 +1,437 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/dynamodb"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
)
// DynamoDBAPI is the subset of the AWS Route53 API that we actually use. Add methods as required. Signatures must match exactly.
type DynamoDBAPI interface {
DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error)
ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error
BatchExecuteStatementWithContext(aws.Context, *dynamodb.BatchExecuteStatementInput, ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error)
}
// DynamoDBRegistry implements registry interface with ownership implemented via an AWS DynamoDB table.
type DynamoDBRegistry struct {
provider provider.Provider
ownerID string // refers to the owner id of the current instance
dynamodbAPI DynamoDBAPI
table string
// cache the dynamodb records owned by us.
labels map[endpoint.EndpointKey]endpoint.Labels
orphanedLabels sets.Set[endpoint.EndpointKey]
// cache the records in memory and update on an interval instead.
recordsCache []*endpoint.Endpoint
recordsCacheRefreshTime time.Time
cacheInterval time.Duration
}
// NewDynamoDBRegistry returns a new DynamoDBRegistry object.
func NewDynamoDBRegistry(provider provider.Provider, ownerID string, dynamodbAPI DynamoDBAPI, table string, cacheInterval time.Duration) (*DynamoDBRegistry, error) {
if ownerID == "" {
return nil, errors.New("owner id cannot be empty")
}
if table == "" {
return nil, errors.New("table cannot be empty")
}
return &DynamoDBRegistry{
provider: provider,
ownerID: ownerID,
dynamodbAPI: dynamodbAPI,
table: table,
cacheInterval: cacheInterval,
}, nil
}
func (im *DynamoDBRegistry) GetDomainFilter() endpoint.DomainFilterInterface {
return im.provider.GetDomainFilter()
}
// Records returns the current records from the registry.
func (im *DynamoDBRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
// If we have the zones cached AND we have refreshed the cache since the
// last given interval, then just use the cached results.
if im.recordsCache != nil && time.Since(im.recordsCacheRefreshTime) < im.cacheInterval {
log.Debug("Using cached records.")
return im.recordsCache, nil
}
if im.labels == nil {
if err := im.readLabels(ctx); err != nil {
return nil, err
}
}
records, err := im.provider.Records(ctx)
if err != nil {
return nil, err
}
orphanedLabels := sets.KeySet(im.labels)
endpoints := make([]*endpoint.Endpoint, 0, len(records))
for _, record := range records {
key := record.Key()
if labels := im.labels[key]; labels != nil {
record.Labels = labels
orphanedLabels.Delete(key)
} else {
record.Labels = endpoint.NewLabels()
}
endpoints = append(endpoints, record)
}
im.orphanedLabels = orphanedLabels
// Update the cache.
if im.cacheInterval > 0 {
im.recordsCache = endpoints
im.recordsCacheRefreshTime = time.Now()
}
return endpoints, nil
}
// ApplyChanges updates the DNS provider and DynamoDB table with the changes.
func (im *DynamoDBRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
filteredChanges := &plan.Changes{
Create: changes.Create,
UpdateNew: filterOwnedRecords(im.ownerID, changes.UpdateNew),
UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld),
Delete: filterOwnedRecords(im.ownerID, changes.Delete),
}
statements := make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Create)+len(filteredChanges.UpdateNew))
for _, r := range filteredChanges.Create {
if r.Labels == nil {
r.Labels = make(map[string]string)
}
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
key := r.Key()
oldLabels := im.labels[key]
if oldLabels == nil {
statements = append(statements, &dynamodb.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("INSERT INTO %q VALUE {'k':?, 'o':?, 'l':?}", im.table)),
Parameters: []*dynamodb.AttributeValue{
toDynamoKey(key),
{S: aws.String(im.ownerID)},
toDynamoLabels(r.Labels),
},
ConsistentRead: aws.Bool(true),
})
} else {
im.orphanedLabels.Delete(key)
statements = im.appendUpdate(statements, key, oldLabels, r.Labels)
}
im.labels[key] = r.Labels
if im.cacheInterval > 0 {
im.addToCache(r)
}
}
for _, r := range filteredChanges.Delete {
delete(im.labels, r.Key())
if im.cacheInterval > 0 {
im.removeFromCache(r)
}
}
oldLabels := make(map[endpoint.EndpointKey]endpoint.Labels, len(filteredChanges.UpdateOld))
for _, r := range filteredChanges.UpdateOld {
oldLabels[r.Key()] = r.Labels
// remove old version of record from cache
if im.cacheInterval > 0 {
im.removeFromCache(r)
}
}
for _, r := range filteredChanges.UpdateNew {
key := r.Key()
statements = im.appendUpdate(statements, key, oldLabels[key], r.Labels)
// add new version of record to caches
im.labels[key] = r.Labels
if im.cacheInterval > 0 {
im.addToCache(r)
}
}
err := im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error {
var context string
if strings.HasPrefix(*request.Statement, "INSERT") {
if aws.StringValue(response.Error.Code) == "DuplicateItem" {
// We lost a race with a different owner or another owner has an orphaned ownership record.
key := fromDynamoKey(request.Parameters[0])
for i, endpoint := range filteredChanges.Create {
if endpoint.Key() == key {
log.Infof("Skipping endpoint %v because owner does not match", endpoint)
filteredChanges.Create = append(filteredChanges.Create[:i], filteredChanges.Create[i+1:]...)
// The dynamodb insertion failed; remove from our cache.
im.removeFromCache(endpoint)
delete(im.labels, key)
return nil
}
}
}
context = fmt.Sprintf("inserting dynamodb record %q", aws.StringValue(request.Parameters[0].S))
} else {
context = fmt.Sprintf("updating dynamodb record %q", aws.StringValue(request.Parameters[1].S))
}
return fmt.Errorf("%s: %s: %s", context, aws.StringValue(response.Error.Code), aws.StringValue(response.Error.Message))
})
if err != nil {
im.recordsCache = nil
im.labels = nil
return err
}
// When caching is enabled, disable the provider from using the cache.
if im.cacheInterval > 0 {
ctx = context.WithValue(ctx, provider.RecordsContextKey, nil)
}
err = im.provider.ApplyChanges(ctx, filteredChanges)
if err != nil {
im.recordsCache = nil
im.labels = nil
return err
}
statements = make([]*dynamodb.BatchStatementRequest, 0, len(filteredChanges.Delete)+len(im.orphanedLabels))
for _, r := range filteredChanges.Delete {
statements = im.appendDelete(statements, r.Key())
}
for r := range im.orphanedLabels {
statements = im.appendDelete(statements, r)
delete(im.labels, r)
}
im.orphanedLabels = nil
return im.executeStatements(ctx, statements, func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error {
im.labels = nil
return fmt.Errorf("deleting dynamodb record %q: %s: %s", aws.StringValue(request.Parameters[0].S), aws.StringValue(response.Error.Code), aws.StringValue(response.Error.Message))
})
}
// PropertyValuesEqual compares two attribute values for equality.
func (im *DynamoDBRegistry) PropertyValuesEqual(name string, previous string, current string) bool {
return im.provider.PropertyValuesEqual(name, previous, current)
}
// AdjustEndpoints modifies the endpoints as needed by the specific provider.
func (im *DynamoDBRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint {
return im.provider.AdjustEndpoints(endpoints)
}
func (im *DynamoDBRegistry) readLabels(ctx context.Context) error {
table, err := im.dynamodbAPI.DescribeTableWithContext(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(im.table),
})
if err != nil {
return fmt.Errorf("describing table %q: %w", im.table, err)
}
foundKey := false
for _, def := range table.Table.AttributeDefinitions {
if aws.StringValue(def.AttributeName) == "k" {
if aws.StringValue(def.AttributeType) != "S" {
return fmt.Errorf("table %q attribute \"k\" must have type \"S\"", im.table)
}
foundKey = true
}
}
if !foundKey {
return fmt.Errorf("table %q must have attribute \"k\" of type \"S\"", im.table)
}
if aws.StringValue(table.Table.KeySchema[0].AttributeName) != "k" {
return fmt.Errorf("table %q must have hash key \"k\"", im.table)
}
if len(table.Table.KeySchema) > 1 {
return fmt.Errorf("table %q must not have a range key", im.table)
}
labels := map[endpoint.EndpointKey]endpoint.Labels{}
err = im.dynamodbAPI.ScanPagesWithContext(ctx, &dynamodb.ScanInput{
TableName: aws.String(im.table),
FilterExpression: aws.String("o = :ownerval"),
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":ownerval": {S: aws.String(im.ownerID)},
},
ProjectionExpression: aws.String("k,l"),
ConsistentRead: aws.Bool(true),
}, func(output *dynamodb.ScanOutput, last bool) bool {
for _, item := range output.Items {
labels[fromDynamoKey(item["k"])] = fromDynamoLabels(item["l"], im.ownerID)
}
return true
})
if err != nil {
return fmt.Errorf("querying dynamodb: %w", err)
}
im.labels = labels
return nil
}
func fromDynamoKey(key *dynamodb.AttributeValue) endpoint.EndpointKey {
split := strings.SplitN(aws.StringValue(key.S), "#", 3)
return endpoint.EndpointKey{
DNSName: split[0],
RecordType: split[1],
SetIdentifier: split[2],
}
}
func toDynamoKey(key endpoint.EndpointKey) *dynamodb.AttributeValue {
return &dynamodb.AttributeValue{
S: aws.String(fmt.Sprintf("%s#%s#%s", key.DNSName, key.RecordType, key.SetIdentifier)),
}
}
func fromDynamoLabels(label *dynamodb.AttributeValue, owner string) endpoint.Labels {
labels := endpoint.NewLabels()
for k, v := range label.M {
labels[k] = aws.StringValue(v.S)
}
labels[endpoint.OwnerLabelKey] = owner
return labels
}
func toDynamoLabels(labels endpoint.Labels) *dynamodb.AttributeValue {
labelMap := make(map[string]*dynamodb.AttributeValue, len(labels))
for k, v := range labels {
if k == endpoint.OwnerLabelKey {
continue
}
labelMap[k] = &dynamodb.AttributeValue{S: aws.String(v)}
}
return &dynamodb.AttributeValue{M: labelMap}
}
func (im *DynamoDBRegistry) appendUpdate(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey, old endpoint.Labels, new endpoint.Labels) []*dynamodb.BatchStatementRequest {
if len(old) == len(new) {
equal := true
for k, v := range old {
if newV, exists := new[k]; !exists || v != newV {
equal = false
break
}
}
if equal {
return statements
}
}
return append(statements, &dynamodb.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("UPDATE %q SET \"l\"=? WHERE \"k\"=?", im.table)),
Parameters: []*dynamodb.AttributeValue{
toDynamoLabels(new),
toDynamoKey(key),
},
})
}
func (im *DynamoDBRegistry) appendDelete(statements []*dynamodb.BatchStatementRequest, key endpoint.EndpointKey) []*dynamodb.BatchStatementRequest {
return append(statements, &dynamodb.BatchStatementRequest{
Statement: aws.String(fmt.Sprintf("DELETE FROM %q WHERE \"k\"=? AND \"o\"=?", im.table)),
Parameters: []*dynamodb.AttributeValue{
toDynamoKey(key),
{S: aws.String(im.ownerID)},
},
})
}
func (im *DynamoDBRegistry) executeStatements(ctx context.Context, statements []*dynamodb.BatchStatementRequest, handleErr func(request *dynamodb.BatchStatementRequest, response *dynamodb.BatchStatementResponse) error) error {
for len(statements) > 0 {
var chunk []*dynamodb.BatchStatementRequest
if len(statements) > 25 {
chunk = chunk[:25]
statements = statements[25:]
} else {
chunk = statements
statements = nil
}
output, err := im.dynamodbAPI.BatchExecuteStatementWithContext(ctx, &dynamodb.BatchExecuteStatementInput{
Statements: chunk,
})
if err != nil {
return err
}
for i, response := range output.Responses {
request := chunk[i]
if response.Error == nil {
op, _, _ := strings.Cut(*request.Statement, " ")
var key string
if op == "UPDATE" {
key = *request.Parameters[1].S
} else {
key = *request.Parameters[0].S
}
log.Infof("%s dynamodb record %q", op, key)
} else {
if err := handleErr(request, response); err != nil {
return err
}
}
}
}
return nil
}
func (im *DynamoDBRegistry) addToCache(ep *endpoint.Endpoint) {
if im.recordsCache != nil {
im.recordsCache = append(im.recordsCache, ep)
}
}
func (im *DynamoDBRegistry) removeFromCache(ep *endpoint.Endpoint) {
if im.recordsCache == nil || ep == nil {
return
}
for i, e := range im.recordsCache {
if e.DNSName == ep.DNSName && e.RecordType == ep.RecordType && e.SetIdentifier == ep.SetIdentifier && e.Targets.Same(ep.Targets) {
// We found a match; delete the endpoint from the cache.
im.recordsCache = append(im.recordsCache[:i], im.recordsCache[i+1:]...)
return
}
}
}

995
registry/dynamodb_test.go Normal file
View File

@ -0,0 +1,995 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"context"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/util/sets"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/internal/testutils"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
"sigs.k8s.io/external-dns/provider/inmemory"
)
func TestDynamoDBRegistryNew(t *testing.T) {
api, p := newDynamoDBAPIStub(t, nil)
_, err := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour)
require.NoError(t, err)
_, err = NewDynamoDBRegistry(p, "", api, "test-table", time.Hour)
require.EqualError(t, err, "owner id cannot be empty")
_, err = NewDynamoDBRegistry(p, "test-owner", api, "", time.Hour)
require.EqualError(t, err, "table cannot be empty")
}
func TestDynamoDBRegistryRecordsBadTable(t *testing.T) {
for _, tc := range []struct {
name string
setup func(desc *dynamodb.TableDescription)
expected string
}{
{
name: "missing attribute k",
setup: func(desc *dynamodb.TableDescription) {
desc.AttributeDefinitions[0].AttributeName = aws.String("wrong")
},
expected: "table \"test-table\" must have attribute \"k\" of type \"S\"",
},
{
name: "wrong attribute type",
setup: func(desc *dynamodb.TableDescription) {
desc.AttributeDefinitions[0].AttributeType = aws.String("SS")
},
expected: "table \"test-table\" attribute \"k\" must have type \"S\"",
},
{
name: "wrong key",
setup: func(desc *dynamodb.TableDescription) {
desc.KeySchema[0].AttributeName = aws.String("wrong")
},
expected: "table \"test-table\" must have hash key \"k\"",
},
{
name: "has range key",
setup: func(desc *dynamodb.TableDescription) {
desc.AttributeDefinitions = append(desc.AttributeDefinitions, &dynamodb.AttributeDefinition{
AttributeName: aws.String("o"),
AttributeType: aws.String("S"),
})
desc.KeySchema = append(desc.KeySchema, &dynamodb.KeySchemaElement{
AttributeName: aws.String("o"),
KeyType: aws.String("RANGE"),
})
},
expected: "table \"test-table\" must not have a range key",
},
} {
t.Run(tc.name, func(t *testing.T) {
api, p := newDynamoDBAPIStub(t, nil)
tc.setup(&api.tableDescription)
r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour)
_, err := r.Records(context.Background())
assert.EqualError(t, err, tc.expected)
})
}
}
func TestDynamoDBRegistryRecords(t *testing.T) {
api, p := newDynamoDBAPIStub(t, nil)
ctx := context.Background()
expectedRecords := []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
}
r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour)
records, err := r.Records(ctx)
require.Nil(t, err)
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
}
func TestDynamoDBRegistryApplyChanges(t *testing.T) {
for _, tc := range []struct {
name string
stubConfig DynamoDBStubConfig
changes plan.Changes
expectedError string
expectedRecords []*endpoint.Endpoint
}{
{
name: "create",
changes: plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "new.test-zone.example.org",
Targets: endpoint.Targets{"new.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
SetIdentifier: "set-new",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectInsert: map[string]map[string]string{
"new.test-zone.example.org#CNAME#set-new": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
},
ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
{
DNSName: "new.test-zone.example.org",
Targets: endpoint.Targets{"new.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
SetIdentifier: "set-new",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
{
name: "create orphaned",
changes: plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "quux.test-zone.example.org",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/quux-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
{
DNSName: "quux.test-zone.example.org",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/quux-ingress",
},
},
},
},
{
name: "create orphaned change",
changes: plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "quux.test-zone.example.org",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectUpdate: map[string]map[string]string{
"quux.test-zone.example.org#A#set-2": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
},
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
{
DNSName: "quux.test-zone.example.org",
Targets: endpoint.Targets{"5.5.5.5"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
{
name: "create duplicate",
changes: plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "new.test-zone.example.org",
Targets: endpoint.Targets{"new.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
SetIdentifier: "set-new",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectInsertError: map[string]string{
"new.test-zone.example.org#CNAME#set-new": "DuplicateItem",
},
ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
{
name: "create error",
changes: plan.Changes{
Create: []*endpoint.Endpoint{
{
DNSName: "new.test-zone.example.org",
Targets: endpoint.Targets{"new.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
SetIdentifier: "set-new",
Labels: map[string]string{
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectInsertError: map[string]string{
"new.test-zone.example.org#CNAME#set-new": "TestingError",
},
},
expectedError: "inserting dynamodb record \"new.test-zone.example.org#CNAME#set-new\": TestingError: testing error",
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
{
name: "update",
changes: plan.Changes{
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"new-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"new-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
{
name: "update change",
changes: plan.Changes{
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"new-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectDelete: sets.New("quux.test-zone.example.org#A#set-2"),
ExpectUpdate: map[string]map[string]string{
"bar.test-zone.example.org#CNAME#": {endpoint.ResourceLabelKey: "ingress/default/new-ingress"},
},
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"new-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
{
name: "update error",
changes: plan.Changes{
UpdateOld: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
},
UpdateNew: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"new-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/new-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectUpdateError: map[string]string{
"bar.test-zone.example.org#CNAME#": "TestingError",
},
},
expectedError: "updating dynamodb record \"bar.test-zone.example.org#CNAME#\": TestingError: testing error",
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
{
name: "delete",
changes: plan.Changes{
Delete: []*endpoint.Endpoint{
{
DNSName: "bar.test-zone.example.org",
Targets: endpoint.Targets{"my-domain.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
},
},
stubConfig: DynamoDBStubConfig{
ExpectDelete: sets.New("bar.test-zone.example.org#CNAME#", "quux.test-zone.example.org#A#set-2"),
},
expectedRecords: []*endpoint.Endpoint{
{
DNSName: "foo.test-zone.example.org",
Targets: endpoint.Targets{"foo.loadbalancer.com"},
RecordType: endpoint.RecordTypeCNAME,
Labels: map[string]string{
endpoint.OwnerLabelKey: "",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"1.1.1.1"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-1",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
},
},
{
DNSName: "baz.test-zone.example.org",
Targets: endpoint.Targets{"2.2.2.2"},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "set-2",
Labels: map[string]string{
endpoint.OwnerLabelKey: "test-owner",
endpoint.ResourceLabelKey: "ingress/default/other-ingress",
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
api, p := newDynamoDBAPIStub(t, &tc.stubConfig)
ctx := context.Background()
r, _ := NewDynamoDBRegistry(p, "test-owner", api, "test-table", time.Hour)
_, err := r.Records(ctx)
require.Nil(t, err)
err = r.ApplyChanges(ctx, &tc.changes)
if tc.expectedError == "" {
assert.Nil(t, err)
} else {
assert.EqualError(t, err, tc.expectedError)
}
assert.Empty(t, tc.stubConfig.ExpectInsert, "all expected inserts made")
assert.Empty(t, tc.stubConfig.ExpectDelete, "all expected deletions made")
records, err := r.Records(ctx)
require.Nil(t, err)
assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords))
r.recordsCache = nil
records, err = r.Records(ctx)
require.Nil(t, err)
assert.True(t, testutils.SameEndpoints(records, tc.expectedRecords))
if tc.expectedError == "" {
assert.Empty(t, r.orphanedLabels)
}
})
}
}
// DynamoDBAPIStub is a minimal implementation of DynamoDBAPI, used primarily for unit testing.
type DynamoDBStub struct {
t *testing.T
stubConfig *DynamoDBStubConfig
tableDescription dynamodb.TableDescription
changesApplied bool
}
type DynamoDBStubConfig struct {
ExpectInsert map[string]map[string]string
ExpectInsertError map[string]string
ExpectUpdate map[string]map[string]string
ExpectUpdateError map[string]string
ExpectDelete sets.Set[string]
}
type wrappedProvider struct {
provider.Provider
stub *DynamoDBStub
}
func (w *wrappedProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
assert.False(w.stub.t, w.stub.changesApplied, "ApplyChanges already called")
w.stub.changesApplied = true
return w.Provider.ApplyChanges(ctx, changes)
}
func newDynamoDBAPIStub(t *testing.T, stubConfig *DynamoDBStubConfig) (*DynamoDBStub, provider.Provider) {
stub := &DynamoDBStub{
t: t,
stubConfig: stubConfig,
tableDescription: dynamodb.TableDescription{
AttributeDefinitions: []*dynamodb.AttributeDefinition{
{
AttributeName: aws.String("k"),
AttributeType: aws.String("S"),
},
},
KeySchema: []*dynamodb.KeySchemaElement{
{
AttributeName: aws.String("k"),
KeyType: aws.String("HASH"),
},
},
},
}
p := inmemory.NewInMemoryProvider()
_ = p.CreateZone(testZone)
_ = p.ApplyChanges(context.Background(), &plan.Changes{
Create: []*endpoint.Endpoint{
endpoint.NewEndpoint("foo.test-zone.example.org", endpoint.RecordTypeCNAME, "foo.loadbalancer.com"),
endpoint.NewEndpoint("bar.test-zone.example.org", endpoint.RecordTypeCNAME, "my-domain.com"),
endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "1.1.1.1").WithSetIdentifier("set-1"),
endpoint.NewEndpoint("baz.test-zone.example.org", endpoint.RecordTypeA, "2.2.2.2").WithSetIdentifier("set-2"),
},
})
return stub, &wrappedProvider{
Provider: p,
stub: stub,
}
}
func (r *DynamoDBStub) DescribeTableWithContext(ctx aws.Context, input *dynamodb.DescribeTableInput, opts ...request.Option) (*dynamodb.DescribeTableOutput, error) {
assert.NotNil(r.t, ctx)
assert.Equal(r.t, "test-table", *input.TableName, "table name")
return &dynamodb.DescribeTableOutput{
Table: &r.tableDescription,
}, nil
}
func (r *DynamoDBStub) ScanPagesWithContext(ctx aws.Context, input *dynamodb.ScanInput, fn func(*dynamodb.ScanOutput, bool) bool, opts ...request.Option) error {
assert.NotNil(r.t, ctx)
assert.Equal(r.t, "test-table", *input.TableName, "table name")
assert.Equal(r.t, "o = :ownerval", *input.FilterExpression)
assert.Len(r.t, input.ExpressionAttributeValues, 1)
assert.Equal(r.t, "test-owner", *input.ExpressionAttributeValues[":ownerval"].S)
assert.Equal(r.t, "k,l", *input.ProjectionExpression)
assert.True(r.t, *input.ConsistentRead)
fn(&dynamodb.ScanOutput{
Items: []map[string]*dynamodb.AttributeValue{
{
"k": &dynamodb.AttributeValue{S: aws.String("bar.test-zone.example.org#CNAME#")},
"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")},
}},
},
{
"k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-1")},
"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
endpoint.ResourceLabelKey: {S: aws.String("ingress/default/my-ingress")},
}},
},
{
"k": &dynamodb.AttributeValue{S: aws.String("baz.test-zone.example.org#A#set-2")},
"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
endpoint.ResourceLabelKey: {S: aws.String("ingress/default/other-ingress")},
}},
},
{
"k": &dynamodb.AttributeValue{S: aws.String("quux.test-zone.example.org#A#set-2")},
"l": &dynamodb.AttributeValue{M: map[string]*dynamodb.AttributeValue{
endpoint.ResourceLabelKey: {S: aws.String("ingress/default/quux-ingress")},
}},
},
},
}, true)
return nil
}
func (r *DynamoDBStub) BatchExecuteStatementWithContext(context aws.Context, input *dynamodb.BatchExecuteStatementInput, option ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) {
assert.NotNil(r.t, context)
hasDelete := strings.HasPrefix(strings.ToLower(aws.StringValue(input.Statements[0].Statement)), "delete")
assert.Equal(r.t, hasDelete, r.changesApplied, "delete after provider changes, everything else before")
assert.LessOrEqual(r.t, len(input.Statements), 25)
responses := make([]*dynamodb.BatchStatementResponse, 0, len(input.Statements))
for _, statement := range input.Statements {
assert.Equal(r.t, hasDelete, strings.HasPrefix(strings.ToLower(aws.StringValue(statement.Statement)), "delete"))
switch aws.StringValue(statement.Statement) {
case "DELETE FROM \"test-table\" WHERE \"k\"=? AND \"o\"=?":
assert.True(r.t, r.changesApplied, "unexpected delete before provider changes")
key := aws.StringValue(statement.Parameters[0].S)
assert.True(r.t, r.stubConfig.ExpectDelete.Has(key), "unexpected delete for key %q", key)
r.stubConfig.ExpectDelete.Delete(key)
assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S))
responses = append(responses, &dynamodb.BatchStatementResponse{})
case "INSERT INTO \"test-table\" VALUE {'k':?, 'o':?, 'l':?}":
assert.False(r.t, r.changesApplied, "unexpected insert after provider changes")
key := aws.StringValue(statement.Parameters[0].S)
if code, exists := r.stubConfig.ExpectInsertError[key]; exists {
delete(r.stubConfig.ExpectInsertError, key)
responses = append(responses, &dynamodb.BatchStatementResponse{
Error: &dynamodb.BatchStatementError{
Code: aws.String(code),
Message: aws.String("testing error"),
},
})
break
}
expectedLabels, found := r.stubConfig.ExpectInsert[key]
assert.True(r.t, found, "unexpected insert for key %q", key)
delete(r.stubConfig.ExpectInsert, key)
assert.Equal(r.t, "test-owner", aws.StringValue(statement.Parameters[1].S))
for label, attribute := range statement.Parameters[2].M {
value := aws.StringValue(attribute.S)
expectedValue, found := expectedLabels[label]
assert.True(r.t, found, "insert for key %q has unexpected label %q", key, label)
delete(expectedLabels, label)
assert.Equal(r.t, expectedValue, value, "insert for key %q label %q value", key, label)
}
for label := range expectedLabels {
r.t.Errorf("insert for key %q did not get expected label %q", key, label)
}
responses = append(responses, &dynamodb.BatchStatementResponse{})
case "UPDATE \"test-table\" SET \"l\"=? WHERE \"k\"=?":
assert.False(r.t, r.changesApplied, "unexpected update after provider changes")
key := aws.StringValue(statement.Parameters[1].S)
if code, exists := r.stubConfig.ExpectUpdateError[key]; exists {
delete(r.stubConfig.ExpectInsertError, key)
responses = append(responses, &dynamodb.BatchStatementResponse{
Error: &dynamodb.BatchStatementError{
Code: aws.String(code),
Message: aws.String("testing error"),
},
})
break
}
expectedLabels, found := r.stubConfig.ExpectUpdate[key]
assert.True(r.t, found, "unexpected update for key %q", key)
delete(r.stubConfig.ExpectUpdate, key)
for label, attribute := range statement.Parameters[0].M {
value := aws.StringValue(attribute.S)
expectedValue, found := expectedLabels[label]
assert.True(r.t, found, "update for key %q has unexpected label %q", key, label)
delete(expectedLabels, label)
assert.Equal(r.t, expectedValue, value, "update for key %q label %q value", key, label)
}
for label := range expectedLabels {
r.t.Errorf("update for key %q did not get expected label %q", key, label)
}
responses = append(responses, &dynamodb.BatchStatementResponse{})
default:
r.t.Errorf("unexpected statement: %s", aws.StringValue(statement.Statement))
}
}
return &dynamodb.BatchExecuteStatementOutput{
Responses: responses,
}, nil
}

View File

@ -45,11 +45,6 @@ func (im *NoopRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, erro
return im.provider.Records(ctx)
}
// MissingRecords returns nil because there is no missing records for Noop registry
func (im *NoopRegistry) MissingRecords() []*endpoint.Endpoint {
return nil
}
// ApplyChanges propagates changes to the dns provider
func (im *NoopRegistry) ApplyChanges(ctx context.Context, changes *plan.Changes) error {
return im.provider.ApplyChanges(ctx, changes)

View File

@ -35,7 +35,6 @@ type Registry interface {
PropertyValuesEqual(attribute string, previous string, current string) bool
AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint
GetDomainFilter() endpoint.DomainFilterInterface
MissingRecords() []*endpoint.Endpoint
}
// TODO(ideahitme): consider moving this to Plan

View File

@ -30,7 +30,10 @@ import (
"sigs.k8s.io/external-dns/provider"
)
const recordTemplate = "%{record_type}"
const (
recordTemplate = "%{record_type}"
providerSpecificForceUpdate = "txt/force-update"
)
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
type TXTRegistry struct {
@ -50,9 +53,6 @@ type TXTRegistry struct {
managedRecordTypes []string
// missingTXTRecords stores TXT records which are missing after the migration to the new format
missingTXTRecords []*endpoint.Endpoint
// encrypt text records
txtEncryptEnabled bool
txtEncryptAESKey []byte
@ -117,7 +117,6 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
}
endpoints := []*endpoint.Endpoint{}
missingEndpoints := []*endpoint.Endpoint{}
labelMap := map[string]endpoint.Labels{}
txtRecordsMap := map[string]struct{}{}
@ -174,17 +173,11 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
if plan.IsManagedRecord(ep.RecordType, im.managedRecordTypes) {
// Get desired TXT records and detect the missing ones
desiredTXTs := im.generateTXTRecord(ep)
missingDesiredTXTs := []*endpoint.Endpoint{}
for _, desiredTXT := range desiredTXTs {
if _, exists := txtRecordsMap[desiredTXT.DNSName]; !exists {
missingDesiredTXTs = append(missingDesiredTXTs, desiredTXT)
ep.WithProviderSpecific(providerSpecificForceUpdate, "true")
}
}
if len(desiredTXTs) > len(missingDesiredTXTs) {
// Add missing TXT records only if those are managed (by externaldns) ones.
// The unmanaged record has both of the desired TXT records missing.
missingEndpoints = append(missingEndpoints, missingDesiredTXTs...)
}
}
}
}
@ -195,17 +188,9 @@ func (im *TXTRegistry) Records(ctx context.Context) ([]*endpoint.Endpoint, error
im.recordsCacheRefreshTime = time.Now()
}
im.missingTXTRecords = missingEndpoints
return endpoints, nil
}
// MissingRecords returns the TXT record to be created.
// The missing records are collected during the run of Records method.
func (im *TXTRegistry) MissingRecords() []*endpoint.Endpoint {
return im.missingTXTRecords
}
// generateTXTRecord generates both "old" and "new" TXT records.
// Once we decide to drop old format we need to drop toTXTName() and rename toNewTXTName
func (im *TXTRegistry) generateTXTRecord(r *endpoint.Endpoint) []*endpoint.Endpoint {
@ -309,10 +294,6 @@ func (im *TXTRegistry) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi
return im.provider.AdjustEndpoints(endpoints)
}
/**
TXT registry specific private methods
*/
/**
nameMapper is the interface for mapping between the endpoint for the source
and the endpoint for the TXT record.
@ -467,7 +448,6 @@ func (im *TXTRegistry) addToCache(ep *endpoint.Endpoint) {
func (im *TXTRegistry) removeFromCache(ep *endpoint.Endpoint) {
if im.recordsCache == nil || ep == nil {
// return early.
return
}

View File

@ -219,7 +219,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
// Ensure prefix is case-insensitive
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "", []string{}, false, nil)
r, _ = NewTXTRegistry(p, "TxT.", "", "owner", time.Hour, "wc", []string{}, false, nil)
records, _ = r.Records(ctx)
assert.True(t, testutils.SameEndpointLabels(records, expectedRecords))
@ -875,6 +875,12 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) {
// owner was added from the TXT record's target
endpoint.OwnerLabelKey: "owner",
},
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{
Name: "txt/force-update",
Value: "true",
},
},
},
{
DNSName: "oldformat2.test-zone.example.org",
@ -883,6 +889,12 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) {
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
},
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{
Name: "txt/force-update",
Value: "true",
},
},
},
{
DNSName: "newformat.test-zone.example.org",
@ -931,32 +943,10 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) {
},
}
expectedMissingRecords := []*endpoint.Endpoint{
{
DNSName: "cname-oldformat.test-zone.example.org",
// owner is taken from the source record (A, CNAME, etc.)
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: endpoint.RecordTypeTXT,
Labels: endpoint.Labels{
endpoint.OwnedRecordLabelKey: "oldformat.test-zone.example.org",
},
},
{
DNSName: "a-oldformat2.test-zone.example.org",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: endpoint.RecordTypeTXT,
Labels: endpoint.Labels{
endpoint.OwnedRecordLabelKey: "oldformat2.test-zone.example.org",
},
},
}
r, _ := NewTXTRegistry(p, "", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, false, nil)
records, _ := r.Records(ctx)
missingRecords := r.MissingRecords()
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords))
}
func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) {
@ -988,6 +978,12 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) {
// owner was added from the TXT record's target
endpoint.OwnerLabelKey: "owner",
},
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{
Name: "txt/force-update",
Value: "true",
},
},
},
{
DNSName: "oldformat2.test-zone.example.org",
@ -996,6 +992,12 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) {
Labels: map[string]string{
endpoint.OwnerLabelKey: "owner",
},
ProviderSpecific: []endpoint.ProviderSpecificProperty{
{
Name: "txt/force-update",
Value: "true",
},
},
},
{
DNSName: "newformat.test-zone.example.org",
@ -1035,32 +1037,10 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) {
},
}
expectedMissingRecords := []*endpoint.Endpoint{
{
DNSName: "txt.cname-oldformat.test-zone.example.org",
// owner is taken from the source record (A, CNAME, etc.)
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: endpoint.RecordTypeTXT,
Labels: endpoint.Labels{
endpoint.OwnedRecordLabelKey: "oldformat.test-zone.example.org",
},
},
{
DNSName: "txt.a-oldformat2.test-zone.example.org",
Targets: endpoint.Targets{"\"heritage=external-dns,external-dns/owner=owner\""},
RecordType: endpoint.RecordTypeTXT,
Labels: endpoint.Labels{
endpoint.OwnedRecordLabelKey: "oldformat2.test-zone.example.org",
},
},
}
r, _ := NewTXTRegistry(p, "txt.", "", "owner", time.Hour, "wc", []string{endpoint.RecordTypeCNAME, endpoint.RecordTypeA, endpoint.RecordTypeNS}, false, nil)
records, _ := r.Records(ctx)
missingRecords := r.MissingRecords()
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
assert.True(t, testutils.SameEndpoints(missingRecords, expectedMissingRecords))
}
func TestCacheMethods(t *testing.T) {

View File

@ -64,20 +64,35 @@ type proxySpecHTTPListener struct {
}
type proxyVirtualHost struct {
Domains []string `json:"domains,omitempty"`
Metadata proxyVirtualHostMetadata `json:"metadata,omitempty"`
Domains []string `json:"domains,omitempty"`
Metadata proxyVirtualHostMetadata `json:"metadata,omitempty"`
MetadataStatic proxyVirtualHostMetadataStatic `json:"metadataStatic,omitempty"`
}
type proxyVirtualHostMetadata struct {
Source []proxyVirtualHostMetadataSource `json:"sources,omitempty"`
}
type proxyVirtualHostMetadataStatic struct {
Source []proxyVirtualHostMetadataStaticSource `json:"sources,omitempty"`
}
type proxyVirtualHostMetadataSource struct {
Kind string `json:"kind,omitempty"`
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
type proxyVirtualHostMetadataStaticSource struct {
ResourceKind string `json:"resourceKind,omitempty"`
ResourceRef proxyVirtualHostMetadataSourceResourceRef `json:"resourceRef,omitempty"`
}
type proxyVirtualHostMetadataSourceResourceRef struct {
Name string `json:"name,omitempty"`
Namespace string `json:"namespace,omitempty"`
}
type glooSource struct {
dynamicKubeClient dynamic.Interface
kubeClient kubernetes.Interface
@ -165,6 +180,18 @@ func (gs *glooSource) annotationsFromProxySource(ctx context.Context, virtualHos
}
}
}
for _, src := range virtualHost.MetadataStatic.Source {
kind := sourceKind(src.ResourceKind)
if kind != nil {
source, err := gs.dynamicKubeClient.Resource(*kind).Namespace(src.ResourceRef.Namespace).Get(ctx, src.ResourceRef.Name, metav1.GetOptions{})
if err != nil {
return nil, err
}
for key, value := range source.GetAnnotations() {
annotations[key] = value
}
}
}
return annotations, nil
}

View File

@ -211,6 +211,97 @@ var externalProxySource = metav1.PartialObjectMetadata{
},
}
// Proxy with metadata static test
var proxyMetadataStatic = proxy{
TypeMeta: metav1.TypeMeta{
APIVersion: proxyGVR.GroupVersion().String(),
Kind: "Proxy",
},
Metadata: metav1.ObjectMeta{
Name: "internal-static",
Namespace: defaultGlooNamespace,
},
Spec: proxySpec{
Listeners: []proxySpecListener{
{
HTTPListener: proxySpecHTTPListener{
VirtualHosts: []proxyVirtualHost{
{
Domains: []string{"f.test", "g.test"},
MetadataStatic: proxyVirtualHostMetadataStatic{
Source: []proxyVirtualHostMetadataStaticSource{
{
ResourceKind: "*v1.Unknown",
ResourceRef: proxyVirtualHostMetadataSourceResourceRef{
Name: "my-unknown-svc",
Namespace: "unknown",
},
},
},
},
},
{
Domains: []string{"h.test"},
MetadataStatic: proxyVirtualHostMetadataStatic{
Source: []proxyVirtualHostMetadataStaticSource{
{
ResourceKind: "*v1.VirtualService",
ResourceRef: proxyVirtualHostMetadataSourceResourceRef{
Name: "my-internal-static-svc",
Namespace: "internal-static",
},
},
},
},
},
},
},
},
},
},
}
var proxyMetadataStaticSvc = corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: proxyMetadataStatic.Metadata.Name,
Namespace: proxyMetadataStatic.Metadata.Namespace,
},
Spec: corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
},
Status: corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "203.0.115.1",
},
{
IP: "203.0.115.2",
},
{
IP: "203.0.115.3",
},
},
},
},
}
var proxyMetadataStaticSource = metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: virtualServiceGVR.GroupVersion().String(),
Kind: "VirtualService",
},
ObjectMeta: metav1.ObjectMeta{
Name: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Name,
Namespace: proxyMetadataStatic.Spec.Listeners[0].HTTPListener.VirtualHosts[1].MetadataStatic.Source[0].ResourceRef.Namespace,
Annotations: map[string]string{
"external-dns.alpha.kubernetes.io/ttl": "420",
"external-dns.alpha.kubernetes.io/aws-geolocation-country-code": "ES",
"external-dns.alpha.kubernetes.io/set-identifier": "identifier",
},
},
}
func TestGlooSource(t *testing.T) {
t.Parallel()
@ -226,9 +317,11 @@ func TestGlooSource(t *testing.T) {
internalProxyUnstructured := unstructured.Unstructured{}
externalProxyUnstructured := unstructured.Unstructured{}
proxyMetadataStaticUnstructured := unstructured.Unstructured{}
internalProxySourceUnstructured := unstructured.Unstructured{}
externalProxySourceUnstructured := unstructured.Unstructured{}
proxyMetadataStaticSourceUnstructured := unstructured.Unstructured{}
internalProxyAsJSON, err := json.Marshal(internalProxy)
assert.NoError(t, err)
@ -236,39 +329,53 @@ func TestGlooSource(t *testing.T) {
externalProxyAsJSON, err := json.Marshal(externalProxy)
assert.NoError(t, err)
proxyMetadataStaticAsJSON, err := json.Marshal(proxyMetadataStatic)
assert.NoError(t, err)
internalProxySvcAsJSON, err := json.Marshal(internalProxySource)
assert.NoError(t, err)
externalProxySvcAsJSON, err := json.Marshal(externalProxySource)
assert.NoError(t, err)
proxyMetadataStaticSvcAsJSON, err := json.Marshal(proxyMetadataStaticSource)
assert.NoError(t, err)
assert.NoError(t, internalProxyUnstructured.UnmarshalJSON(internalProxyAsJSON))
assert.NoError(t, externalProxyUnstructured.UnmarshalJSON(externalProxyAsJSON))
assert.NoError(t, proxyMetadataStaticUnstructured.UnmarshalJSON(proxyMetadataStaticAsJSON))
assert.NoError(t, internalProxySourceUnstructured.UnmarshalJSON(internalProxySvcAsJSON))
assert.NoError(t, externalProxySourceUnstructured.UnmarshalJSON(externalProxySvcAsJSON))
assert.NoError(t, proxyMetadataStaticSourceUnstructured.UnmarshalJSON(proxyMetadataStaticSvcAsJSON))
// Create proxy resources
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &internalProxyUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &externalProxyUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(proxyGVR).Namespace(defaultGlooNamespace).Create(context.Background(), &proxyMetadataStaticUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy source
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(internalProxySource.Namespace).Create(context.Background(), &internalProxySourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(externalProxySource.Namespace).Create(context.Background(), &externalProxySourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeDynamicClient.Resource(virtualServiceGVR).Namespace(proxyMetadataStaticSource.Namespace).Create(context.Background(), &proxyMetadataStaticSourceUnstructured, metav1.CreateOptions{})
assert.NoError(t, err)
// Create proxy service resources
_, err = fakeKubernetesClient.CoreV1().Services(internalProxySvc.GetNamespace()).Create(context.Background(), &internalProxySvc, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeKubernetesClient.CoreV1().Services(externalProxySvc.GetNamespace()).Create(context.Background(), &externalProxySvc, metav1.CreateOptions{})
assert.NoError(t, err)
_, err = fakeKubernetesClient.CoreV1().Services(proxyMetadataStaticSvc.GetNamespace()).Create(context.Background(), &proxyMetadataStaticSvc, metav1.CreateOptions{})
assert.NoError(t, err)
endpoints, err := source.Endpoints(context.Background())
assert.NoError(t, err)
assert.Len(t, endpoints, 5)
assert.Len(t, endpoints, 8)
assert.ElementsMatch(t, endpoints, []*endpoint.Endpoint{
{
DNSName: "a.test",
@ -322,5 +429,35 @@ func TestGlooSource(t *testing.T) {
},
},
},
{
DNSName: "f.test",
Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
{
DNSName: "g.test",
Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
RecordTTL: 0,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{},
},
{
DNSName: "h.test",
Targets: []string{proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[0].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[1].IP, proxyMetadataStaticSvc.Status.LoadBalancer.Ingress[2].IP},
RecordType: endpoint.RecordTypeA,
SetIdentifier: "identifier",
RecordTTL: 420,
Labels: endpoint.Labels{},
ProviderSpecific: endpoint.ProviderSpecific{
endpoint.ProviderSpecificProperty{
Name: "aws/geolocation-country-code",
Value: "ES",
},
},
},
})
}

View File

@ -149,7 +149,7 @@ func (sc *gatewaySource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, e
// apply template if host is missing on gateway
if (sc.combineFQDNAnnotation || len(gwHostnames) == 0) && sc.fqdnTemplate != nil {
iHostnames, err := execTemplate(sc.fqdnTemplate, &gateway)
iHostnames, err := execTemplate(sc.fqdnTemplate, gateway)
if err != nil {
return nil, err
}
@ -196,7 +196,7 @@ func (sc *gatewaySource) AddEventHandler(ctx context.Context, handler func()) {
}
// filterByAnnotations filters a list of configs by a given annotation selector.
func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gateway) ([]networkingv1alpha3.Gateway, error) {
func (sc *gatewaySource) filterByAnnotations(gateways []*networkingv1alpha3.Gateway) ([]*networkingv1alpha3.Gateway, error) {
labelSelector, err := metav1.ParseToLabelSelector(sc.annotationFilter)
if err != nil {
return nil, err
@ -211,7 +211,7 @@ func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gatew
return gateways, nil
}
var filteredList []networkingv1alpha3.Gateway
var filteredList []*networkingv1alpha3.Gateway
for _, gw := range gateways {
// convert the annotations to an equivalent label selector
@ -226,13 +226,13 @@ func (sc *gatewaySource) filterByAnnotations(gateways []networkingv1alpha3.Gatew
return filteredList, nil
}
func (sc *gatewaySource) setResourceLabel(gateway networkingv1alpha3.Gateway, endpoints []*endpoint.Endpoint) {
func (sc *gatewaySource) setResourceLabel(gateway *networkingv1alpha3.Gateway, endpoints []*endpoint.Endpoint) {
for _, ep := range endpoints {
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("gateway/%s/%s", gateway.Namespace, gateway.Name)
}
}
func (sc *gatewaySource) targetsFromGateway(gateway networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
func (sc *gatewaySource) targetsFromGateway(gateway *networkingv1alpha3.Gateway) (targets endpoint.Targets, err error) {
targets = getTargetsFromTargetAnnotation(gateway.Annotations)
if len(targets) > 0 {
return
@ -262,7 +262,7 @@ func (sc *gatewaySource) targetsFromGateway(gateway networkingv1alpha3.Gateway)
}
// endpointsFromGatewayConfig extracts the endpoints from an Istio Gateway Config object
func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) {
func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway *networkingv1alpha3.Gateway) ([]*endpoint.Endpoint, error) {
var endpoints []*endpoint.Endpoint
annotations := gateway.Annotations
@ -289,7 +289,7 @@ func (sc *gatewaySource) endpointsFromGateway(hostnames []string, gateway networ
return endpoints, nil
}
func (sc *gatewaySource) hostNamesFromGateway(gateway networkingv1alpha3.Gateway) ([]string, error) {
func (sc *gatewaySource) hostNamesFromGateway(gateway *networkingv1alpha3.Gateway) ([]string, error) {
var hostnames []string
for _, server := range gateway.Spec.Servers {
for _, host := range server.Hosts {

View File

@ -1192,7 +1192,7 @@ func testGatewayEndpoints(t *testing.T) {
fakeIstioClient := istiofake.NewSimpleClientset()
for _, config := range ti.configItems {
gatewayCfg := config.Config()
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(context.Background(), &gatewayCfg, metav1.CreateOptions{})
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(ti.targetNamespace).Create(context.Background(), gatewayCfg, metav1.CreateOptions{})
require.NoError(t, err)
}
@ -1301,8 +1301,8 @@ type fakeGatewayConfig struct {
selector map[string]string
}
func (c fakeGatewayConfig) Config() networkingv1alpha3.Gateway {
gw := networkingv1alpha3.Gateway{
func (c fakeGatewayConfig) Config() *networkingv1alpha3.Gateway {
gw := &networkingv1alpha3.Gateway{
ObjectMeta: metav1.ObjectMeta{
Name: c.name,
Namespace: c.namespace,

View File

@ -23,12 +23,12 @@ import (
"strings"
"text/template"
networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
log "github.com/sirupsen/logrus"
networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
istioclient "istio.io/client-go/pkg/clientset/versioned"
istioinformers "istio.io/client-go/pkg/informers/externalversions"
networkingv1alpha3informer "istio.io/client-go/pkg/informers/externalversions/networking/v1alpha3"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
kubeinformers "k8s.io/client-go/informers"
@ -203,7 +203,10 @@ func (sc *virtualServiceSource) getGateway(ctx context.Context, gatewayStr strin
}
gateway, err := sc.istioClient.NetworkingV1alpha3().Gateways(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
log.Warnf("VirtualService (%s/%s) references non-existent gateway: %s ", virtualService.Namespace, virtualService.Name, gatewayStr)
return nil, nil
} else if err != nil {
log.Errorf("Failed retrieving gateway %s referenced by VirtualService %s/%s: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
return nil, err
}
@ -211,7 +214,6 @@ func (sc *virtualServiceSource) getGateway(ctx context.Context, gatewayStr strin
log.Debugf("Gateway %s referenced by VirtualService %s/%s not found: %v", gatewayStr, virtualService.Namespace, virtualService.Name, err)
return nil, nil
}
return gateway, nil
}

View File

@ -18,19 +18,23 @@ package source
import (
"context"
"fmt"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"istio.io/api/meta/v1alpha1"
istionetworking "istio.io/api/networking/v1alpha3"
networkingv1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
istiofake "istio.io/client-go/pkg/clientset/versioned/fake"
fakenetworking3 "istio.io/client-go/pkg/clientset/versioned/typed/networking/v1alpha3/fake"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8sclienttesting "k8s.io/client-go/testing"
"sigs.k8s.io/external-dns/endpoint"
)
@ -42,7 +46,7 @@ type VirtualServiceSuite struct {
suite.Suite
source Source
lbServices []*v1.Service
gwconfig networkingv1alpha3.Gateway
gwconfig *networkingv1alpha3.Gateway
vsconfig *networkingv1alpha3.VirtualService
}
@ -76,7 +80,7 @@ func (suite *VirtualServiceSuite) SetupTest() {
namespace: "istio-system",
dnsnames: [][]string{{"*"}},
}).Config()
_, err = fakeIstioClient.NetworkingV1alpha3().Gateways(suite.gwconfig.Namespace).Create(context.Background(), &suite.gwconfig, metav1.CreateOptions{})
_, err = fakeIstioClient.NetworkingV1alpha3().Gateways(suite.gwconfig.Namespace).Create(context.Background(), suite.gwconfig, metav1.CreateOptions{})
suite.NoError(err, "should succeed")
suite.vsconfig = (fakeVirtualServiceConfig{
@ -362,7 +366,7 @@ func testVirtualServiceBindsToGateway(t *testing.T) {
t.Run(ti.title, func(t *testing.T) {
vsconfig := ti.vsconfig.Config()
gwconfig := ti.gwconfig.Config()
require.Equal(t, ti.expected, virtualServiceBindsToGateway(vsconfig, &gwconfig, ti.vsHost))
require.Equal(t, ti.expected, virtualServiceBindsToGateway(vsconfig, gwconfig, ti.vsHost))
})
}
}
@ -1479,7 +1483,7 @@ func testVirtualServiceEndpoints(t *testing.T) {
t.Run(ti.title, func(t *testing.T) {
t.Parallel()
var gateways []networkingv1alpha3.Gateway
var gateways []*networkingv1alpha3.Gateway
var virtualservices []*networkingv1alpha3.VirtualService
for _, gwItem := range ti.gwConfigs {
@ -1500,7 +1504,7 @@ func testVirtualServiceEndpoints(t *testing.T) {
fakeIstioClient := istiofake.NewSimpleClientset()
for _, gateway := range gateways {
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gateway.Namespace).Create(context.Background(), &gateway, metav1.CreateOptions{})
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gateway.Namespace).Create(context.Background(), gateway, metav1.CreateOptions{})
require.NoError(t, err)
}
@ -1579,7 +1583,9 @@ func newTestVirtualServiceSource(loadBalancerList []fakeIngressGatewayService, g
for _, gw := range gwList {
gwObj := gw.Config()
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gw.namespace).Create(context.Background(), &gwObj, metav1.CreateOptions{})
// use create instead of add
// https://github.com/kubernetes/client-go/blob/92512ee2b8cf6696e9909245624175b7f0c971d9/testing/fixture.go#LL336C3-L336C52
_, err := fakeIstioClient.NetworkingV1alpha3().Gateways(gw.namespace).Create(context.Background(), gwObj, metav1.CreateOptions{})
if err != nil {
return nil, err
}
@ -1631,6 +1637,127 @@ func (c fakeVirtualServiceConfig) Config() *networkingv1alpha3.VirtualService {
Namespace: c.namespace,
Annotations: c.annotations,
},
Spec: vs,
Spec: *vs.DeepCopy(),
}
}
func TestVirtualServiceSourceGetGateway(t *testing.T) {
type fields struct {
virtualServiceSource *virtualServiceSource
}
type args struct {
ctx context.Context
gatewayStr string
virtualService *networkingv1alpha3.VirtualService
}
tests := []struct {
name string
fields fields
args args
want *networkingv1alpha3.Gateway
expectedErrStr string
}{
{name: "EmptyGateway", fields: fields{
virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(),
}, args: args{
ctx: context.TODO(),
gatewayStr: "",
virtualService: nil,
}, want: nil, expectedErrStr: ""},
{name: "MeshGateway", fields: fields{
virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(),
}, args: args{
ctx: context.TODO(),
gatewayStr: IstioMeshGateway,
virtualService: nil,
}, want: nil, expectedErrStr: ""},
{name: "MissingGateway", fields: fields{
virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(),
}, args: args{
ctx: context.TODO(),
gatewayStr: "doesnt/exist",
virtualService: &networkingv1alpha3.VirtualService{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Name: "exist", Namespace: "doesnt"},
Spec: istionetworking.VirtualService{},
Status: v1alpha1.IstioStatus{},
},
}, want: nil, expectedErrStr: ""},
{name: "InvalidGatewayStr", fields: fields{
virtualServiceSource: func() *virtualServiceSource { vs, _ := newTestVirtualServiceSource(nil, nil); return vs }(),
}, args: args{
ctx: context.TODO(),
gatewayStr: "1/2/3/",
virtualService: &networkingv1alpha3.VirtualService{},
}, want: nil, expectedErrStr: "invalid gateway name (name or namespace/name) found '1/2/3/'"},
{name: "ExistingGateway", fields: fields{
virtualServiceSource: func() *virtualServiceSource {
vs, _ := newTestVirtualServiceSource(nil, []fakeGatewayConfig{{
namespace: "bar",
name: "foo",
}})
return vs
}(),
}, args: args{
ctx: context.TODO(),
gatewayStr: "bar/foo",
virtualService: &networkingv1alpha3.VirtualService{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
Spec: istionetworking.VirtualService{},
Status: v1alpha1.IstioStatus{},
},
}, want: &networkingv1alpha3.Gateway{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "bar"},
Spec: istionetworking.Gateway{},
Status: v1alpha1.IstioStatus{},
}, expectedErrStr: ""},
{name: "ErrorGettingGateway", fields: fields{
virtualServiceSource: func() *virtualServiceSource {
istioFake := istiofake.NewSimpleClientset()
istioFake.NetworkingV1alpha3().(*fakenetworking3.FakeNetworkingV1alpha3).PrependReactor("get", "gateways", func(action k8sclienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, &networkingv1alpha3.Gateway{}, fmt.Errorf("error getting gateway")
})
vs, _ := NewIstioVirtualServiceSource(
context.TODO(),
fake.NewSimpleClientset(),
istioFake,
"",
"",
"{{.Name}}",
false,
false,
)
return vs.(*virtualServiceSource)
}(),
}, args: args{
ctx: context.TODO(),
gatewayStr: "foo/bar",
virtualService: &networkingv1alpha3.VirtualService{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "error"},
Spec: istionetworking.VirtualService{},
Status: v1alpha1.IstioStatus{},
},
}, want: nil, expectedErrStr: "error getting gateway"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fields.virtualServiceSource.getGateway(tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService)
if tt.expectedErrStr != "" {
assert.EqualError(t, err, tt.expectedErrStr, fmt.Sprintf("getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService))
return
} else {
require.NoError(t, err)
}
if tt.want != nil && got != nil {
tt.want.Spec.ProtoReflect()
tt.want.Status.ProtoReflect()
assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService)
} else {
assert.Equalf(t, tt.want, got, "getGateway(%v, %v, %v)", tt.args.ctx, tt.args.gatewayStr, tt.args.virtualService)
}
})
}
}

View File

@ -88,7 +88,7 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
return nil, err
}
endpoints := map[endpointKey]*endpoint.Endpoint{}
endpoints := map[endpoint.EndpointKey]*endpoint.Endpoint{}
// create endpoints for all nodes
for _, node := range nodes {
@ -136,13 +136,13 @@ func (ns *nodeSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro
ep.Labels = endpoint.NewLabels()
for _, addr := range addrs {
log.Debugf("adding endpoint %s target %s", ep, addr)
key := endpointKey{
dnsName: ep.DNSName,
recordType: suitableType(addr),
key := endpoint.EndpointKey{
DNSName: ep.DNSName,
RecordType: suitableType(addr),
}
if _, ok := endpoints[key]; !ok {
epCopy := *ep
epCopy.RecordType = key.recordType
epCopy.RecordType = key.RecordType
endpoints[key] = &epCopy
}
endpoints[key].Targets = append(endpoints[key].Targets, addr)

View File

@ -21,6 +21,7 @@ import (
"fmt"
"sort"
"text/template"
"time"
routev1 "github.com/openshift/api/route/v1"
versioned "github.com/openshift/client-go/route/clientset/versioned"
@ -71,7 +72,7 @@ func NewOcpRouteSource(
// Use a shared informer to listen for add/update/delete of Routes in the specified namespace.
// Set resync period to 0, to prevent processing when nothing has changed.
informerFactory := extInformers.NewSharedInformerFactoryWithOptions(ocpClient, 0, extInformers.WithNamespace(namespace))
informerFactory := extInformers.NewFilteredSharedInformerFactory(ocpClient, 0*time.Second, namespace, nil)
informer := informerFactory.Route().V1().Routes()
// Add default resource event handlers to properly initialize informer.

View File

@ -82,7 +82,7 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
return nil, err
}
endpointMap := make(map[endpointKey][]string)
endpointMap := make(map[endpoint.EndpointKey][]string)
for _, pod := range pods {
if !pod.Spec.HostNetwork {
log.Debugf("skipping pod %s. hostNetwork=false", pod.Name)
@ -135,15 +135,15 @@ func (ps *podSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error
}
endpoints := []*endpoint.Endpoint{}
for key, targets := range endpointMap {
endpoints = append(endpoints, endpoint.NewEndpoint(key.dnsName, key.recordType, targets...))
endpoints = append(endpoints, endpoint.NewEndpoint(key.DNSName, key.RecordType, targets...))
}
return endpoints, nil
}
func addToEndpointMap(endpointMap map[endpointKey][]string, domain string, recordType string, address string) {
key := endpointKey{
dnsName: domain,
recordType: recordType,
func addToEndpointMap(endpointMap map[endpoint.EndpointKey][]string, domain string, recordType string, address string) {
key := endpoint.EndpointKey{
DNSName: domain,
RecordType: recordType,
}
if _, ok := endpointMap[key]; !ok {
endpointMap[key] = []string{}

View File

@ -271,7 +271,7 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
endpointsType := getEndpointsTypeFromAnnotations(svc.Annotations)
targetsByHeadlessDomainAndType := make(map[endpointKey]endpoint.Targets)
targetsByHeadlessDomainAndType := make(map[endpoint.EndpointKey]endpoint.Targets)
for _, subset := range endpointsObject.Subsets {
addresses := subset.Addresses
if svc.Spec.PublishNotReadyAddresses || sc.alwaysPublishNotReadyAddresses {
@ -325,9 +325,9 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
}
}
for _, target := range targets {
key := endpointKey{
dnsName: headlessDomain,
recordType: suitableType(target),
key := endpoint.EndpointKey{
DNSName: headlessDomain,
RecordType: suitableType(target),
}
targetsByHeadlessDomainAndType[key] = append(targetsByHeadlessDomainAndType[key], target)
}
@ -335,15 +335,15 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
}
}
headlessKeys := []endpointKey{}
headlessKeys := []endpoint.EndpointKey{}
for headlessKey := range targetsByHeadlessDomainAndType {
headlessKeys = append(headlessKeys, headlessKey)
}
sort.Slice(headlessKeys, func(i, j int) bool {
if headlessKeys[i].dnsName != headlessKeys[j].dnsName {
return headlessKeys[i].dnsName < headlessKeys[j].dnsName
if headlessKeys[i].DNSName != headlessKeys[j].DNSName {
return headlessKeys[i].DNSName < headlessKeys[j].DNSName
}
return headlessKeys[i].recordType < headlessKeys[j].recordType
return headlessKeys[i].RecordType < headlessKeys[j].RecordType
})
for _, headlessKey := range headlessKeys {
allTargets := targetsByHeadlessDomainAndType[headlessKey]
@ -361,9 +361,9 @@ func (sc *serviceSource) extractHeadlessEndpoints(svc *v1.Service, hostname stri
}
if ttl.IsConfigured() {
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessKey.dnsName, headlessKey.recordType, ttl, targets...))
endpoints = append(endpoints, endpoint.NewEndpointWithTTL(headlessKey.DNSName, headlessKey.RecordType, ttl, targets...))
} else {
endpoints = append(endpoints, endpoint.NewEndpoint(headlessKey.dnsName, headlessKey.recordType, targets...))
endpoints = append(endpoints, endpoint.NewEndpoint(headlessKey.DNSName, headlessKey.RecordType, targets...))
}
}

View File

@ -86,12 +86,6 @@ type Source interface {
AddEventHandler(context.Context, func())
}
// endpointKey is the type of a map key for separating endpoints or targets.
type endpointKey struct {
dnsName string
recordType string
}
func getTTLFromAnnotations(annotations map[string]string) (endpoint.TTL, error) {
ttlNotConfigured := endpoint.TTL(0)
ttlAnnotation, exists := annotations[ttlAnnotationKey]
@ -338,9 +332,9 @@ func matchLabelSelector(selector labels.Selector, srcAnnotations map[string]stri
type eventHandlerFunc func()
func (fn eventHandlerFunc) OnAdd(obj interface{}) { fn() }
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() }
func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() }
func (fn eventHandlerFunc) OnAdd(obj interface{}, isInInitialList bool) { fn() }
func (fn eventHandlerFunc) OnUpdate(oldObj, newObj interface{}) { fn() }
func (fn eventHandlerFunc) OnDelete(obj interface{}) { fn() }
type informerFactory interface {
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool

View File

@ -292,6 +292,16 @@ func BuildWithConfig(ctx context.Context, source string, p ClientGenerator, cfg
return nil, err
}
return NewGlooSource(dynamicClient, kubernetesClient, cfg.GlooNamespace)
case "traefik-proxy":
kubernetesClient, err := p.KubeClient()
if err != nil {
return nil, err
}
dynamicClient, err := p.DynamicKubernetesClient()
if err != nil {
return nil, err
}
return NewTraefikSource(ctx, dynamicClient, kubernetesClient, cfg.Namespace, cfg.AnnotationFilter)
case "openshift-route":
ocpClient, err := p.OpenShiftClient()
if err != nil {

View File

@ -130,11 +130,41 @@ func (suite *ByNamesTestSuite) TestAllInitialized() {
Version: "v1",
Resource: "virtualservers",
}: "VirtualServersList",
{
Group: "traefik.containo.us",
Version: "v1alpha1",
Resource: "ingressroutes",
}: "IngressRouteList",
{
Group: "traefik.containo.us",
Version: "v1alpha1",
Resource: "ingressroutetcps",
}: "IngressRouteTCPList",
{
Group: "traefik.containo.us",
Version: "v1alpha1",
Resource: "ingressrouteudps",
}: "IngressRouteUDPList",
{
Group: "traefik.io",
Version: "v1alpha1",
Resource: "ingressroutes",
}: "IngressRouteList",
{
Group: "traefik.io",
Version: "v1alpha1",
Resource: "ingressroutetcps",
}: "IngressRouteTCPList",
{
Group: "traefik.io",
Version: "v1alpha1",
Resource: "ingressrouteudps",
}: "IngressRouteUDPList",
}), nil)
sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "f5-virtualserver", "fake"}, minimalConfig)
sources, err := ByNames(context.TODO(), mockClientGenerator, []string{"service", "ingress", "istio-gateway", "contour-httpproxy", "kong-tcpingress", "f5-virtualserver", "traefik-proxy", "fake"}, minimalConfig)
suite.NoError(err, "should not generate errors")
suite.Len(sources, 7, "should generate all seven sources")
suite.Len(sources, 8, "should generate all eight sources")
}
func (suite *ByNamesTestSuite) TestOnlyFake() {
@ -171,9 +201,6 @@ func (suite *ByNamesTestSuite) TestKubeClientFails() {
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"kong-tcpingress"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
_, err = ByNames(context.TODO(), mockClientGenerator, []string{"f5-virtualserver"}, minimalConfig)
suite.Error(err, "should return an error if kubernetes client cannot be created")
}
func (suite *ByNamesTestSuite) TestIstioClientFails() {

1125
source/traefik_proxy.go Normal file

File diff suppressed because it is too large Load Diff

1330
source/traefik_proxy_test.go Normal file

File diff suppressed because it is too large Load Diff