Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional rootHost #55

Merged
merged 1 commit into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
maleck13 marked this conversation as resolved.
Show resolved Hide resolved

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
Loading