Skip to content

Commit c43c7d3

Browse files
Merge pull request #474 from beagles/osprh-30254-external-master
Support configuring external access to DNS masters
2 parents 57124f0 + ff0e04e commit c43c7d3

6 files changed

Lines changed: 330 additions & 15 deletions

File tree

internal/controller/designate_controller.go

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"maps"
2424
"slices"
25+
"sort"
2526
"strings"
2627
"time"
2728

@@ -111,6 +112,41 @@ func getSecret(
111112
return ctrl.Result{}, nil
112113
}
113114

115+
// getExternalMasterServiceIPs returns the LoadBalancer ingress IPs for external master services
116+
// matching the given pool name. The second return value is true when at least one matching service
117+
// has no ingress IP yet, signalling that the caller should requeue and retry later.
118+
func getExternalMasterServiceIPs(ctx context.Context, helper *helper.Helper, instance *designatev1beta1.Designate, poolName string, Log logr.Logger) ([]string, bool, error) {
119+
services := &corev1.ServiceList{}
120+
err := helper.GetClient().List(ctx, services, client.InNamespace(instance.Namespace), client.MatchingLabels{
121+
designate.ExternalMasterServiceLabel: "true",
122+
"service": "designate-mdns",
123+
})
124+
if err != nil {
125+
return nil, false, err
126+
}
127+
ips := make([]string, 0, len(services.Items))
128+
pendingIngress := false
129+
for _, service := range services.Items {
130+
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
131+
continue
132+
}
133+
// If the service is missing the annotation, apply to all pools by skipping the pool name check.
134+
servicePool, ok := service.Annotations[designate.ExternalPoolServiceAnnotation]
135+
if ok && servicePool != poolName {
136+
continue
137+
}
138+
139+
if len(service.Status.LoadBalancer.Ingress) == 0 {
140+
Log.Info(fmt.Sprintf("External master Service %s/%s has no LoadBalancer ingress IP yet, will requeue", service.Namespace, service.Name))
141+
pendingIngress = true
142+
continue
143+
}
144+
ips = append(ips, service.Status.LoadBalancer.Ingress[0].IP)
145+
}
146+
sort.Strings(ips)
147+
return ips, pendingIngress, nil
148+
}
149+
114150
// GetClient -
115151
func (r *DesignateReconciler) GetClient() client.Client {
116152
return r.Client
@@ -299,6 +335,7 @@ func (r *DesignateReconciler) validatePoolRemovals(
299335
// +kubebuilder:rbac:groups="security.openshift.io",resourceNames=anyuid;privileged,resources=securitycontextconstraints,verbs=use
300336
// +kubebuilder:rbac:groups="",resources=pods,verbs=create;delete;get;list;patch;update;watch
301337
// +kubebuilder:rbac:groups="",resources=pods/log,verbs=get
338+
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch
302339

303340
// Reconcile -
304341
func (r *DesignateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) {
@@ -503,6 +540,25 @@ func (r *DesignateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
503540
return result
504541
}
505542

543+
externalMasterServiceFn := func(ctx context.Context, o client.Object) []reconcile.Request {
544+
if o.GetLabels()[designate.ExternalMasterServiceLabel] != "true" {
545+
return nil
546+
}
547+
548+
designates := &designatev1beta1.DesignateList{}
549+
if err := r.List(ctx, designates, client.InNamespace(o.GetNamespace())); err != nil {
550+
Log.Error(err, "Unable to retrieve Designate CRs")
551+
return nil
552+
}
553+
var result []reconcile.Request
554+
for _, cr := range designates.Items {
555+
result = append(result, reconcile.Request{
556+
NamespacedName: client.ObjectKey{Namespace: o.GetNamespace(), Name: cr.Name},
557+
})
558+
}
559+
return result
560+
}
561+
506562
// TODO(beagles):
507563
// - Watch for changes to the redis PODs and resync the headless hostnames for the PODs if necessary.
508564
return ctrl.NewControllerManagedBy(mgr).
@@ -541,6 +597,12 @@ func (r *DesignateReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Man
541597
handler.EnqueueRequestsFromMapFunc(redisWatchFn),
542598
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
543599
).
600+
// Watch for external master Service changes (LoadBalancer IP assignments)
601+
Watches(
602+
&corev1.Service{},
603+
handler.EnqueueRequestsFromMapFunc(externalMasterServiceFn),
604+
builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}),
605+
).
544606
Complete(r)
545607
}
546608

@@ -1005,6 +1067,7 @@ func (r *DesignateReconciler) reconcileNormal(ctx context.Context, instance *des
10051067

10061068
externalBindData := make(map[string][]designate.ExternalBind)
10071069
externalRndcSecretData := make(map[string]string)
1070+
externalMasterServiceIPs := make(map[string][]string)
10081071
updateRndcSecret := false
10091072

10101073
if instance.Spec.ExternalBindsSecret != "" {
@@ -1048,9 +1111,18 @@ func (r *DesignateReconciler) reconcileNormal(ctx context.Context, instance *des
10481111
externalBindData[poolName][i].RndcFile = rndcFilename
10491112
externalRndcSecretData[rndcFilename] = rndcBlock
10501113
}
1114+
var pendingIngress bool
1115+
externalMasterServiceIPs[poolName], pendingIngress, err = getExternalMasterServiceIPs(ctx, helper, instance, poolName, Log)
1116+
if err != nil {
1117+
return ctrl.Result{}, err
1118+
}
1119+
if pendingIngress {
1120+
return ctrl.Result{RequeueAfter: 10 * time.Second}, nil
1121+
}
10511122
}
10521123
updateRndcSecret = hash != instance.Status.Hash[designate.ExternalBindsData]
10531124
instance.Status.Hash[designate.ExternalBindsData] = hash
1125+
10541126
} else {
10551127
// No external binds secret, so we need to check if the external rndc secret data is present
10561128
// and if it is - remove the contents so the workers have up to date.
@@ -1265,7 +1337,7 @@ func (r *DesignateReconciler) reconcileNormal(ctx context.Context, instance *des
12651337
Data: make(map[string]string),
12661338
}
12671339

1268-
poolsYaml, poolsYamlHash, err := designate.GeneratePoolsYamlDataAndHash(updatedBindMap, mdnsConfigMap.Data, nsRecords, multipoolConfig, externalBindData)
1340+
poolsYaml, poolsYamlHash, err := designate.GeneratePoolsYamlDataAndHash(updatedBindMap, mdnsConfigMap.Data, nsRecords, multipoolConfig, externalBindData, externalMasterServiceIPs)
12691341
if err != nil {
12701342
return ctrl.Result{}, err
12711343
}

internal/designate/const.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,4 +114,18 @@ const (
114114
// ExternalRndcData is the name of the secret for mounting the external binds rndc keys
115115
// in the workers.
116116
ExternalRndcData = "designate-external-rndc"
117+
118+
// Fields for Service objects for accessing mDNS/DNS masters from outside the cluster for
119+
// external DNS secondaries. Why a label AND an annotation? The label is used to identify the
120+
// service as an external master, while the annotation is used to specify the pool name the
121+
// external service is intended for. This allows different pools to have different masters
122+
// and be configured with different networking as needed.
123+
124+
// ExternalMasterServiceLabel is the label name for identifying services to be as mDNS
125+
// endpoints
126+
ExternalMasterServiceLabel = "designate.openstack.org/external_master"
127+
128+
// ExternalPoolServiceAnnotation is the annotation name for specifying the pool name the external
129+
// service is intended for
130+
ExternalPoolServiceAnnotation = "designate.openstack.org/external_pool"
117131
)

internal/designate/generate_bind9_pools_yaml.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func validateMultipoolConfig(config *MultipoolConfig, azAwareMode designatev1.De
229229
}
230230

231231
// GeneratePoolsYamlDataAndHash sorts all pool resources to get the correct hash every time
232-
func GeneratePoolsYamlDataAndHash(BindMap, MdnsMap map[string]string, nsRecords []designatev1.DesignateNSRecord, multipoolConfig *MultipoolConfig, externalBinds map[string][]ExternalBind) (string, string, error) {
232+
func GeneratePoolsYamlDataAndHash(BindMap, MdnsMap map[string]string, nsRecords []designatev1.DesignateNSRecord, multipoolConfig *MultipoolConfig, externalBinds map[string][]ExternalBind, externalMasterServiceIPs map[string][]string) (string, string, error) {
233233
masterHosts := make([]string, 0, len(MdnsMap))
234234
for _, host := range MdnsMap {
235235
masterHosts = append(masterHosts, host)
@@ -239,7 +239,7 @@ func GeneratePoolsYamlDataAndHash(BindMap, MdnsMap map[string]string, nsRecords
239239
var pools []Pool
240240

241241
if multipoolConfig == nil {
242-
pool, err := generateDefaultPool(BindMap, masterHosts, nsRecords, externalBinds["default"])
242+
pool, err := generateDefaultPool(BindMap, masterHosts, nsRecords, externalBinds["default"], externalMasterServiceIPs["default"])
243243
if err != nil {
244244
return "", "", err
245245
}
@@ -348,7 +348,7 @@ func createExternalTargetAndNameserver(bindInfo ExternalBind, masters []Master,
348348
return target, nameserver
349349
}
350350

351-
func generateDefaultPool(BindMap map[string]string, masterHosts []string, nsRecords []designatev1.DesignateNSRecord, externalBinds []ExternalBind) (Pool, error) {
351+
func generateDefaultPool(BindMap map[string]string, masterHosts []string, nsRecords []designatev1.DesignateNSRecord, externalBinds []ExternalBind, externalMasterServiceIPs []string) (Pool, error) {
352352
sortNSRecords(nsRecords)
353353

354354
bindIPs := make([]string, len(BindMap))
@@ -364,11 +364,14 @@ func generateDefaultPool(BindMap map[string]string, masterHosts []string, nsReco
364364
targets[i], nameservers[i] = createTargetAndNameserver(bindIPs[i], i, masters, description, "", "")
365365
}
366366

367-
// NOTE: external BIND9 servers inherit the internal mDNS masters list. These masters may not be
368-
// reachable from an external network — the deployer must ensure connectivity (e.g. via NAT, an
369-
// exposed mDNS endpoint, or custom master addresses in a future iteration of this feature).
370367
externalTargets := make([]Target, len(externalBinds))
371368
externalNameservers := make([]Nameserver, len(externalBinds))
369+
// Prefer external master service IPs as masters if available; fallback to internal masters if not (may require additional network routing/configuration by the admin).
370+
if len(externalMasterServiceIPs) > 0 {
371+
masters = createMasters(externalMasterServiceIPs)
372+
} else {
373+
masters = createMasters(masterHosts)
374+
}
372375
for i := range externalBinds {
373376
description := fmt.Sprintf("External BIND9 Server %d (%s)", i, externalBinds[i].Name)
374377
externalTargets[i], externalNameservers[i] = createExternalTargetAndNameserver(externalBinds[i], masters, description)

internal/designate/generate_bind9_pools_yaml_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ func TestGenerateDefaultPool(t *testing.T) {
283283
{Hostname: "ns2.example.org.", Priority: 2},
284284
}
285285

286-
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, []ExternalBind{})
286+
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, []ExternalBind{}, []string{})
287287
if err != nil {
288288
t.Fatalf("generateDefaultPool() error = %v", err)
289289
}
@@ -457,7 +457,7 @@ func TestSinglePoolModeUsesCRNSRecords(t *testing.T) {
457457
}
458458

459459
// In single-pool mode, generateDefaultPool should use CR NS records
460-
pool, err := generateDefaultPool(bindMap, masterHosts, crNSRecords, []ExternalBind{})
460+
pool, err := generateDefaultPool(bindMap, masterHosts, crNSRecords, []ExternalBind{}, []string{})
461461
if err != nil {
462462
t.Fatalf("generateDefaultPool() error = %v", err)
463463
}
@@ -602,7 +602,7 @@ func TestGenerateDefaultPoolWithExternalBinds(t *testing.T) {
602602
},
603603
}
604604

605-
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, externalBinds)
605+
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, externalBinds, []string{})
606606
if err != nil {
607607
t.Fatalf("generateDefaultPool() error = %v", err)
608608
}
@@ -675,7 +675,7 @@ func TestGenerateDefaultPoolWithMultipleExternalBinds(t *testing.T) {
675675
},
676676
}
677677

678-
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, externalBinds)
678+
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, externalBinds, []string{})
679679
if err != nil {
680680
t.Fatalf("generateDefaultPool() error = %v", err)
681681
}
@@ -733,7 +733,7 @@ func TestGenerateDefaultPoolWithExternalBindsFromYAML(t *testing.T) {
733733
}
734734
parsedBinds[0].RndcFile = "default-rndc-0"
735735

736-
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, parsedBinds)
736+
pool, err := generateDefaultPool(bindMap, masterHosts, nsRecords, parsedBinds, []string{})
737737
if err != nil {
738738
t.Fatalf("generateDefaultPool() error = %v", err)
739739
}

test/functional/base_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ const (
5050
ExternalBindAddress = "192.168.100.50"
5151
ExternalBindName = "bind9-external"
5252
// Fake RNDC secret for functional tests only; not a real credential.
53-
ExternalRndcSecretValue = "c2VjcmV0MTIz" // #nosec G101
53+
ExternalRndcSecretValue = "c2VjcmV0MTIz" // #nosec G101
54+
ExternalMasterServiceIP = "10.0.0.100"
55+
ExternalMasterServiceAltIP = "10.0.0.101"
5456

5557
PublicCertSecretName = "public-tls-certs" // #nosec G101
5658
InternalCertSecretName = "internal-tls-certs" // #nosec G101
@@ -638,6 +640,44 @@ func createAndSimulateExternalBinds(secretName types.NamespacedName) {
638640
DeferCleanup(k8sClient.Delete, ctx, secret)
639641
}
640642

643+
func createExternalMasterService(ns, name, poolAnnotation, ingressIP string) *corev1.Service {
644+
labels := map[string]string{
645+
designate.ExternalMasterServiceLabel: "true",
646+
"service": "designate-mdns",
647+
}
648+
annotations := map[string]string{}
649+
if poolAnnotation != "" {
650+
annotations[designate.ExternalPoolServiceAnnotation] = poolAnnotation
651+
}
652+
svc := &corev1.Service{
653+
ObjectMeta: metav1.ObjectMeta{
654+
Name: name,
655+
Namespace: ns,
656+
Labels: labels,
657+
Annotations: annotations,
658+
},
659+
Spec: corev1.ServiceSpec{
660+
Type: corev1.ServiceTypeLoadBalancer,
661+
Ports: []corev1.ServicePort{
662+
{Name: "mdns", Port: 5354},
663+
},
664+
},
665+
}
666+
Expect(k8sClient.Create(ctx, svc)).Should(Succeed())
667+
668+
if ingressIP != "" {
669+
Eventually(func(g Gomega) {
670+
current := &corev1.Service{}
671+
g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(svc), current)).Should(Succeed())
672+
current.Status.LoadBalancer.Ingress = []corev1.LoadBalancerIngress{{IP: ingressIP}}
673+
g.Expect(k8sClient.Status().Update(ctx, current)).Should(Succeed())
674+
}, timeout, interval).Should(Succeed())
675+
}
676+
677+
DeferCleanup(k8sClient.Delete, ctx, svc)
678+
return svc
679+
}
680+
641681
func CreateDesignateNSRecordsConfigMap(name types.NamespacedName) client.Object {
642682
return &corev1.ConfigMap{
643683
ObjectMeta: metav1.ObjectMeta{

0 commit comments

Comments
 (0)