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  })