github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/controller/rsm/transformer_member_reconfiguration_test.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package rsm 21 22 import ( 23 "context" 24 25 . "github.com/onsi/ginkgo/v2" 26 . "github.com/onsi/gomega" 27 28 "github.com/golang/mock/gomock" 29 apps "k8s.io/api/apps/v1" 30 batchv1 "k8s.io/api/batch/v1" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 33 workloads "github.com/1aal/kubeblocks/apis/workloads/v1alpha1" 34 "github.com/1aal/kubeblocks/pkg/constant" 35 "github.com/1aal/kubeblocks/pkg/controller/builder" 36 "github.com/1aal/kubeblocks/pkg/controller/graph" 37 "github.com/1aal/kubeblocks/pkg/controller/model" 38 ) 39 40 var _ = Describe("member reconfiguration transformer test.", func() { 41 buildMembersStatus := func(replicas int) []workloads.MemberStatus { 42 var membersStatus []workloads.MemberStatus 43 for i := 0; i < replicas; i++ { 44 status := workloads.MemberStatus{ 45 PodName: getPodName(rsm.Name, i), 46 ReplicaRole: workloads.ReplicaRole{Name: "follower"}, 47 } 48 membersStatus = append(membersStatus, status) 49 } 50 leaderIndex := 0 51 if replicas > 1 { 52 leaderIndex = 1 53 } 54 membersStatus[leaderIndex].ReplicaRole = workloads.ReplicaRole{Name: "leader", IsLeader: true} 55 return membersStatus 56 } 57 setRSMStatus := func(replicas int) { 58 membersStatus := buildMembersStatus(replicas) 59 rsm.Status.InitReplicas = 3 60 rsm.Status.ReadyInitReplicas = rsm.Status.InitReplicas 61 rsm.Status.MembersStatus = membersStatus 62 rsm.Status.Replicas = *rsm.Spec.Replicas 63 rsm.Status.ReadyReplicas = rsm.Status.Replicas 64 rsm.Status.AvailableReplicas = rsm.Status.Replicas 65 rsm.Status.UpdatedReplicas = rsm.Status.Replicas 66 } 67 mockAction := func(ordinal int, actionType string, succeed bool) *batchv1.Job { 68 actionName := getActionName(rsm.Name, int(rsm.Generation), ordinal, actionType) 69 action := builder.NewJobBuilder(name, actionName). 70 AddLabelsInMap(map[string]string{ 71 constant.AppInstanceLabelKey: rsm.Name, 72 constant.KBManagedByKey: kindReplicatedStateMachine, 73 jobScenarioLabel: jobScenarioMembership, 74 jobTypeLabel: actionType, 75 jobHandledLabel: jobHandledFalse, 76 }). 77 SetSuspend(false). 78 GetObject() 79 if succeed { 80 action.Status.Succeeded = 1 81 k8sMock.EXPECT(). 82 List(gomock.Any(), &batchv1.JobList{}, gomock.Any()). 83 DoAndReturn(func(_ context.Context, list *batchv1.JobList, _ ...client.ListOption) error { 84 Expect(list).ShouldNot(BeNil()) 85 list.Items = []batchv1.Job{*action} 86 return nil 87 }).Times(1) 88 } 89 return action 90 } 91 mockDAG := func(stsOld, stsNew *apps.StatefulSet) *graph.DAG { 92 d := graph.NewDAG() 93 graphCli.Root(d, transCtx.rsmOrig, transCtx.rsm, model.ActionStatusPtr()) 94 graphCli.Update(d, stsOld, stsNew) 95 return d 96 } 97 expectStsNoopAction := func(d *graph.DAG, noop bool) { 98 stsList := graphCli.FindAll(d, &apps.StatefulSet{}) 99 Expect(stsList).Should(HaveLen(1)) 100 sts, _ := stsList[0].(*apps.StatefulSet) 101 Expect(graphCli.IsAction(d, sts, model.ActionNoopPtr())).Should(Equal(noop)) 102 } 103 104 BeforeEach(func() { 105 rsm = builder.NewReplicatedStateMachineBuilder(namespace, name). 106 SetUID(uid). 107 SetServiceName(headlessSvcName). 108 AddMatchLabelsInMap(selectors). 109 SetReplicas(3). 110 SetRoles(roles). 111 SetRoleProbe(roleProbe). 112 SetMembershipReconfiguration(&reconfiguration). 113 SetService(service). 114 GetObject() 115 116 transCtx = &rsmTransformContext{ 117 Context: ctx, 118 Client: graphCli, 119 EventRecorder: nil, 120 Logger: logger, 121 rsmOrig: rsm.DeepCopy(), 122 rsm: rsm, 123 } 124 125 dag = graph.NewDAG() 126 graphCli.Root(dag, transCtx.rsmOrig, transCtx.rsm, model.ActionStatusPtr()) 127 transformer = &MemberReconfigurationTransformer{} 128 }) 129 130 Context("roleful cluster initialization", func() { 131 It("should initialize well", func() { 132 By("initialReplicas=0") 133 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 134 Expect(rsm.Status.InitReplicas).Should(Equal(*rsm.Spec.Replicas)) 135 136 By("init one member") 137 membersStatus := buildMembersStatus(1) 138 rsm.Status.MembersStatus = membersStatus 139 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 140 Expect(rsm.Status.ReadyInitReplicas).Should(BeEquivalentTo(1)) 141 142 By("all members initialized") 143 setRSMStatus(int(*rsm.Spec.Replicas)) 144 k8sMock.EXPECT(). 145 List(gomock.Any(), &batchv1.JobList{}, gomock.Any()). 146 DoAndReturn(func(_ context.Context, list *batchv1.JobList, _ ...client.ListOption) error { 147 return nil 148 }).Times(1) 149 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 150 Expect(rsm.Status.ReadyInitReplicas).Should(Equal(rsm.Status.InitReplicas)) 151 }) 152 }) 153 154 Context("stateful cluster initialization", func() { 155 It("should work well", func() { 156 By("set spec.roles to nil") 157 rsm.Spec.Roles = nil 158 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 159 Expect(rsm.Status.InitReplicas).Should(BeEquivalentTo(0)) 160 Expect(rsm.Status.ReadyInitReplicas).Should(BeEquivalentTo(0)) 161 }) 162 }) 163 164 Context("scale-out", func() { 165 It("should work well", func() { 166 By("make rsm ready for scale-out") 167 setRSMStatus(int(*rsm.Spec.Replicas)) 168 generation := int64(2) 169 rsm.Generation = generation 170 rsm.Status.ObservedGeneration = generation 171 rsm.Status.CurrentGeneration = generation 172 stsOld := mockUnderlyingSts(*rsm, rsm.Generation) 173 // rsm spec updated 174 rsm.Generation = 3 175 replicas := int32(5) 176 rsm.Spec.Replicas = &replicas 177 sts := mockUnderlyingSts(*rsm, rsm.Generation) 178 graphCli.Update(dag, stsOld, sts) 179 180 By("update the underlying sts") 181 k8sMock.EXPECT(). 182 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 183 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 184 Expect(obj).ShouldNot(BeNil()) 185 *obj = *stsOld 186 return nil 187 }).Times(1) 188 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 189 expectStsNoopAction(dag, false) 190 191 rsm.Status.ObservedGeneration = rsm.Generation 192 rsm.Status.CurrentGeneration = rsm.Generation 193 194 By("prepare member 3 joining") 195 sts = mockUnderlyingSts(*rsm, rsm.Generation) 196 k8sMock.EXPECT(). 197 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 198 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 199 Expect(obj).ShouldNot(BeNil()) 200 *obj = *sts 201 return nil 202 }).Times(1) 203 k8sMock.EXPECT(). 204 List(gomock.Any(), &batchv1.JobList{}, gomock.Any()). 205 DoAndReturn(func(_ context.Context, list *batchv1.JobList, _ ...client.ListOption) error { 206 return nil 207 }).Times(1) 208 dag = mockDAG(sts, sts) 209 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 210 expectStsNoopAction(dag, true) 211 dagExpected := mockDAG(sts, sts) 212 graphCli.Noop(dagExpected, sts) 213 action := mockAction(3, jobTypeMemberJoinNotifying, false) 214 graphCli.Create(dagExpected, action) 215 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 216 217 By("make member 3 joining successfully and prepare member 4 joining") 218 setRSMStatus(4) 219 action = mockAction(3, jobTypeMemberJoinNotifying, true) 220 dag = mockDAG(sts, sts) 221 k8sMock.EXPECT(). 222 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 223 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 224 Expect(obj).ShouldNot(BeNil()) 225 *obj = *sts 226 return nil 227 }).Times(1) 228 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 229 expectStsNoopAction(dag, true) 230 dagExpected = mockDAG(sts, sts) 231 graphCli.Noop(dagExpected, sts) 232 graphCli.Update(dagExpected, action, action) 233 action = mockAction(4, jobTypeMemberJoinNotifying, false) 234 graphCli.Create(dagExpected, action) 235 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 236 237 By("make member 4 joining successfully and cleanup") 238 setRSMStatus(int(*rsm.Spec.Replicas)) 239 action = mockAction(4, jobTypeMemberJoinNotifying, true) 240 dag = mockDAG(sts, sts) 241 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 242 expectStsNoopAction(dag, false) 243 dagExpected = mockDAG(sts, sts) 244 graphCli.Update(dagExpected, action, action) 245 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 246 }) 247 }) 248 249 Context("scale-in", func() { 250 It("should work well", func() { 251 setRSMMembersStatus := func(replicas int) { 252 membersStatus := buildMembersStatus(replicas) 253 rsm.Status.InitReplicas = 3 254 rsm.Status.ReadyInitReplicas = rsm.Status.InitReplicas 255 rsm.Status.MembersStatus = membersStatus 256 rsm.Status.UpdatedReplicas = int32(replicas) 257 } 258 By("make rsm ready for scale-in") 259 setRSMStatus(int(*rsm.Spec.Replicas)) 260 generation := int64(2) 261 rsm.Generation = generation 262 rsm.Status.ObservedGeneration = generation 263 rsm.Status.CurrentGeneration = generation 264 stsOld := mockUnderlyingSts(*rsm, rsm.Generation) 265 // rsm spec updated 266 rsm.Generation = 3 267 replicas := int32(1) 268 rsm.Spec.Replicas = &replicas 269 sts := mockUnderlyingSts(*rsm, rsm.Generation) 270 graphCli.Update(dag, stsOld, sts) 271 272 By("prepare member 2 leaving") 273 k8sMock.EXPECT(). 274 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 275 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 276 Expect(obj).ShouldNot(BeNil()) 277 *obj = *stsOld 278 return nil 279 }).Times(1) 280 k8sMock.EXPECT(). 281 List(gomock.Any(), &batchv1.JobList{}, gomock.Any()). 282 DoAndReturn(func(_ context.Context, list *batchv1.JobList, _ ...client.ListOption) error { 283 return nil 284 }).Times(1) 285 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 286 expectStsNoopAction(dag, true) 287 dagExpected := mockDAG(stsOld, sts) 288 graphCli.Noop(dagExpected, sts) 289 action := mockAction(2, jobTypeMemberLeaveNotifying, false) 290 graphCli.Create(dagExpected, action) 291 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 292 293 // after the first reconciliation, observedGeneration should be updated 294 rsm.Status.ObservedGeneration = rsm.Generation 295 296 By("make member 2 leaving successfully and prepare member 1 switchover") 297 setRSMMembersStatus(2) 298 action = mockAction(2, jobTypeMemberLeaveNotifying, true) 299 dag = mockDAG(stsOld, sts) 300 k8sMock.EXPECT(). 301 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 302 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 303 Expect(obj).ShouldNot(BeNil()) 304 *obj = *stsOld 305 return nil 306 }).Times(1) 307 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 308 expectStsNoopAction(dag, true) 309 dagExpected = mockDAG(stsOld, sts) 310 graphCli.Noop(dagExpected, sts) 311 graphCli.Update(dagExpected, action, action) 312 action = mockAction(1, jobTypeSwitchover, false) 313 graphCli.Create(dagExpected, action) 314 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 315 316 By("make member 1 switchover successfully and prepare member 1 leaving") 317 membersStatus := []workloads.MemberStatus{ 318 { 319 PodName: getPodName(rsm.Name, 0), 320 ReplicaRole: workloads.ReplicaRole{Name: "leader", IsLeader: true}, 321 }, 322 { 323 PodName: getPodName(rsm.Name, 1), 324 ReplicaRole: workloads.ReplicaRole{Name: "follower"}, 325 }, 326 } 327 rsm.Status.MembersStatus = membersStatus 328 action = mockAction(1, jobTypeSwitchover, true) 329 dag = mockDAG(stsOld, sts) 330 k8sMock.EXPECT(). 331 Get(gomock.Any(), gomock.Any(), &apps.StatefulSet{}, gomock.Any()). 332 DoAndReturn(func(_ context.Context, objKey client.ObjectKey, obj *apps.StatefulSet, _ ...client.GetOption) error { 333 Expect(obj).ShouldNot(BeNil()) 334 *obj = *stsOld 335 return nil 336 }).Times(1) 337 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 338 expectStsNoopAction(dag, true) 339 dagExpected = mockDAG(stsOld, sts) 340 graphCli.Noop(dagExpected, sts) 341 graphCli.Update(dagExpected, action, action) 342 action = mockAction(1, jobTypeMemberLeaveNotifying, false) 343 graphCli.Create(dagExpected, action) 344 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 345 346 By("make member 1 leaving successfully") 347 setRSMMembersStatus(1) 348 dag = mockDAG(stsOld, sts) 349 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 350 expectStsNoopAction(dag, false) 351 dagExpected = mockDAG(stsOld, sts) 352 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 353 354 By("update rsm status") 355 rsm.Status.CurrentGeneration = rsm.Generation 356 rsm.Status.Replicas = replicas 357 rsm.Status.ReadyReplicas = replicas 358 rsm.Status.AvailableReplicas = replicas 359 rsm.Status.UpdatedReplicas = replicas 360 action = mockAction(1, jobTypeMemberLeaveNotifying, true) 361 dag = mockDAG(stsOld, sts) 362 Expect(transformer.Transform(transCtx, dag)).Should(Succeed()) 363 expectStsNoopAction(dag, false) 364 dagExpected = mockDAG(stsOld, sts) 365 graphCli.Update(dagExpected, action, action) 366 Expect(dag.Equals(dagExpected, less)).Should(BeTrue()) 367 }) 368 }) 369 })