mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
Multi target plan ( Page Not Found ) (#404)
* Make suitableType() be Endpoint method With this change it becomes possible to work with endpoint of empty type in packages other than "provider". Also it seems logical for a smart property getter without side effects to be a method rather than a function in different package * Make plan computation work correctly with multi-target domains * fix drawing * drop comments * fix boilerplate header * fix comment * fix the bug with empty map * rework registry to support random lables * serialize->serializeLabel function rename * golint for err variable naming * add additional test * add tests for current case where one resource can generate multiple endpoints * make labels have its own type, add serialization as a method * add comment for exported error * use greater rather than not equal zero * update changelog
This commit is contained in:
parent
5f88867e75
commit
ec07f45c8e
@ -1,3 +1,9 @@
|
|||||||
|
- Every record managed by External DNS is now mapped to a kubernetes resource (service/ingress) @ideahitme
|
||||||
|
- New field is stored in TXT DNS record which reflects which kubernetes resource has acquired the DNS name
|
||||||
|
- Target of DNS record is changed only if corresponding kubernetes resource target changes
|
||||||
|
- If kubernetes resource is deleted, then another resource may acquire DNS name
|
||||||
|
- "Flapping" target issue is resolved by providing a consistent and defined mechanism for choosing a target
|
||||||
|
|
||||||
## v0.4.8 - 2017-11-22
|
## v0.4.8 - 2017-11-22
|
||||||
|
|
||||||
- Allow filtering by source annotation via `--annotation-filter` (#354) @khrisrichardson
|
- Allow filtering by source annotation via `--annotation-filter` (#354) @khrisrichardson
|
||||||
|
@ -22,8 +22,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// OwnerLabelKey is the name of the label that defines the owner of an Endpoint.
|
|
||||||
OwnerLabelKey = "owner"
|
|
||||||
// RecordTypeA is a RecordType enum value
|
// RecordTypeA is a RecordType enum value
|
||||||
RecordTypeA = "A"
|
RecordTypeA = "A"
|
||||||
// RecordTypeCNAME is a RecordType enum value
|
// RecordTypeCNAME is a RecordType enum value
|
||||||
@ -51,7 +49,7 @@ type Endpoint struct {
|
|||||||
// TTL for the record
|
// TTL for the record
|
||||||
RecordTTL TTL
|
RecordTTL TTL
|
||||||
// Labels stores labels defined for the Endpoint
|
// Labels stores labels defined for the Endpoint
|
||||||
Labels map[string]string
|
Labels Labels
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEndpoint initialization method to be used to create an endpoint
|
// NewEndpoint initialization method to be used to create an endpoint
|
||||||
@ -65,20 +63,11 @@ func NewEndpointWithTTL(dnsName, target, recordType string, ttl TTL) *Endpoint {
|
|||||||
DNSName: strings.TrimSuffix(dnsName, "."),
|
DNSName: strings.TrimSuffix(dnsName, "."),
|
||||||
Target: strings.TrimSuffix(target, "."),
|
Target: strings.TrimSuffix(target, "."),
|
||||||
RecordType: recordType,
|
RecordType: recordType,
|
||||||
Labels: map[string]string{},
|
Labels: NewLabels(),
|
||||||
RecordTTL: ttl,
|
RecordTTL: ttl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MergeLabels adds keys to labels if not defined for the endpoint
|
|
||||||
func (e *Endpoint) MergeLabels(labels map[string]string) {
|
|
||||||
for k, v := range labels {
|
|
||||||
if e.Labels[k] == "" {
|
|
||||||
e.Labels[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Endpoint) String() string {
|
func (e *Endpoint) String() string {
|
||||||
return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Target)
|
return fmt.Sprintf("%s %d IN %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.Target)
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,6 @@ package endpoint
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewEndpoint(t *testing.T) {
|
func TestNewEndpoint(t *testing.T) {
|
||||||
@ -36,13 +34,3 @@ func TestNewEndpoint(t *testing.T) {
|
|||||||
t.Error("endpoint is not initialized correctly")
|
t.Error("endpoint is not initialized correctly")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMergeLabels(t *testing.T) {
|
|
||||||
e := NewEndpoint("abc.com", "1.2.3.4", "A")
|
|
||||||
e.Labels = map[string]string{
|
|
||||||
"foo": "bar",
|
|
||||||
"baz": "qux",
|
|
||||||
}
|
|
||||||
e.MergeLabels(map[string]string{"baz": "baz", "new": "fox"})
|
|
||||||
assert.Equal(t, map[string]string{"foo": "bar", "baz": "qux", "new": "fox"}, e.Labels)
|
|
||||||
}
|
|
||||||
|
99
endpoint/labels.go
Normal file
99
endpoint/labels.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidHeritage is returned when heritage was not found, or different heritage is found
|
||||||
|
ErrInvalidHeritage = errors.New("heritage is unknown or not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
heritage = "external-dns"
|
||||||
|
// OwnerLabelKey is the name of the label that defines the owner of an Endpoint.
|
||||||
|
OwnerLabelKey = "owner"
|
||||||
|
// ResourceLabelKey is the name of the label that identifies k8s resource which wants to acquire the DNS name
|
||||||
|
ResourceLabelKey = "resource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Labels store metadata related to the endpoint
|
||||||
|
// it is then stored in a persistent storage via serialization
|
||||||
|
type Labels map[string]string
|
||||||
|
|
||||||
|
// NewLabels returns empty Labels
|
||||||
|
func NewLabels() Labels {
|
||||||
|
return map[string]string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLabelsFromString constructs endpoints labels from a provided format string
|
||||||
|
// if heritage set to another value is found then error is returned
|
||||||
|
// no heritage automatically assumes is not owned by external-dns and returns invalidHeritage error
|
||||||
|
func NewLabelsFromString(labelText string) (Labels, error) {
|
||||||
|
endpointLabels := map[string]string{}
|
||||||
|
labelText = strings.Trim(labelText, "\"") // drop quotes
|
||||||
|
tokens := strings.Split(labelText, ",")
|
||||||
|
foundExternalDNSHeritage := false
|
||||||
|
for _, token := range tokens {
|
||||||
|
if len(strings.Split(token, "=")) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.Split(token, "=")[0]
|
||||||
|
val := strings.Split(token, "=")[1]
|
||||||
|
if key == "heritage" && val != heritage {
|
||||||
|
return nil, ErrInvalidHeritage
|
||||||
|
}
|
||||||
|
if key == "heritage" {
|
||||||
|
foundExternalDNSHeritage = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(key, heritage) {
|
||||||
|
endpointLabels[strings.TrimPrefix(key, heritage+"/")] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundExternalDNSHeritage {
|
||||||
|
return nil, ErrInvalidHeritage
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointLabels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize transforms endpoints labels into a external-dns recognizable format string
|
||||||
|
// withQuotes adds additional quotes
|
||||||
|
func (l Labels) Serialize(withQuotes bool) string {
|
||||||
|
var tokens []string
|
||||||
|
tokens = append(tokens, fmt.Sprintf("heritage=%s", heritage))
|
||||||
|
var keys []string
|
||||||
|
for key := range l {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys) // sort for consistency
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
tokens = append(tokens, fmt.Sprintf("%s/%s=%s", heritage, key, l[key]))
|
||||||
|
}
|
||||||
|
if withQuotes {
|
||||||
|
return fmt.Sprintf("\"%s\"", strings.Join(tokens, ","))
|
||||||
|
}
|
||||||
|
return strings.Join(tokens, ",")
|
||||||
|
}
|
92
endpoint/labels_test.go
Normal file
92
endpoint/labels_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package endpoint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LabelsSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
foo Labels
|
||||||
|
fooAsText string
|
||||||
|
fooAsTextWithQuotes string
|
||||||
|
barText string
|
||||||
|
barTextAsMap Labels
|
||||||
|
noHeritageText string
|
||||||
|
noHeritageAsMap Labels
|
||||||
|
wrongHeritageText string
|
||||||
|
multipleHeritageText string //considered invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LabelsSuite) SetupTest() {
|
||||||
|
suite.foo = map[string]string{
|
||||||
|
"owner": "foo-owner",
|
||||||
|
"resource": "foo-resource",
|
||||||
|
}
|
||||||
|
suite.fooAsText = "heritage=external-dns,external-dns/owner=foo-owner,external-dns/resource=foo-resource"
|
||||||
|
suite.fooAsTextWithQuotes = fmt.Sprintf(`"%s"`, suite.fooAsText)
|
||||||
|
|
||||||
|
suite.barTextAsMap = map[string]string{
|
||||||
|
"owner": "bar-owner",
|
||||||
|
"resource": "bar-resource",
|
||||||
|
"new-key": "bar-new-key",
|
||||||
|
}
|
||||||
|
suite.barText = "heritage=external-dns,,external-dns/owner=bar-owner,external-dns/resource=bar-resource,external-dns/new-key=bar-new-key,random=stuff,no-equal-sign,," //also has some random gibberish
|
||||||
|
|
||||||
|
suite.noHeritageText = "external-dns/owner=random-owner"
|
||||||
|
suite.wrongHeritageText = "heritage=mate,external-dns/owner=random-owner"
|
||||||
|
suite.multipleHeritageText = "heritage=mate,heritage=external-dns,external-dns/owner=random-owner"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LabelsSuite) TestSerialize() {
|
||||||
|
suite.Equal(suite.fooAsText, suite.foo.Serialize(false), "should serializeLabel")
|
||||||
|
suite.Equal(suite.fooAsTextWithQuotes, suite.foo.Serialize(true), "should serializeLabel")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *LabelsSuite) TestDeserialize() {
|
||||||
|
foo, err := NewLabelsFromString(suite.fooAsText)
|
||||||
|
suite.NoError(err, "should succeed for valid label text")
|
||||||
|
suite.Equal(suite.foo, foo, "should reconstruct original label map")
|
||||||
|
|
||||||
|
foo, err = NewLabelsFromString(suite.fooAsTextWithQuotes)
|
||||||
|
suite.NoError(err, "should succeed for valid label text")
|
||||||
|
suite.Equal(suite.foo, foo, "should reconstruct original label map")
|
||||||
|
|
||||||
|
bar, err := NewLabelsFromString(suite.barText)
|
||||||
|
suite.NoError(err, "should succeed for valid label text")
|
||||||
|
suite.Equal(suite.barTextAsMap, bar, "should reconstruct original label map")
|
||||||
|
|
||||||
|
noHeritage, err := NewLabelsFromString(suite.noHeritageText)
|
||||||
|
suite.Equal(ErrInvalidHeritage, err, "should fail if no heritage is found")
|
||||||
|
suite.Nil(noHeritage, "should return nil")
|
||||||
|
|
||||||
|
wrongHeritage, err := NewLabelsFromString(suite.wrongHeritageText)
|
||||||
|
suite.Equal(ErrInvalidHeritage, err, "should fail if wrong heritage is found")
|
||||||
|
suite.Nil(wrongHeritage, "if error should return nil")
|
||||||
|
|
||||||
|
multipleHeritage, err := NewLabelsFromString(suite.multipleHeritageText)
|
||||||
|
suite.Equal(ErrInvalidHeritage, err, "should fail if multiple heritage is found")
|
||||||
|
suite.Nil(multipleHeritage, "if error should return nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLabels(t *testing.T) {
|
||||||
|
suite.Run(t, new(LabelsSuite))
|
||||||
|
}
|
@ -16,8 +16,11 @@ limitations under the License.
|
|||||||
|
|
||||||
package testutils
|
package testutils
|
||||||
|
|
||||||
import "github.com/kubernetes-incubator/external-dns/endpoint"
|
import (
|
||||||
import "sort"
|
"sort"
|
||||||
|
|
||||||
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
/** test utility functions for endpoints verifications */
|
/** test utility functions for endpoints verifications */
|
||||||
|
|
||||||
@ -45,7 +48,8 @@ func (b byAllFields) Less(i, j int) bool {
|
|||||||
// considers example.org. and example.org DNSName/Target as different endpoints
|
// considers example.org. and example.org DNSName/Target as different endpoints
|
||||||
func SameEndpoint(a, b *endpoint.Endpoint) bool {
|
func SameEndpoint(a, b *endpoint.Endpoint) bool {
|
||||||
return a.DNSName == b.DNSName && a.Target == b.Target && a.RecordType == b.RecordType &&
|
return a.DNSName == b.DNSName && a.Target == b.Target && a.RecordType == b.RecordType &&
|
||||||
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL
|
a.Labels[endpoint.OwnerLabelKey] == b.Labels[endpoint.OwnerLabelKey] && a.RecordTTL == b.RecordTTL &&
|
||||||
|
a.Labels[endpoint.ResourceLabelKey] == b.Labels[endpoint.ResourceLabelKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// SameEndpoints compares two slices of endpoints regardless of order
|
// SameEndpoints compares two slices of endpoints regardless of order
|
||||||
|
70
plan/conflict.go
Normal file
70
plan/conflict.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConflictResolver is used to make a decision in case of two or more different kubernetes resources
|
||||||
|
// are trying to acquire same DNS name
|
||||||
|
type ConflictResolver interface {
|
||||||
|
ResolveCreate(candidates []*endpoint.Endpoint) *endpoint.Endpoint
|
||||||
|
ResolveUpdate(current *endpoint.Endpoint, candidates []*endpoint.Endpoint) *endpoint.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
// PerResource allows only one resource to own a given dns name
|
||||||
|
type PerResource struct{}
|
||||||
|
|
||||||
|
// ResolveCreate is invoked when dns name is not owned by any resource
|
||||||
|
// ResolveCreate takes "minimal" (string comparison of Target) endpoint to acquire the DNS record
|
||||||
|
func (s PerResource) ResolveCreate(candidates []*endpoint.Endpoint) *endpoint.Endpoint {
|
||||||
|
var min *endpoint.Endpoint
|
||||||
|
for _, ep := range candidates {
|
||||||
|
if min == nil || s.less(ep, min) {
|
||||||
|
min = ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveUpdate is invoked when dns name is already owned by "current" endpoint
|
||||||
|
// ResolveUpdate uses "current" record as base and updates it accordingly with new version of same resource
|
||||||
|
// if it doesn't exist then pick min
|
||||||
|
func (s PerResource) ResolveUpdate(current *endpoint.Endpoint, candidates []*endpoint.Endpoint) *endpoint.Endpoint {
|
||||||
|
currentResource := current.Labels[endpoint.ResourceLabelKey] // resource which has already acquired the DNS
|
||||||
|
// TODO: sort candidates only needed because we can still have two endpoints from same resource here. We sort for consistency
|
||||||
|
// TODO: remove once single endpoint can have multiple targets
|
||||||
|
sort.SliceStable(candidates, func(i, j int) bool {
|
||||||
|
return s.less(candidates[i], candidates[j])
|
||||||
|
})
|
||||||
|
for _, ep := range candidates {
|
||||||
|
if ep.Labels[endpoint.ResourceLabelKey] == currentResource {
|
||||||
|
return ep
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.ResolveCreate(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// less returns true if endpoint x is less than y
|
||||||
|
func (s PerResource) less(x, y *endpoint.Endpoint) bool {
|
||||||
|
return x.Target < y.Target
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: with cross-resource/cross-cluster setup alternative variations of ConflictResolver can be used
|
137
plan/conflict_test.go
Normal file
137
plan/conflict_test.go
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2017 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package plan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ ConflictResolver = PerResource{}
|
||||||
|
|
||||||
|
type ResolverSuite struct {
|
||||||
|
// resolvers
|
||||||
|
perResource PerResource
|
||||||
|
// endpoints
|
||||||
|
fooV1Cname *endpoint.Endpoint
|
||||||
|
fooV2Cname *endpoint.Endpoint
|
||||||
|
fooV2CnameDuplicate *endpoint.Endpoint
|
||||||
|
fooA5 *endpoint.Endpoint
|
||||||
|
bar127A *endpoint.Endpoint
|
||||||
|
bar192A *endpoint.Endpoint
|
||||||
|
bar127AAnother *endpoint.Endpoint
|
||||||
|
legacyBar192A *endpoint.Endpoint // record created in AWS now without resource label
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ResolverSuite) SetupTest() {
|
||||||
|
suite.perResource = PerResource{}
|
||||||
|
// initialize endpoints used in tests
|
||||||
|
suite.fooV1Cname = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v1",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.fooV2Cname = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v2",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.fooV2CnameDuplicate = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v2",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v2-duplicate",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.fooA5 = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "5.5.5.5",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-5",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar127A = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "127.0.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-127",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar127AAnother = &endpoint.Endpoint{ //TODO: remove this once we move to multiple targets under same endpoint
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "8.8.8.8",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-127",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar192A = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "192.168.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-192",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.legacyBar192A = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "192.168.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ResolverSuite) TestStrictResolver() {
|
||||||
|
// test that perResource resolver picks min for create list
|
||||||
|
suite.Equal(suite.bar127A, suite.perResource.ResolveCreate([]*endpoint.Endpoint{suite.bar127A, suite.bar192A}), "should pick min one")
|
||||||
|
suite.Equal(suite.fooA5, suite.perResource.ResolveCreate([]*endpoint.Endpoint{suite.fooA5, suite.fooV1Cname}), "should pick min one")
|
||||||
|
suite.Equal(suite.fooV1Cname, suite.perResource.ResolveCreate([]*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname}), "should pick min one")
|
||||||
|
|
||||||
|
//test that perResource resolver preserves resource if it still exists
|
||||||
|
suite.Equal(suite.bar127A, suite.perResource.ResolveUpdate(suite.bar127A, []*endpoint.Endpoint{suite.bar127AAnother, suite.bar127A}), "should pick min for update when same resource endpoint occurs multiple times (remove after multiple-target support") // TODO:remove this test
|
||||||
|
suite.Equal(suite.bar127A, suite.perResource.ResolveUpdate(suite.bar127A, []*endpoint.Endpoint{suite.bar192A, suite.bar127A}), "should pick existing resource")
|
||||||
|
suite.Equal(suite.fooV2Cname, suite.perResource.ResolveUpdate(suite.fooV2Cname, []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV2CnameDuplicate}), "should pick existing resource even if targets are same")
|
||||||
|
suite.Equal(suite.fooA5, suite.perResource.ResolveUpdate(suite.fooV1Cname, []*endpoint.Endpoint{suite.fooA5, suite.fooV2Cname}), "should pick new if resource was deleted")
|
||||||
|
// should actually get the updated record (note ttl is different)
|
||||||
|
newFooV1Cname := &endpoint.Endpoint{
|
||||||
|
DNSName: suite.fooV1Cname.DNSName,
|
||||||
|
Target: suite.fooV1Cname.Target,
|
||||||
|
Labels: suite.fooV1Cname.Labels,
|
||||||
|
RecordType: suite.fooV1Cname.RecordType,
|
||||||
|
RecordTTL: suite.fooV1Cname.RecordTTL + 1, // ttl is different
|
||||||
|
}
|
||||||
|
suite.Equal(newFooV1Cname, suite.perResource.ResolveUpdate(suite.fooV1Cname, []*endpoint.Endpoint{suite.fooA5, suite.fooV2Cname, newFooV1Cname}), "should actually pick same resource with updates")
|
||||||
|
|
||||||
|
// legacy record's resource value will not match any candidates resource label
|
||||||
|
// therefore pick minimum again
|
||||||
|
suite.Equal(suite.bar127A, suite.perResource.ResolveUpdate(suite.legacyBar192A, []*endpoint.Endpoint{suite.bar127A, suite.bar192A}), " legacy record's resource value will not match, should pick minimum")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConflictResolver(t *testing.T) {
|
||||||
|
suite.Run(t, new(ResolverSuite))
|
||||||
|
}
|
149
plan/plan.go
149
plan/plan.go
@ -18,7 +18,6 @@ package plan
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Plan can convert a list of desired and current records to a series of create,
|
// Plan can convert a list of desired and current records to a series of create,
|
||||||
@ -47,55 +46,102 @@ type Changes struct {
|
|||||||
Delete []*endpoint.Endpoint
|
Delete []*endpoint.Endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// planTable is a supplementary struct for Plan
|
||||||
|
// each row correspond to a dnsName -> (current record + all desired records)
|
||||||
|
/*
|
||||||
|
planTable: (-> = target)
|
||||||
|
--------------------------------------------------------
|
||||||
|
DNSName | Current record | Desired Records |
|
||||||
|
--------------------------------------------------------
|
||||||
|
foo.com | -> 1.1.1.1 | [->1.1.1.1, ->elb.com] | = no action
|
||||||
|
--------------------------------------------------------
|
||||||
|
bar.com | | [->191.1.1.1, ->190.1.1.1] | = create (bar.com -> 190.1.1.1)
|
||||||
|
--------------------------------------------------------
|
||||||
|
"=", i.e. result of calculation relies on supplied ConflictResolver
|
||||||
|
*/
|
||||||
|
type planTable struct {
|
||||||
|
rows map[string]*planTableRow
|
||||||
|
resolver ConflictResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlanTable() planTable { //TODO: make resolver configurable
|
||||||
|
return planTable{map[string]*planTableRow{}, PerResource{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// planTableRow
|
||||||
|
// current corresponds to the record currently occupying dns name on the dns provider
|
||||||
|
// candidates corresponds to the list of records which would like to have this dnsName
|
||||||
|
type planTableRow struct {
|
||||||
|
current *endpoint.Endpoint
|
||||||
|
candidates []*endpoint.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t planTable) addCurrent(e *endpoint.Endpoint) {
|
||||||
|
if _, ok := t.rows[e.DNSName]; !ok {
|
||||||
|
t.rows[e.DNSName] = &planTableRow{}
|
||||||
|
}
|
||||||
|
t.rows[e.DNSName].current = e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t planTable) addCandidate(e *endpoint.Endpoint) {
|
||||||
|
if _, ok := t.rows[e.DNSName]; !ok {
|
||||||
|
t.rows[e.DNSName] = &planTableRow{}
|
||||||
|
}
|
||||||
|
t.rows[e.DNSName].candidates = append(t.rows[e.DNSName].candidates, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: allows record type change, which might not be supported by all dns providers
|
||||||
|
func (t planTable) getUpdates() (updateNew []*endpoint.Endpoint, updateOld []*endpoint.Endpoint) {
|
||||||
|
for _, row := range t.rows {
|
||||||
|
if row.current != nil && len(row.candidates) > 0 { //dns name is taken
|
||||||
|
update := t.resolver.ResolveUpdate(row.current, row.candidates)
|
||||||
|
// compare "update" to "current" to figure out if actual update is required
|
||||||
|
if shouldUpdateTTL(update, row.current) || targetChanged(update, row.current) {
|
||||||
|
inheritOwner(row.current, update)
|
||||||
|
updateNew = append(updateNew, update)
|
||||||
|
updateOld = append(updateOld, row.current)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t planTable) getCreates() (createList []*endpoint.Endpoint) {
|
||||||
|
for _, row := range t.rows {
|
||||||
|
if row.current == nil { //dns name not taken
|
||||||
|
createList = append(createList, t.resolver.ResolveCreate(row.candidates))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t planTable) getDeletes() (deleteList []*endpoint.Endpoint) {
|
||||||
|
for _, row := range t.rows {
|
||||||
|
if row.current != nil && len(row.candidates) == 0 {
|
||||||
|
deleteList = append(deleteList, row.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate computes the actions needed to move current state towards desired
|
// Calculate computes the actions needed to move current state towards desired
|
||||||
// state. It then passes those changes to the current policy for further
|
// state. It then passes those changes to the current policy for further
|
||||||
// processing. It returns a copy of Plan with the changes populated.
|
// processing. It returns a copy of Plan with the changes populated.
|
||||||
func (p *Plan) Calculate() *Plan {
|
func (p *Plan) Calculate() *Plan {
|
||||||
changes := &Changes{}
|
t := newPlanTable()
|
||||||
|
|
||||||
// Ensure all desired records exist. For each desired record make sure it's
|
|
||||||
// either created or updated.
|
|
||||||
for _, desired := range p.Desired {
|
|
||||||
// Get the matching current record if it exists.
|
|
||||||
current, exists := recordExists(desired, p.Current)
|
|
||||||
|
|
||||||
// If there's no current record create desired record.
|
|
||||||
if !exists {
|
|
||||||
changes.Create = append(changes.Create, desired)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
targetChanged := targetChanged(desired, current)
|
|
||||||
shouldUpdateTTL := shouldUpdateTTL(desired, current)
|
|
||||||
|
|
||||||
if !targetChanged && !shouldUpdateTTL {
|
|
||||||
log.Debugf("Skipping endpoint %v because nothing has changed", desired)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.UpdateOld = append(changes.UpdateOld, current)
|
|
||||||
desired.MergeLabels(current.Labels) // inherit the labels from the dns provider, including Owner ID
|
|
||||||
|
|
||||||
if targetChanged {
|
|
||||||
desired.RecordType = current.RecordType // inherit the type from the dns provider
|
|
||||||
}
|
|
||||||
|
|
||||||
if !shouldUpdateTTL {
|
|
||||||
desired.RecordTTL = current.RecordTTL
|
|
||||||
}
|
|
||||||
|
|
||||||
changes.UpdateNew = append(changes.UpdateNew, desired)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure all undesired records are removed. Each current record that cannot
|
|
||||||
// be found in the list of desired records is removed.
|
|
||||||
for _, current := range p.Current {
|
for _, current := range p.Current {
|
||||||
if _, exists := recordExists(current, p.Desired); !exists {
|
t.addCurrent(current)
|
||||||
changes.Delete = append(changes.Delete, current)
|
|
||||||
}
|
}
|
||||||
|
for _, desired := range p.Desired {
|
||||||
|
t.addCandidate(desired)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply policies to list of changes.
|
changes := &Changes{}
|
||||||
|
changes.Create = t.getCreates()
|
||||||
|
changes.Delete = t.getDeletes()
|
||||||
|
changes.UpdateNew, changes.UpdateOld = t.getUpdates()
|
||||||
for _, pol := range p.Policies {
|
for _, pol := range p.Policies {
|
||||||
changes = pol.Apply(changes)
|
changes = pol.Apply(changes)
|
||||||
}
|
}
|
||||||
@ -109,6 +155,16 @@ func (p *Plan) Calculate() *Plan {
|
|||||||
return plan
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inheritOwner(from, to *endpoint.Endpoint) {
|
||||||
|
if to.Labels == nil {
|
||||||
|
to.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
if from.Labels == nil {
|
||||||
|
from.Labels = map[string]string{}
|
||||||
|
}
|
||||||
|
to.Labels[endpoint.OwnerLabelKey] = from.Labels[endpoint.OwnerLabelKey]
|
||||||
|
}
|
||||||
|
|
||||||
func targetChanged(desired, current *endpoint.Endpoint) bool {
|
func targetChanged(desired, current *endpoint.Endpoint) bool {
|
||||||
return desired.Target != current.Target
|
return desired.Target != current.Target
|
||||||
}
|
}
|
||||||
@ -119,14 +175,3 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool {
|
|||||||
}
|
}
|
||||||
return desired.RecordTTL != current.RecordTTL
|
return desired.RecordTTL != current.RecordTTL
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordExists checks whether a record can be found in a list of records.
|
|
||||||
func recordExists(needle *endpoint.Endpoint, haystack []*endpoint.Endpoint) (*endpoint.Endpoint, bool) {
|
|
||||||
for _, record := range haystack {
|
|
||||||
if record.DNSName == needle.DNSName {
|
|
||||||
return record, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
@ -17,173 +17,343 @@ limitations under the License.
|
|||||||
package plan
|
package plan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
"github.com/kubernetes-incubator/external-dns/internal/testutils"
|
"github.com/kubernetes-incubator/external-dns/internal/testutils"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestCalculate tests that a plan can calculate actions to move a list of
|
type PlanTestSuite struct {
|
||||||
// current records to a list of desired records.
|
suite.Suite
|
||||||
func TestCalculate(t *testing.T) {
|
fooV1Cname *endpoint.Endpoint
|
||||||
// empty list of records
|
fooV2Cname *endpoint.Endpoint
|
||||||
empty := []*endpoint.Endpoint{}
|
fooV2CnameNoLabel *endpoint.Endpoint
|
||||||
// a simple entry
|
fooV3CnameSameResource *endpoint.Endpoint
|
||||||
fooV1 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", endpoint.RecordTypeCNAME)}
|
fooA5 *endpoint.Endpoint
|
||||||
// the same entry but with different target
|
bar127A *endpoint.Endpoint
|
||||||
fooV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeCNAME)}
|
bar127AWithTTL *endpoint.Endpoint
|
||||||
// another simple entry
|
bar192A *endpoint.Endpoint
|
||||||
bar := []*endpoint.Endpoint{endpoint.NewEndpoint("bar", "v1", endpoint.RecordTypeCNAME)}
|
|
||||||
|
|
||||||
// test case with labels
|
|
||||||
noLabels := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeCNAME)}
|
|
||||||
labeledV2 := []*endpoint.Endpoint{newEndpointWithOwner("foo", "v2", "123")}
|
|
||||||
labeledV1 := []*endpoint.Endpoint{newEndpointWithOwner("foo", "v1", "123")}
|
|
||||||
|
|
||||||
// test case with type inheritance
|
|
||||||
noType := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", "")}
|
|
||||||
typedV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeA)}
|
|
||||||
typedV1 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", endpoint.RecordTypeA)}
|
|
||||||
|
|
||||||
// test case with TTL
|
|
||||||
ttl := endpoint.TTL(300)
|
|
||||||
ttl2 := endpoint.TTL(50)
|
|
||||||
ttlV1 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl)}
|
|
||||||
ttlV2 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v1", endpoint.RecordTypeCNAME)}
|
|
||||||
ttlV3 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl)}
|
|
||||||
ttlV4 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v1", endpoint.RecordTypeCNAME, ttl2)}
|
|
||||||
ttlV5 := []*endpoint.Endpoint{endpoint.NewEndpoint("foo", "v2", endpoint.RecordTypeCNAME)}
|
|
||||||
ttlV6 := []*endpoint.Endpoint{endpoint.NewEndpointWithTTL("foo", "v2", endpoint.RecordTypeCNAME, ttl)}
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
policies []Policy
|
|
||||||
current, desired []*endpoint.Endpoint
|
|
||||||
create, updateOld, updateNew, delete []*endpoint.Endpoint
|
|
||||||
}{
|
|
||||||
// Nothing exists and nothing desired doesn't change anything.
|
|
||||||
{[]Policy{&SyncPolicy{}}, empty, empty, empty, empty, empty, empty},
|
|
||||||
// More desired than current creates the desired.
|
|
||||||
{[]Policy{&SyncPolicy{}}, empty, fooV1, fooV1, empty, empty, empty},
|
|
||||||
// Desired equals current doesn't change anything.
|
|
||||||
{[]Policy{&SyncPolicy{}}, fooV1, fooV1, empty, empty, empty, empty},
|
|
||||||
// Nothing is desired deletes the current.
|
|
||||||
{[]Policy{&SyncPolicy{}}, fooV1, empty, empty, empty, empty, fooV1},
|
|
||||||
// Current and desired match but Target is different triggers an update.
|
|
||||||
{[]Policy{&SyncPolicy{}}, fooV1, fooV2, empty, fooV1, fooV2, empty},
|
|
||||||
// Both exist but are different creates desired and deletes current.
|
|
||||||
{[]Policy{&SyncPolicy{}}, fooV1, bar, bar, empty, empty, fooV1},
|
|
||||||
// Nothing is desired but policy doesn't allow deletions.
|
|
||||||
{[]Policy{&UpsertOnlyPolicy{}}, fooV1, empty, empty, empty, empty, empty},
|
|
||||||
// Labels should be inherited
|
|
||||||
{[]Policy{&SyncPolicy{}}, labeledV1, noLabels, empty, labeledV1, labeledV2, empty},
|
|
||||||
// RecordType should be inherited
|
|
||||||
{[]Policy{&SyncPolicy{}}, typedV1, noType, empty, typedV1, typedV2, empty},
|
|
||||||
// If desired TTL is not configured, do not update
|
|
||||||
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV2, empty, empty, empty, empty},
|
|
||||||
// If desired TTL is configured but is the same as current TTL, do not update
|
|
||||||
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV3, empty, empty, empty, empty},
|
|
||||||
// If desired TTL is configured and is not the same as current TTL, need to update
|
|
||||||
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV4, empty, ttlV1, ttlV4, empty},
|
|
||||||
// If target changed and desired TTL is not configured, do not update TTL
|
|
||||||
{[]Policy{&SyncPolicy{}}, ttlV1, ttlV5, empty, ttlV1, ttlV6, empty},
|
|
||||||
} {
|
|
||||||
// setup plan
|
|
||||||
plan := &Plan{
|
|
||||||
Policies: tc.policies,
|
|
||||||
Current: tc.current,
|
|
||||||
Desired: tc.desired,
|
|
||||||
}
|
}
|
||||||
// calculate actions
|
|
||||||
plan = plan.Calculate()
|
|
||||||
|
|
||||||
// validate actions
|
func (suite *PlanTestSuite) SetupTest() {
|
||||||
validateEntries(t, plan.Changes.Create, tc.create)
|
suite.fooV1Cname = &endpoint.Endpoint{
|
||||||
validateEntries(t, plan.Changes.UpdateOld, tc.updateOld)
|
DNSName: "foo",
|
||||||
validateEntries(t, plan.Changes.UpdateNew, tc.updateNew)
|
Target: "v1",
|
||||||
validateEntries(t, plan.Changes.Delete, tc.delete)
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
|
||||||
|
endpoint.OwnerLabelKey: "pwner",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// same resource as fooV1Cname, but target is different. It will never be picked because its target lexicographically bigger than "v1"
|
||||||
|
suite.fooV3CnameSameResource = &endpoint.Endpoint{ // TODO: remove this once endpoint can support multiple targets
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v3",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v1",
|
||||||
|
endpoint.OwnerLabelKey: "pwner",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.fooV2Cname = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v2",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-v2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.fooV2CnameNoLabel = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "v2",
|
||||||
|
RecordType: "CNAME",
|
||||||
|
}
|
||||||
|
suite.fooA5 = &endpoint.Endpoint{
|
||||||
|
DNSName: "foo",
|
||||||
|
Target: "5.5.5.5",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/foo-5",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar127A = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "127.0.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-127",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar127AWithTTL = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "127.0.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
RecordTTL: 300,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-127",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
suite.bar192A = &endpoint.Endpoint{
|
||||||
|
DNSName: "bar",
|
||||||
|
Target: "192.168.0.1",
|
||||||
|
RecordType: "A",
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/bar-192",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// BenchmarkCalculate benchmarks the Calculate method.
|
func (suite *PlanTestSuite) TestSyncFirstRound() {
|
||||||
func BenchmarkCalculate(b *testing.B) {
|
current := []*endpoint.Endpoint{}
|
||||||
foo := endpoint.NewEndpoint("foo", "v1", "")
|
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname, suite.bar127A}
|
||||||
barV1 := endpoint.NewEndpoint("bar", "v1", "")
|
expectedCreate := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar127A} //v1 is chosen because of resolver taking "min"
|
||||||
barV2 := endpoint.NewEndpoint("bar", "v2", "")
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
baz := endpoint.NewEndpoint("baz", "v1", "")
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
plan := &Plan{
|
p := &Plan{
|
||||||
Current: []*endpoint.Endpoint{foo, barV1},
|
|
||||||
Desired: []*endpoint.Endpoint{barV2, baz},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
plan.Calculate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExamplePlan shows how plan can be used.
|
|
||||||
func ExamplePlan() {
|
|
||||||
foo := endpoint.NewEndpoint("foo.example.com", "1.2.3.4", "")
|
|
||||||
barV1 := endpoint.NewEndpoint("bar.example.com", "8.8.8.8", "")
|
|
||||||
barV2 := endpoint.NewEndpoint("bar.example.com", "8.8.4.4", "")
|
|
||||||
baz := endpoint.NewEndpoint("baz.example.com", "6.6.6.6", "")
|
|
||||||
|
|
||||||
// Plan where
|
|
||||||
// * foo should be deleted
|
|
||||||
// * bar should be updated from v1 to v2
|
|
||||||
// * baz should be created
|
|
||||||
plan := &Plan{
|
|
||||||
Policies: []Policy{&SyncPolicy{}},
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
Current: []*endpoint.Endpoint{foo, barV1},
|
Current: current,
|
||||||
Desired: []*endpoint.Endpoint{barV2, baz},
|
Desired: desired,
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate actions
|
changes := p.Calculate().Changes
|
||||||
plan = plan.Calculate()
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
// print actions
|
func (suite *PlanTestSuite) TestSyncSecondRound() {
|
||||||
fmt.Println("Create:")
|
current := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
for _, ep := range plan.Changes.Create {
|
desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A}
|
||||||
fmt.Println(ep)
|
expectedCreate := []*endpoint.Endpoint{suite.bar127A}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
}
|
}
|
||||||
fmt.Println("UpdateOld:")
|
|
||||||
for _, ep := range plan.Changes.UpdateOld {
|
changes := p.Calculate().Changes
|
||||||
fmt.Println(ep)
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
}
|
}
|
||||||
fmt.Println("UpdateNew:")
|
|
||||||
for _, ep := range plan.Changes.UpdateNew {
|
func (suite *PlanTestSuite) TestSyncSecondRoundMigration() {
|
||||||
fmt.Println(ep)
|
current := []*endpoint.Endpoint{suite.fooV2CnameNoLabel}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooV1Cname, suite.bar127A}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{suite.bar127A}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{suite.fooV2CnameNoLabel}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
}
|
}
|
||||||
fmt.Println("Delete:")
|
|
||||||
for _, ep := range plan.Changes.Delete {
|
changes := p.Calculate().Changes
|
||||||
fmt.Println(ep)
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
}
|
}
|
||||||
// Create:
|
|
||||||
// &{baz.example.com 6.6.6.6 map[] }
|
func (suite *PlanTestSuite) TestSyncSecondRoundWithTTLChange() {
|
||||||
// UpdateOld:
|
current := []*endpoint.Endpoint{suite.bar127A}
|
||||||
// &{bar.example.com 8.8.8.8 map[] }
|
desired := []*endpoint.Endpoint{suite.bar127AWithTTL}
|
||||||
// UpdateNew:
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
// &{bar.example.com 8.8.4.4 map[] }
|
expectedUpdateOld := []*endpoint.Endpoint{suite.bar127A}
|
||||||
// Delete:
|
expectedUpdateNew := []*endpoint.Endpoint{suite.bar127AWithTTL}
|
||||||
// &{foo.example.com 1.2.3.4 map[] }
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlanTestSuite) TestSyncSecondRoundWithOwnerInherited() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV2Cname}
|
||||||
|
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{{
|
||||||
|
DNSName: suite.fooV2Cname.DNSName,
|
||||||
|
Target: suite.fooV2Cname.Target,
|
||||||
|
RecordType: suite.fooV2Cname.RecordType,
|
||||||
|
RecordTTL: suite.fooV2Cname.RecordTTL,
|
||||||
|
Labels: map[string]string{
|
||||||
|
endpoint.ResourceLabelKey: suite.fooV2Cname.Labels[endpoint.ResourceLabelKey],
|
||||||
|
endpoint.OwnerLabelKey: suite.fooV1Cname.Labels[endpoint.OwnerLabelKey],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlanTestSuite) TestIdempotency() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV2Cname}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlanTestSuite) TestDifferentTypes() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV2Cname, suite.fooA5}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{suite.fooA5}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlanTestSuite) TestRemoveEndpoint() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *PlanTestSuite) TestRemoveEndpointWithUpsert() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&UpsertOnlyPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: remove once multiple-target per endpoint is supported
|
||||||
|
func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceReplace() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV3CnameSameResource, suite.bar192A}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{suite.fooV3CnameSameResource}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{suite.fooV1Cname}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: remove once multiple-target per endpoint is supported
|
||||||
|
func (suite *PlanTestSuite) TestDuplicatedEndpointsForSameResourceRetain() {
|
||||||
|
current := []*endpoint.Endpoint{suite.fooV1Cname, suite.bar192A}
|
||||||
|
desired := []*endpoint.Endpoint{suite.fooV1Cname, suite.fooV3CnameSameResource}
|
||||||
|
expectedCreate := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateOld := []*endpoint.Endpoint{}
|
||||||
|
expectedUpdateNew := []*endpoint.Endpoint{}
|
||||||
|
expectedDelete := []*endpoint.Endpoint{suite.bar192A}
|
||||||
|
|
||||||
|
p := &Plan{
|
||||||
|
Policies: []Policy{&SyncPolicy{}},
|
||||||
|
Current: current,
|
||||||
|
Desired: desired,
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := p.Calculate().Changes
|
||||||
|
validateEntries(suite.T(), changes.Create, expectedCreate)
|
||||||
|
validateEntries(suite.T(), changes.UpdateNew, expectedUpdateNew)
|
||||||
|
validateEntries(suite.T(), changes.UpdateOld, expectedUpdateOld)
|
||||||
|
validateEntries(suite.T(), changes.Delete, expectedDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlan(t *testing.T) {
|
||||||
|
suite.Run(t, new(PlanTestSuite))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateEntries validates that the list of entries matches expected.
|
// validateEntries validates that the list of entries matches expected.
|
||||||
func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
|
func validateEntries(t *testing.T, entries, expected []*endpoint.Endpoint) {
|
||||||
if len(entries) != len(expected) {
|
if !testutils.SameEndpoints(entries, expected) {
|
||||||
t.Fatalf("expected %q to match %q", entries, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range entries {
|
|
||||||
if !testutils.SameEndpoint(entries[i], expected[i]) {
|
|
||||||
t.Fatalf("expected %q to match %q", entries, expected)
|
t.Fatalf("expected %q to match %q", entries, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func newEndpointWithOwner(dnsName, target, ownerID string) *endpoint.Endpoint {
|
|
||||||
e := endpoint.NewEndpoint(dnsName, target, endpoint.RecordTypeCNAME)
|
|
||||||
e.Labels[endpoint.OwnerLabelKey] = ownerID
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
@ -23,7 +23,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Registry is an interface which should enables ownership concept in external-dns
|
// Registry is an interface which should enables ownership concept in external-dns
|
||||||
// Records() returns ALL records registered with DNS provider (TODO: for multi-zone support return all records)
|
// Records() returns ALL records registered with DNS provider
|
||||||
// each entry includes owner information
|
// each entry includes owner information
|
||||||
// ApplyChanges(changes *plan.Changes) propagates the changes to the DNS Provider API and correspondingly updates ownership depending on type of registry being used
|
// ApplyChanges(changes *plan.Changes) propagates the changes to the DNS Provider API and correspondingly updates ownership depending on type of registry being used
|
||||||
type Registry interface {
|
type Registry interface {
|
||||||
|
@ -19,8 +19,6 @@ package registry
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kubernetes-incubator/external-dns/endpoint"
|
"github.com/kubernetes-incubator/external-dns/endpoint"
|
||||||
@ -28,14 +26,6 @@ import (
|
|||||||
"github.com/kubernetes-incubator/external-dns/provider"
|
"github.com/kubernetes-incubator/external-dns/provider"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
txtLabelFormat = "\"heritage=external-dns,external-dns/owner=%s\""
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
txtLabelRegex = regexp.MustCompile("^\"heritage=external-dns,external-dns/owner=(.+)\"")
|
|
||||||
)
|
|
||||||
|
|
||||||
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
|
// TXTRegistry implements registry interface with ownership implemented via associated TXT records
|
||||||
type TXTRegistry struct {
|
type TXTRegistry struct {
|
||||||
provider provider.Provider
|
provider provider.Provider
|
||||||
@ -67,31 +57,40 @@ func (im *TXTRegistry) Records() ([]*endpoint.Endpoint, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoints := make([]*endpoint.Endpoint, 0)
|
endpoints := []*endpoint.Endpoint{}
|
||||||
|
|
||||||
ownerMap := map[string]string{}
|
labelMap := map[string]endpoint.Labels{}
|
||||||
|
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
if record.RecordType != endpoint.RecordTypeTXT {
|
if record.RecordType != endpoint.RecordTypeTXT {
|
||||||
endpoints = append(endpoints, record)
|
endpoints = append(endpoints, record)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ownerID := im.extractOwnerID(record.Target)
|
labels, err := endpoint.NewLabelsFromString(record.Target)
|
||||||
if ownerID == "" {
|
if err == endpoint.ErrInvalidHeritage {
|
||||||
|
//if no heritage is found or it is invalid
|
||||||
//case when value of txt record cannot be identified
|
//case when value of txt record cannot be identified
|
||||||
//record will not be removed as it will have empty owner
|
//record will not be removed as it will have empty owner
|
||||||
endpoints = append(endpoints, record)
|
endpoints = append(endpoints, record)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
endpointDNSName := im.mapper.toEndpointName(record.DNSName)
|
endpointDNSName := im.mapper.toEndpointName(record.DNSName)
|
||||||
ownerMap[endpointDNSName] = ownerID
|
labelMap[endpointDNSName] = labels
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ep := range endpoints {
|
for _, ep := range endpoints {
|
||||||
ep.Labels[endpoint.OwnerLabelKey] = ownerMap[ep.DNSName]
|
if labels, ok := labelMap[ep.DNSName]; ok {
|
||||||
|
ep.Labels = labels
|
||||||
|
} else {
|
||||||
|
//this indicates that owner could not be identified, as there is no corresponding TXT record
|
||||||
|
ep.Labels = endpoint.NewLabels()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpoints, err
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyChanges updates dns provider with the changes
|
// ApplyChanges updates dns provider with the changes
|
||||||
@ -103,18 +102,33 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
|
|||||||
UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld),
|
UpdateOld: filterOwnedRecords(im.ownerID, changes.UpdateOld),
|
||||||
Delete: filterOwnedRecords(im.ownerID, changes.Delete),
|
Delete: filterOwnedRecords(im.ownerID, changes.Delete),
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range filteredChanges.Create {
|
for _, r := range filteredChanges.Create {
|
||||||
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), im.getTXTLabel(), endpoint.RecordTypeTXT)
|
r.Labels[endpoint.OwnerLabelKey] = im.ownerID
|
||||||
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
|
||||||
filteredChanges.Create = append(filteredChanges.Create, txt)
|
filteredChanges.Create = append(filteredChanges.Create, txt)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range filteredChanges.Delete {
|
for _, r := range filteredChanges.Delete {
|
||||||
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), im.getTXTLabel(), endpoint.RecordTypeTXT)
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
|
||||||
|
|
||||||
|
// when we delete TXT records for which value has changed (due to new label) this would still work because
|
||||||
|
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
|
||||||
filteredChanges.Delete = append(filteredChanges.Delete, txt)
|
filteredChanges.Delete = append(filteredChanges.Delete, txt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure TXT records are consistently updated as well
|
||||||
|
for _, r := range filteredChanges.UpdateNew {
|
||||||
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
|
||||||
|
filteredChanges.UpdateNew = append(filteredChanges.UpdateNew, txt)
|
||||||
|
}
|
||||||
|
// make sure TXT records are consistently updated as well
|
||||||
|
for _, r := range filteredChanges.UpdateOld {
|
||||||
|
txt := endpoint.NewEndpoint(im.mapper.toTXTName(r.DNSName), r.Labels.Serialize(true), endpoint.RecordTypeTXT)
|
||||||
|
// when we updateOld TXT records for which value has changed (due to new label) this would still work because
|
||||||
|
// !!! TXT record value is uniquely generated from the Labels of the endpoint. Hence old TXT record can be uniquely reconstructed
|
||||||
|
filteredChanges.UpdateOld = append(filteredChanges.UpdateOld, txt)
|
||||||
|
}
|
||||||
|
|
||||||
return im.provider.ApplyChanges(filteredChanges)
|
return im.provider.ApplyChanges(filteredChanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,17 +136,6 @@ func (im *TXTRegistry) ApplyChanges(changes *plan.Changes) error {
|
|||||||
TXT registry specific private methods
|
TXT registry specific private methods
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func (im *TXTRegistry) getTXTLabel() string {
|
|
||||||
return fmt.Sprintf(txtLabelFormat, im.ownerID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (im *TXTRegistry) extractOwnerID(txtLabel string) string {
|
|
||||||
if matches := txtLabelRegex.FindStringSubmatch(txtLabel); len(matches) == 2 {
|
|
||||||
return matches[1]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
nameMapper defines interface which maps the dns name defined for the source
|
nameMapper defines interface which maps the dns name defined for the source
|
||||||
to the dns name which TXT record will be created with
|
to the dns name which TXT record will be created with
|
||||||
|
@ -132,6 +132,7 @@ func testTXTRegistryRecordsPrefixed(t *testing.T) {
|
|||||||
|
|
||||||
r, _ := NewTXTRegistry(p, "txt.", "owner")
|
r, _ := NewTXTRegistry(p, "txt.", "owner")
|
||||||
records, _ := r.Records()
|
records, _ := r.Records()
|
||||||
|
|
||||||
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
assert.True(t, testutils.SameEndpoints(records, expectedRecords))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,7 +143,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
|
|||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
|
newEndpointWithOwner("foo.test-zone.example.org", "foo.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
|
||||||
newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""),
|
newEndpointWithOwner("bar.test-zone.example.org", "my-domain.com", endpoint.RecordTypeCNAME, ""),
|
||||||
newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt.bar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
|
||||||
newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""),
|
newEndpointWithOwner("txt.bar.test-zone.example.org", "baz.test-zone.example.org", endpoint.RecordTypeCNAME, ""),
|
||||||
newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("qux.test-zone.example.org", "random", endpoint.RecordTypeTXT, ""),
|
||||||
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
|
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, ""),
|
||||||
@ -174,6 +175,7 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) {
|
|||||||
RecordType: endpoint.RecordTypeCNAME,
|
RecordType: endpoint.RecordTypeCNAME,
|
||||||
Labels: map[string]string{
|
Labels: map[string]string{
|
||||||
endpoint.OwnerLabelKey: "owner",
|
endpoint.OwnerLabelKey: "owner",
|
||||||
|
endpoint.ResourceLabelKey: "ingress/default/my-ingress",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -233,13 +235,13 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
|
|||||||
|
|
||||||
changes := &plan.Changes{
|
changes := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""),
|
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "", "ingress/default/my-ingress"),
|
||||||
},
|
},
|
||||||
Delete: []*endpoint.Endpoint{
|
Delete: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
},
|
},
|
||||||
UpdateNew: []*endpoint.Endpoint{
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
|
||||||
},
|
},
|
||||||
UpdateOld: []*endpoint.Endpoint{
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
@ -247,18 +249,20 @@ func testTXTRegistryApplyChangesWithPrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expected := &plan.Changes{
|
expected := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""),
|
newEndpointWithOwnerResource("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", "owner", "ingress/default/my-ingress"),
|
||||||
newEndpointWithOwner("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt.new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
Delete: []*endpoint.Endpoint{
|
Delete: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("txt.foobar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
UpdateNew: []*endpoint.Endpoint{
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwnerResource("tar.test-zone.example.org", "new-tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner", "ingress/default/my-ingress-2"),
|
||||||
|
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner,external-dns/resource=ingress/default/my-ingress-2\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
UpdateOld: []*endpoint.Endpoint{
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwner("tar.test-zone.example.org", "tar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
|
newEndpointWithOwner("txt.tar.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
p.OnApplyChanges = func(got *plan.Changes) {
|
p.OnApplyChanges = func(got *plan.Changes) {
|
||||||
@ -300,7 +304,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
|
|||||||
|
|
||||||
changes := &plan.Changes{
|
changes := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""),
|
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, ""),
|
||||||
},
|
},
|
||||||
Delete: []*endpoint.Endpoint{
|
Delete: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
newEndpointWithOwner("foobar.test-zone.example.org", "foobar.loadbalancer.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
@ -314,7 +318,7 @@ func testTXTRegistryApplyChangesNoPrefix(t *testing.T) {
|
|||||||
}
|
}
|
||||||
expected := &plan.Changes{
|
expected := &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", "", ""),
|
newEndpointWithOwner("new-record-1.test-zone.example.org", "new-loadbalancer-1.lb.com", endpoint.RecordTypeCNAME, "owner"),
|
||||||
newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
newEndpointWithOwner("new-record-1.test-zone.example.org", "\"heritage=external-dns,external-dns/owner=owner\"", endpoint.RecordTypeTXT, ""),
|
||||||
},
|
},
|
||||||
Delete: []*endpoint.Endpoint{
|
Delete: []*endpoint.Endpoint{
|
||||||
@ -354,3 +358,10 @@ func newEndpointWithOwner(dnsName, target, recordType, ownerID string) *endpoint
|
|||||||
e.Labels[endpoint.OwnerLabelKey] = ownerID
|
e.Labels[endpoint.OwnerLabelKey] = ownerID
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newEndpointWithOwnerResource(dnsName, target, recordType, ownerID, resource string) *endpoint.Endpoint {
|
||||||
|
e := endpoint.NewEndpoint(dnsName, target, recordType)
|
||||||
|
e.Labels[endpoint.OwnerLabelKey] = ownerID
|
||||||
|
e.Labels[endpoint.ResourceLabelKey] = resource
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
@ -105,6 +105,7 @@ func (sc *ingressSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Endpoints generated from ingress: %s/%s: %v", ing.Namespace, ing.Name, ingEndpoints)
|
log.Debugf("Endpoints generated from ingress: %s/%s: %v", ing.Namespace, ing.Name, ingEndpoints)
|
||||||
|
sc.setResourceLabel(ing, ingEndpoints)
|
||||||
endpoints = append(endpoints, ingEndpoints...)
|
endpoints = append(endpoints, ingEndpoints...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +198,12 @@ func (sc *ingressSource) filterByAnnotations(ingresses []v1beta1.Ingress) ([]v1b
|
|||||||
return filteredList, nil
|
return filteredList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *ingressSource) setResourceLabel(ingress v1beta1.Ingress, endpoints []*endpoint.Endpoint) {
|
||||||
|
for _, ep := range endpoints {
|
||||||
|
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("ingress/%s/%s", ingress.Namespace, ingress.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// endpointsFromIngress extracts the endpoints from ingress object
|
// endpointsFromIngress extracts the endpoints from ingress object
|
||||||
func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
|
func endpointsFromIngress(ing *v1beta1.Ingress) []*endpoint.Endpoint {
|
||||||
var endpoints []*endpoint.Endpoint
|
var endpoints []*endpoint.Endpoint
|
||||||
|
@ -28,12 +28,50 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validates that ingressSource is a Source
|
// Validates that ingressSource is a Source
|
||||||
var _ Source = &ingressSource{}
|
var _ Source = &ingressSource{}
|
||||||
|
|
||||||
|
type IngressSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
sc Source
|
||||||
|
fooWithTargets *v1beta1.Ingress
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *IngressSuite) SetupTest() {
|
||||||
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
suite.sc, err = NewIngressSource(
|
||||||
|
fakeClient,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"{{.Name}}",
|
||||||
|
)
|
||||||
|
suite.NoError(err, "should initialize ingress source")
|
||||||
|
|
||||||
|
suite.fooWithTargets = (fakeIngress{
|
||||||
|
name: "foo-with-targets",
|
||||||
|
namespace: "default",
|
||||||
|
dnsnames: []string{"foo"},
|
||||||
|
ips: []string{"8.8.8.8"},
|
||||||
|
hostnames: []string{"v1"},
|
||||||
|
}).Ingress()
|
||||||
|
_, err = fakeClient.Extensions().Ingresses(suite.fooWithTargets.Namespace).Create(suite.fooWithTargets)
|
||||||
|
suite.NoError(err, "should succeed")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *IngressSuite) TestResourceLabelIsSet() {
|
||||||
|
endpoints, _ := suite.sc.Endpoints()
|
||||||
|
for _, ep := range endpoints {
|
||||||
|
suite.Equal("ingress/default/foo-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestIngress(t *testing.T) {
|
func TestIngress(t *testing.T) {
|
||||||
|
suite.Run(t, new(IngressSuite))
|
||||||
t.Run("endpointsFromIngress", testEndpointsFromIngress)
|
t.Run("endpointsFromIngress", testEndpointsFromIngress)
|
||||||
t.Run("Endpoints", testIngressEndpoints)
|
t.Run("Endpoints", testIngressEndpoints)
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,7 @@ func (sc *serviceSource) Endpoints() ([]*endpoint.Endpoint, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Endpoints generated from service: %s/%s: %v", svc.Namespace, svc.Name, svcEndpoints)
|
log.Debugf("Endpoints generated from service: %s/%s: %v", svc.Namespace, svc.Name, svcEndpoints)
|
||||||
|
sc.setResourceLabel(svc, svcEndpoints)
|
||||||
endpoints = append(endpoints, svcEndpoints...)
|
endpoints = append(endpoints, svcEndpoints...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +211,12 @@ func (sc *serviceSource) filterByAnnotations(services []v1.Service) ([]v1.Servic
|
|||||||
return filteredList, nil
|
return filteredList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sc *serviceSource) setResourceLabel(service v1.Service, endpoints []*endpoint.Endpoint) {
|
||||||
|
for _, ep := range endpoints {
|
||||||
|
ep.Labels[endpoint.ResourceLabelKey] = fmt.Sprintf("service/%s/%s", service.Namespace, service.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
|
func (sc *serviceSource) generateEndpoints(svc *v1.Service, hostname string) []*endpoint.Endpoint {
|
||||||
var endpoints []*endpoint.Endpoint
|
var endpoints []*endpoint.Endpoint
|
||||||
|
|
||||||
|
@ -28,9 +28,62 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ServiceSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
sc Source
|
||||||
|
fooWithTargets *v1.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ServiceSuite) SetupTest() {
|
||||||
|
fakeClient := fake.NewSimpleClientset()
|
||||||
|
var err error
|
||||||
|
|
||||||
|
suite.sc, err = NewServiceSource(
|
||||||
|
fakeClient,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"{{.Name}}",
|
||||||
|
"",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
suite.fooWithTargets = &v1.Service{
|
||||||
|
Spec: v1.ServiceSpec{
|
||||||
|
Type: v1.ServiceTypeLoadBalancer,
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "foo-with-targets",
|
||||||
|
Annotations: map[string]string{},
|
||||||
|
},
|
||||||
|
Status: v1.ServiceStatus{
|
||||||
|
LoadBalancer: v1.LoadBalancerStatus{
|
||||||
|
Ingress: []v1.LoadBalancerIngress{
|
||||||
|
{IP: "8.8.8.8"},
|
||||||
|
{Hostname: "foo"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.NoError(err, "should initialize service source")
|
||||||
|
|
||||||
|
_, err = fakeClient.CoreV1().Services(suite.fooWithTargets.Namespace).Create(suite.fooWithTargets)
|
||||||
|
suite.NoError(err, "should successfully create service")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ServiceSuite) TestResourceLabelIsSet() {
|
||||||
|
endpoints, _ := suite.sc.Endpoints()
|
||||||
|
for _, ep := range endpoints {
|
||||||
|
suite.Equal("service/default/foo-with-targets", ep.Labels[endpoint.ResourceLabelKey], "should set correct resource label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServiceSource(t *testing.T) {
|
func TestServiceSource(t *testing.T) {
|
||||||
|
suite.Run(t, new(ServiceSuite))
|
||||||
t.Run("Interface", testServiceSourceImplementsSource)
|
t.Run("Interface", testServiceSourceImplementsSource)
|
||||||
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
|
t.Run("NewServiceSource", testServiceSourceNewServiceSource)
|
||||||
t.Run("Endpoints", testServiceSourceEndpoints)
|
t.Run("Endpoints", testServiceSourceEndpoints)
|
||||||
|
Loading…
Reference in New Issue
Block a user