Skip to content

Commit 545a63e

Browse files
Add AZ-aware mode CR field and extend ConfigMap validation
Add AZAwareMode string enum field (DesignateAZMode type) to DesignateSpecBase with allowed values "Enabled" and "Disabled", following OpenShift API conventions (no booleans in CRDs). Provides a declarative way to enable AZ-aware multipool with BIND views, per-pool TSIG keys, and per-AZ Unbound instances. Extend validateMultipoolConfig to reject pools missing a view when AZ mode is enabled. Pass DesignateAZMode enum directly through GetMultipoolConfig for type-safe AZ mode propagation. Regenerate CRD manifests and add unit tests for AZ-aware validation rules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Abhiram R N <abhiramrn@gmail.com>
1 parent ede1afa commit 545a63e

9 files changed

Lines changed: 180 additions & 21 deletions

api/bases/designate.openstack.org_designates.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ spec:
5252
default: 120
5353
description: Designate API timeout
5454
type: integer
55+
azAwareMode:
56+
description: |-
57+
AZAwareMode - when set to "Enabled", activates AZ-aware multipool mode with
58+
BIND views, per-pool TSIG keys, and per-AZ Unbound instances. Requires a valid
59+
multipool ConfigMap with view fields defined for each pool.
60+
enum:
61+
- ""
62+
- Enabled
63+
- Disabled
64+
type: string
5565
backendMdnsServerProtocol:
5666
description: |-
5767
BackendTypeProtocol - Defines the backend protocol to be used between the designate-worker &

api/v1beta1/designate_types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ import (
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
)
2828

29+
// DesignateAZMode describes the AZ-aware multipool mode setting
30+
// +kubebuilder:validation:Enum="";Enabled;Disabled
31+
type DesignateAZMode string
32+
2933
const (
34+
// AZModeEnabled activates AZ-aware multipool with BIND views and per-pool TSIG keys
35+
AZModeEnabled DesignateAZMode = "Enabled"
36+
37+
// AZModeDisabled explicitly disables AZ-aware multipool mode
38+
AZModeDisabled DesignateAZMode = "Disabled"
39+
3040
// DbSyncHash hash
3141
DbSyncHash = "dbsync"
3242

@@ -235,6 +245,12 @@ type DesignateSpecBase struct {
235245
// +kubebuilder:validation:Optional
236246
// ExternalBindsSecret is the name of the secret containing external BIND9 configurations
237247
ExternalBindsSecret string `json:"externalBindsSecret,omitempty"`
248+
249+
// +kubebuilder:validation:Optional
250+
// AZAwareMode - when set to "Enabled", activates AZ-aware multipool mode with
251+
// BIND views, per-pool TSIG keys, and per-AZ Unbound instances. Requires a valid
252+
// multipool ConfigMap with view fields defined for each pool.
253+
AZAwareMode DesignateAZMode `json:"azAwareMode,omitempty"`
238254
}
239255

240256
// DesignateStatus defines the observed state of Designate

config/crd/bases/designate.openstack.org_designates.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ spec:
5252
default: 120
5353
description: Designate API timeout
5454
type: integer
55+
azAwareMode:
56+
description: |-
57+
AZAwareMode - when set to "Enabled", activates AZ-aware multipool mode with
58+
BIND views, per-pool TSIG keys, and per-AZ Unbound instances. Requires a valid
59+
multipool ConfigMap with view fields defined for each pool.
60+
enum:
61+
- ""
62+
- Enabled
63+
- Disabled
64+
type: string
5565
backendMdnsServerProtocol:
5666
description: |-
5767
BackendTypeProtocol - Defines the backend protocol to be used between the designate-worker &

internal/controller/designate_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,7 +1108,7 @@ func (r *DesignateReconciler) reconcileNormal(ctx context.Context, instance *des
11081108
}
11091109

11101110
// Get multipool configuration once for use in bind IP allocation and pools.yaml generation
1111-
multipoolConfig, err := designate.GetMultipoolConfig(ctx, helper.GetClient(), instance.Namespace)
1111+
multipoolConfig, err := designate.GetMultipoolConfig(ctx, helper.GetClient(), instance.Namespace, instance.Spec.AZAwareMode)
11121112
if err != nil {
11131113
Log.Error(err, "Failed to get multipool configuration")
11141114
return ctrl.Result{}, err
@@ -1690,7 +1690,7 @@ func (r *DesignateReconciler) generateServiceConfigMaps(
16901690
cmLabels := labels.GetLabels(instance, labels.GetGroupLabel(designate.ServiceName), map[string]string{})
16911691

16921692
var replicas int
1693-
multipoolConfig, err := designate.GetMultipoolConfig(ctx, h.GetClient(), instance.Namespace)
1693+
multipoolConfig, err := designate.GetMultipoolConfig(ctx, h.GetClient(), instance.Namespace, instance.Spec.AZAwareMode)
16941694
if err != nil {
16951695
Log.Error(err, "Failed to get multipool configuration")
16961696
return err

internal/controller/designatebackendbind9_multipool.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,7 @@ func (r *DesignateBackendbind9Reconciler) reconcileByPoolMode(
11711171
) (ctrl.Result, error) {
11721172
Log := r.GetLogger(ctx)
11731173

1174-
multipoolConfig, err := designate.GetMultipoolConfig(ctx, helper.GetClient(), instance.Namespace)
1174+
multipoolConfig, err := designate.GetMultipoolConfig(ctx, helper.GetClient(), instance.Namespace, "")
11751175
if err != nil {
11761176
Log.Error(err, "Failed to get multipool configuration")
11771177
return ctrl.Result{}, err

internal/designate/generate_bind9_pools_yaml.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ var (
5151
ErrMultipoolConfigKeyMissing = errors.New("multipool ConfigMap missing pools key")
5252
// ErrPoolMissingNSRecords is returned when a pool doesn't define NS records in multipool mode
5353
ErrPoolMissingNSRecords = errors.New("pool missing NS records in multipool mode")
54+
// ErrPoolMissingView is returned when AZ mode is enabled and a pool doesn't have a view
55+
ErrPoolMissingView = errors.New("pool missing view in AZ-aware mode")
5456
)
5557

5658
const (
@@ -140,7 +142,7 @@ type MultipoolConfig struct {
140142

141143
// GetMultipoolConfig reads and parses the multipool ConfigMap
142144
// Returns nil if ConfigMap doesn't exist
143-
func GetMultipoolConfig(ctx context.Context, k8sClient client.Client, namespace string) (*MultipoolConfig, error) {
145+
func GetMultipoolConfig(ctx context.Context, k8sClient client.Client, namespace string, azAwareMode designatev1.DesignateAZMode) (*MultipoolConfig, error) {
144146
configMap := &corev1.ConfigMap{}
145147
err := k8sClient.Get(ctx, types.NamespacedName{
146148
Name: MultipoolConfigMapName,
@@ -171,15 +173,15 @@ func GetMultipoolConfig(ctx context.Context, k8sClient client.Client, namespace
171173

172174
config := &MultipoolConfig{Pools: pools}
173175

174-
if err := validateMultipoolConfig(config); err != nil {
176+
if err := validateMultipoolConfig(config, azAwareMode); err != nil {
175177
return nil, fmt.Errorf("invalid multipool config: %w", err)
176178
}
177179

178180
return config, nil
179181
}
180182

181183
// validateMultipoolConfig validates the multipool configuration
182-
func validateMultipoolConfig(config *MultipoolConfig) error {
184+
func validateMultipoolConfig(config *MultipoolConfig, azAwareMode designatev1.DesignateAZMode) error {
183185
if len(config.Pools) == 0 {
184186
return ErrNoPoolsDefined
185187
}
@@ -208,6 +210,14 @@ func validateMultipoolConfig(config *MultipoolConfig) error {
208210
if pool.BindReplicas <= 0 {
209211
return fmt.Errorf("%w: pool %s has %d", ErrInvalidBindReplicas, pool.Name, pool.BindReplicas)
210212
}
213+
214+
// AZ-aware mode validations
215+
if azAwareMode == designatev1.AZModeEnabled {
216+
if pool.View == "" {
217+
return fmt.Errorf("%w: pool %s", ErrPoolMissingView, pool.Name)
218+
}
219+
// TODO: Validate pool TSIGKeyID is non-empty once per-pool TSIG keys are implemented
220+
}
211221
}
212222

213223
// Ensure default pool exists and cannot be removed

internal/designate/generate_bind9_pools_yaml_test.go

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,120 @@ func TestValidateMultipoolConfig(t *testing.T) {
145145

146146
for _, tt := range tests {
147147
t.Run(tt.name, func(t *testing.T) {
148-
err := validateMultipoolConfig(tt.config)
148+
err := validateMultipoolConfig(tt.config, "")
149+
if (err != nil) != tt.wantErr {
150+
t.Errorf("validateMultipoolConfig() error = %v, wantErr %v", err, tt.wantErr)
151+
return
152+
}
153+
if err != nil && tt.errMsg != "" {
154+
if !strings.Contains(err.Error(), tt.errMsg) {
155+
t.Errorf("validateMultipoolConfig() error = %v, want error containing %v", err, tt.errMsg)
156+
}
157+
}
158+
})
159+
}
160+
}
161+
162+
func TestValidateMultipoolConfigAZMode(t *testing.T) {
163+
tests := []struct {
164+
name string
165+
config *MultipoolConfig
166+
wantErr bool
167+
errMsg string
168+
}{
169+
{
170+
name: "valid AZ config with views on all pools",
171+
config: &MultipoolConfig{
172+
Pools: []PoolConfig{
173+
{
174+
Name: "default",
175+
BindReplicas: 2,
176+
View: "default",
177+
NSRecords: []designatev1.DesignateNSRecord{
178+
{Hostname: "ns1.example.org.", Priority: 1},
179+
},
180+
},
181+
{
182+
Name: "pool1",
183+
BindReplicas: 2,
184+
View: "az1-view",
185+
NSRecords: []designatev1.DesignateNSRecord{
186+
{Hostname: "ns2.example.org.", Priority: 1},
187+
},
188+
},
189+
},
190+
},
191+
wantErr: false,
192+
},
193+
{
194+
name: "multiple pools sharing same view is valid",
195+
config: &MultipoolConfig{
196+
Pools: []PoolConfig{
197+
{
198+
Name: "default",
199+
BindReplicas: 2,
200+
View: "shared-view",
201+
NSRecords: []designatev1.DesignateNSRecord{
202+
{Hostname: "ns1.example.org.", Priority: 1},
203+
},
204+
},
205+
{
206+
Name: "pool1",
207+
BindReplicas: 2,
208+
View: "shared-view",
209+
NSRecords: []designatev1.DesignateNSRecord{
210+
{Hostname: "ns2.example.org.", Priority: 1},
211+
},
212+
},
213+
},
214+
},
215+
wantErr: false,
216+
},
217+
{
218+
name: "reject pool missing view in AZ mode",
219+
config: &MultipoolConfig{
220+
Pools: []PoolConfig{
221+
{
222+
Name: "default",
223+
BindReplicas: 2,
224+
View: "default",
225+
NSRecords: []designatev1.DesignateNSRecord{
226+
{Hostname: "ns1.example.org.", Priority: 1},
227+
},
228+
},
229+
{
230+
Name: "pool1",
231+
BindReplicas: 2,
232+
NSRecords: []designatev1.DesignateNSRecord{
233+
{Hostname: "ns2.example.org.", Priority: 1},
234+
},
235+
},
236+
},
237+
},
238+
wantErr: true,
239+
errMsg: "pool missing view in AZ-aware mode",
240+
},
241+
{
242+
name: "reject default pool missing view in AZ mode",
243+
config: &MultipoolConfig{
244+
Pools: []PoolConfig{
245+
{
246+
Name: "default",
247+
BindReplicas: 2,
248+
NSRecords: []designatev1.DesignateNSRecord{
249+
{Hostname: "ns1.example.org.", Priority: 1},
250+
},
251+
},
252+
},
253+
},
254+
wantErr: true,
255+
errMsg: "pool missing view in AZ-aware mode: pool default",
256+
},
257+
}
258+
259+
for _, tt := range tests {
260+
t.Run(tt.name, func(t *testing.T) {
261+
err := validateMultipoolConfig(tt.config, designatev1.AZModeEnabled)
149262
if (err != nil) != tt.wantErr {
150263
t.Errorf("validateMultipoolConfig() error = %v, wantErr %v", err, tt.wantErr)
151264
return

test/functional/designate_multipool_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ var _ = Describe("Designate multipool controller", func() {
6161
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
6262

6363
// Verify multipool config can be parsed
64-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
64+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
6565
Expect(err).ShouldNot(HaveOccurred())
6666
Expect(config).ShouldNot(BeNil())
6767
Expect(config.Pools).To(HaveLen(2))
@@ -88,7 +88,7 @@ var _ = Describe("Designate multipool controller", func() {
8888
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
8989

9090
// Verify only one pool exists
91-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
91+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
9292
Expect(err).ShouldNot(HaveOccurred())
9393
Expect(config).ShouldNot(BeNil())
9494
Expect(config.Pools).To(HaveLen(1))
@@ -123,7 +123,7 @@ var _ = Describe("Designate multipool controller", func() {
123123
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
124124

125125
// Verify initial state
126-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
126+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
127127
Expect(err).ShouldNot(HaveOccurred())
128128
Expect(config.Pools).To(HaveLen(2))
129129

@@ -149,7 +149,7 @@ var _ = Describe("Designate multipool controller", func() {
149149
Expect(k8sClient.Update(ctx, multipoolConfig)).Should(Succeed())
150150

151151
// Verify new state
152-
config, err = designate.GetMultipoolConfig(ctx, k8sClient, namespace)
152+
config, err = designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
153153
Expect(err).ShouldNot(HaveOccurred())
154154
Expect(config.Pools).To(HaveLen(3))
155155
Expect(config.Pools[2].Name).To(Equal("pool2"))
@@ -182,7 +182,7 @@ var _ = Describe("Designate multipool controller", func() {
182182
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
183183

184184
// Verify replica counts
185-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
185+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
186186
Expect(err).ShouldNot(HaveOccurred())
187187
Expect(config.Pools).To(HaveLen(2))
188188
Expect(config.Pools[0].BindReplicas).To(Equal(int32(2)))
@@ -222,7 +222,7 @@ var _ = Describe("Designate multipool controller", func() {
222222
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
223223

224224
// Verify attributes
225-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
225+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
226226
Expect(err).ShouldNot(HaveOccurred())
227227
Expect(config.Pools).To(HaveLen(2))
228228
Expect(config.Pools[0].Attributes).To(HaveKeyWithValue("availability_zone", "az1"))
@@ -264,7 +264,7 @@ var _ = Describe("Designate multipool controller", func() {
264264
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
265265

266266
// Verify pool order matches alphabetical sorting (from code implementation)
267-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
267+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
268268
Expect(err).ShouldNot(HaveOccurred())
269269
Expect(config.Pools).To(HaveLen(3))
270270
// Pools should be sorted alphabetically by name
@@ -304,7 +304,7 @@ var _ = Describe("Designate multipool controller", func() {
304304
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
305305

306306
// Verify per-pool NS records
307-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
307+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
308308
Expect(err).ShouldNot(HaveOccurred())
309309
Expect(config.Pools).To(HaveLen(2))
310310

@@ -347,7 +347,7 @@ var _ = Describe("Designate multipool controller", func() {
347347
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
348348

349349
// Verify initial replica counts
350-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
350+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
351351
Expect(err).ShouldNot(HaveOccurred())
352352
Expect(config.Pools[0].BindReplicas).To(Equal(int32(1)))
353353
Expect(config.Pools[1].BindReplicas).To(Equal(int32(1)))
@@ -368,7 +368,7 @@ var _ = Describe("Designate multipool controller", func() {
368368
Expect(k8sClient.Update(ctx, multipoolConfig)).Should(Succeed())
369369

370370
// Verify updated replica count
371-
config, err = designate.GetMultipoolConfig(ctx, k8sClient, namespace)
371+
config, err = designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
372372
Expect(err).ShouldNot(HaveOccurred())
373373
Expect(config.Pools[0].BindReplicas).To(Equal(int32(1)))
374374
Expect(config.Pools[1].BindReplicas).To(Equal(int32(3)))

test/functional/designatebackendbind9_controller_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ var _ = Describe("DesignateBackendbind9 controller", func() {
302302
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
303303

304304
// Verify we can fetch and parse the config
305-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
305+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
306306
Expect(err).ShouldNot(HaveOccurred())
307307
Expect(config).ShouldNot(BeNil())
308308
Expect(config.Pools).To(HaveLen(2))
@@ -313,7 +313,7 @@ var _ = Describe("DesignateBackendbind9 controller", func() {
313313
})
314314

315315
It("should not fail if multipool ConfigMap is missing", func() {
316-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
316+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
317317
Expect(err).ShouldNot(HaveOccurred())
318318
Expect(config).Should(BeNil())
319319
})
@@ -332,7 +332,7 @@ var _ = Describe("DesignateBackendbind9 controller", func() {
332332
Expect(k8sClient.Create(ctx, invalidConfig)).Should(Succeed())
333333
DeferCleanup(k8sClient.Delete, ctx, invalidConfig)
334334

335-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
335+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
336336
Expect(err).Should(HaveOccurred())
337337
Expect(config).Should(BeNil())
338338
})
@@ -369,7 +369,7 @@ var _ = Describe("DesignateBackendbind9 controller", func() {
369369
DeferCleanup(k8sClient.Delete, ctx, multipoolConfig)
370370

371371
// Fetch and verify pools are sorted alphabetically
372-
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace)
372+
config, err := designate.GetMultipoolConfig(ctx, k8sClient, namespace, "")
373373
Expect(err).ShouldNot(HaveOccurred())
374374
Expect(config).ShouldNot(BeNil())
375375
Expect(config.Pools).To(HaveLen(3))

0 commit comments

Comments
 (0)