Skip to content

Commit e385213

Browse files
Merge pull request #123 from numtide/ssa-cluster-handler
refactor(cluster-handler): implement Server-Side Apply for all controllers
2 parents 0310da4 + efd4c96 commit e385213

File tree

12 files changed

+238
-592
lines changed

12 files changed

+238
-592
lines changed

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,9 @@ func setupIntegration(t *testing.T) (client.Client, *testutil.ResourceWatcher) {
7070

7171
// 3. Setup Controller
7272
reconciler := &multigrescluster.MultigresClusterReconciler{
73-
Client: mgr.GetClient(),
74-
Scheme: mgr.GetScheme(),
73+
Client: mgr.GetClient(),
74+
Scheme: mgr.GetScheme(),
75+
Recorder: mgr.GetEventRecorderFor("multigres-cluster-controller"),
7576
}
7677

7778
if err := reconciler.SetupWithManager(mgr, controller.Options{

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -550,12 +550,12 @@ func TestMultigresClusterReconciler_Lifecycle(t *testing.T) {
550550
existingObjects: []client.Object{coreTpl, cellTpl, shardTpl},
551551
wantErrMsg: "exceeds 50 characters",
552552
},
553-
"Error: Create TableGroup Failed": {
553+
"Error: Apply TableGroup Failed": {
554554
existingObjects: []client.Object{coreTpl, cellTpl, shardTpl},
555555
failureConfig: &testutil.FailureConfig{
556-
OnCreate: testutil.FailOnObjectName(clusterName+"-db1-tg1", errSimulated),
556+
OnPatch: testutil.FailOnObjectName(clusterName+"-db1-tg1", errSimulated),
557557
},
558-
wantErrMsg: "failed to create tablegroup",
558+
wantErrMsg: "failed to apply tablegroup",
559559
},
560560
"Error: Delete Orphan TableGroup Failed": {
561561
existingObjects: []client.Object{

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

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66

7-
"k8s.io/apimachinery/pkg/api/errors"
87
"sigs.k8s.io/controller-runtime/pkg/client"
98

109
multigresv1alpha1 "github.com/numtide/multigres-operator/api/v1alpha1"
@@ -55,28 +54,19 @@ func (r *MultigresClusterReconciler) reconcileCells(
5554
return fmt.Errorf("failed to build cell '%s': %w", cellCfg.Name, err)
5655
}
5756

58-
existing := &multigresv1alpha1.Cell{}
59-
err = r.Get(
57+
// Server Side Apply
58+
desired.SetGroupVersionKind(multigresv1alpha1.GroupVersion.WithKind("Cell"))
59+
if err := r.Patch(
6060
ctx,
61-
client.ObjectKey{Namespace: cluster.Namespace, Name: desired.Name},
62-
existing,
63-
)
64-
if err != nil {
65-
if errors.IsNotFound(err) {
66-
if err := r.Create(ctx, desired); err != nil {
67-
return fmt.Errorf("failed to create cell '%s': %w", cellCfg.Name, err)
68-
}
69-
r.Recorder.Eventf(cluster, "Normal", "Created", "Created Cell %s", desired.Name)
70-
continue
71-
}
72-
return fmt.Errorf("failed to get cell '%s': %w", cellCfg.Name, err)
61+
desired,
62+
client.Apply,
63+
client.ForceOwnership,
64+
client.FieldOwner("multigres-operator"),
65+
); err != nil {
66+
return fmt.Errorf("failed to apply cell '%s': %w", cellCfg.Name, err)
7367
}
68+
r.Recorder.Eventf(cluster, "Normal", "Applied", "Applied Cell %s", desired.Name)
7469

75-
existing.Spec = desired.Spec
76-
existing.Labels = desired.Labels
77-
if err := r.Update(ctx, existing); err != nil {
78-
return fmt.Errorf("failed to update cell '%s': %w", cellCfg.Name, err)
79-
}
8070
}
8171

8272
for _, item := range existingCells.Items {

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

Lines changed: 21 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,8 @@ import (
77

88
multigresv1alpha1 "github.com/numtide/multigres-operator/api/v1alpha1"
99
"github.com/numtide/multigres-operator/pkg/testutil"
10-
appsv1 "k8s.io/api/apps/v1"
1110
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12-
"k8s.io/apimachinery/pkg/runtime"
13-
"k8s.io/apimachinery/pkg/types"
14-
"k8s.io/client-go/tools/record"
15-
ctrl "sigs.k8s.io/controller-runtime"
1611
"sigs.k8s.io/controller-runtime/pkg/client"
17-
"sigs.k8s.io/controller-runtime/pkg/client/fake"
1812
)
1913

2014
func TestReconcile_Cells(t *testing.T) {
@@ -46,34 +40,13 @@ func TestReconcile_Cells(t *testing.T) {
4640
},
4741
wantErrMsg: "failed to list existing cells",
4842
},
49-
"Error: Create Cell Failed": {
43+
"Error: Apply Cell Failed": {
5044
failureConfig: &testutil.FailureConfig{
51-
OnCreate: testutil.FailOnObjectName(clusterName+"-zone-a", errSimulated),
45+
OnPatch: testutil.FailOnObjectName(clusterName+"-zone-a", errSimulated),
5246
},
53-
wantErrMsg: "failed to create cell",
54-
},
55-
"Error: Get Cell Failed (Unexpected Error)": {
56-
failureConfig: &testutil.FailureConfig{
57-
OnGet: testutil.FailOnKeyName(clusterName+"-zone-a", errSimulated),
58-
},
59-
wantErrMsg: "failed to get cell",
60-
},
61-
"Error: Update Cell Failed": {
62-
existingObjects: []client.Object{
63-
coreTpl, cellTpl, shardTpl,
64-
&multigresv1alpha1.Cell{
65-
ObjectMeta: metav1.ObjectMeta{
66-
Name: clusterName + "-zone-a",
67-
Namespace: namespace,
68-
Labels: map[string]string{"multigres.com/cluster": clusterName},
69-
},
70-
},
71-
},
72-
failureConfig: &testutil.FailureConfig{
73-
OnUpdate: testutil.FailOnObjectName(clusterName+"-zone-a", errSimulated),
74-
},
75-
wantErrMsg: "failed to update cell",
47+
wantErrMsg: "failed to apply cell",
7648
},
49+
7750
"Error: Prune Cell Failed": {
7851
existingObjects: []client.Object{
7952
coreTpl, shardTpl,
@@ -170,78 +143,25 @@ func TestReconcile_Cells(t *testing.T) {
170143
},
171144
},
172145
},
173-
}
174-
175-
runReconcileTest(t, tests)
176-
}
177-
178-
func TestReconcile_Cells_BuildFailure(t *testing.T) {
179-
t.Parallel()
180-
scheme := runtime.NewScheme()
181-
_ = multigresv1alpha1.AddToScheme(scheme)
182-
_ = appsv1.AddToScheme(scheme)
183-
184-
cluster := &multigresv1alpha1.MultigresCluster{
185-
TypeMeta: metav1.TypeMeta{
186-
Kind: "MultigresCluster",
187-
APIVersion: multigresv1alpha1.GroupVersion.String(),
188-
},
189-
ObjectMeta: metav1.ObjectMeta{
190-
Name: "c1",
191-
Namespace: "ns1",
192-
Finalizers: []string{"multigres.com/finalizer"},
193-
},
194-
Spec: multigresv1alpha1.MultigresClusterSpec{
195-
// Need to disable global components to avoid them failing first (on cluster name if long? No, we use normal cluster name here)
196-
// But for Cells, we use Long Cell Name.
197-
GlobalTopoServer: &multigresv1alpha1.GlobalTopoServerSpec{
198-
External: &multigresv1alpha1.ExternalTopoServerSpec{
199-
Endpoints: []multigresv1alpha1.EndpointUrl{"http://ext:2379"},
200-
},
201-
},
202-
MultiAdmin: nil,
203-
Cells: []multigresv1alpha1.CellConfig{ // This will trigger Build failure due to long name
204-
{Name: strings.Repeat("a", 64), CellTemplate: "cell-tpl"},
146+
"Error: Build Cell Failed (Name Too Long)": {
147+
preReconcileUpdate: func(t testing.TB, c *multigresv1alpha1.MultigresCluster) {
148+
// Use normal cluster name, but put a long name in Cells config
149+
c.Spec.Cells = []multigresv1alpha1.CellConfig{
150+
{Name: strings.Repeat("a", 64), CellTemplate: "default-cell"},
151+
}
152+
// Ensure GlobalTopo doesn't fail
153+
c.Spec.GlobalTopoServer = &multigresv1alpha1.GlobalTopoServerSpec{
154+
External: &multigresv1alpha1.ExternalTopoServerSpec{
155+
Endpoints: []multigresv1alpha1.EndpointUrl{"http://ext:2379"},
156+
},
157+
}
158+
// Disable MultiAdmin to avoid distractions
159+
c.Spec.MultiAdmin = nil
205160
},
161+
existingObjects: []client.Object{coreTpl, cellTpl, shardTpl},
162+
wantErrMsg: "failed to build cell",
206163
},
207164
}
208165

209-
tmpl := &multigresv1alpha1.CellTemplate{
210-
TypeMeta: metav1.TypeMeta{
211-
Kind: "CellTemplate",
212-
APIVersion: multigresv1alpha1.GroupVersion.String(),
213-
},
214-
ObjectMeta: metav1.ObjectMeta{Name: "cell-tpl", Namespace: "ns1"},
215-
Spec: multigresv1alpha1.CellTemplateSpec{},
216-
}
217-
218-
c := fake.NewClientBuilder().
219-
WithScheme(scheme).
220-
WithObjects(cluster, tmpl).
221-
Build()
222-
223-
r := &MultigresClusterReconciler{
224-
Client: c,
225-
Scheme: scheme,
226-
Recorder: record.NewFakeRecorder(100),
227-
}
228-
229-
req := ctrl.Request{
230-
NamespacedName: types.NamespacedName{Name: cluster.Name, Namespace: cluster.Namespace},
231-
}
232-
233-
_, err := r.Reconcile(t.Context(), req)
234-
if err == nil {
235-
t.Fatal("Expected error from Reconcile, got nil")
236-
}
237-
if !contains(err.Error(), "failed to build cell") {
238-
// If MultiAdmin fails, we will see it here. But since we used normal cluster name and nil MultiAdmin (which likely defaults),
239-
// we might hit MultiAdmin build.
240-
// If MultiAdmin defaults, it builds.
241-
// Does MultiAdmin validation fail? No.
242-
// So MultiAdmin succeeds.
243-
// Then Cells run. Cell name is long. Validation fails.
244-
// WE SHOULD SEE "failed to build cell".
245-
t.Errorf("Unexpected error: %v", err)
246-
}
166+
runReconcileTest(t, tests)
247167
}

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

Lines changed: 25 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,12 @@ import (
66
"testing"
77

88
"github.com/numtide/multigres-operator/pkg/testutil"
9-
appsv1 "k8s.io/api/apps/v1"
10-
"k8s.io/client-go/tools/record"
119
"k8s.io/utils/ptr"
1210

1311
multigresv1alpha1 "github.com/numtide/multigres-operator/api/v1alpha1"
1412
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15-
"k8s.io/apimachinery/pkg/runtime"
1613
"k8s.io/apimachinery/pkg/types"
17-
ctrl "sigs.k8s.io/controller-runtime"
1814
"sigs.k8s.io/controller-runtime/pkg/client"
19-
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2015
)
2116

2217
func TestReconcile_Databases(t *testing.T) {
@@ -145,34 +140,13 @@ func TestReconcile_Databases(t *testing.T) {
145140
},
146141
wantErrMsg: "failed to resolve shard",
147142
},
148-
"Error: Create TableGroup Failed": {
143+
"Error: Apply TableGroup Failed": {
149144
failureConfig: &testutil.FailureConfig{
150-
OnCreate: testutil.FailOnObjectName(clusterName+"-db1-tg1", errSimulated),
145+
OnPatch: testutil.FailOnObjectName(clusterName+"-db1-tg1", errSimulated),
151146
},
152-
wantErrMsg: "failed to create tablegroup",
153-
},
154-
"Error: Get TableGroup Failed (Unexpected Error)": {
155-
failureConfig: &testutil.FailureConfig{
156-
OnGet: testutil.FailOnKeyName(clusterName+"-db1-tg1", errSimulated),
157-
},
158-
wantErrMsg: "failed to get tablegroup",
159-
},
160-
"Error: Update TableGroup Failed": {
161-
existingObjects: []client.Object{
162-
coreTpl, cellTpl, shardTpl,
163-
&multigresv1alpha1.TableGroup{
164-
ObjectMeta: metav1.ObjectMeta{
165-
Name: clusterName + "-db1-tg1",
166-
Namespace: namespace,
167-
Labels: map[string]string{"multigres.com/cluster": clusterName},
168-
},
169-
},
170-
},
171-
failureConfig: &testutil.FailureConfig{
172-
OnUpdate: testutil.FailOnObjectName(clusterName+"-db1-tg1", errSimulated),
173-
},
174-
wantErrMsg: "failed to update tablegroup",
147+
wantErrMsg: "failed to apply tablegroup",
175148
},
149+
176150
"Error: Prune TableGroup Failed": {
177151
existingObjects: []client.Object{
178152
coreTpl, cellTpl, shardTpl,
@@ -235,79 +209,32 @@ func TestReconcile_Databases(t *testing.T) {
235209
},
236210
wantErrMsg: "exceeds 50 characters",
237211
},
238-
}
239-
240-
runReconcileTest(t, tests)
241-
}
242-
243-
func TestReconcile_Databases_BuildFailure(t *testing.T) {
244-
t.Parallel()
245-
scheme := runtime.NewScheme()
246-
_ = multigresv1alpha1.AddToScheme(scheme)
247-
_ = appsv1.AddToScheme(scheme)
248-
249-
cluster := &multigresv1alpha1.MultigresCluster{
250-
TypeMeta: metav1.TypeMeta{
251-
Kind: "MultigresCluster",
252-
APIVersion: multigresv1alpha1.GroupVersion.String(),
253-
},
254-
ObjectMeta: metav1.ObjectMeta{
255-
Name: "c1",
256-
Namespace: "ns1",
257-
Finalizers: []string{"multigres.com/finalizer"},
258-
},
259-
Spec: multigresv1alpha1.MultigresClusterSpec{
260-
GlobalTopoServer: &multigresv1alpha1.GlobalTopoServerSpec{
261-
External: &multigresv1alpha1.ExternalTopoServerSpec{
262-
Endpoints: []multigresv1alpha1.EndpointUrl{"http://ext:2379"},
263-
},
264-
},
265-
MultiAdmin: nil, // MultiAdmin will default and succeed.
266-
Databases: []multigresv1alpha1.DatabaseConfig{
267-
{
268-
Name: strings.Repeat("a", 64), // Long Name triggers failure in BuildTableGroup
269-
TableGroups: []multigresv1alpha1.TableGroupConfig{
270-
{
271-
Name: "tg1",
272-
Shards: []multigresv1alpha1.ShardConfig{
273-
{Name: "0", ShardTemplate: "shard-tpl"},
212+
"Error: Build TableGroup Failed (Name Too Long)": {
213+
preReconcileUpdate: func(t testing.TB, c *multigresv1alpha1.MultigresCluster) {
214+
c.Spec.GlobalTopoServer = &multigresv1alpha1.GlobalTopoServerSpec{
215+
External: &multigresv1alpha1.ExternalTopoServerSpec{
216+
Endpoints: []multigresv1alpha1.EndpointUrl{"http://ext:2379"},
217+
},
218+
}
219+
c.Spec.MultiAdmin = nil
220+
c.Spec.Databases = []multigresv1alpha1.DatabaseConfig{
221+
{
222+
Name: strings.Repeat("a", 64),
223+
TableGroups: []multigresv1alpha1.TableGroupConfig{
224+
{
225+
Name: "tg1",
226+
Shards: []multigresv1alpha1.ShardConfig{
227+
{Name: "0", ShardTemplate: "default-shard"},
228+
},
274229
},
275230
},
276231
},
277-
},
232+
}
278233
},
234+
existingObjects: []client.Object{coreTpl, cellTpl, shardTpl},
235+
wantErrMsg: "failed to build tablegroup",
279236
},
280237
}
281238

282-
tmpl := &multigresv1alpha1.ShardTemplate{
283-
TypeMeta: metav1.TypeMeta{
284-
Kind: "ShardTemplate",
285-
APIVersion: multigresv1alpha1.GroupVersion.String(),
286-
},
287-
ObjectMeta: metav1.ObjectMeta{Name: "shard-tpl", Namespace: "ns1"},
288-
Spec: multigresv1alpha1.ShardTemplateSpec{},
289-
}
290-
291-
c := fake.NewClientBuilder().
292-
WithScheme(scheme).
293-
WithObjects(cluster, tmpl).
294-
Build()
295-
296-
r := &MultigresClusterReconciler{
297-
Client: c,
298-
Scheme: scheme,
299-
Recorder: record.NewFakeRecorder(100),
300-
}
301-
302-
req := ctrl.Request{
303-
NamespacedName: types.NamespacedName{Name: cluster.Name, Namespace: cluster.Namespace},
304-
}
305-
306-
_, err := r.Reconcile(t.Context(), req)
307-
if err == nil {
308-
t.Fatal("Expected error from Reconcile, got nil")
309-
}
310-
if !contains(err.Error(), "failed to build tablegroup") {
311-
t.Errorf("Unexpected error: %v", err)
312-
}
239+
runReconcileTest(t, tests)
313240
}

0 commit comments

Comments
 (0)