Skip to content

Commit

Permalink
add optional rootHost
Browse files Browse the repository at this point in the history
add validate call to publish

remove GetRootDomain. Improve validation and tests

expect wildcard endpoint
  • Loading branch information
maleck13 committed Mar 14, 2024
1 parent 41d07c2 commit 32e1cad
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)

Check failure on line 231 in internal/controller/dnsrecord_controller.go

View workflow job for this annotation

GitHub Actions / lint

SA4001: *&x will be simplified to x. It will not copy x. (staticcheck)
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 32e1cad

Please sign in to comment.