mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2026-05-04 22:26:11 +02:00
Merge branch 'master' into master
This commit is contained in:
commit
27f9f5286c
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -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'
|
||||
|
||||
|
||||
2
.github/workflows/gh-workflow-approve.yaml
vendored
2
.github/workflows/gh-workflow-approve.yaml
vendored
@ -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,
|
||||
|
||||
2
.github/workflows/lint-test-chart.yaml
vendored
2
.github/workflows/lint-test-chart.yaml
vendored
@ -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)
|
||||
|
||||
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@ -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
|
||||
|
||||
2
.github/workflows/release-chart.yaml
vendored
2
.github/workflows/release-chart.yaml
vendored
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
1
charts/external-dns/ci/ci-values.yaml
Normal file
1
charts/external-dns/ci/ci-values.yaml
Normal file
@ -0,0 +1 @@
|
||||
provider: inmemory
|
||||
@ -43,8 +43,11 @@ shareProcessNamespace: false
|
||||
|
||||
podSecurityContext:
|
||||
fsGroup: 65534
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
readOnlyRootFilesystem: true
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
41
docs/registry/dynamodb.md
Normal 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.
|
||||
@ -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.
|
||||
|
||||
96
docs/tutorials/traefik-proxy.md
Normal file
96
docs/tutorials/traefik-proxy.md
Normal 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
|
||||
```
|
||||
@ -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
200
go.mod
@ -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
|
||||
|
||||
@ -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
31
main.go
@ -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":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
75
provider/aws/session.go
Normal 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
|
||||
}
|
||||
@ -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),
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
437
registry/dynamodb.go
Normal 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
995
registry/dynamodb_test.go
Normal 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
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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{}
|
||||
|
||||
@ -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...))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
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
1330
source/traefik_proxy_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user