Skip to content

Commit 2c79c83

Browse files
feat(defaults): implement hybrid defaulting strategy with deep resource validation
This commit introduces a robust defaulting mechanism that strictly separates Admission (Webhook) concerns from Reconciliation (Controller) concerns, aligned with the v1alpha1 design. Key Changes: - **pkg/defaults**: - Added `constants.go` with hardcoded safety defaults for Resources (CPU/Mem), Storage (1Gi), and Images (using GHCR main). - Implemented `PopulateClusterDefaults` in `resolver.go` for the Webhook. This applies strictly *inline* defaults and static values without fetching external templates, adhering to the design's "Non-Goal". - Implemented `Resolve...` methods (e.g., `ResolveGlobalTopo`) for the Controller. These actively fetch templates, merge them with inline specs, and apply "Level 4" operator defaults to guarantee valid, runnable configs (preventing nil-pointer crashes). - Added `resolver_test.go` with 100% coverage, using table-driven tests and `go-cmp` to verify deep defaulting logic and error wrapping. - **pkg/cluster-handler**: - Updated `multigrescluster_controller.go` to call `PopulateClusterDefaults` at the start of reconciliation. This ensures the controller operates on a valid state even if the admission webhook is disabled. - Simplified reconciliation logic by relying on the Resolver's guarantee of non-nil resource specs. - Updated all tests to ensure they are 100% coverage.
1 parent 4952cc9 commit 2c79c83

File tree

6 files changed

+653
-421
lines changed

6 files changed

+653
-421
lines changed

pkg/cluster-handler/controller/multigrescluster/integration_test.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919

2020
multigresv1alpha1 "github.com/numtide/multigres-operator/api/v1alpha1"
2121
"github.com/numtide/multigres-operator/pkg/cluster-handler/controller/multigrescluster"
22+
"github.com/numtide/multigres-operator/pkg/defaults"
2223
"github.com/numtide/multigres-operator/pkg/testutil"
2324
)
2425

@@ -124,8 +125,10 @@ func TestMultigresClusterReconciliation(t *testing.T) {
124125
},
125126
Spec: multigresv1alpha1.TopoServerSpec{
126127
Etcd: &multigresv1alpha1.EtcdSpec{
127-
Image: "etcd:latest",
128-
Replicas: ptr.To(int32(3)), // Default from logic
128+
Image: "etcd:latest",
129+
Replicas: ptr.To(int32(3)), // Default from logic
130+
Storage: multigresv1alpha1.StorageSpec{Size: defaults.DefaultEtcdStorageSize},
131+
Resources: defaults.DefaultResourcesEtcd,
129132
},
130133
},
131134
},
@@ -149,8 +152,9 @@ func TestMultigresClusterReconciliation(t *testing.T) {
149152
Spec: corev1.PodSpec{
150153
Containers: []corev1.Container{
151154
{
152-
Name: "multiadmin",
153-
Image: "admin:latest",
155+
Name: "multiadmin",
156+
Image: "admin:latest",
157+
Resources: defaults.DefaultResourcesAdmin,
154158
},
155159
},
156160
},
@@ -278,18 +282,45 @@ func TestMultigresClusterReconciliation(t *testing.T) {
278282
t.Fatalf("Failed to create controller, %v", err)
279283
}
280284

281-
// 4. Create the Input
285+
// 4. Create Defaults (Templates)
286+
// The controller expects "default" templates to exist when TemplateDefaults are not specified.
287+
// We create empty templates so the resolution succeeds and falls back to hardcoded defaults or inline specs.
288+
emptyCore := &multigresv1alpha1.CoreTemplate{
289+
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: namespace},
290+
Spec: multigresv1alpha1.CoreTemplateSpec{},
291+
}
292+
if err := k8sClient.Create(ctx, emptyCore); client.IgnoreAlreadyExists(err) != nil {
293+
t.Fatalf("Failed to create default core template: %v", err)
294+
}
295+
296+
emptyCell := &multigresv1alpha1.CellTemplate{
297+
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: namespace},
298+
Spec: multigresv1alpha1.CellTemplateSpec{},
299+
}
300+
if err := k8sClient.Create(ctx, emptyCell); client.IgnoreAlreadyExists(err) != nil {
301+
t.Fatalf("Failed to create default cell template: %v", err)
302+
}
303+
304+
emptyShard := &multigresv1alpha1.ShardTemplate{
305+
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: namespace},
306+
Spec: multigresv1alpha1.ShardTemplateSpec{},
307+
}
308+
if err := k8sClient.Create(ctx, emptyShard); client.IgnoreAlreadyExists(err) != nil {
309+
t.Fatalf("Failed to create default shard template: %v", err)
310+
}
311+
312+
// 5. Create the Input
282313
if err := k8sClient.Create(ctx, tc.cluster); err != nil {
283314
t.Fatalf("Failed to create the initial cluster, %v", err)
284315
}
285316

286-
// 5. Assert Logic: Wait for Children
317+
// 6. Assert Logic: Wait for Children
287318
// This ensures the controller has run and reconciled at least once successfully
288319
if err := watcher.WaitForMatch(tc.wantResources...); err != nil {
289320
t.Errorf("Resources mismatch:\n%v", err)
290321
}
291322

292-
// 6. Verify Parent Finalizer (Manual Check)
323+
// 7. Verify Parent Finalizer (Manual Check)
293324
// We check this manually to avoid fighting with status/spec diffs in the watcher
294325
fetchedCluster := &multigresv1alpha1.MultigresCluster{}
295326
if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(tc.cluster), fetchedCluster); err != nil {

pkg/cluster-handler/controller/multigrescluster/multigrescluster_controller.go

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ func (r *MultigresClusterReconciler) Reconcile(
7171
// This now returns *defaults.Resolver
7272
resolver := defaults.NewResolver(r.Client, cluster.Namespace, cluster.Spec.TemplateDefaults)
7373

74+
// Apply defaults (in-memory) to ensure we have images/configs even if webhook didn't run
75+
resolver.PopulateClusterDefaults(cluster)
76+
7477
if err := r.reconcileGlobalComponents(ctx, cluster, resolver); err != nil {
7578
l.Error(err, "Failed to reconcile global components")
7679
return ctrl.Result{}, err
@@ -171,6 +174,8 @@ func (r *MultigresClusterReconciler) reconcileGlobalTopoServer(
171174
}
172175

173176
spec := defaults.ResolveGlobalTopo(&cluster.Spec.GlobalTopoServer, tpl)
177+
// If Etcd is nil, it means we are using External topology (or invalid config handled by validations).
178+
// We only create a TopoServer CR if we are managing Etcd.
174179
if spec.Etcd != nil {
175180
ts := &multigresv1alpha1.TopoServer{
176181
ObjectMeta: metav1.ObjectMeta{
@@ -180,14 +185,10 @@ func (r *MultigresClusterReconciler) reconcileGlobalTopoServer(
180185
},
181186
}
182187
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, ts, func() error {
183-
replicas := defaults.DefaultEtcdReplicas
184-
if spec.Etcd.Replicas != nil {
185-
replicas = *spec.Etcd.Replicas
186-
}
187-
188+
// defaults.ResolveGlobalTopo guarantees spec.Etcd.Replicas is set
188189
ts.Spec.Etcd = &multigresv1alpha1.EtcdSpec{
189190
Image: spec.Etcd.Image,
190-
Replicas: &replicas,
191+
Replicas: spec.Etcd.Replicas,
191192
Storage: spec.Etcd.Storage,
192193
Resources: spec.Etcd.Resources,
193194
}
@@ -214,48 +215,46 @@ func (r *MultigresClusterReconciler) reconcileMultiAdmin(
214215
return fmt.Errorf("failed to resolve admin template: %w", err)
215216
}
216217

218+
// defaults.ResolveMultiAdmin guarantees a non-nil spec with defaults applied
217219
spec := defaults.ResolveMultiAdmin(&cluster.Spec.MultiAdmin, tpl)
218-
if spec != nil {
219-
deploy := &appsv1.Deployment{
220-
ObjectMeta: metav1.ObjectMeta{
221-
Name: cluster.Name + "-multiadmin",
222-
Namespace: cluster.Namespace,
223-
Labels: map[string]string{
224-
"multigres.com/cluster": cluster.Name,
225-
"app": "multiadmin",
226-
},
220+
221+
deploy := &appsv1.Deployment{
222+
ObjectMeta: metav1.ObjectMeta{
223+
Name: cluster.Name + "-multiadmin",
224+
Namespace: cluster.Namespace,
225+
Labels: map[string]string{
226+
"multigres.com/cluster": cluster.Name,
227+
"app": "multiadmin",
227228
},
229+
},
230+
}
231+
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
232+
// defaults.ResolveMultiAdmin guarantees Replicas is set
233+
deploy.Spec.Replicas = spec.Replicas
234+
deploy.Spec.Selector = &metav1.LabelSelector{
235+
MatchLabels: map[string]string{"app": "multiadmin", "multigres.com/cluster": cluster.Name},
228236
}
229-
if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, deploy, func() error {
230-
replicas := defaults.DefaultAdminReplicas
231-
if spec.Replicas != nil {
232-
replicas = *spec.Replicas
233-
}
234-
deploy.Spec.Replicas = &replicas
235-
deploy.Spec.Selector = &metav1.LabelSelector{
236-
MatchLabels: map[string]string{"app": "multiadmin", "multigres.com/cluster": cluster.Name},
237-
}
238-
deploy.Spec.Template = corev1.PodTemplateSpec{
239-
ObjectMeta: metav1.ObjectMeta{
240-
Labels: map[string]string{"app": "multiadmin", "multigres.com/cluster": cluster.Name},
241-
},
242-
Spec: corev1.PodSpec{
243-
ImagePullSecrets: cluster.Spec.Images.ImagePullSecrets,
244-
Containers: []corev1.Container{
245-
{
246-
Name: "multiadmin",
247-
Image: cluster.Spec.Images.MultiAdmin,
248-
Resources: spec.Resources,
249-
},
237+
deploy.Spec.Template = corev1.PodTemplateSpec{
238+
ObjectMeta: metav1.ObjectMeta{
239+
Labels: map[string]string{"app": "multiadmin", "multigres.com/cluster": cluster.Name},
240+
},
241+
Spec: corev1.PodSpec{
242+
ImagePullSecrets: cluster.Spec.Images.ImagePullSecrets,
243+
Containers: []corev1.Container{
244+
{
245+
Name: "multiadmin",
246+
Image: cluster.Spec.Images.MultiAdmin,
247+
Resources: spec.Resources,
250248
},
251-
Affinity: spec.Affinity,
252249
},
253-
}
254-
return controllerutil.SetControllerReference(cluster, deploy, r.Scheme)
255-
}); err != nil {
256-
return fmt.Errorf("failed to create/update multiadmin: %w", err)
250+
Affinity: spec.Affinity,
251+
},
257252
}
253+
return controllerutil.SetControllerReference(cluster, deploy, r.Scheme)
254+
}); err != nil {
255+
return fmt.Errorf("failed to create/update multiadmin: %w", err)
258256
}
257+
259258
return nil
260259
}
261260

0 commit comments

Comments
 (0)