Skip to content

Commit 0bb87d9

Browse files
Merge pull request #475 from abhiramnarayana/osprh-30643-az-mode
Add AZ-aware mode CR field and extend ConfigMap validation
2 parents b161dd5 + 545a63e commit 0bb87d9

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)