diff --git a/apis/v1alpha1/dnsendpoint.go b/apis/v1alpha1/dnsendpoint.go index 522bd1beeb..eaf6ea5c41 100644 --- a/apis/v1alpha1/dnsendpoint.go +++ b/apis/v1alpha1/dnsendpoint.go @@ -18,8 +18,6 @@ package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "sigs.k8s.io/external-dns/endpoint" ) // +genclient @@ -51,7 +49,37 @@ type DNSEndpointList struct { // DNSEndpointSpec defines the desired state of DNSEndpoint type DNSEndpointSpec struct { - Endpoints []*endpoint.Endpoint `json:"endpoints,omitempty"` + Endpoints []*Endpoint `json:"endpoints,omitempty"` +} + +// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers +type ProviderSpecificProperty struct { + Name string `json:"name,omitempty"` + Value string `json:"value,omitempty"` +} + +// ProviderSpecific holds configuration which is specific to individual DNS providers +type ProviderSpecific []ProviderSpecificProperty + +// Endpoint is a high-level way of a connection between a service and an IP +// +kubebuilder:object:generate=true +type Endpoint struct { + // The hostname of the DNS record + DNSName string `json:"dnsName,omitempty"` + // The targets the DNS record points to + Targets []string `json:"targets,omitempty"` + // RecordType type of record, e.g. CNAME, A, AAAA, SRV, TXT etc + RecordType string `json:"recordType,omitempty"` + // Identifier to distinguish multiple records with the same name and type (e.g. Route53 records with routing policies other than 'simple') + SetIdentifier string `json:"setIdentifier,omitempty"` + // TTL for the record + RecordTTL int64 `json:"recordTTL,omitempty"` + // Labels stores labels defined for the Endpoint + // +optional + Labels map[string]string `json:"labels,omitempty"` + // ProviderSpecific stores provider specific config + // +optional + ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"` } // DNSEndpointStatus defines the observed state of DNSEndpoint diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index e2f5bf1b85..f9fa4852dc 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -6,7 +6,6 @@ package v1alpha1 import ( runtime "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/external-dns/endpoint" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. @@ -73,11 +72,11 @@ func (in *DNSEndpointSpec) DeepCopyInto(out *DNSEndpointSpec) { *out = *in if in.Endpoints != nil { in, out := &in.Endpoints, &out.Endpoints - *out = make([]*endpoint.Endpoint, len(*in)) + *out = make([]*Endpoint, len(*in)) for i := range *in { if (*in)[i] != nil { in, out := &(*in)[i], &(*out)[i] - *out = new(endpoint.Endpoint) + *out = new(Endpoint) (*in).DeepCopyInto(*out) } } @@ -108,3 +107,69 @@ func (in *DNSEndpointStatus) DeepCopy() *DNSEndpointStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Endpoint) DeepCopyInto(out *Endpoint) { + *out = *in + if in.Targets != nil { + in, out := &in.Targets, &out.Targets + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ProviderSpecific != nil { + in, out := &in.ProviderSpecific, &out.ProviderSpecific + *out = make(ProviderSpecific, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. +func (in *Endpoint) DeepCopy() *Endpoint { + if in == nil { + return nil + } + out := new(Endpoint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ProviderSpecific) DeepCopyInto(out *ProviderSpecific) { + { + in := &in + *out = make(ProviderSpecific, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpecific. +func (in ProviderSpecific) DeepCopy() ProviderSpecific { + if in == nil { + return nil + } + out := new(ProviderSpecific) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderSpecificProperty) DeepCopyInto(out *ProviderSpecificProperty) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderSpecificProperty. +func (in *ProviderSpecificProperty) DeepCopy() *ProviderSpecificProperty { + if in == nil { + return nil + } + out := new(ProviderSpecificProperty) + in.DeepCopyInto(out) + return out +} diff --git a/endpoint/endpoint.go b/endpoint/endpoint.go index c95aa78c18..745f2e5e10 100644 --- a/endpoint/endpoint.go +++ b/endpoint/endpoint.go @@ -206,14 +206,21 @@ func (t Targets) IsLess(o Targets) bool { return false } -// ProviderSpecificProperty holds the name and value of a configuration which is specific to individual DNS providers -type ProviderSpecificProperty struct { - Name string `json:"name,omitempty"` - Value string `json:"value,omitempty"` +// ProviderSpecific holds configuration which is specific to individual DNS providers +type ProviderSpecific map[string]string + +func (ps ProviderSpecific) Set(key, value string) { + ps[key] = value } -// ProviderSpecific holds configuration which is specific to individual DNS providers -type ProviderSpecific []ProviderSpecificProperty +func (ps ProviderSpecific) Get(key string) (string, bool) { + value, ok := ps[key] + return value, ok +} + +func (ps ProviderSpecific) Delete(key string) { + delete(ps, key) +} // EndpointKey is the type of a map key for separating endpoints or targets. type EndpointKey struct { @@ -224,7 +231,6 @@ type EndpointKey struct { } // Endpoint is a high-level way of a connection between a service and an IP -// +kubebuilder:object:generate=true type Endpoint struct { // The hostname of the DNS record DNSName string `json:"dnsName,omitempty"` @@ -300,37 +306,26 @@ func (e *Endpoint) WithProviderSpecific(key, value string) *Endpoint { // 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.Value, true - } + if e.ProviderSpecific == nil { + return "", false } - return "", false + return e.ProviderSpecific.Get(key) } // 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 - } + if e.ProviderSpecific == nil { + e.ProviderSpecific = make(ProviderSpecific) } - - e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{Name: key, Value: value}) + e.ProviderSpecific.Set(key, 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 - } + if e.ProviderSpecific == nil { + return } + e.ProviderSpecific.Delete(key) } // WithLabel adds or updates a label for the Endpoint. diff --git a/endpoint/endpoint_test.go b/endpoint/endpoint_test.go index ef5fbc7821..2c0ce88c59 100644 --- a/endpoint/endpoint_test.go +++ b/endpoint/endpoint_test.go @@ -191,48 +191,72 @@ func TestIsLess(t *testing.T) { } func TestGetProviderSpecificProperty(t *testing.T) { - e := &Endpoint{ - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name", - Value: "value", + tests := []struct { + name string + endpoint *Endpoint + key string + expectedValue string + expectedOk bool + }{ + { + name: "key exists", + endpoint: &Endpoint{ + ProviderSpecific: ProviderSpecific{ + "region": "us-west-1", + }, + }, + key: "region", + expectedValue: "us-west-1", + expectedOk: true, + }, + { + name: "key does not exist", + endpoint: &Endpoint{ + ProviderSpecific: ProviderSpecific{ + "region": "us-west-1", + }, + }, + key: "zone", + expectedValue: "", + expectedOk: false, + }, + { + name: "empty ProviderSpecific", + endpoint: &Endpoint{ + ProviderSpecific: nil, }, + key: "region", + expectedValue: "", + expectedOk: false, }, } - t.Run("key is not present in provider specific", func(t *testing.T) { - val, ok := e.GetProviderSpecificProperty("hello") - assert.False(t, ok) - assert.Empty(t, val) - }) - - t.Run("key is present in provider specific", func(t *testing.T) { - val, ok := e.GetProviderSpecificProperty("name") - assert.True(t, ok) - assert.NotEmpty(t, val) - - }) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + value, ok := tt.endpoint.GetProviderSpecificProperty(tt.key) + if value != tt.expectedValue || ok != tt.expectedOk { + t.Errorf("GetProviderSpecificProperty(%q) = (%q, %v); want (%q, %v)", tt.key, value, ok, tt.expectedValue, tt.expectedOk) + } + }) + } } -func TestSetProviderSpecficProperty(t *testing.T) { +func TestSetProviderSpecificProperty(t *testing.T) { cases := []struct { name string endpoint Endpoint key string value string expectedIdentifier string - expected []ProviderSpecificProperty + expected ProviderSpecific }{ { name: "endpoint is empty", endpoint: Endpoint{}, key: "key1", value: "value1", - expected: []ProviderSpecificProperty{ - { - Name: "key1", - Value: "value1", - }, + expected: ProviderSpecific{ + "key1": "value1", }, }, { @@ -245,26 +269,17 @@ func TestSetProviderSpecficProperty(t *testing.T) { Targets: Targets{ "example.org", "example.com", "1.2.4.5", }, - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", }, }, expectedIdentifier: "newIdentifier", key: "name2", value: "value2", - expected: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, - { - Name: "name2", - Value: "value2", - }, + expected: ProviderSpecific{ + "name1": "value1", + "name2": "value2", }, }, { @@ -277,37 +292,19 @@ func TestSetProviderSpecficProperty(t *testing.T) { Targets: Targets{ "example.org", "example.com", "1.2.4.5", }, - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, - { - Name: "name2", - Value: "value2", - }, - { - Name: "name3", - Value: "value3", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", + "name2": "value2", + "name3": "value3", }, }, key: "name2", value: "value2", expectedIdentifier: "newIdentifier", - expected: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, - { - Name: "name2", - Value: "value2", - }, - { - Name: "name3", - Value: "value3", - }, + expected: ProviderSpecific{ + "name1": "value1", + "name2": "value2", + "name3": "value3", }, }, { @@ -320,21 +317,34 @@ func TestSetProviderSpecficProperty(t *testing.T) { Targets: Targets{ "example.org", "example.com", "1.2.4.5", }, - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", }, }, key: "name1", value: "value2", expectedIdentifier: "identifier", - expected: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value2", + expected: ProviderSpecific{ + "name1": "value2", + }, + }, + { + name: "provider specific is nil", + endpoint: Endpoint{ + DNSName: "example.org", + RecordTTL: TTL(0), + RecordType: RecordTypeA, + SetIdentifier: "identifier", + Targets: Targets{ + "example.org", "example.com", "1.2.4.5", }, + ProviderSpecific: nil, + }, + expectedIdentifier: "identifier", + key: "name1", + value: "value1", + expected: ProviderSpecific{ + "name1": "value1", }, }, } @@ -346,7 +356,7 @@ func TestSetProviderSpecficProperty(t *testing.T) { identifier := c.endpoint.WithSetIdentifier(c.endpoint.SetIdentifier) assert.Equal(t, c.expectedIdentifier, identifier.SetIdentifier) assert.Equal(t, expectedString, c.endpoint.String()) - if !reflect.DeepEqual([]ProviderSpecificProperty(c.endpoint.ProviderSpecific), c.expected) { + if !reflect.DeepEqual(c.endpoint.ProviderSpecific, c.expected) { t.Errorf("unexpected ProviderSpecific:\nGot: %#v\nExpected: %#v", c.endpoint.ProviderSpecific, c.expected) } }) @@ -358,75 +368,59 @@ func TestDeleteProviderSpecificProperty(t *testing.T) { name string endpoint Endpoint key string - expected []ProviderSpecificProperty + expected ProviderSpecific }{ { name: "name and key are not matching", endpoint: Endpoint{ - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", }, }, key: "name2", - expected: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, + expected: ProviderSpecific{ + "name1": "value1", }, }, { name: "some keys are matching and some keys are not matching", endpoint: Endpoint{ - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, - { - Name: "name2", - Value: "value2", - }, - { - Name: "name3", - Value: "value3", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", + "name2": "value2", + "name3": "value3", }, }, key: "name2", - expected: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, - { - Name: "name3", - Value: "value3", - }, + expected: ProviderSpecific{ + "name1": "value1", + "name3": "value3", }, }, { name: "name and key are matching", endpoint: Endpoint{ - ProviderSpecific: []ProviderSpecificProperty{ - { - Name: "name1", - Value: "value1", - }, + ProviderSpecific: ProviderSpecific{ + "name1": "value1", }, }, key: "name1", - expected: []ProviderSpecificProperty{}, + expected: ProviderSpecific{}, + }, + { + name: "provider specific is nil", + endpoint: Endpoint{ + ProviderSpecific: nil, + }, + key: "name1", + expected: nil, }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { c.endpoint.DeleteProviderSpecificProperty(c.key) - if !reflect.DeepEqual([]ProviderSpecificProperty(c.endpoint.ProviderSpecific), c.expected) { + if !reflect.DeepEqual(c.endpoint.ProviderSpecific, c.expected) { t.Errorf("unexpected ProviderSpecific:\nGot: %#v\nExpected: %#v", c.endpoint.ProviderSpecific, c.expected) } }) diff --git a/endpoint/zz_generated.deepcopy.go b/endpoint/zz_generated.deepcopy.go deleted file mode 100644 index e86498d37d..0000000000 --- a/endpoint/zz_generated.deepcopy.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build !ignore_autogenerated - -// Code generated by controller-gen. DO NOT EDIT. - -package endpoint - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Endpoint) DeepCopyInto(out *Endpoint) { - *out = *in - if in.Targets != nil { - in, out := &in.Targets, &out.Targets - *out = make(Targets, len(*in)) - copy(*out, *in) - } - if in.Labels != nil { - in, out := &in.Labels, &out.Labels - *out = make(Labels, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.ProviderSpecific != nil { - in, out := &in.ProviderSpecific, &out.ProviderSpecific - *out = make(ProviderSpecific, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. -func (in *Endpoint) DeepCopy() *Endpoint { - if in == nil { - return nil - } - out := new(Endpoint) - in.DeepCopyInto(out) - return out -} diff --git a/internal/testutils/endpoint.go b/internal/testutils/endpoint.go index 0a33d3a218..74b4fe43a5 100644 --- a/internal/testutils/endpoint.go +++ b/internal/testutils/endpoint.go @@ -18,6 +18,7 @@ package testutils import ( "fmt" + "maps" "math/rand" "net/netip" "reflect" @@ -29,12 +30,6 @@ import ( /** test utility functions for endpoints verifications */ -type byNames endpoint.ProviderSpecific - -func (p byNames) Len() int { return len(p) } -func (p byNames) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (p byNames) Less(i, j int) bool { return p[i].Name < p[j].Name } - type byAllFields []*endpoint.Endpoint func (b byAllFields) Len() int { return len(b) } @@ -47,11 +42,7 @@ func (b byAllFields) Less(i, j int) bool { // This rather bad, we need a more complex comparison for Targets, which considers all elements if b[i].Targets.Same(b[j].Targets) { if b[i].RecordType == (b[j].RecordType) { - sa := b[i].ProviderSpecific - sb := b[j].ProviderSpecific - sort.Sort(byNames(sa)) - sort.Sort(byNames(sb)) - return reflect.DeepEqual(sa, sb) + return SameProviderSpecific(b[i].ProviderSpecific, b[j].ProviderSpecific) } return b[i].RecordType <= b[j].RecordType } @@ -118,13 +109,9 @@ func SamePlanChanges(a, b map[string][]*endpoint.Endpoint) bool { SameEndpoints(a["UpdateOld"], b["UpdateOld"]) && SameEndpoints(a["UpdateNew"], b["UpdateNew"]) } -// SameProviderSpecific verifies that two maps contain the same string/string key/value pairs +// SameProviderSpecific verifies that two maps contain the same key/value pairs func SameProviderSpecific(a, b endpoint.ProviderSpecific) bool { - sa := a - sb := b - sort.Sort(byNames(sa)) - sort.Sort(byNames(sb)) - return reflect.DeepEqual(sa, sb) + return maps.Equal(a, b) } // NewTargetsFromAddr convert an array of netip.Addr to Targets (array of string) diff --git a/internal/testutils/endpoint_test.go b/internal/testutils/endpoint_test.go index 8daff40423..3b64a3472b 100644 --- a/internal/testutils/endpoint_test.go +++ b/internal/testutils/endpoint_test.go @@ -66,7 +66,7 @@ func TestExampleSameEndpoints(t *testing.T) { DNSName: "example.org", Targets: endpoint.Targets{"load-balancer.org"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{Name: "foo", Value: "bar"}, + "foo": "bar", }, }, } @@ -97,7 +97,7 @@ func makeEndpoint(DNSName string) *endpoint.Endpoint { endpoint.OwnedRecordLabelKey: "owned", }, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "key", Value: "val"}, + "key": "val", }, } } @@ -129,7 +129,7 @@ func TestSameEndpoint(t *testing.T) { endpoint.OwnedRecordLabelKey: "owned-true", }, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "key1", Value: "val1"}, + "key1": "val1", }, }, b: &endpoint.Endpoint{ @@ -144,7 +144,7 @@ func TestSameEndpoint(t *testing.T) { endpoint.OwnedRecordLabelKey: "owned-true", }, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "key1", Value: "val1"}, + "key1": "val1", }, }, isSameEndpoint: true, @@ -194,13 +194,13 @@ func TestSameEndpoint(t *testing.T) { a: &endpoint.Endpoint{ DNSName: "example.org", ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "key1", Value: "val1"}, + "key1": "val1", }, }, b: &endpoint.Endpoint{ DNSName: "example.org", ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "key1", Value: "val2"}, + "key1": "val2", }, }, isSameEndpoint: false, diff --git a/pkg/adapter/endpoint.go b/pkg/adapter/endpoint.go new file mode 100644 index 0000000000..ff217bb7fb --- /dev/null +++ b/pkg/adapter/endpoint.go @@ -0,0 +1,88 @@ +/* +Copyright 2025 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 adapter + +import ( + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" + "sigs.k8s.io/external-dns/endpoint" +) + +// FromAPIEndpoint converts an API endpoint (v1alpha1.Endpoint) to an internal endpoint (endpoint.Endpoint). +func FromAPIEndpoint(apiEndpoint *apiv1alpha1.Endpoint) *endpoint.Endpoint { + if apiEndpoint == nil { + return nil + } + p := make(endpoint.ProviderSpecific) + for _, ps := range apiEndpoint.ProviderSpecific { + p.Set(ps.Name, ps.Value) + } + return &endpoint.Endpoint{ + DNSName: apiEndpoint.DNSName, + Targets: apiEndpoint.Targets, + RecordType: apiEndpoint.RecordType, + SetIdentifier: apiEndpoint.SetIdentifier, + RecordTTL: endpoint.TTL(apiEndpoint.RecordTTL), + Labels: apiEndpoint.Labels, + ProviderSpecific: p, + } +} + +// FromAPIEndpoints converts a slice of API endpoints to internal endpoints. +func FromAPIEndpoints(apiEndpoints []*apiv1alpha1.Endpoint) []*endpoint.Endpoint { + if len(apiEndpoints) == 0 { + return nil + } + eps := make([]*endpoint.Endpoint, 0, len(apiEndpoints)) + for _, apiEndpoint := range apiEndpoints { + eps = append(eps, FromAPIEndpoint(apiEndpoint)) + } + return eps +} + +// ToAPIEndpoint converts an internal endpoint (endpoint.Endpoint) to an API endpoint (v1alpha1.Endpoint). +func ToAPIEndpoint(internalEndpoint *endpoint.Endpoint) *apiv1alpha1.Endpoint { + if internalEndpoint == nil { + return nil + } + ps := make([]apiv1alpha1.ProviderSpecificProperty, 0, len(internalEndpoint.ProviderSpecific)) + for k, v := range internalEndpoint.ProviderSpecific { + ps = append(ps, apiv1alpha1.ProviderSpecificProperty{ + Name: k, + Value: v, + }) + } + return &apiv1alpha1.Endpoint{ + DNSName: internalEndpoint.DNSName, + Targets: internalEndpoint.Targets, + RecordType: internalEndpoint.RecordType, + SetIdentifier: internalEndpoint.SetIdentifier, + RecordTTL: int64(internalEndpoint.RecordTTL), + Labels: internalEndpoint.Labels, + ProviderSpecific: ps, + } +} + +// ToAPIEndpoints converts a slice of internal endpoints to API endpoints. +func ToAPIEndpoints(internalEndpoints []*endpoint.Endpoint) []*apiv1alpha1.Endpoint { + if len(internalEndpoints) == 0 { + return nil + } + apiEndpoints := make([]*apiv1alpha1.Endpoint, 0, len(internalEndpoints)) + for _, internalEndpoint := range internalEndpoints { + apiEndpoints = append(apiEndpoints, ToAPIEndpoint(internalEndpoint)) + } + return apiEndpoints +} diff --git a/pkg/adapter/endpoint_test.go b/pkg/adapter/endpoint_test.go new file mode 100644 index 0000000000..02c3562ef7 --- /dev/null +++ b/pkg/adapter/endpoint_test.go @@ -0,0 +1,519 @@ +/* +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 adapter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/internal/testutils" +) + +func TestToInternalEndpoint(t *testing.T) { + tests := []struct { + name string + input *apiv1alpha1.Endpoint + expected *endpoint.Endpoint + }{ + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "basic endpoint conversion", + input: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + }, + expected: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + { + name: "endpoint with provider specific properties", + input: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "aws-alias", Value: "true"}, + {Name: "aws-zone-id", Value: "Z12345"}, + }, + }, + expected: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + "aws-alias": "true", + "aws-zone-id": "Z12345", + }, + }, + }, + { + name: "endpoint with labels and set identifier", + input: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + SetIdentifier: "us-east-1", + Labels: map[string]string{ + "owner": "external-dns", + "resource": "ingress/default/test", + "namespace": "default", + }, + }, + expected: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + SetIdentifier: "us-east-1", + Labels: map[string]string{ + "owner": "external-dns", + "resource": "ingress/default/test", + "namespace": "default", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + { + name: "complete endpoint with all fields", + input: &apiv1alpha1.Endpoint{ + DNSName: "api.example.com", + Targets: []string{"10.0.0.1", "10.0.0.2"}, + RecordType: "A", + RecordTTL: 600, + SetIdentifier: "primary", + Labels: map[string]string{ + "owner": "external-dns", + "type": "public", + }, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "weight", Value: "100"}, + {Name: "policy", Value: "weighted"}, + }, + }, + expected: &endpoint.Endpoint{ + DNSName: "api.example.com", + Targets: []string{"10.0.0.1", "10.0.0.2"}, + RecordType: "A", + RecordTTL: 600, + SetIdentifier: "primary", + Labels: map[string]string{ + "owner": "external-dns", + "type": "public", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "weight": "100", + "policy": "weighted", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FromAPIEndpoint(tt.input) + if tt.expected == nil { + assert.Nil(t, result) + } else { + assert.True(t, testutils.SameEndpoint(tt.expected, result)) + } + }) + } +} + +func TestToAPIEndpoint(t *testing.T) { + tests := []struct { + name string + input *endpoint.Endpoint + expected *apiv1alpha1.Endpoint + }{ + { + name: "nil input", + input: nil, + expected: nil, + }, + { + name: "basic endpoint conversion", + input: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + expected: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{}, + }, + }, + { + name: "endpoint with provider specific properties", + input: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + "aws-alias": "true", + "aws-zone-id": "Z12345", + }, + }, + expected: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "aws-alias", Value: "true"}, + {Name: "aws-zone-id", Value: "Z12345"}, + }, + }, + }, + { + name: "endpoint with labels and set identifier", + input: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + SetIdentifier: "us-east-1", + Labels: map[string]string{ + "owner": "external-dns", + "resource": "ingress/default/test", + "namespace": "default", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + expected: &apiv1alpha1.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + SetIdentifier: "us-east-1", + Labels: map[string]string{ + "owner": "external-dns", + "resource": "ingress/default/test", + "namespace": "default", + }, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToAPIEndpoint(tt.input) + if tt.expected == nil { + assert.Nil(t, result) + } else { + assert.Equal(t, tt.expected.DNSName, result.DNSName) + assert.Equal(t, tt.expected.Targets, result.Targets) + assert.Equal(t, tt.expected.RecordType, result.RecordType) + assert.Equal(t, tt.expected.RecordTTL, result.RecordTTL) + assert.Equal(t, tt.expected.SetIdentifier, result.SetIdentifier) + assert.Equal(t, tt.expected.Labels, result.Labels) + + // Convert ProviderSpecific slice to map for comparison since order doesn't matter + expectedPS := make(map[string]string) + for _, ps := range tt.expected.ProviderSpecific { + expectedPS[ps.Name] = ps.Value + } + resultPS := make(map[string]string) + for _, ps := range result.ProviderSpecific { + resultPS[ps.Name] = ps.Value + } + assert.Equal(t, expectedPS, resultPS) + } + }) + } +} + +func TestToInternalEndpoints(t *testing.T) { + tests := []struct { + name string + input []*apiv1alpha1.Endpoint + expected []*endpoint.Endpoint + }{ + { + name: "empty slice", + input: []*apiv1alpha1.Endpoint{}, + expected: nil, + }, + { + name: "nil slice", + input: nil, + expected: nil, + }, + { + name: "single endpoint", + input: []*apiv1alpha1.Endpoint{ + { + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + { + name: "multiple endpoints", + input: []*apiv1alpha1.Endpoint{ + { + DNSName: "api.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "type", Value: "api"}, + }, + }, + { + DNSName: "www.example.com", + Targets: []string{"example.com"}, + RecordType: "CNAME", + RecordTTL: 600, + Labels: map[string]string{ + "owner": "external-dns", + }, + }, + }, + expected: []*endpoint.Endpoint{ + { + DNSName: "api.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + "type": "api", + }, + }, + { + DNSName: "www.example.com", + Targets: []string{"example.com"}, + RecordType: "CNAME", + RecordTTL: 600, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := FromAPIEndpoints(tt.input) + assert.True(t, testutils.SameEndpoints(tt.expected, result)) + }) + } +} + +func TestToAPIEndpoints(t *testing.T) { + tests := []struct { + name string + input []*endpoint.Endpoint + expected []*apiv1alpha1.Endpoint + }{ + { + name: "empty slice", + input: []*endpoint.Endpoint{}, + expected: nil, + }, + { + name: "nil slice", + input: nil, + expected: nil, + }, + { + name: "single endpoint", + input: []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + expected: []*apiv1alpha1.Endpoint{ + { + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{}, + }, + }, + }, + { + name: "multiple endpoints with provider specific", + input: []*endpoint.Endpoint{ + { + DNSName: "api.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{ + "weight": "100", + }, + }, + { + DNSName: "www.example.com", + Targets: []string{"example.com"}, + RecordType: "CNAME", + RecordTTL: 600, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "alias": "true", + }, + }, + }, + expected: []*apiv1alpha1.Endpoint{ + { + DNSName: "api.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "weight", Value: "100"}, + }, + }, + { + DNSName: "www.example.com", + Targets: []string{"example.com"}, + RecordType: "CNAME", + RecordTTL: 600, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + {Name: "alias", Value: "true"}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ToAPIEndpoints(tt.input) + assert.Len(t, result, len(tt.expected)) + + for i, expected := range tt.expected { + if expected == nil { + assert.Nil(t, result[i]) + continue + } + + assert.Equal(t, expected.DNSName, result[i].DNSName) + assert.Equal(t, expected.Targets, result[i].Targets) + assert.Equal(t, expected.RecordType, result[i].RecordType) + assert.Equal(t, expected.RecordTTL, result[i].RecordTTL) + assert.Equal(t, expected.SetIdentifier, result[i].SetIdentifier) + assert.Equal(t, expected.Labels, result[i].Labels) + + // Convert ProviderSpecific slice to map for comparison since order doesn't matter + expectedPS := make(map[string]string) + for _, ps := range expected.ProviderSpecific { + expectedPS[ps.Name] = ps.Value + } + resultPS := make(map[string]string) + for _, ps := range result[i].ProviderSpecific { + resultPS[ps.Name] = ps.Value + } + assert.Equal(t, expectedPS, resultPS) + } + }) + } +} + +func TestRoundTripConversion(t *testing.T) { + tests := []struct { + name string + original *endpoint.Endpoint + }{ + { + name: "basic endpoint round trip", + original: &endpoint.Endpoint{ + DNSName: "test.example.com", + Targets: []string{"1.2.3.4"}, + RecordType: "A", + RecordTTL: 300, + ProviderSpecific: endpoint.ProviderSpecific{}, + }, + }, + { + name: "complex endpoint round trip", + original: &endpoint.Endpoint{ + DNSName: "api.example.com", + Targets: []string{"10.0.0.1", "10.0.0.2"}, + RecordType: "A", + RecordTTL: 600, + SetIdentifier: "primary", + Labels: map[string]string{ + "owner": "external-dns", + "type": "public", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "weight": "100", + "policy": "weighted", + "health-check": "enabled", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Convert internal -> API -> internal + apiEndpoint := ToAPIEndpoint(tt.original) + result := FromAPIEndpoint(apiEndpoint) + + assert.True(t, testutils.SameEndpoint(tt.original, result)) + }) + } +} diff --git a/plan/plan.go b/plan/plan.go index 325a3d80fa..7dc5f69cc7 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -18,6 +18,7 @@ package plan import ( "fmt" + "maps" "slices" "strings" @@ -305,23 +306,7 @@ func shouldUpdateTTL(desired, current *endpoint.Endpoint) bool { } func (p *Plan) shouldUpdateProviderSpecific(desired, current *endpoint.Endpoint) bool { - desiredProperties := map[string]endpoint.ProviderSpecificProperty{} - - for _, d := range desired.ProviderSpecific { - desiredProperties[d.Name] = d - } - for _, c := range current.ProviderSpecific { - if d, ok := desiredProperties[c.Name]; ok { - if c.Value != d.Value { - return true - } - delete(desiredProperties, c.Name) - } else { - return true - } - } - - return len(desiredProperties) > 0 + return !maps.Equal(desired.ProviderSpecific, current.ProviderSpecific) } // filterRecordsForPlan removes records that are not relevant to the planner. diff --git a/plan/plan_test.go b/plan/plan_test.go index acce96b27d..be6ccb02ff 100644 --- a/plan/plan_test.go +++ b/plan/plan_test.go @@ -159,14 +159,8 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "alias", - Value: "false", - }, - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, + "alias": "false", + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", }, } suite.bar127AWithProviderSpecificFalse = &endpoint.Endpoint{ @@ -177,14 +171,8 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, - endpoint.ProviderSpecificProperty{ - Name: "alias", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", + "alias": "false", }, } suite.bar127AWithProviderSpecificUnset = &endpoint.Endpoint{ @@ -195,10 +183,7 @@ func (suite *PlanTestSuite) SetupTest() { endpoint.ResourceLabelKey: "ingress/default/bar-127", }, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "alias", - Value: "false", - }, + "alias": "false", }, } suite.bar192A = &endpoint.Endpoint{ @@ -1163,14 +1148,14 @@ func TestShouldUpdateProviderSpecific(tt *testing.T) { name: "skip AWS target health", current: &endpoint.Endpoint{ DNSName: "foo.com", - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "aws/evaluate-target-health", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "aws/evaluate-target-health": "true", }, }, desired: &endpoint.Endpoint{ DNSName: "bar.com", - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "aws/evaluate-target-health", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "aws/evaluate-target-health": "true", }, }, shouldUpdate: false, @@ -1178,13 +1163,13 @@ func TestShouldUpdateProviderSpecific(tt *testing.T) { { name: "custom property unchanged", current: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "custom/property", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "custom/property": "true", }, }, desired: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "custom/property", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "custom/property": "true", }, }, shouldUpdate: false, @@ -1192,13 +1177,13 @@ func TestShouldUpdateProviderSpecific(tt *testing.T) { { name: "custom property value changed", current: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "custom/property", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "custom/property": "true", }, }, desired: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "custom/property", Value: "false"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "custom/property": "false", }, }, shouldUpdate: true, @@ -1206,13 +1191,13 @@ func TestShouldUpdateProviderSpecific(tt *testing.T) { { name: "custom property key changed", current: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "custom/property", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "custom/property": "true", }, }, desired: &endpoint.Endpoint{ - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - {Name: "new/property", Value: "true"}, + ProviderSpecific: endpoint.ProviderSpecific{ + "new/property": "true", }, }, shouldUpdate: true, diff --git a/provider/aws/aws_test.go b/provider/aws/aws_test.go index a269c3ede8..7d5d3f220b 100644 --- a/provider/aws/aws_test.go +++ b/provider/aws/aws_test.go @@ -2084,14 +2084,8 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeA, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: providerSpecificAlias, - Value: "true", - }, - endpoint.ProviderSpecificProperty{ - Name: providerSpecificEvaluateTargetHealth, - Value: key, - }, + providerSpecificAlias: "true", + providerSpecificEvaluateTargetHealth: key, }, }, { @@ -2099,14 +2093,8 @@ func TestAWSCreateRecordsWithALIAS(t *testing.T) { Targets: endpoint.Targets{"foo.eu-central-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeAAAA, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: providerSpecificAlias, - Value: "true", - }, - endpoint.ProviderSpecificProperty{ - Name: providerSpecificEvaluateTargetHealth, - Value: key, - }, + providerSpecificAlias: "true", + providerSpecificEvaluateTargetHealth: key, }, }, } diff --git a/provider/cloudflare/cloudflare.go b/provider/cloudflare/cloudflare.go index c852962da1..691b561a59 100644 --- a/provider/cloudflare/cloudflare.go +++ b/provider/cloudflare/cloudflare.go @@ -957,15 +957,12 @@ func getCustomHostnamesSSLOptions(customHostnamesConfig CustomHostnamesConfig) * func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool { proxied := proxiedByDefault - for _, v := range ep.ProviderSpecific { - if v.Name == annotations.CloudflareProxiedKey { - b, err := strconv.ParseBool(v.Value) - if err != nil { - log.Errorf("Failed to parse annotation [%q]: %v", annotations.CloudflareProxiedKey, err) - } else { - proxied = b - } - break + if value, exists := ep.ProviderSpecific.Get(annotations.CloudflareProxiedKey); exists { + b, err := strconv.ParseBool(value) + if err != nil { + log.Errorf("Failed to parse annotation [%q]: %v", annotations.CloudflareProxiedKey, err) + } else { + proxied = b } } @@ -976,11 +973,9 @@ func shouldBeProxied(ep *endpoint.Endpoint, proxiedByDefault bool) bool { } func getEndpointCustomHostnames(ep *endpoint.Endpoint) []string { - for _, v := range ep.ProviderSpecific { - if v.Name == annotations.CloudflareCustomHostnameKey { - customHostnames := strings.Split(v.Value, ",") - return customHostnames - } + if value, exists := ep.ProviderSpecific.Get(annotations.CloudflareCustomHostnameKey); exists { + customHostnames := strings.Split(value, ",") + return customHostnames } return []string{} } diff --git a/provider/cloudflare/cloudflare_regional_test.go b/provider/cloudflare/cloudflare_regional_test.go index b114adafcc..74d3ebb13f 100644 --- a/provider/cloudflare/cloudflare_regional_test.go +++ b/provider/cloudflare/cloudflare_regional_test.go @@ -119,10 +119,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { DNSName: "create.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -174,10 +171,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { DNSName: "update.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -264,10 +258,7 @@ func TestCloudflareRegionalHostnameActions(t *testing.T) { DNSName: "nochange.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -396,10 +387,7 @@ func Test_regionalHostname(t *testing.T) { RecordType: "A", DNSName: "example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, config: RegionalServicesConfig{ @@ -419,10 +407,7 @@ func Test_regionalHostname(t *testing.T) { RecordType: "A", DNSName: "example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "", }, }, config: RegionalServicesConfig{ @@ -442,10 +427,7 @@ func Test_regionalHostname(t *testing.T) { RecordType: "TXT", DNSName: "example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "eu", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, config: RegionalServicesConfig{ @@ -462,10 +444,7 @@ func Test_regionalHostname(t *testing.T) { RecordType: "A", DNSName: "example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", - Value: "us", - }, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "us", }, }, config: RegionalServicesConfig{ @@ -869,7 +848,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "foo.error.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -892,7 +871,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "rherror.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -924,7 +903,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "rherror.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -934,7 +913,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "rherror.bar.com", Targets: endpoint.Targets{"127.0.0.2"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -987,7 +966,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "foo.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, { @@ -995,7 +974,7 @@ func TestApplyChangesWithRegionalHostnamesFaillures(t *testing.T) { DNSName: "foo.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "us"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "us", }, }, }, @@ -1073,7 +1052,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { DNSName: "foo.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -1105,7 +1084,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { DNSName: "foo.bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -1115,7 +1094,7 @@ func TestApplyChangesWithRegionalHostnamesDryRun(t *testing.T) { DNSName: "foo.bar.com", Targets: endpoint.Targets{"127.0.0.2"}, ProviderSpecific: endpoint.ProviderSpecific{ - {Name: "external-dns.alpha.kubernetes.io/cloudflare-region-key", Value: "eu"}, + "external-dns.alpha.kubernetes.io/cloudflare-region-key": "eu", }, }, }, @@ -1265,10 +1244,7 @@ func TestCloudflareAdjustEndpointsRegionalServices(t *testing.T) { if tc.initialRegionKey != "" { testEndpoint.ProviderSpecific = endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: annotations.CloudflareRegionKey, - Value: tc.initialRegionKey, - }, + annotations.CloudflareRegionKey: tc.initialRegionKey, } } diff --git a/provider/cloudflare/cloudflare_test.go b/provider/cloudflare/cloudflare_test.go index 2de2f8e19f..6476d348d0 100644 --- a/provider/cloudflare/cloudflare_test.go +++ b/provider/cloudflare/cloudflare_test.go @@ -154,10 +154,7 @@ func TestCloudflareProviderTags(t *testing.T) { Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareTagsKey, - Value: "owner:team-b, env:prod, app:api ", // Unsorted and messy - }, + annotations.CloudflareTagsKey: "owner:team-b, env:prod, app:api", }, }, } @@ -182,10 +179,7 @@ func TestCloudflareProviderTags(t *testing.T) { Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareTagsKey, - Value: "owner:team-b, env:prod, app:api ", - }, + annotations.CloudflareTagsKey: "owner:team-b, env:prod, app:api ", }, } expectedTagsSlice := []string{"app:api", "env:prod", "owner:team-b"} @@ -732,10 +726,7 @@ func TestCloudflareProxiedOverrideTrue(t *testing.T) { DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", }, }, } @@ -766,10 +757,7 @@ func TestCloudflareProxiedOverrideFalse(t *testing.T) { DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, } @@ -800,10 +788,7 @@ func TestCloudflareProxiedOverrideIllegal(t *testing.T) { DNSName: "bar.com", Targets: endpoint.Targets{"127.0.0.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "asfasdfa", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "asfasdfa", }, }, } @@ -864,10 +849,7 @@ func TestCloudflareSetProxied(t *testing.T) { DNSName: testCase.domain, Targets: endpoint.Targets{targets[0]}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", }, }, } @@ -1340,10 +1322,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, }, @@ -1374,10 +1353,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, }, @@ -1422,10 +1398,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, { @@ -1435,10 +1408,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, }, @@ -1476,10 +1446,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, { @@ -1489,10 +1456,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, }, @@ -1530,10 +1494,7 @@ func TestCloudflareGroupByNameAndType(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "false", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "false", }, }, }, @@ -1811,10 +1772,7 @@ func TestCloudflareComplexUpdate(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", }, }, }) @@ -1899,10 +1857,7 @@ func TestCustomTTLWithEnabledProxyNotChanged(t *testing.T) { RecordTTL: 300, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }, + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", }, }, } @@ -1989,10 +1944,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { RecordType: "A", Targets: []string{"192.0.2.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareRecordCommentKey, - Value: freeValidComment, - }, + annotations.CloudflareRecordCommentKey: freeValidComment, }, }, expected: len(freeValidComment), @@ -2005,10 +1957,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { RecordType: "A", Targets: []string{"192.0.2.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareRecordCommentKey, - Value: freeInvalidComment, - }, + annotations.CloudflareRecordCommentKey: freeInvalidComment, }, }, expected: freeZoneMaxCommentLength, @@ -2021,10 +1970,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { RecordType: "A", Targets: []string{"192.0.2.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareRecordCommentKey, - Value: paidValidComment, - }, + annotations.CloudflareRecordCommentKey: paidValidComment, }, }, expected: len(paidValidComment), @@ -2037,10 +1983,7 @@ func TestCloudFlareProvider_newCloudFlareChange(t *testing.T) { RecordType: "A", Targets: []string{"192.0.2.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: annotations.CloudflareRecordCommentKey, - Value: paidInvalidComment, - }, + annotations.CloudflareRecordCommentKey: paidInvalidComment, }, }, expected: paidZoneMaxCommentLength, @@ -2434,10 +2377,7 @@ func TestCloudflareDisabledCustomHostnameOperations(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "a.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "a.foo.fancybar.com", }, }, { @@ -2454,10 +2394,7 @@ func TestCloudflareDisabledCustomHostnameOperations(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "c1.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "c1.foo.fancybar.com", }, }, }, @@ -2480,10 +2417,7 @@ func TestCloudflareDisabledCustomHostnameOperations(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "b.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "b.foo.fancybar.com", }, }, { @@ -2493,10 +2427,7 @@ func TestCloudflareDisabledCustomHostnameOperations(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "c2.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "c2.foo.fancybar.com", }, }, }, @@ -2557,10 +2488,7 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "newerror-getCustomHostnameOrigin.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "newerror-getCustomHostnameOrigin.foo.fancybar.com", }, }, }, @@ -2589,10 +2517,7 @@ func TestCloudflareCustomHostnameNotFoundOnRecordDeletion(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "a.foo.fancybar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "a.foo.fancybar.com", }, }, }, @@ -2677,10 +2602,7 @@ func TestCloudflareListCustomHostnamesWithPagionation(t *testing.T) { RecordTTL: endpoint.TTL(defaultTTL), Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: fmt.Sprintf("host-%d.foo.fancybar.com", i), - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": fmt.Sprintf("host-%d.foo.fancybar.com", i), }, }, } @@ -2759,10 +2681,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-create-custom.bar.com", - }, + "extenal-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad-create-custom.bar.com", }, }}, }, @@ -2777,10 +2696,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-delete-custom.bar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad-delete-custom.bar.com", }, }}, }, @@ -2795,10 +2711,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-update-add-custom.bar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad-update-add-custom.bar.com", }, }}, UpdateOld: []*endpoint.Endpoint{{ @@ -2806,10 +2719,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx-but-still-updated"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-update-add-custom.bar.com", - }, + "exntelnal-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad-update-add-custom.bar.com", }, }}, }, @@ -2824,10 +2734,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-update-leave-custom.bar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad-update-leave-custom.bar.com", }, }}, UpdateNew: []*endpoint.Endpoint{{ @@ -2835,10 +2742,7 @@ func TestCloudflareApplyChanges_AllErrorLogPaths(t *testing.T) { RecordType: "MX", Targets: endpoint.Targets{"not-a-valid-mx"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname", - Value: "bad-update-leave-custom.bar.com", - }, + "external-dns.alpha.kubernetes.io/cloudflare-custom-hostname": "bad", }, }}, }, diff --git a/provider/inmemory/inmemory.go b/provider/inmemory/inmemory.go index b17ef900e7..142793fac2 100644 --- a/provider/inmemory/inmemory.go +++ b/provider/inmemory/inmemory.go @@ -19,6 +19,7 @@ package inmemory import ( "context" "errors" + "maps" "strings" log "github.com/sirupsen/logrus" @@ -207,7 +208,10 @@ func copyEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { for k, v := range ep.Labels { newEp.Labels[k] = v } - newEp.ProviderSpecific = append(endpoint.ProviderSpecific(nil), ep.ProviderSpecific...) + if ep.ProviderSpecific != nil { + newEp.ProviderSpecific = make(endpoint.ProviderSpecific) + maps.Copy(newEp.ProviderSpecific, ep.ProviderSpecific) + } records = append(records, newEp) } return records diff --git a/provider/scaleway/scaleway_test.go b/provider/scaleway/scaleway_test.go index eec95c6953..b1d3b54e6f 100644 --- a/provider/scaleway/scaleway_test.go +++ b/provider/scaleway/scaleway_test.go @@ -180,10 +180,7 @@ func TestScalewayProvider_AdjustEndpoints(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, { @@ -192,10 +189,7 @@ func TestScalewayProvider_AdjustEndpoints(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "10", - }, + scalewayPriorityKey: "10", }, }, { @@ -214,10 +208,7 @@ func TestScalewayProvider_AdjustEndpoints(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, { @@ -226,10 +217,7 @@ func TestScalewayProvider_AdjustEndpoints(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "10", - }, + scalewayPriorityKey: "10", }, }, { @@ -238,10 +226,7 @@ func TestScalewayProvider_AdjustEndpoints(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, } @@ -296,10 +281,7 @@ func TestScalewayProvider_Records(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, { @@ -308,10 +290,7 @@ func TestScalewayProvider_Records(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.2", "1.1.1.3"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, { @@ -320,10 +299,7 @@ func TestScalewayProvider_Records(t *testing.T) { RecordType: "A", Targets: []string{"1.1.1.1"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "0", - }, + scalewayPriorityKey: "0", }, }, { @@ -332,10 +308,7 @@ func TestScalewayProvider_Records(t *testing.T) { RecordType: "CNAME", Targets: []string{"test.example.com"}, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "30", - }, + scalewayPriorityKey: "30", }, }, } @@ -498,10 +471,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) { DNSName: "test.example.com", RecordType: "CNAME", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "20", - }, + scalewayPriorityKey: "20", }, RecordTTL: 600, Targets: []string{"example.com"}, @@ -523,10 +493,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) { { DNSName: "me.example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "30", - }, + scalewayPriorityKey: "30", }, RecordType: "A", RecordTTL: 600, @@ -542,10 +509,7 @@ func TestScalewayProvider_generateApplyRequests(t *testing.T) { { DNSName: "me.example.com", ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: scalewayPriorityKey, - Value: "1234", - }, + scalewayPriorityKey: "1234", }, RecordType: "A", Targets: []string{"3.3.3.3"}, diff --git a/provider/webhook/api/httpapi.go b/provider/webhook/api/httpapi.go index fde7d3ab87..6739bd733b 100644 --- a/provider/webhook/api/httpapi.go +++ b/provider/webhook/api/httpapi.go @@ -23,7 +23,8 @@ import ( "net/http" "time" - "sigs.k8s.io/external-dns/endpoint" + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" + "sigs.k8s.io/external-dns/pkg/adapter" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" @@ -38,6 +39,13 @@ const ( UrlRecords = "/records" ) +type Changes struct { + Create []*apiv1alpha1.Endpoint `json:"create,omitempty"` + Delete []*apiv1alpha1.Endpoint `json:"delete,omitempty"` + UpdateOld []*apiv1alpha1.Endpoint `json:"updateOld,omitempty"` + UpdateNew []*apiv1alpha1.Endpoint `json:"updateNew,omitempty"` +} + type WebhookServer struct { Provider provider.Provider } @@ -53,18 +61,25 @@ func (p *WebhookServer) RecordsHandler(w http.ResponseWriter, req *http.Request) } w.Header().Set(ContentTypeHeader, MediaTypeFormatAndVersion) w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(records); err != nil { + apiRecords := adapter.ToAPIEndpoints(records) + if err := json.NewEncoder(w).Encode(apiRecords); err != nil { log.Errorf("Failed to encode records: %v", err) } return case http.MethodPost: - var changes plan.Changes + var changes Changes if err := json.NewDecoder(req.Body).Decode(&changes); err != nil { log.Errorf("Failed to decode changes: %v", err) w.WriteHeader(http.StatusBadRequest) return } - err := p.Provider.ApplyChanges(context.Background(), &changes) + internalChanges := &plan.Changes{ + Create: adapter.FromAPIEndpoints(changes.Create), + Delete: adapter.FromAPIEndpoints(changes.Delete), + UpdateOld: adapter.FromAPIEndpoints(changes.UpdateOld), + UpdateNew: adapter.FromAPIEndpoints(changes.UpdateNew), + } + err := p.Provider.ApplyChanges(context.Background(), internalChanges) if err != nil { log.Errorf("Failed to apply changes: %v", err) w.WriteHeader(http.StatusInternalServerError) @@ -85,18 +100,20 @@ func (p *WebhookServer) AdjustEndpointsHandler(w http.ResponseWriter, req *http. return } - var pve []*endpoint.Endpoint + var pve []*apiv1alpha1.Endpoint if err := json.NewDecoder(req.Body).Decode(&pve); err != nil { log.Errorf("Failed to decode in adjustEndpointsHandler: %v", err) w.WriteHeader(http.StatusBadRequest) return } w.Header().Set(ContentTypeHeader, MediaTypeFormatAndVersion) - pve, err := p.Provider.AdjustEndpoints(pve) + endpoints := adapter.FromAPIEndpoints(pve) + endpoints, err := p.Provider.AdjustEndpoints(endpoints) if err != nil { log.Errorf("Failed to call adjust endpoints: %v", err) w.WriteHeader(http.StatusInternalServerError) } + pve = adapter.ToAPIEndpoints(endpoints) if err := json.NewEncoder(w).Encode(&pve); err != nil { log.Errorf("Failed to encode in adjustEndpointsHandler: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/provider/webhook/api/httpapi_test.go b/provider/webhook/api/httpapi_test.go index 795e59b524..f0c073491b 100644 --- a/provider/webhook/api/httpapi_test.go +++ b/provider/webhook/api/httpapi_test.go @@ -31,7 +31,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/adapter" "sigs.k8s.io/external-dns/plan" ) @@ -78,6 +80,13 @@ func TestMain(m *testing.M) { { DNSName: "foo.bar.com", RecordType: "A", + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value", + }, + Labels: map[string]string{ + "label1": "value1", + }, + RecordTTL: 10, }, } m.Run() @@ -98,11 +107,11 @@ func TestRecordsHandlerRecords(t *testing.T) { // require that the res has the same endpoints as the records slice defer res.Body.Close() require.NotNil(t, res.Body) - var endpoints []*endpoint.Endpoint + var endpoints []*apiv1alpha1.Endpoint if err := json.NewDecoder(res.Body).Decode(&endpoints); err != nil { t.Errorf("Failed to decode response body: %s", err.Error()) } - require.Equal(t, records, endpoints) + require.Equal(t, adapter.ToAPIEndpoints(records), endpoints) } func TestRecordsHandlerRecordsWithErrors(t *testing.T) { @@ -132,12 +141,22 @@ func TestRecordsHandlerApplyChangesWithBadRequest(t *testing.T) { } func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { - changes := &plan.Changes{ - Create: []*endpoint.Endpoint{ + changes := &Changes{ + Create: []*apiv1alpha1.Endpoint{ { DNSName: "foo.bar.com", RecordType: "A", Targets: endpoint.Targets{}, + RecordTTL: 10, + ProviderSpecific: []apiv1alpha1.ProviderSpecificProperty{ + { + Name: "prop1", + Value: "value", + }, + }, + Labels: map[string]string{ + "label1": "value1", + }, }, }, } @@ -150,7 +169,25 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &WebhookServer{ - Provider: &FakeWebhookProvider{}, + Provider: &FakeWebhookProvider{ + assertChanges: func(changes *plan.Changes) { + t.Helper() + require.Equal(t, []*endpoint.Endpoint{ + { + DNSName: "foo.bar.com", + RecordType: "A", + Targets: nil, + RecordTTL: 10, + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value", + }, + Labels: map[string]string{ + "label1": "value1", + }, + }, + }, changes.Create) + }, + }, } providerAPIServer.RecordsHandler(w, req) res := w.Result() @@ -158,8 +195,8 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { } func TestRecordsHandlerApplyChangesWithErrors(t *testing.T) { - changes := &plan.Changes{ - Create: []*endpoint.Endpoint{ + changes := &Changes{ + Create: []*apiv1alpha1.Endpoint{ { DNSName: "foo.bar.com", RecordType: "A", @@ -198,7 +235,7 @@ func TestRecordsHandlerWithWrongHTTPMethod(t *testing.T) { } func TestRecordsHandlerWithMixedCase(t *testing.T) { - input := `{"Create":[{"dnsName":"foo"}],"updateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"Delete":[{"dnsName":"qux"}]}` + input := `{"Create":[{"dnsName":"foo","providerSpecific":[{"name":"prop1","value":"value"}]}],"updateOld":[{"dnsName":"bar"}],"updateNew":[{"dnsName":"baz"}],"Delete":[{"dnsName":"qux","labels":{"label1":"value1"}}]}` req := httptest.NewRequest(http.MethodPost, UrlRecords, strings.NewReader(input)) w := httptest.NewRecorder() @@ -211,16 +248,24 @@ func TestRecordsHandlerWithMixedCase(t *testing.T) { require.Equal(t, []*endpoint.Endpoint{ { DNSName: "foo", + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value", + }, }, }, changes.Create) require.Equal(t, []*endpoint.Endpoint{ { - DNSName: "bar", + DNSName: "bar", + ProviderSpecific: endpoint.ProviderSpecific{}, }, }, changes.UpdateOld) require.Equal(t, []*endpoint.Endpoint{ { - DNSName: "qux", + DNSName: "qux", + ProviderSpecific: endpoint.ProviderSpecific{}, + Labels: map[string]string{ + "label1": "value1", + }, }, }, changes.Delete) }, diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 787ace17f6..1922c12f4a 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -24,7 +24,9 @@ import ( "net/http" "net/url" + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/adapter" "sigs.k8s.io/external-dns/pkg/metrics" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" @@ -184,13 +186,14 @@ func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err return nil, err } - var endpoints []*endpoint.Endpoint - if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { + var apiEndpoints []*apiv1alpha1.Endpoint + if err := json.NewDecoder(resp.Body).Decode(&apiEndpoints); err != nil { recordsErrorsGauge.Gauge.Inc() log.Debugf("Failed to decode response body: %s", err.Error()) return nil, err } - return endpoints, nil + + return adapter.FromAPIEndpoints(apiEndpoints), nil } // ApplyChanges will make a POST to remoteServerURL/records with the changes @@ -199,7 +202,16 @@ func (p WebhookProvider) ApplyChanges(_ context.Context, changes *plan.Changes) u := p.remoteServerURL.JoinPath(webhookapi.UrlRecords).String() b := new(bytes.Buffer) - if err := json.NewEncoder(b).Encode(changes); err != nil { + var webhookChanges *webhookapi.Changes + if changes != nil { + webhookChanges = &webhookapi.Changes{ + Create: adapter.ToAPIEndpoints(changes.Create), + Delete: adapter.ToAPIEndpoints(changes.Delete), + UpdateOld: adapter.ToAPIEndpoints(changes.UpdateOld), + UpdateNew: adapter.ToAPIEndpoints(changes.UpdateNew), + } + } + if err := json.NewEncoder(b).Encode(webhookChanges); err != nil { applyChangesErrorsGauge.Gauge.Inc() log.Debugf("Failed to encode changes: %s", err.Error()) return err @@ -240,7 +252,6 @@ func (p WebhookProvider) ApplyChanges(_ context.Context, changes *plan.Changes) // This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { adjustEndpointsRequestsGauge.Gauge.Inc() - var endpoints []*endpoint.Endpoint u, err := url.JoinPath(p.remoteServerURL.String(), webhookapi.UrlAdjustEndpoints) if err != nil { adjustEndpointsErrorsGauge.Gauge.Inc() @@ -249,7 +260,8 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.En } b := new(bytes.Buffer) - if err := json.NewEncoder(b).Encode(e); err != nil { + apiEps := adapter.ToAPIEndpoints(e) + if err := json.NewEncoder(b).Encode(apiEps); err != nil { adjustEndpointsErrorsGauge.Gauge.Inc() log.Debugf("Failed to encode endpoints, %s", err) return nil, err @@ -282,14 +294,14 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.En } return nil, err } - - if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { + apiEps = []*apiv1alpha1.Endpoint{} + if err := json.NewDecoder(resp.Body).Decode(&apiEps); err != nil { adjustEndpointsErrorsGauge.Gauge.Inc() log.Debugf("Failed to decode response body: %s", err.Error()) return nil, err } - return endpoints, nil + return adapter.FromAPIEndpoints(apiEps), nil } // GetDomainFilter make calls to get the serialized version of the domain filter diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index 85c2a3b55c..ff8494f887 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -28,7 +28,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - + apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" "sigs.k8s.io/external-dns/provider" @@ -127,7 +127,12 @@ func TestRecords(t *testing.T) { } assert.Equal(t, "/records", r.URL.Path) w.Write([]byte(`[{ - "dnsName" : "test.example.com" + "dnsName" : "test.example.com", + "recordType" : "A", + "targets" : ["1.1.1.1"], + "recordTTL" : 300, + "labels" : {"owner": "external-dns"}, + "providerSpecific" : [{"name": "prop1", "value": "value1"}] }]`)) })) defer svr.Close() @@ -139,6 +144,17 @@ func TestRecords(t *testing.T) { require.NotNil(t, endpoints) require.Equal(t, []*endpoint.Endpoint{{ DNSName: "test.example.com", + Targets: endpoint.Targets{ + "1.1.1.1", + }, + RecordType: "A", + RecordTTL: 300, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value1", + }, }}, endpoints) } @@ -224,29 +240,84 @@ func TestRecords_NonOKStatusCode(t *testing.T) { } func TestApplyChanges(t *testing.T) { - successfulApplyChanges := true svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.Header().Set(webhookapi.ContentTypeHeader, webhookapi.MediaTypeFormatAndVersion) w.Write([]byte(`{}`)) return } + assert.Equal(t, "/records", r.URL.Path) - if successfulApplyChanges { - w.WriteHeader(http.StatusNoContent) - } else { - w.WriteHeader(http.StatusInternalServerError) - } + defer r.Body.Close() + body, err := io.ReadAll(r.Body) + assert.NoError(t, err) + assert.JSONEq(t, `{ + "create": [ + { + "dnsName": "foo", + "providerSpecific": [{ "name": "prop1", "value": "value1" }], + "labels": { "owner": "external-dns" }, + "recordTTL": 10 + } + ], + "updateOld": [{ "dnsName": "bar" }], + "updateNew": [{ "dnsName": "baz" }], + "delete": [{ "dnsName": "qux" }] + }`, string(body), + ) + w.WriteHeader(http.StatusNoContent) })) defer svr.Close() p, err := NewWebhookProvider(svr.URL) require.NoError(t, err) - err = p.ApplyChanges(context.TODO(), nil) + err = p.ApplyChanges(context.TODO(), &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "foo", + RecordTTL: 10, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value1", + }, + }, + }, + UpdateOld: []*endpoint.Endpoint{ + { + DNSName: "bar", + }, + }, + UpdateNew: []*endpoint.Endpoint{ + { + DNSName: "baz", + }, + }, + Delete: []*endpoint.Endpoint{ + { + DNSName: "qux", + }, + }, + }) require.NoError(t, err) +} - successfulApplyChanges = false +func TestApplyChanges_InternalServerError(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(webhookapi.ContentTypeHeader, webhookapi.MediaTypeFormatAndVersion) + w.Write([]byte(`{}`)) + return + } + + assert.Equal(t, "/records", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer svr.Close() + p, err := NewWebhookProvider(svr.URL) + require.NoError(t, err) err = p.ApplyChanges(context.TODO(), nil) require.Error(t, err) require.ErrorIs(t, err, provider.SoftError) @@ -304,7 +375,7 @@ func TestAdjustEndpoints(t *testing.T) { } assert.Equal(t, webhookapi.UrlAdjustEndpoints, r.URL.Path) - var endpoints []*endpoint.Endpoint + var endpoints []*apiv1alpha1.Endpoint defer r.Body.Close() b, err := io.ReadAll(r.Body) if err != nil { @@ -333,6 +404,12 @@ func TestAdjustEndpoints(t *testing.T) { Targets: endpoint.Targets{ "", }, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value1", + }, }, } adjustedEndpoints, err := provider.AdjustEndpoints(endpoints) @@ -344,6 +421,12 @@ func TestAdjustEndpoints(t *testing.T) { Targets: endpoint.Targets{ "", }, + Labels: map[string]string{ + "owner": "external-dns", + }, + ProviderSpecific: endpoint.ProviderSpecific{ + "prop1": "value1", + }, }}, adjustedEndpoints) } @@ -387,7 +470,7 @@ func TestApplyChangesWithProviderSpecificProperty(t *testing.T) { if r.URL.Path == "/records" { w.Header().Set(webhookapi.ContentTypeHeader, webhookapi.MediaTypeFormatAndVersion) // assert that the request contains the provider-specific property - var changes plan.Changes + var changes webhookapi.Changes defer r.Body.Close() b, err := io.ReadAll(r.Body) assert.NoError(t, err) @@ -395,8 +478,8 @@ func TestApplyChangesWithProviderSpecificProperty(t *testing.T) { assert.NoError(t, err) assert.Len(t, changes.Create, 1) assert.Len(t, changes.Create[0].ProviderSpecific, 1) - assert.Equal(t, "prop1", changes.Create[0].ProviderSpecific[0].Name) - assert.Equal(t, "value1", changes.Create[0].ProviderSpecific[0].Value) + v := changes.Create[0].ProviderSpecific[0].Value + assert.Equal(t, "value1", v) w.WriteHeader(http.StatusNoContent) return } @@ -409,14 +492,11 @@ func TestApplyChangesWithProviderSpecificProperty(t *testing.T) { DNSName: "test.example.com", RecordTTL: 10, RecordType: "A", - Targets: endpoint.Targets{ + Targets: []string{ "", }, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "prop1", - Value: "value1", - }, + "prop1": "value1", }, } err = p.ApplyChanges(context.TODO(), &plan.Changes{ diff --git a/registry/dynamodb_test.go b/registry/dynamodb_test.go index 091927fabd..12b5f3a9a8 100644 --- a/registry/dynamodb_test.go +++ b/registry/dynamodb_test.go @@ -213,10 +213,7 @@ func TestDynamoDBRegistryRecords(t *testing.T) { endpoint.ResourceLabelKey: "ingress/default/other-ingress", }, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: dynamodbAttributeMigrate, - Value: "true", - }, + dynamodbAttributeMigrate: "true", }, }, { @@ -868,10 +865,7 @@ func TestDynamoDBRegistryApplyChanges(t *testing.T) { endpoint.ResourceLabelKey: "ingress/default/my-ingress", }, ProviderSpecific: endpoint.ProviderSpecific{ - { - Name: dynamodbAttributeMigrate, - Value: "true", - }, + dynamodbAttributeMigrate: "true", }, }, }, diff --git a/registry/txt_test.go b/registry/txt_test.go index b5146aa49b..d5ad9e58c3 100644 --- a/registry/txt_test.go +++ b/registry/txt_test.go @@ -424,11 +424,8 @@ func testTXTRegistryRecordsNoPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - { - Name: "alias", - Value: "true", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "alias": "true", }, }, { @@ -990,11 +987,8 @@ 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", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "txt/force-update": "true", }, }, { @@ -1004,11 +998,8 @@ func testTXTRegistryMissingRecordsNoPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - { - Name: "txt/force-update", - Value: "true", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "txt/force-update": "true", }, }, { @@ -1095,11 +1086,8 @@ 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", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "txt/force-update": "true", }, }, { @@ -1109,11 +1097,8 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - { - Name: "txt/force-update", - Value: "true", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "txt/force-update": "true", }, }, { @@ -1123,11 +1108,8 @@ func testTXTRegistryMissingRecordsWithPrefix(t *testing.T) { Labels: map[string]string{ endpoint.OwnerLabelKey: "owner", }, - ProviderSpecific: []endpoint.ProviderSpecificProperty{ - { - Name: "txt/force-update", - Value: "true", - }, + ProviderSpecific: endpoint.ProviderSpecific{ + "txt/force-update": "true", }, }, { diff --git a/source/ambassador_host_test.go b/source/ambassador_host_test.go index 561bc1e02a..3b18875047 100644 --- a/source/ambassador_host_test.go +++ b/source/ambassador_host_test.go @@ -273,10 +273,9 @@ func TestAmbassadorHostSource(t *testing.T) { DNSName: "www.example.org", RecordType: endpoint.RecordTypeA, Targets: endpoint.Targets{"1.1.1.1"}, - ProviderSpecific: endpoint.ProviderSpecific{{ - Name: "external-dns.alpha.kubernetes.io/cloudflare-proxied", - Value: "true", - }}, + ProviderSpecific: endpoint.ProviderSpecific{ + "external-dns.alpha.kubernetes.io/cloudflare-proxied": "true", + }, }, }, }, { diff --git a/source/annotations/provider_specific.go b/source/annotations/provider_specific.go index 31f059a51d..77b216c63e 100644 --- a/source/annotations/provider_specific.go +++ b/source/annotations/provider_specific.go @@ -21,65 +21,35 @@ import ( ) func ProviderSpecificAnnotations(annotations map[string]string) (endpoint.ProviderSpecific, string) { - providerSpecificAnnotations := endpoint.ProviderSpecific{} + providerSpecificAnnotations := make(endpoint.ProviderSpecific) if hasAliasFromAnnotations(annotations) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: "alias", - Value: "true", - }) + providerSpecificAnnotations.Set("alias", "true") } setIdentifier := "" for k, v := range annotations { if k == SetIdentifierKey { setIdentifier = v } else if attr, ok := strings.CutPrefix(k, AWSPrefix); ok { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: fmt.Sprintf("aws/%s", attr), - Value: v, - }) + providerSpecificAnnotations.Set(fmt.Sprintf("aws/%s", attr), v) } else if attr, ok := strings.CutPrefix(k, SCWPrefix); ok { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: fmt.Sprintf("scw/%s", attr), - Value: v, - }) + providerSpecificAnnotations.Set(fmt.Sprintf("scw/%s", attr), v) } else if attr, ok := strings.CutPrefix(k, WebhookPrefix); ok { // Support for wildcard annotations for webhook providers - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: fmt.Sprintf("webhook/%s", attr), - Value: v, - }) + providerSpecificAnnotations.Set(fmt.Sprintf("webhook/%s", attr), v) } else if attr, ok := strings.CutPrefix(k, CoreDNSPrefix); ok { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: fmt.Sprintf("coredns/%s", attr), - Value: v, - }) + providerSpecificAnnotations.Set(fmt.Sprintf("coredns/%s", attr), v) } else if strings.HasPrefix(k, CloudflarePrefix) { if strings.Contains(k, CloudflareCustomHostnameKey) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: CloudflareCustomHostnameKey, - Value: v, - }) + providerSpecificAnnotations.Set(CloudflareCustomHostnameKey, v) } else if strings.Contains(k, CloudflareProxiedKey) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: CloudflareProxiedKey, - Value: v, - }) + providerSpecificAnnotations.Set(CloudflareProxiedKey, v) } else if strings.Contains(k, CloudflareRegionKey) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: CloudflareRegionKey, - Value: v, - }) + providerSpecificAnnotations.Set(CloudflareRegionKey, v) } else if strings.Contains(k, CloudflareRecordCommentKey) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: CloudflareRecordCommentKey, - Value: v, - }) + providerSpecificAnnotations.Set(CloudflareRecordCommentKey, v) } else if strings.Contains(k, CloudflareTagsKey) { - providerSpecificAnnotations = append(providerSpecificAnnotations, endpoint.ProviderSpecificProperty{ - Name: CloudflareTagsKey, - Value: v, - }) + providerSpecificAnnotations.Set(CloudflareTagsKey, v) } } } diff --git a/source/annotations/provider_specific_test.go b/source/annotations/provider_specific_test.go index 684ea431ff..c534896d86 100644 --- a/source/annotations/provider_specific_test.go +++ b/source/annotations/provider_specific_test.go @@ -40,7 +40,7 @@ func TestProviderSpecificAnnotations(t *testing.T) { CloudflareProxiedKey: "true", }, expected: endpoint.ProviderSpecific{ - {Name: CloudflareProxiedKey, Value: "true"}, + CloudflareProxiedKey: "true", }, setIdentifier: "", }, @@ -50,7 +50,7 @@ func TestProviderSpecificAnnotations(t *testing.T) { CloudflareCustomHostnameKey: "custom.example.com", }, expected: endpoint.ProviderSpecific{ - {Name: CloudflareCustomHostnameKey, Value: "custom.example.com"}, + CloudflareCustomHostnameKey: "custom.example.com", }, setIdentifier: "", }, @@ -60,7 +60,7 @@ func TestProviderSpecificAnnotations(t *testing.T) { "external-dns.alpha.kubernetes.io/aws-weight": "100", }, expected: endpoint.ProviderSpecific{ - {Name: "aws/weight", Value: "100"}, + "aws/weight": "100", }, setIdentifier: "", }, @@ -70,7 +70,7 @@ func TestProviderSpecificAnnotations(t *testing.T) { "external-dns.alpha.kubernetes.io/coredns-group": "g1", }, expected: endpoint.ProviderSpecific{ - {Name: "coredns/group", Value: "g1"}, + "coredns/group": "g1", }, setIdentifier: "", }, @@ -119,11 +119,10 @@ func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == tc.expectedKey { - assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value) - return - } + value, exists := providerSpecificAnnotations.Get(tc.expectedKey) + if exists { + assert.Equal(t, tc.expectedValue, value) + return } t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue) }) @@ -160,11 +159,9 @@ func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == tc.expectedKey { - assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value) - return - } + if value, exists := providerSpecificAnnotations.Get(tc.expectedKey); exists { + assert.Equal(t, strconv.FormatBool(tc.expectedValue), value) + return } t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue) }) @@ -203,11 +200,9 @@ func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == tc.expectedKey { - assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value) - return - } + if value, exists := providerSpecificAnnotations.Get(tc.expectedKey); exists { + assert.Equal(t, tc.expectedValue, value) + return } t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %v", tc.expectedKey, tc.expectedValue) }) @@ -237,11 +232,9 @@ func TestGetProviderSpecificCloudflareAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == tc.expectedKey { - assert.Equal(t, tc.expectedValue, providerSpecificAnnotation.Value) - return - } + if value, exists := providerSpecificAnnotations.Get(tc.expectedKey); exists { + assert.Equal(t, tc.expectedValue, value) + return } t.Errorf("Cloudflare provider specific annotation %s is not set correctly to %s", tc.expectedKey, tc.expectedValue) }) @@ -274,11 +267,9 @@ func TestGetProviderSpecificAliasAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == "alias" { - assert.Equal(t, strconv.FormatBool(tc.expectedValue), providerSpecificAnnotation.Value) - return - } + if value, exists := providerSpecificAnnotations.Get("alias"); exists { + assert.Equal(t, strconv.FormatBool(tc.expectedValue), value) + return } t.Errorf("provider specific annotation alias is not set correctly to %v", tc.expectedValue) }) @@ -302,10 +293,8 @@ func TestGetProviderSpecificAliasAnnotations(t *testing.T) { } { t.Run(tc.title, func(t *testing.T) { providerSpecificAnnotations, _ := ProviderSpecificAnnotations(tc.annotations) - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == "alias" { - t.Error("provider specific annotation alias is not expected to be set") - } + if _, exists := providerSpecificAnnotations.Get("alias"); exists { + t.Error("provider specific annotation alias is not expected to be set") } }) @@ -363,15 +352,9 @@ func TestGetProviderSpecificIdentifierAnnotations(t *testing.T) { providerSpecificAnnotations, identifier := ProviderSpecificAnnotations(tc.annotations) assert.Equal(t, tc.expectedIdentifier, identifier) for expectedAnnotationKey, expectedAnnotationValue := range tc.expectedResult { - expectedResultFound := false - for _, providerSpecificAnnotation := range providerSpecificAnnotations { - if providerSpecificAnnotation.Name == expectedAnnotationKey { - assert.Equal(t, expectedAnnotationValue, providerSpecificAnnotation.Value) - expectedResultFound = true - break - } - } - if !expectedResultFound { + if value, exists := providerSpecificAnnotations.Get(expectedAnnotationKey); exists { + assert.Equal(t, expectedAnnotationValue, value) + } else { t.Errorf("provider specific annotation %s has not been set", expectedAnnotationKey) } } diff --git a/source/crd.go b/source/crd.go index b9d79bb6fa..c423eb05b2 100644 --- a/source/crd.go +++ b/source/crd.go @@ -26,6 +26,7 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/external-dns/pkg/adapter" "sigs.k8s.io/external-dns/source/annotations" log "github.com/sirupsen/logrus" @@ -181,7 +182,8 @@ func (cs *crdSource) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, error for _, dnsEndpoint := range result.Items { var crdEndpoints []*endpoint.Endpoint - for _, ep := range dnsEndpoint.Spec.Endpoints { + for _, crdEp := range dnsEndpoint.Spec.Endpoints { + ep := adapter.FromAPIEndpoint(crdEp) if (ep.RecordType == endpoint.RecordTypeCNAME || ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA) && len(ep.Targets) < 1 { log.Debugf("Endpoint %s with DNSName %s has an empty list of targets, allowing it to pass through for default-targets processing", dnsEndpoint.Name, ep.DNSName) } diff --git a/source/crd_test.go b/source/crd_test.go index ec87b4149d..46afabc3cd 100644 --- a/source/crd_test.go +++ b/source/crd_test.go @@ -43,6 +43,7 @@ import ( cachetesting "k8s.io/client-go/tools/cache/testing" apiv1alpha1 "sigs.k8s.io/external-dns/apis/v1alpha1" "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/pkg/adapter" ) type CRDSuite struct { @@ -62,7 +63,7 @@ func objBody(codec runtime.Encoder, obj runtime.Object) io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) } -func fakeRESTClient(endpoints []*endpoint.Endpoint, apiVersion, kind, namespace, name string, annotations map[string]string, labels map[string]string, _ *testing.T) rest.Interface { +func fakeRESTClient(endpoints []*apiv1alpha1.Endpoint, apiVersion, kind, namespace, name string, annotations map[string]string, labels map[string]string, _ *testing.T) rest.Interface { groupVersion, _ := schema.ParseGroupVersion(apiVersion) scheme := runtime.NewScheme() _ = addKnownTypes(scheme, groupVersion) @@ -143,7 +144,7 @@ func testCRDSourceEndpoints(t *testing.T) { apiVersion string registeredKind string kind string - endpoints []*endpoint.Endpoint + endpoints []*apiv1alpha1.Endpoint expectEndpoints bool expectError bool annotationFilter string @@ -157,7 +158,7 @@ func testCRDSourceEndpoints(t *testing.T) { apiVersion: "blah.k8s.io/v1alpha1", registeredKind: "DNSEndpoint", kind: "DNSEndpoint", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -174,7 +175,7 @@ func testCRDSourceEndpoints(t *testing.T) { apiVersion: "test.k8s.io/v1alpha1", registeredKind: "DNSEndpoint", kind: "JustEndpoint", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -193,7 +194,7 @@ func testCRDSourceEndpoints(t *testing.T) { kind: "DNSEndpoint", namespace: "foo", registeredNamespace: "foo", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -212,7 +213,7 @@ func testCRDSourceEndpoints(t *testing.T) { kind: "DNSEndpoint", namespace: "foo", registeredNamespace: "bar", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -231,7 +232,7 @@ func testCRDSourceEndpoints(t *testing.T) { kind: "DNSEndpoint", namespace: "foo", registeredNamespace: "foo", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "no-targets.example.org", Targets: endpoint.Targets{}, @@ -250,7 +251,7 @@ func testCRDSourceEndpoints(t *testing.T) { kind: "DNSEndpoint", namespace: "foo", registeredNamespace: "foo", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -269,7 +270,7 @@ func testCRDSourceEndpoints(t *testing.T) { kind: "DNSEndpoint", namespace: "foo", registeredNamespace: "foo", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -296,7 +297,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", annotations: map[string]string{"test": "that"}, annotationFilter: "test=filter_something_else", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -317,7 +318,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", annotations: map[string]string{"test": "that"}, annotationFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -338,7 +339,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=filter_something_else", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -359,7 +360,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"1.2.3.4"}, @@ -380,7 +381,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "abc.example.org", Targets: endpoint.Targets{"ns1.k8s.io", "ns2.k8s.io"}, @@ -401,7 +402,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "_svc._tcp.example.org", Targets: endpoint.Targets{"0 0 80 abc.example.org", "0 0 80 def.example.org"}, @@ -422,7 +423,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org.`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org.`}, @@ -443,7 +444,7 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "example.org", Targets: endpoint.Targets{"foo.example.org."}, @@ -464,10 +465,10 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "example.org", - Targets: endpoint.Targets{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org`}, + Targets: []string{`100 10 "S" "SIP+D2U" "!^.*$!sip:customer-service@example.org!" _sip._udp.example.org`, `102 10 "S" "SIP+D2T" "!^.*$!sip:customer-service@example.org!" _sip._tcp.example.org`}, RecordType: endpoint.RecordTypeNAPTR, RecordTTL: 180, }, @@ -485,10 +486,10 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "example.org", - Targets: endpoint.Targets{"foo.example.org."}, + Targets: []string{"foo.example.org."}, RecordType: endpoint.RecordTypeTXT, RecordTTL: 180, }, @@ -506,10 +507,10 @@ func testCRDSourceEndpoints(t *testing.T) { registeredNamespace: "foo", labels: map[string]string{"test": "that"}, labelFilter: "test=that", - endpoints: []*endpoint.Endpoint{ + endpoints: []*apiv1alpha1.Endpoint{ { DNSName: "example.org", - Targets: endpoint.Targets{"1.2.3.4."}, + Targets: []string{"1.2.3.4."}, RecordType: endpoint.RecordTypeA, RecordTTL: 180, }, @@ -555,7 +556,8 @@ func testCRDSourceEndpoints(t *testing.T) { } // Validate received endpoints against expected endpoints. - validateEndpoints(t, receivedEndpoints, ti.endpoints) + endpoints := adapter.FromAPIEndpoints(ti.endpoints) + validateEndpoints(t, receivedEndpoints, endpoints) for _, e := range receivedEndpoints { // TODO: at the moment not all sources apply ResourceLabelKey @@ -796,7 +798,7 @@ func generateTestFixtureDNSEndpointsByType(namespace string, typeCounts map[stri Namespace: namespace, }, Spec: apiv1alpha1.DNSEndpointSpec{ - Endpoints: []*endpoint.Endpoint{ + Endpoints: []*apiv1alpha1.Endpoint{ { DNSName: strings.ToLower(fmt.Sprintf("%s-%d.example.com", rt, idx)), RecordType: rt, diff --git a/source/endpoints_test.go b/source/endpoints_test.go index e64f903e18..9caa1e4749 100644 --- a/source/endpoints_test.go +++ b/source/endpoints_test.go @@ -43,7 +43,7 @@ func TestEndpointsForHostname(t *testing.T) { targets: endpoint.Targets{"192.0.2.1", "192.0.2.2"}, ttl: endpoint.TTL(300), providerSpecific: endpoint.ProviderSpecific{ - {Name: "provider", Value: "value"}, + "provider": "value", }, setIdentifier: "identifier", resource: "resource", @@ -53,7 +53,7 @@ func TestEndpointsForHostname(t *testing.T) { Targets: endpoint.Targets{"192.0.2.1", "192.0.2.2"}, RecordType: endpoint.RecordTypeA, RecordTTL: endpoint.TTL(300), - ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}}, + ProviderSpecific: endpoint.ProviderSpecific{"provider": "value"}, SetIdentifier: "identifier", Labels: map[string]string{endpoint.ResourceLabelKey: "resource"}, }, @@ -65,7 +65,7 @@ func TestEndpointsForHostname(t *testing.T) { targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, ttl: endpoint.TTL(300), providerSpecific: endpoint.ProviderSpecific{ - {Name: "provider", Value: "value"}, + "provider": "value", }, setIdentifier: "identifier", resource: "resource", @@ -75,7 +75,7 @@ func TestEndpointsForHostname(t *testing.T) { Targets: endpoint.Targets{"2001:db8::1", "2001:db8::2"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: endpoint.TTL(300), - ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}}, + ProviderSpecific: endpoint.ProviderSpecific{"provider": "value"}, SetIdentifier: "identifier", Labels: map[string]string{endpoint.ResourceLabelKey: "resource"}, }, @@ -87,7 +87,7 @@ func TestEndpointsForHostname(t *testing.T) { targets: endpoint.Targets{"cname.example.com"}, ttl: endpoint.TTL(300), providerSpecific: endpoint.ProviderSpecific{ - {Name: "provider", Value: "value"}, + "provider": "value", }, setIdentifier: "identifier", resource: "resource", @@ -97,7 +97,7 @@ func TestEndpointsForHostname(t *testing.T) { Targets: endpoint.Targets{"cname.example.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: endpoint.TTL(300), - ProviderSpecific: endpoint.ProviderSpecific{{Name: "provider", Value: "value"}}, + ProviderSpecific: endpoint.ProviderSpecific{"provider": "value"}, SetIdentifier: "identifier", Labels: map[string]string{endpoint.ResourceLabelKey: "resource"}, }, diff --git a/source/gateway_httproute_test.go b/source/gateway_httproute_test.go index 2edbf89654..10957ce568 100644 --- a/source/gateway_httproute_test.go +++ b/source/gateway_httproute_test.go @@ -1471,8 +1471,8 @@ func TestGatewayHTTPRouteSourceEndpoints(t *testing.T) { newTestEndpoint("test.two.internal", "A", "2.3.4.5"), }, logExpectations: []string{ - "Endpoints generated from HTTPRoute default/one: [test.one.internal 0 IN A 1.2.3.4 []]", - "Endpoints generated from HTTPRoute default/two: [test.two.internal 0 IN A 2.3.4.5 []]", + "Endpoints generated from HTTPRoute default/one: [test.one.internal 0 IN A 1.2.3.4 map[]]", + "Endpoints generated from HTTPRoute default/two: [test.two.internal 0 IN A 2.3.4.5 map[]]", }, }, { diff --git a/source/gloo_proxy_test.go b/source/gloo_proxy_test.go index 03eb12b364..23e69af3ef 100644 --- a/source/gloo_proxy_test.go +++ b/source/gloo_proxy_test.go @@ -507,10 +507,7 @@ func TestGlooSource(t *testing.T) { RecordTTL: 42, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "aws/geolocation-country-code", - Value: "LU", - }, + "aws/geolocation-country-code": "LU", }, }, { @@ -529,10 +526,7 @@ func TestGlooSource(t *testing.T) { RecordTTL: 24, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "aws/geolocation-country-code", - Value: "JP", - }, + "aws/geolocation-country-code": "JP", }, }, { @@ -559,10 +553,7 @@ func TestGlooSource(t *testing.T) { RecordTTL: 420, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "aws/geolocation-country-code", - Value: "ES", - }, + "aws/geolocation-country-code": "ES", }, }, { @@ -580,10 +571,7 @@ func TestGlooSource(t *testing.T) { RecordTTL: 460, Labels: endpoint.Labels{}, ProviderSpecific: endpoint.ProviderSpecific{ - endpoint.ProviderSpecificProperty{ - Name: "aws/geolocation-country-code", - Value: "IT", - }, + "aws/geolocation-country-code": "IT", }, }, }) diff --git a/source/ingress_test.go b/source/ingress_test.go index 6452383e20..08de7dfeb7 100644 --- a/source/ingress_test.go +++ b/source/ingress_test.go @@ -1084,9 +1084,9 @@ func testIngressEndpoints(t *testing.T) { DNSName: "example.org", Targets: endpoint.Targets{"ingress-target.com"}, RecordType: endpoint.RecordTypeCNAME, - ProviderSpecific: endpoint.ProviderSpecific{{ - Name: "alias", Value: "true", - }}, + ProviderSpecific: endpoint.ProviderSpecific{ + "alias": "true", + }, }, }, }, diff --git a/source/wrappers/multisource_test.go b/source/wrappers/multisource_test.go index 082ed1c7cf..61e5c44735 100644 --- a/source/wrappers/multisource_test.go +++ b/source/wrappers/multisource_test.go @@ -47,6 +47,17 @@ func testMultiSourceImplementsSource(t *testing.T) { func testMultiSourceEndpoints(t *testing.T) { foo := &endpoint.Endpoint{DNSName: "foo", Targets: endpoint.Targets{"8.8.8.8"}} bar := &endpoint.Endpoint{DNSName: "bar", Targets: endpoint.Targets{"8.8.4.4"}} + // Create thread-safe copies of endpoints for concurrent test execution + cloneEndpointsForTesting := func(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { + copies := make([]*endpoint.Endpoint, 0, len(endpoints)) + for _, ep := range endpoints { + copies = append(copies, &endpoint.Endpoint{ + DNSName: ep.DNSName, + Targets: ep.Targets, + }) + } + return copies + } for _, tc := range []struct { title string @@ -65,13 +76,16 @@ func testMultiSourceEndpoints(t *testing.T) { }, { "single non-empty child source returns child's endpoints", - [][]*endpoint.Endpoint{{foo.DeepCopy()}}, - []*endpoint.Endpoint{foo.DeepCopy()}, + [][]*endpoint.Endpoint{cloneEndpointsForTesting([]*endpoint.Endpoint{foo})}, + cloneEndpointsForTesting([]*endpoint.Endpoint{foo}), }, { "multiple non-empty child sources returns merged children's endpoints", - [][]*endpoint.Endpoint{{foo.DeepCopy()}, {bar.DeepCopy()}}, - []*endpoint.Endpoint{foo.DeepCopy(), bar.DeepCopy()}, + [][]*endpoint.Endpoint{ + cloneEndpointsForTesting([]*endpoint.Endpoint{foo}), + cloneEndpointsForTesting([]*endpoint.Endpoint{bar}), + }, + cloneEndpointsForTesting([]*endpoint.Endpoint{foo, bar}), }, } { diff --git a/source/wrappers/nat64source.go b/source/wrappers/nat64source.go index 2c2527db4d..1d8b0ddd68 100644 --- a/source/wrappers/nat64source.go +++ b/source/wrappers/nat64source.go @@ -19,6 +19,7 @@ package wrappers import ( "context" "fmt" + "maps" "net/netip" log "github.com/sirupsen/logrus" @@ -102,9 +103,23 @@ func (s *nat64Source) Endpoints(ctx context.Context) ([]*endpoint.Endpoint, erro continue } - v4EP := ep.DeepCopy() - v4EP.Targets = v4Targets - v4EP.RecordType = endpoint.RecordTypeA + v4EP := &endpoint.Endpoint{ + DNSName: ep.DNSName, + Targets: v4Targets, + RecordType: endpoint.RecordTypeA, + SetIdentifier: ep.SetIdentifier, + RecordTTL: ep.RecordTTL, + } + + if ep.Labels != nil { + v4EP.Labels = make(endpoint.Labels, len(ep.Labels)) + maps.Copy(v4EP.Labels, ep.Labels) + } + + if ep.ProviderSpecific != nil { + v4EP.ProviderSpecific = make(endpoint.ProviderSpecific, len(ep.ProviderSpecific)) + maps.Copy(v4EP.ProviderSpecific, ep.ProviderSpecific) + } additionalEndpoints = append(additionalEndpoints, v4EP) }