Skip to content

Commit

Permalink
Merge pull request #55 from Kuadrant/gh-50
Browse files Browse the repository at this point in the history
add optional rootHost
  • Loading branch information
maleck13 authored Mar 14, 2024
2 parents 41d07c2 + ae6846a commit 6144e5f
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 84 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*.dll
*.so
*.dylib
*.env
bin/*
Dockerfile.cross

Expand All @@ -29,3 +30,4 @@ Dockerfile.cross
tmp

config/local-setup/**/*.env
local
53 changes: 32 additions & 21 deletions api/v1alpha1/dnsrecord_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ import (

// DNSRecordSpec defines the desired state of DNSRecord
type DNSRecordSpec struct {

// rootHost is the single root for all endpoints in a DNSRecord.
//If rootHost is set, it is expected all defined endpoints are children of or equal to this rootHost
// +optional
RootHost *string `json:"rootHost,omitempty"`
// +kubebuilder:validation:Required
// +required
ManagedZoneRef *ManagedZoneReference `json:"managedZone,omitempty"`
Expand Down Expand Up @@ -101,31 +106,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 == "" || len(domain) > len(dnsNames[idx]) {
domain = dnsNames[idx]
}
}
const WildcardPrefix = "*."

if domain == "" {
return "", fmt.Errorf("unable to determine root domain from %v", dnsNames)
}
func (s *DNSRecord) Validate() error {
if s.Spec.RootHost != nil {
root := *s.Spec.RootHost
if len(strings.Split(root, ".")) <= 1 {
return fmt.Errorf("invalid domain format no tld discovered")
}
if len(s.Spec.Endpoints) == 0 {
return fmt.Errorf("no endpoints defined for DNSRecord. Nothing to do.")
}

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

rootEndpointFound := false
for _, ep := range s.Spec.Endpoints {
if !strings.HasSuffix(ep.DNSName, root) {
return fmt.Errorf("invalid endpoint discovered %s all endpoints should be equal to or end with the rootHost %s", ep.DNSName, root)
}
if !rootEndpointFound {
//check original root
if ep.DNSName == *s.Spec.RootHost {
rootEndpointFound = true
}
}
}
if !rootEndpointFound {
return fmt.Errorf("invalid endpoint set. rootHost is set but found no endpoint defining a record for the rootHost %s", root)
}
}

return domain, nil
return nil
}

func init() {
Expand Down
84 changes: 46 additions & 38 deletions api/v1alpha1/dnsrecord_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,79 +3,87 @@ package v1alpha1
import (
"testing"

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

func TestDNSRecord_GetRootDomain(t *testing.T) {
func TestValidate(t *testing.T) {
tests := []struct {
name string
rootHost string
dnsNames []string
want string
wantErr bool
}{
{
name: "single endpoint",
name: "invalid domain",
rootHost: "example",
wantErr: true,
},
{
name: "no endpoints",
rootHost: "example.com",
wantErr: true,
},
{
name: "invalid domain",
rootHost: "example.com",
dnsNames: []string{
"test.example.com",
"example.com",
"a.exmple.com",
},
want: "test.example.com",
wantErr: false,
wantErr: true,
},
{
name: "multiple endpoints matching",
name: "valid domain",
rootHost: "example.com",
dnsNames: []string{
"bar.baz.test.example.com",
"bar.test.example.com",
"test.example.com",
"foo.bar.baz.test.example.com",
"example.com",
"a.b.example.com",
"b.a.example.com",
"a.example.com",
"b.example.com",
},
want: "test.example.com",
wantErr: false,
},
{
name: "no endpoints",
dnsNames: []string{},
want: "",
wantErr: true,
name: "valid wildcard domain",
rootHost: "*.example.com",
dnsNames: []string{
"*.example.com",
"a.b.example.com",
"b.a.example.com",
"a.example.com",
"b.example.com",
},
wantErr: false,
},
{
name: "multiple endpoints mismatching",
name: "valid wildcard domain no endpoint",
rootHost: "*.example.com",
dnsNames: []string{
"foo.bar.test.example.com",
"bar.test.example.com",
"baz.example.com",
"a.b.example.com",
"b.a.example.com",
"a.example.com",
"b.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",
},
record := &DNSRecord{
Spec: DNSRecordSpec{
Endpoints: []*endpoint.Endpoint{},
RootHost: &tt.rootHost,
},
}
for idx := range tt.dnsNames {
s.Spec.Endpoints = append(s.Spec.Endpoints, &endpoint.Endpoint{DNSName: tt.dnsNames[idx]})
record.Spec.Endpoints = append(record.Spec.Endpoints, &endpoint.Endpoint{DNSName: tt.dnsNames[idx]})
}
got, err := s.GetRootDomain()
err := record.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("GetRootDomain() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GetRootDomain() got = %v, want %v", got, tt.want)
}
})
}
}
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

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

2 changes: 1 addition & 1 deletion bundle/manifests/dns-operator.clusterserviceversion.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ metadata:
capabilities: Basic Install
categories: Integration & Delivery
containerImage: quay.io/kuadrant/dns-operator:latest
createdAt: "2024-03-11T11:01:25Z"
createdAt: "2024-03-11T14:53:47Z"
description: A Kubernetes Operator to manage the lifecycle of DNS resources
operators.operatorframework.io/builder: operator-sdk-v1.33.0
operators.operatorframework.io/project_layout: go.kubebuilder.io/v4
Expand Down
5 changes: 5 additions & 0 deletions bundle/manifests/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ spec:
required:
- name
type: object
rootHost:
description: "rootHost is the single root for all endpoints in a DNSRecord.
If rootHost is set, it is expected all defined endpoints are children
\tof or equal to this rootHost"
type: string
type: object
status:
description: DNSRecordStatus defines the observed state of DNSRecord
Expand Down
5 changes: 5 additions & 0 deletions config/crd/bases/kuadrant.io_dnsrecords.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ spec:
required:
- name
type: object
rootHost:
description: "rootHost is the single root for all endpoints in a DNSRecord.
If rootHost is set, it is expected all defined endpoints are children
\tof or equal to this rootHost"
type: string
type: object
status:
description: DNSRecordStatus defines the observed state of DNSRecord
Expand Down
52 changes: 28 additions & 24 deletions internal/controller/dnsrecord_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
externaldnsendpoint "sigs.k8s.io/external-dns/endpoint"
externaldnsplan "sigs.k8s.io/external-dns/plan"
externaldnsprovider "sigs.k8s.io/external-dns/provider"
Expand Down Expand Up @@ -99,31 +100,39 @@ func (r *DNSRecordReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
status := metav1.ConditionTrue
reason = "ProviderSuccess"
message = "Provider ensured the dns record"

err = dnsRecord.Validate()
if err != nil {
status = metav1.ConditionFalse
reason = "ValidationError"
message = fmt.Sprintf("validation of DNSRecord failed: %v", err)
setDNSRecordCondition(dnsRecord, string(conditions.ConditionTypeReady), status, reason, message)
return r.updateStatus(ctx, previous, dnsRecord)
}
// Publish the record
err = r.publishRecord(ctx, dnsRecord)
if err != nil {
status = metav1.ConditionFalse
reason = "ProviderError"
message = fmt.Sprintf("The DNS provider failed to ensure the record: %v", provider.SanitizeError(err))
} else {
dnsRecord.Status.ObservedGeneration = dnsRecord.Generation
dnsRecord.Status.Endpoints = dnsRecord.Spec.Endpoints
setDNSRecordCondition(dnsRecord, string(conditions.ConditionTypeReady), status, reason, message)
return r.updateStatus(ctx, previous, dnsRecord)
}
// success
setDNSRecordCondition(dnsRecord, string(conditions.ConditionTypeReady), status, reason, message)
dnsRecord.Status.ObservedGeneration = dnsRecord.Generation
dnsRecord.Status.Endpoints = dnsRecord.Spec.Endpoints
return r.updateStatus(ctx, previous, dnsRecord)
}

if !equality.Semantic.DeepEqual(previous.Status, dnsRecord.Status) {
updateErr := r.Status().Update(ctx, dnsRecord)
if updateErr != nil {
// Ignore conflicts, resource might just be outdated.
if apierrors.IsConflict(updateErr) {
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, updateErr
func (r *DNSRecordReconciler) updateStatus(ctx context.Context, previous, current *v1alpha1.DNSRecord) (reconcile.Result, error) {
if !equality.Semantic.DeepEqual(previous.Status, current.Status) {
updateError := r.Status().Update(ctx, current)
if apierrors.IsConflict(updateError) {
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, updateError
}

return ctrl.Result{}, err
return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
Expand Down Expand Up @@ -175,7 +184,6 @@ func (r *DNSRecordReconciler) deleteRecord(ctx context.Context, dnsRecord *v1alp
// DNSRecord (dnsRecord.Status.ParentManagedZone).
func (r *DNSRecordReconciler) publishRecord(ctx context.Context, dnsRecord *v1alpha1.DNSRecord) error {
logger := log.FromContext(ctx)

managedZone := &v1alpha1.ManagedZone{
ObjectMeta: metav1.ObjectMeta{
Name: dnsRecord.Spec.ManagedZoneRef.Name,
Expand Down Expand Up @@ -220,22 +228,18 @@ func setDNSRecordCondition(dnsRecord *v1alpha1.DNSRecord, conditionType string,

func (r *DNSRecordReconciler) applyChanges(ctx context.Context, dnsRecord *v1alpha1.DNSRecord, managedZone *v1alpha1.ManagedZone, isDelete bool) error {
logger := log.FromContext(ctx)

rootDomain, err := dnsRecord.GetRootDomain()
if err != nil {
return err
}
if !strings.HasSuffix(rootDomain, managedZone.Spec.DomainName) {
return fmt.Errorf("inconsitent domains, does not match managedzone, got %s, expected suffix %s", rootDomain, managedZone.Spec.DomainName)
filterDomain, _ := strings.CutPrefix(managedZone.Spec.DomainName, v1alpha1.WildcardPrefix)
if dnsRecord.Spec.RootHost != nil {
filterDomain = *dnsRecord.Spec.RootHost
}
rootDomainFilter := externaldnsendpoint.NewDomainFilter([]string{rootDomain})
rootDomainFilter := externaldnsendpoint.NewDomainFilter([]string{filterDomain})

providerConfig := provider.Config{
DomainFilter: externaldnsendpoint.NewDomainFilter([]string{managedZone.Spec.DomainName}),
ZoneTypeFilter: externaldnsprovider.NewZoneTypeFilter(""),
ZoneIDFilter: externaldnsprovider.NewZoneIDFilter([]string{managedZone.Status.ID}),
}
logger.V(3).Info("applyChanges", "rootDomain", rootDomain, "rootDomainFilter", rootDomainFilter, "providerConfig", providerConfig)
logger.V(3).Info("applyChanges", "zone", managedZone.Spec.DomainName, "rootDomainFilter", rootDomainFilter, "providerConfig", providerConfig)
dnsProvider, err := r.ProviderFactory.ProviderFor(ctx, managedZone, providerConfig)
if err != nil {
return err
Expand Down

0 comments on commit 6144e5f

Please sign in to comment.