Skip to content

Commit

Permalink
feat: Add external-dns(v0.14.0) as a dependency
Browse files Browse the repository at this point in the history
Adds external-dns (latest released version v0.14.0) as a go module
dependency and updates the dnsrecord controller and dns provider logic
to align with an external-dns approach to dns management.

dnsrecord changes:
* Replace v1alpha1.Endpoint with externaldns Endpoint
* Add GetRootDomain to DNSRecord, returns the shortest domain that is
  shared across all spec.Endpoints dns names.

provider changes:

* Remove Ensure and Delete and instead include
  externaldnsprovider.Provider in provider.Provider interface.
* Add provider.Config struct to hold common provider configuration,
  currently it only handles filters. Update provider Factory
constructors accept the config as input.
* Set DomainFilter, ZoneTypeFilter and ZoneIDFilter in provider
  implementations from provider.Config.

provider aws changes:

* Implement AdjustEndpoints to change our generic provider specific
  fields for weight and geo into aws ones understood by external-dns.

provider google changes:

* Copy external-dns google provider
* Adds implementations of funtions required to translate endpoints and
  resourceRecordSets into the different states.
  - endpointsToProviderFormat - converts a list of endpoints into a
    google specific format.
  - endpointsFromResourceRecordSets - converts a list of
    `ResourceRecordSet` into endpoints (google format).
  - resourceRecordSetFromEndpoint - converts an endpoint(google format)
    into a `ResourceRecordSet`.

controller changes:

* Update DNSRecord controller to use applyChanges which follows an
  external-dns process to determine a set of
changes(Create/Update/Delete) that are required to be made against the
provider.
* Use noop registry, no owner is currently set or checked for any
  records.
* Setup provider filters:
  - DomainFilter = root domain for endpoints determined by calling
    dnsRecord.GetRootDomain()
  - ZoneTypeFilter = "", public or private zones considered.
  - ZoneIDFilter = set to single hosted zone id (managedZone.Status.ID)
* Use sync policy, all changes will be applied (Create, Update and
  Deletes)
* Use plan with above and using the current zone(plan.Current) and spec
  endpoints(plan.Desired) as input.

additional changes:

downgrade gatewayapiv1 to v0.7.1. This is required since the external
dns aws provider is including the external dns source package which has
a dependency on Gateway API v0.7.1. Downgrading is less than ideal, but
dns-operator shouldn't have a dependency on gateway api, and the health
check code that is currently causing it will likely be removed soon
anyway in favour of provider health checks. There is also an opportunity
to change external-dns to prevent it including the source package since
it's including here for no reason other than getting a version
https://github.com/kubernetes-sigs/external-dns/blob/master/provider/aws/session.go#L72
  • Loading branch information
mikenairn committed Mar 5, 2024
1 parent f07b39a commit 1b87bf4
Show file tree
Hide file tree
Showing 18 changed files with 2,372 additions and 912 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ build: manifests generate fmt vet ## Build manager binary.

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
go run ./cmd/main.go
go run ./cmd/main.go --zap-log-level=3

# If you wish built the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it.
Expand Down
120 changes: 35 additions & 85 deletions api/v1alpha1/dnsrecord_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,101 +18,20 @@ package v1alpha1

import (
"fmt"
"strings"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
externaldns "sigs.k8s.io/external-dns/endpoint"
)

// 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"`
}

// Targets is a representation of a list of targets for an endpoint.
type Targets []string

// TTL is a structure defining the TTL of a DNS record
type TTL int64

// Labels store metadata related to the endpoint
// it is then stored in a persistent storage via serialization
type Labels map[string]string

// 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
type Endpoint struct {
// The hostname of the DNS record
DNSName string `json:"dnsName,omitempty"`
// The targets the DNS record points to
Targets Targets `json:"targets,omitempty"`
// RecordType type of record, e.g. CNAME, A, 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 TTL `json:"recordTTL,omitempty"`
// Labels stores labels defined for the Endpoint
// +optional
Labels Labels `json:"labels,omitempty"`
// ProviderSpecific stores provider specific config
// +optional
ProviderSpecific ProviderSpecific `json:"providerSpecific,omitempty"`
}

// SetID returns an id that should be unique across a set of endpoints
func (e *Endpoint) SetID() string {
return e.DNSName + e.SetIdentifier
}

// WithSetIdentifier applies the given set identifier to the endpoint.
func (e *Endpoint) WithSetIdentifier(setIdentifier string) *Endpoint {
e.SetIdentifier = setIdentifier
return e
}

// GetProviderSpecificProperty returns a ProviderSpecificProperty if the property exists.
func (e *Endpoint) GetProviderSpecificProperty(key string) (ProviderSpecificProperty, bool) {
for _, providerSpecific := range e.ProviderSpecific {
if providerSpecific.Name == key {
return providerSpecific, true
}
}
return ProviderSpecificProperty{}, false
}

// SetProviderSpecific sets a provider specific key/value pair.
func (e *Endpoint) SetProviderSpecific(name, value string) {
if e.ProviderSpecific == nil {
e.ProviderSpecific = ProviderSpecific{}
}

for i, pair := range e.ProviderSpecific {
if pair.Name == name {
e.ProviderSpecific[i].Value = value
return
}
}

e.ProviderSpecific = append(e.ProviderSpecific, ProviderSpecificProperty{
Name: name,
Value: value,
})
}

func (e *Endpoint) String() string {
return fmt.Sprintf("%s %d IN %s %s %s %s", e.DNSName, e.RecordTTL, e.RecordType, e.SetIdentifier, e.Targets, e.ProviderSpecific)
}

// DNSRecordSpec defines the desired state of DNSRecord
type DNSRecordSpec struct {
// +kubebuilder:validation:Required
// +required
ManagedZoneRef *ManagedZoneReference `json:"managedZone,omitempty"`
// +kubebuilder:validation:MinItems=1
// +optional
Endpoints []*Endpoint `json:"endpoints,omitempty"`
Endpoints []*externaldns.Endpoint `json:"endpoints,omitempty"`
}

// DNSRecordStatus defines the observed state of DNSRecord
Expand Down Expand Up @@ -140,7 +59,7 @@ type DNSRecordStatus struct {
//
// Note: This will not be required if/when we switch to using external-dns since when
// running with a "sync" policy it will clean up unused records automatically.
Endpoints []*Endpoint `json:"endpoints,omitempty"`
Endpoints []*externaldns.Endpoint `json:"endpoints,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down Expand Up @@ -182,6 +101,37 @@ const (
DefaultGeo string = "default"
)

// GetRootDomain returns the shortest domain that is shared across all spec.Endpoints dns names.
// Validates that all endpoints share an equal root domain and returns an error if they don't.
func (s *DNSRecord) GetRootDomain() (string, error) {
domain := ""
dnsNames := []string{}
for idx := range s.Spec.Endpoints {
dnsNames = append(dnsNames, s.Spec.Endpoints[idx].DNSName)
}
for idx := range dnsNames {
if domain == "" {
domain = dnsNames[0]
continue
}
if len(domain) > len(dnsNames[idx]) {
domain = dnsNames[idx]
}
}

if domain == "" {
return "", fmt.Errorf("unable to determine root domain from %v", dnsNames)
}

for idx := range dnsNames {
if !strings.HasSuffix(dnsNames[idx], domain) {
return "", fmt.Errorf("inconsitent domains, got %s, expected suffix %s", dnsNames[idx], domain)
}
}

return domain, nil
}

func init() {
SchemeBuilder.Register(&DNSRecord{}, &DNSRecordList{})
}
81 changes: 81 additions & 0 deletions api/v1alpha1/dnsrecord_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package v1alpha1

import (
"testing"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/external-dns/endpoint"
)

func TestDNSRecord_GetRootDomain(t *testing.T) {
tests := []struct {
name string
dnsNames []string
want string
wantErr bool
}{
{
name: "single endpoint",
dnsNames: []string{
"test.example.com",
},
want: "test.example.com",
wantErr: false,
},
{
name: "multiple endpoints matching",
dnsNames: []string{
"bar.baz.test.example.com",
"bar.test.example.com",
"test.example.com",
"foo.bar.baz.test.example.com",
},
want: "test.example.com",
wantErr: false,
},
{
name: "no endpoints",
dnsNames: []string{},
want: "",
wantErr: true,
},
{
name: "multiple endpoints mismatching",
dnsNames: []string{
"foo.bar.test.example.com",
"bar.test.example.com",
"baz.example.com",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &DNSRecord{
TypeMeta: metav1.TypeMeta{
Kind: "DNSRecord",
APIVersion: GroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: "testRecord",
Namespace: "testNS",
},
Spec: DNSRecordSpec{
Endpoints: []*endpoint.Endpoint{},
},
}
for idx := range tt.dnsNames {
s.Spec.Endpoints = append(s.Spec.Endpoints, &endpoint.Endpoint{DNSName: tt.dnsNames[idx]})
}
got, err := s.GetRootDomain()
if (err != nil) != tt.wantErr {
t.Errorf("GetRootDomain() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetRootDomain() got = %v, want %v", got, tt.want)
}
})
}
}
115 changes: 5 additions & 110 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1b87bf4

Please sign in to comment.