From 286680b2e48fb46190622b09f7fcef1e3fe8d333 Mon Sep 17 00:00:00 2001 From: Bo Liu Date: Fri, 10 Jan 2025 11:43:09 +0800 Subject: [PATCH] chore(pd): add uts for pd (#6026) Signed-off-by: liubo02 --- .github/licenserc.yaml | 1 - Makefile | 3 +- hack/boilerplate/boilerplate.txt | 13 + pkg/controllers/pd/builder.go | 6 +- pkg/controllers/pd/tasks/cm.go | 5 +- pkg/controllers/pd/tasks/cm_test.go | 182 ++++++++ pkg/controllers/pd/tasks/finalizer.go | 2 +- pkg/controllers/pd/tasks/finalizer_test.go | 430 ++++++++++++++++++ pkg/controllers/pd/tasks/pod.go | 5 +- pkg/controllers/pd/tasks/pod_test.go | 480 ++++++++++++++++++++ pkg/controllers/pd/tasks/pvc.go | 3 +- pkg/controllers/pd/tasks/pvc_test.go | 165 +++++++ pkg/controllers/pd/tasks/state_test.go | 132 ++++++ pkg/controllers/pd/tasks/status.go | 116 ++--- pkg/controllers/pd/tasks/status_test.go | 448 ++++++++++++++++++ pkg/controllers/tidb/tasks/state_test.go | 88 ++++ pkg/controllers/tiflash/tasks/state_test.go | 88 ++++ pkg/controllers/tiflash/tasks/status.go | 1 + pkg/controllers/tikv/tasks/state_test.go | 88 ++++ pkg/controllers/tikv/tasks/status.go | 1 + pkg/pdapi/v1/client.go | 1 + pkg/pdapi/v1/mock_generated.go | 316 +++++++++++++ pkg/timanager/pd/mock_generated.go | 242 ++++++++++ pkg/timanager/pd/pd.go | 1 + pkg/utils/k8s/pod.go | 3 + pkg/volumes/{mock.go => mock_generated.go} | 18 +- pkg/volumes/types.go | 2 +- pkg/volumes/utils.go | 3 +- pkg/volumes/utils_test.go | 6 +- 29 files changed, 2778 insertions(+), 71 deletions(-) create mode 100644 hack/boilerplate/boilerplate.txt create mode 100644 pkg/controllers/pd/tasks/cm_test.go create mode 100644 pkg/controllers/pd/tasks/finalizer_test.go create mode 100644 pkg/controllers/pd/tasks/pod_test.go create mode 100644 pkg/controllers/pd/tasks/pvc_test.go create mode 100644 pkg/controllers/pd/tasks/state_test.go create mode 100644 pkg/controllers/pd/tasks/status_test.go create mode 100644 pkg/controllers/tidb/tasks/state_test.go create mode 100644 pkg/controllers/tiflash/tasks/state_test.go create mode 100644 pkg/controllers/tikv/tasks/state_test.go create mode 100644 pkg/pdapi/v1/mock_generated.go create mode 100644 pkg/timanager/pd/mock_generated.go rename pkg/volumes/{mock.go => mock_generated.go} (81%) diff --git a/.github/licenserc.yaml b/.github/licenserc.yaml index 54ac24b794..8b4f28c04b 100644 --- a/.github/licenserc.yaml +++ b/.github/licenserc.yaml @@ -23,7 +23,6 @@ header: - '**/go.work.sum' - '**/LICENSE' - third_party/** - - pkg/**/*mock.go - '**/OWNERS' - OWNERS_ALIASES comment: on-failure diff --git a/Makefile b/Makefile index 990fd026cc..d327e6c5f6 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ PD_API_PATH = $(ROOT)/pkg/timanager/apis/pd GO_MODULE := github.com/pingcap/tidb-operator OVERLAY_PKG_DIR = $(ROOT)/pkg/overlay BOILERPLATE_FILE = $(ROOT)/hack/boilerplate/boilerplate.go.txt +MOCK_BOILERPLATE_FILE = $(ROOT)/hack/boilerplate/boilerplate.txt KIND_VERSION ?= v0.24.0 @@ -93,7 +94,7 @@ tidy: gengo: GEN_DIR ?= ./... gengo: bin/mockgen - GOBIN=$(BIN_DIR) GO_MODULE=$(GO_MODULE) go generate $(GEN_DIR) + BOILERPLATE_FILE=${MOCK_BOILERPLATE_FILE} GOBIN=$(BIN_DIR) GO_MODULE=$(GO_MODULE) go generate $(GEN_DIR) .PHONY: license license: bin/license-eye diff --git a/hack/boilerplate/boilerplate.txt b/hack/boilerplate/boilerplate.txt new file mode 100644 index 0000000000..f5c21cce48 --- /dev/null +++ b/hack/boilerplate/boilerplate.txt @@ -0,0 +1,13 @@ +Copyright 2024 PingCAP, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pkg/controllers/pd/builder.go b/pkg/controllers/pd/builder.go index 29dafb0e60..8346416c2d 100644 --- a/pkg/controllers/pd/builder.go +++ b/pkg/controllers/pd/builder.go @@ -50,14 +50,14 @@ func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.Task ), common.TaskContextPDSlice(state, r.Client), - tasks.TaskConfigMap(state, r.Logger, r.Client), + tasks.TaskConfigMap(state, r.Client), tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifier), - tasks.TaskPod(state, r.Logger, r.Client), + tasks.TaskPod(state, r.Client), // If pd client has not been registered yet, do not update status of the pd task.IfBreak(tasks.CondPDClientIsNotRegisterred(state), tasks.TaskStatusUnknown(), ), - tasks.TaskStatus(state, r.Logger, r.Client), + tasks.TaskStatus(state, r.Client), ) return runner diff --git a/pkg/controllers/pd/tasks/cm.go b/pkg/controllers/pd/tasks/cm.go index ec29684d85..fd0c415e71 100644 --- a/pkg/controllers/pd/tasks/cm.go +++ b/pkg/controllers/pd/tasks/cm.go @@ -17,7 +17,6 @@ package tasks import ( "context" - "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,7 +29,7 @@ import ( "github.com/pingcap/tidb-operator/pkg/utils/toml" ) -func TaskConfigMap(state *ReconcileContext, _ logr.Logger, c client.Client) task.Task { +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { // TODO: DON'T add bootstrap config back // We need to check current config and forbid adding bootstrap cfg back @@ -40,6 +39,7 @@ func TaskConfigMap(state *ReconcileContext, _ logr.Logger, c client.Client) task if err := decoder.Decode([]byte(state.PD().Spec.Config), &cfg); err != nil { return task.Fail().With("pd config cannot be decoded: %v", err) } + if err := cfg.Overlay(state.Cluster(), state.PD(), state.PDSlice()); err != nil { return task.Fail().With("cannot generate pd config: %v", err) } @@ -49,6 +49,7 @@ func TaskConfigMap(state *ReconcileContext, _ logr.Logger, c client.Client) task return task.Fail().With("pd config cannot be encoded: %v", err) } + // TODO(liubo02): avoid decode toml twice hash, err := hasher.GenerateHash(state.PD().Spec.Config) if err != nil { return task.Fail().With("failed to generate hash for `pd.spec.config`: %v", err) diff --git a/pkg/controllers/pd/tasks/cm_test.go b/pkg/controllers/pd/tasks/cm_test.go new file mode 100644 index 0000000000..d9dc7f1bf7 --- /dev/null +++ b/pkg/controllers/pd/tasks/cm_test.go @@ -0,0 +1,182 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +const fakePDAddr = "any string, useless in test" + +func TestTaskConfigMap(t *testing.T) { + cases := []struct { + desc string + state *ReconcileContext + objs []client.Object + unexpectedErr bool + invalidConfig bool + + expectedStatus task.Status + }{ + { + desc: "no config", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj[v1alpha1.PD]("aaa-xxx"), + cluster: fake.FakeObj("cluster", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster { + obj.Status.PD = fakePDAddr + return obj + }), + }, + }, + expectedStatus: task.SComplete, + }, + { + desc: "invalid config", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Config = `invalid` + return obj + }), + cluster: fake.FakeObj("cluster", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster { + obj.Status.PD = fakePDAddr + return obj + }), + }, + }, + invalidConfig: true, + expectedStatus: task.SFail, + }, + { + desc: "with managed field", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Config = `name = 'xxx'` + return obj + }), + cluster: fake.FakeObj("cluster", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster { + obj.Status.PD = fakePDAddr + return obj + }), + }, + }, + invalidConfig: true, + expectedStatus: task.SFail, + }, + { + desc: "has config map", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + cluster: fake.FakeObj("cluster", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster { + obj.Status.PD = fakePDAddr + return obj + }), + }, + }, + objs: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "aaa-pd-xxx", + }, + }, + }, + expectedStatus: task.SComplete, + }, + { + desc: "update config map failed", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + cluster: fake.FakeObj("cluster", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster { + obj.Status.PD = fakePDAddr + return obj + }), + }, + }, + objs: []client.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "aaa-pd-xxx", + }, + }, + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + ctx := context.Background() + var objs []client.Object + objs = append(objs, c.state.PD(), c.state.Cluster()) + for _, pd := range c.state.PDSlice() { + objs = append(objs, pd) + } + fc := client.NewFakeClient(objs...) + for _, obj := range c.objs { + require.NoError(tt, fc.Apply(ctx, obj), c.desc) + } + + if c.unexpectedErr { + // cannot update svc + fc.WithError("patch", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + } + + res, done := task.RunTask(ctx, TaskConfigMap(c.state, fc)) + assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), res.Message()) + assert.False(tt, done, c.desc) + + if !c.invalidConfig { + // config hash should be set + assert.NotEmpty(tt, c.state.ConfigHash, c.desc) + } + + if c.expectedStatus == task.SComplete { + cm := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "aaa-pd-xxx", + }, + } + require.NoError(tt, fc.Get(ctx, client.ObjectKeyFromObject(&cm), &cm), c.desc) + assert.Equal(tt, c.state.ConfigHash, cm.Labels[v1alpha1.LabelKeyConfigHash], c.desc) + } + }) + } +} diff --git a/pkg/controllers/pd/tasks/finalizer.go b/pkg/controllers/pd/tasks/finalizer.go index a9639ff509..51435abfe2 100644 --- a/pkg/controllers/pd/tasks/finalizer.go +++ b/pkg/controllers/pd/tasks/finalizer.go @@ -32,7 +32,7 @@ func TaskFinalizerDel(state *ReconcileContext, c client.Client) task.Task { // get member info successfully and the member still exists case state.IsAvailable && state.MemberID != "": // TODO: check whether quorum will be lost? - if err := state.PDClient.Underlay().DeleteMember(ctx, state.PD().Name); err != nil { + if err := state.PDClient.Underlay().DeleteMember(ctx, state.MemberID); err != nil { return task.Fail().With("cannot delete member: %v", err) } diff --git a/pkg/controllers/pd/tasks/finalizer_test.go b/pkg/controllers/pd/tasks/finalizer_test.go new file mode 100644 index 0000000000..63458a7724 --- /dev/null +++ b/pkg/controllers/pd/tasks/finalizer_test.go @@ -0,0 +1,430 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/pdapi/v1" + pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +func TestTaskFinalizerDel(t *testing.T) { + cases := []struct { + desc string + state *ReconcileContext + subresources []client.Object + needDelMember bool + unexpectedDelMemberErr bool + unexpectedErr bool + + expectedStatus task.Status + expectedObj *v1alpha1.PD + }{ + { + desc: "available, member id is set, no sub resources, no finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + needDelMember: true, + + expectedStatus: task.SComplete, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + }, + { + desc: "available, member id is set, failed to delete member", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + needDelMember: true, + unexpectedDelMemberErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is set, has sub resources, has finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + fake.FakeObj("aaa", func(obj *corev1.ConfigMap) *corev1.ConfigMap { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + fake.FakeObj("aaa", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + needDelMember: true, + + expectedStatus: task.SWait, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + { + desc: "available, member id is set, has sub resources(pod), failed to del sub resource", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + needDelMember: true, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is set, has sub resources(cm), failed to del sub resource", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.ConfigMap) *corev1.ConfigMap { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + needDelMember: true, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is set, has sub resources(pvc), failed to del sub resource", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + needDelMember: true, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is set, no sub resources, has finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + needDelMember: true, + + expectedStatus: task.SComplete, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = []string{} + return obj + }), + }, + { + desc: "available, member id is set, no sub resources, has finalizer, failed to del finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + MemberID: "aaa", + }, + needDelMember: true, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is not set, no sub resources, no finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + }, + IsAvailable: true, + }, + expectedStatus: task.SComplete, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + return obj + }), + }, + { + desc: "available, member id is not set, has sub resources, has finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.ConfigMap) *corev1.ConfigMap { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + + expectedStatus: task.SWait, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + { + desc: "available, member id is not set, has sub resources, failed to del sub resource", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + }, + subresources: []client.Object{ + fake.FakeObj("aaa", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyInstance: "aaa", + v1alpha1.LabelKeyCluster: "", + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + } + return obj + }), + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "available, member id is not set, no sub resources, has finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + }, + + expectedStatus: task.SComplete, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = []string{} + return obj + }), + }, + { + desc: "available, member id is not set, no sub resources, has finalizer, failed to del finalizer", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: true, + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "unavailable", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + IsAvailable: false, + }, + + expectedStatus: task.SFail, + expectedObj: fake.FakeObj("aaa", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Finalizers = append(obj.Finalizers, v1alpha1.Finalizer) + return obj + }), + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + objs := []client.Object{ + c.state.PD(), + } + + objs = append(objs, c.subresources...) + + fc := client.NewFakeClient(objs...) + if c.unexpectedErr { + // cannot remove finalizer + fc.WithError("update", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + // cannot delete sub resources + fc.WithError("delete", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var acts []action + if c.needDelMember { + var retErr error + if c.unexpectedDelMemberErr { + retErr = fmt.Errorf("fake err") + } + acts = append(acts, deleteMember(ctx, c.state.MemberID, retErr)) + } + + pdc := NewFakePDClient(tt, acts...) + c.state.PDClient = pdc + + res, done := task.RunTask(ctx, TaskFinalizerDel(c.state, fc)) + assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), c.desc) + assert.False(tt, done, c.desc) + + // no need to check update result + if c.unexpectedErr || c.unexpectedDelMemberErr { + return + } + + pd := &v1alpha1.PD{} + require.NoError(tt, fc.Get(ctx, client.ObjectKey{Name: "aaa"}, pd), c.desc) + assert.Equal(tt, c.expectedObj, pd, c.desc) + }) + } +} + +func deleteMember(ctx context.Context, name string, err error) action { + return func(ctrl *gomock.Controller, pdc *pdm.MockPDClient) { + underlay := pdapi.NewMockPDClient(ctrl) + pdc.EXPECT().Underlay().Return(underlay) + underlay.EXPECT().DeleteMember(ctx, name).Return(err) + } +} diff --git a/pkg/controllers/pd/tasks/pod.go b/pkg/controllers/pd/tasks/pod.go index a569a10328..2a742e8247 100644 --- a/pkg/controllers/pd/tasks/pod.go +++ b/pkg/controllers/pd/tasks/pod.go @@ -39,8 +39,9 @@ const ( defaultReadinessProbeInitialDelaySeconds = 5 ) -func TaskPod(state *ReconcileContext, logger logr.Logger, c client.Client) task.Task { +func TaskPod(state *ReconcileContext, c client.Client) task.Task { return task.NameTaskFunc("Pod", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) expected := newPod(state.Cluster(), state.PD(), state.ConfigHash) if state.Pod() == nil { // We have to refresh cache of members to make sure a pd without pod is unhealthy. @@ -86,7 +87,7 @@ func TaskPod(state *ReconcileContext, logger logr.Logger, c client.Client) task. state.PodIsTerminating = true - return task.Complete().With("pod is deleting") + return task.Wait().With("pod is deleting") } else if res == k8s.CompareResultUpdate { logger.Info("will update the pod in place") if err := c.Apply(ctx, expected); err != nil { diff --git a/pkg/controllers/pd/tasks/pod_test.go b/pkg/controllers/pd/tasks/pod_test.go new file mode 100644 index 0000000000..eabe13cbec --- /dev/null +++ b/pkg/controllers/pd/tasks/pod_test.go @@ -0,0 +1,480 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/pdapi/v1" + pdv1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" + pdm "github.com/pingcap/tidb-operator/pkg/timanager/pd" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +const fakeVersion = "v1.2.3" + +func TestTaskPod(t *testing.T) { + cases := []struct { + desc string + state *ReconcileContext + objs []client.Object + needRefresh bool + needTrasferTo string + // if true, cannot apply pod + unexpectedErr bool + + expectUpdatedPod bool + expectedPodIsTerminating bool + expectedStatus task.Status + }{ + { + desc: "no pod but healthy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + }, + Healthy: true, + }, + needRefresh: true, + + expectedStatus: task.SWait, + }, + { + desc: "no pod and unhealthy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + }, + }, + + expectUpdatedPod: true, + expectedStatus: task.SComplete, + }, + { + desc: "no pod and unhealthy, failed to apply", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + }, + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "pod spec changed", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + return obj + }), + }, + }, + + expectUpdatedPod: false, + expectedPodIsTerminating: true, + expectedStatus: task.SWait, + }, + { + desc: "pod spec changed, failed to delete", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + return obj + }), + }, + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "pod spec changed, pod is healthy, pod is leader", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + }, + } + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + return obj + }), + pds: []*v1alpha1.PD{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }, + } + return obj + }), + fake.FakeObj("aaa-yyy", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + }, + } + return obj + }), + }, + }, + Healthy: true, + IsLeader: true, + }, + needTrasferTo: "aaa-yyy", + + expectedStatus: task.SWait, + }, + { + desc: "pod spec changed, pod is healthy, pod is leader, no transferee", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + }, + } + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + return obj + }), + pds: []*v1alpha1.PD{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }, + } + return obj + }), + }, + }, + Healthy: true, + IsLeader: true, + }, + + expectedStatus: task.SFail, + }, + { + desc: "pod spec changed, pod is healthy, pod is not leader", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + }, + } + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + return obj + }), + pds: []*v1alpha1.PD{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }, + } + return obj + }), + fake.FakeObj("aaa-yyy", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + }, + } + return obj + }), + }, + }, + Healthy: true, + }, + + expectUpdatedPod: false, + expectedPodIsTerminating: true, + expectedStatus: task.SWait, + }, + { + desc: "pod spec hash not changed, config changed, hot reload policy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Spec.UpdateStrategy.Config = v1alpha1.ConfigUpdateStrategyHotReload + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyConfigHash: "newest", + v1alpha1.LabelKeyPodSpecHash: "6d6499ffc7", + } + return obj + }), + }, + ConfigHash: "newest", + }, + + expectUpdatedPod: true, + expectedStatus: task.SComplete, + }, + { + desc: "pod spec hash not changed, config changed, restart policy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Spec.UpdateStrategy.Config = v1alpha1.ConfigUpdateStrategyRestart + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyConfigHash: "old", + v1alpha1.LabelKeyPodSpecHash: "7cd7474797", + } + return obj + }), + }, + ConfigHash: "newest", + }, + + expectedPodIsTerminating: true, + expectUpdatedPod: false, + expectedStatus: task.SWait, + }, + { + desc: "pod spec hash not changed, pod labels changed, config not changed", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Spec.UpdateStrategy.Config = v1alpha1.ConfigUpdateStrategyRestart + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyConfigHash: "newest", + v1alpha1.LabelKeyPodSpecHash: "6d6499ffc7", + "xxx": "yyy", + } + return obj + }), + }, + ConfigHash: "newest", + }, + + expectUpdatedPod: true, + expectedStatus: task.SComplete, + }, + { + desc: "pod spec hash not changed, pod labels changed, config not changed, apply failed", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Spec.UpdateStrategy.Config = v1alpha1.ConfigUpdateStrategyRestart + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyConfigHash: "newest", + v1alpha1.LabelKeyPodSpecHash: "6d6499ffc7", + "xxx": "yyy", + } + return obj + }), + }, + ConfigHash: "newest", + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + { + desc: "all are not changed", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Version = fakeVersion + obj.Spec.UpdateStrategy.Config = v1alpha1.ConfigUpdateStrategyRestart + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstance: "aaa-xxx", + v1alpha1.LabelKeyConfigHash: "newest", + v1alpha1.LabelKeyPodSpecHash: "6d6499ffc7", + } + return obj + }), + }, + ConfigHash: "newest", + }, + + expectedStatus: task.SComplete, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + ctx := context.Background() + var objs []client.Object + objs = append(objs, c.state.PD(), c.state.Cluster()) + if c.state.Pod() != nil { + objs = append(objs, c.state.Pod()) + } + for _, pd := range c.state.PDSlice() { + if pd.Name == c.state.PD().Name { + continue + } + objs = append(objs, pd) + } + fc := client.NewFakeClient(objs...) + for _, obj := range c.objs { + require.NoError(tt, fc.Apply(ctx, obj), c.desc) + } + + var acts []action + if c.needRefresh { + acts = append(acts, refresh()) + } + if c.needTrasferTo != "" { + acts = append(acts, transferLeader(ctx, c.needTrasferTo, nil)) + } + + c.state.PDClient = NewFakePDClient(t, acts...) + + if c.unexpectedErr { + // cannot update pod + fc.WithError("patch", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + fc.WithError("delete", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + } + + res, done := task.RunTask(ctx, TaskPod(c.state, fc)) + assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), res.Message()) + assert.False(tt, done, c.desc) + + assert.Equal(tt, c.expectedPodIsTerminating, c.state.PodIsTerminating, c.desc) + + if c.expectUpdatedPod { + expectedPod := newPod(c.state.Cluster(), c.state.PD(), c.state.ConfigHash) + actual := c.state.Pod().DeepCopy() + actual.Kind = "" + actual.APIVersion = "" + actual.ManagedFields = nil + assert.Equal(tt, expectedPod, actual, c.desc) + } + }) + } +} + +func NewFakePDClient(t *testing.T, acts ...action) pdm.PDClient { + ctrl := gomock.NewController(t) + pdc := pdm.NewMockPDClient(ctrl) + for _, act := range acts { + act(ctrl, pdc) + } + + return pdc +} + +type action func(ctrl *gomock.Controller, pdc *pdm.MockPDClient) + +func refresh() action { + return func(ctrl *gomock.Controller, pdc *pdm.MockPDClient) { + cache := pdm.NewMockMemberCache[pdv1.Member](ctrl) + cache.EXPECT().Refresh() + pdc.EXPECT().Members().Return(cache) + } +} + +func transferLeader(ctx context.Context, name string, err error) action { + return func(ctrl *gomock.Controller, pdc *pdm.MockPDClient) { + underlay := pdapi.NewMockPDClient(ctrl) + pdc.EXPECT().Underlay().Return(underlay) + underlay.EXPECT().TransferPDLeader(ctx, name).Return(err) + } +} diff --git a/pkg/controllers/pd/tasks/pvc.go b/pkg/controllers/pd/tasks/pvc.go index e9e44d2ceb..ad347cd838 100644 --- a/pkg/controllers/pd/tasks/pvc.go +++ b/pkg/controllers/pd/tasks/pvc.go @@ -23,12 +23,13 @@ import ( "github.com/pingcap/tidb-operator/apis/core/v1alpha1" "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" maputil "github.com/pingcap/tidb-operator/pkg/utils/map" "github.com/pingcap/tidb-operator/pkg/utils/task/v3" "github.com/pingcap/tidb-operator/pkg/volumes" ) -func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task { +func TaskPVC(state common.PDState, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task { return task.NameTaskFunc("PVC", func(ctx context.Context) task.Result { pvcs := newPVCs(state.PD()) if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm, logger); err != nil { diff --git a/pkg/controllers/pd/tasks/pvc_test.go b/pkg/controllers/pd/tasks/pvc_test.go new file mode 100644 index 0000000000..1933144dde --- /dev/null +++ b/pkg/controllers/pd/tasks/pvc_test.go @@ -0,0 +1,165 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "fmt" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/pkg/volumes" +) + +func TestTaskPVC(t *testing.T) { + cases := []struct { + desc string + state common.PDState + pvcs []*corev1.PersistentVolumeClaim + unexpectedErr bool + + expectedStatus task.Status + expectedPVCNum int + }{ + { + desc: "no pvc", + state: &state{ + pd: fake.FakeObj[v1alpha1.PD]("aaa-xxx"), + }, + expectedStatus: task.SComplete, + expectedPVCNum: 0, + }, + { + desc: "create a data vol", + state: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Volumes = []v1alpha1.Volume{ + { + Name: "data", + Storage: resource.MustParse("10Gi"), + }, + } + return obj + }), + }, + expectedStatus: task.SComplete, + expectedPVCNum: 1, + }, + { + desc: "has a data vol", + state: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Volumes = []v1alpha1.Volume{ + { + Name: "data", + Storage: resource.MustParse("10Gi"), + }, + } + return obj + }), + }, + pvcs: []*corev1.PersistentVolumeClaim{ + fake.FakeObj("pd-aaa-pd-xxx-data", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + obj.Status.Phase = corev1.ClaimBound + return obj + }), + }, + expectedStatus: task.SComplete, + expectedPVCNum: 1, + }, + { + desc: "has a data vol, but failed to apply", + state: &state{ + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Volumes = []v1alpha1.Volume{ + { + Name: "data", + Storage: resource.MustParse("10Gi"), + }, + } + return obj + }), + }, + pvcs: []*corev1.PersistentVolumeClaim{ + fake.FakeObj("pd-aaa-pd-xxx-data", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + obj.Status.Phase = corev1.ClaimBound + return obj + }), + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + expectedPVCNum: 1, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + ctx := context.Background() + var objs []client.Object + objs = append(objs, c.state.PD()) + fc := client.NewFakeClient(objs...) + for _, obj := range c.pvcs { + require.NoError(tt, fc.Apply(ctx, obj), c.desc) + } + + ctrl := gomock.NewController(tt) + vm := volumes.NewMockModifier(ctrl) + expectedPVCs := newPVCs(c.state.PD()) + for _, expected := range expectedPVCs { + for _, current := range c.pvcs { + if current.Name == expected.Name { + vm.EXPECT().GetActualVolume(ctx, expected, current).Return(&volumes.ActualVolume{ + Desired: &volumes.DesiredVolume{}, + PVC: current, + }, nil) + vm.EXPECT().ShouldModify(ctx, &volumes.ActualVolume{ + Desired: &volumes.DesiredVolume{}, + PVC: current, + }).Return(false) + } + } + } + + if c.unexpectedErr { + // cannot update pvc + fc.WithError("patch", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + } + + res, done := task.RunTask(ctx, TaskPVC(c.state, logr.Discard(), fc, vm)) + assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), res.Message()) + assert.False(tt, done, c.desc) + + pvcs := corev1.PersistentVolumeClaimList{} + require.NoError(tt, fc.List(ctx, &pvcs), c.desc) + assert.Len(tt, pvcs.Items, c.expectedPVCNum, c.desc) + }) + } +} diff --git a/pkg/controllers/pd/tasks/state_test.go b/pkg/controllers/pd/tasks/state_test.go new file mode 100644 index 0000000000..47fc533127 --- /dev/null +++ b/pkg/controllers/pd/tasks/state_test.go @@ -0,0 +1,132 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +const ( + fakeClusterName = "cluster" +) + +func TestState(t *testing.T) { + cases := []struct { + desc string + key types.NamespacedName + objs []client.Object + + expected State + }{ + { + desc: "normal", + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + objs: []client.Object{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Cluster.Name = fakeClusterName + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + v1alpha1.LabelKeyCluster: fakeClusterName, + } + return obj + }), + fake.FakeObj[v1alpha1.Cluster](fakeClusterName), + fake.FakeObj("aaa-yyy", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Cluster.Name = fakeClusterName + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + v1alpha1.LabelKeyCluster: fakeClusterName, + } + return obj + }), + fake.FakeObj[corev1.Pod]("aaa-pd-xxx"), + }, + + expected: &state{ + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + pd: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Cluster.Name = fakeClusterName + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + v1alpha1.LabelKeyCluster: fakeClusterName, + } + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster](fakeClusterName), + pds: []*v1alpha1.PD{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Cluster.Name = fakeClusterName + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + v1alpha1.LabelKeyCluster: fakeClusterName, + } + return obj + }), + fake.FakeObj("aaa-yyy", func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Spec.Cluster.Name = fakeClusterName + obj.Labels = map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentPD, + v1alpha1.LabelKeyCluster: fakeClusterName, + } + return obj + }), + }, + pod: fake.FakeObj[corev1.Pod]("aaa-pd-xxx"), + }, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + fc := client.NewFakeClient(c.objs...) + + s := NewState(c.key) + + ctx := context.Background() + res, done := task.RunTask(ctx, task.Block( + common.TaskContextPD(s, fc), + common.TaskContextCluster(s, fc), + common.TaskContextPDSlice(s, fc), + common.TaskContextPod(s, fc), + )) + assert.Equal(tt, task.SComplete, res.Status(), c.desc) + assert.False(tt, done, c.desc) + assert.Equal(tt, c.expected, s, c.desc) + }) + } +} diff --git a/pkg/controllers/pd/tasks/status.go b/pkg/controllers/pd/tasks/status.go index 5d80e10fc7..8355ed3ef8 100644 --- a/pkg/controllers/pd/tasks/status.go +++ b/pkg/controllers/pd/tasks/status.go @@ -18,7 +18,6 @@ import ( "context" "time" - "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -35,61 +34,37 @@ func TaskStatusUnknown() task.Task { } //nolint:gocyclo // refactor if possible -func TaskStatus(state *ReconcileContext, _ logr.Logger, c client.Client) task.Task { +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { - var ( - healthStatus = metav1.ConditionFalse - healthMessage = "pd is not healthy" - - suspendStatus = metav1.ConditionFalse - suspendMessage = "pd is not suspended" + needUpdate := false + pd := state.PD() + pod := state.Pod() + // TODO(liubo02): simplify it + var healthy bool + + if pod != nil && + statefulset.IsPodRunningAndReady(pod) && + !state.PodIsTerminating && + state.Healthy { + healthy = true + } - needUpdate = false - ) + needUpdate = syncInitializedCond(pd, state.Initialized) || needUpdate + needUpdate = syncHealthCond(pd, healthy) || needUpdate + needUpdate = syncSuspendCond(pd) || needUpdate if state.MemberID != "" { - needUpdate = SetIfChanged(&state.PD().Status.ID, state.MemberID) || needUpdate - } - - needUpdate = SetIfChanged(&state.PD().Status.IsLeader, state.IsLeader) || needUpdate - needUpdate = syncInitializedCond(state.PD(), state.Initialized) || needUpdate - - needUpdate = meta.SetStatusCondition(&state.PD().Status.Conditions, metav1.Condition{ - Type: v1alpha1.PDCondSuspended, - Status: suspendStatus, - ObservedGeneration: state.PD().Generation, - Reason: v1alpha1.PDSuspendReason, - Message: suspendMessage, - }) || needUpdate - - needUpdate = SetIfChanged(&state.PD().Status.ObservedGeneration, state.PD().Generation) || needUpdate - needUpdate = SetIfChanged(&state.PD().Status.UpdateRevision, state.PD().Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate - - if state.Pod() == nil || state.PodIsTerminating { - state.Healthy = false - } else if statefulset.IsPodRunningAndReady(state.Pod()) && state.Healthy { - if state.PD().Status.CurrentRevision != state.Pod().Labels[v1alpha1.LabelKeyInstanceRevisionHash] { - state.PD().Status.CurrentRevision = state.Pod().Labels[v1alpha1.LabelKeyInstanceRevisionHash] - needUpdate = true - } - } else { - state.Healthy = false + needUpdate = SetIfChanged(&pd.Status.ID, state.MemberID) || needUpdate } - - if state.Healthy { - healthStatus = metav1.ConditionTrue - healthMessage = "pd is healthy" + needUpdate = SetIfChanged(&pd.Status.IsLeader, state.IsLeader) || needUpdate + needUpdate = SetIfChanged(&pd.Status.ObservedGeneration, pd.Generation) || needUpdate + needUpdate = SetIfChanged(&pd.Status.UpdateRevision, pd.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate + if healthy { + needUpdate = SetIfChanged(&pd.Status.CurrentRevision, pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash]) || needUpdate } - needUpdate = meta.SetStatusCondition(&state.PD().Status.Conditions, metav1.Condition{ - Type: v1alpha1.PDCondHealth, - Status: healthStatus, - ObservedGeneration: state.PD().Generation, - Reason: v1alpha1.PDHealthReason, - Message: healthMessage, - }) || needUpdate if needUpdate { - if err := c.Status().Update(ctx, state.PD()); err != nil { + if err := c.Status().Update(ctx, pd); err != nil { return task.Fail().With("cannot update status: %v", err) } } @@ -98,7 +73,7 @@ func TaskStatus(state *ReconcileContext, _ logr.Logger, c client.Client) task.Ta return task.Retry(5 * time.Second).With("pod is terminating, retry after it's terminated") } - if !state.Initialized || !state.Healthy { + if !healthy || !state.Initialized { return task.Wait().With("pd may not be initialized or healthy, wait for next event") } @@ -106,25 +81,64 @@ func TaskStatus(state *ReconcileContext, _ logr.Logger, c client.Client) task.Ta }) } +func syncHealthCond(pd *v1alpha1.PD, healthy bool) bool { + var ( + status = metav1.ConditionFalse + reason = "Unhealthy" + msg = "instance is not healthy" + ) + if healthy { + status = metav1.ConditionTrue + reason = "Healthy" + msg = "instance is healthy" + } + + return meta.SetStatusCondition(&pd.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondHealth, + Status: status, + ObservedGeneration: pd.Generation, + Reason: reason, + Message: msg, + }) +} + +func syncSuspendCond(pd *v1alpha1.PD) bool { + // always set it as unsuspended + return meta.SetStatusCondition(&pd.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: pd.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }) +} + +// TODO(liubo02): remove it, it seems useless // Status of this condition can only transfer as the below // 1. false => true // 2. true <=> unknown func syncInitializedCond(pd *v1alpha1.PD, initialized bool) bool { cond := meta.FindStatusCondition(pd.Status.Conditions, v1alpha1.PDCondInitialized) status := metav1.ConditionUnknown + reason := "Unavailable" + msg := "pd is unavailable" switch { case initialized: status = metav1.ConditionTrue + reason = "Initialized" + msg = "instance is initialized" case !initialized && (cond == nil || cond.Status == metav1.ConditionFalse): status = metav1.ConditionFalse + reason = "Uninitialized" + msg = "instance has not been initialized yet" } return meta.SetStatusCondition(&pd.Status.Conditions, metav1.Condition{ Type: v1alpha1.PDCondInitialized, Status: status, ObservedGeneration: pd.Generation, - Reason: "initialized", - Message: "instance has joined the cluster", + Reason: reason, + Message: msg, }) } diff --git a/pkg/controllers/pd/tasks/status_test.go b/pkg/controllers/pd/tasks/status_test.go new file mode 100644 index 0000000000..a2bc94a773 --- /dev/null +++ b/pkg/controllers/pd/tasks/status_test.go @@ -0,0 +1,448 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +const ( + newRevision = "new" + oldRevision = "old" + + fakePDName = "aaa-xxx" +) + +func TestTaskStatus(t *testing.T) { + now := metav1.Now() + cases := []struct { + desc string + state *ReconcileContext + unexpectedErr bool + + expectedStatus task.Status + expectedObj *v1alpha1.PD + }{ + { + desc: "no pod but healthy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + obj.Status.CurrentRevision = "keep" + return obj + }), + }, + Healthy: true, + Initialized: true, + IsLeader: true, + MemberID: fakePDName, + }, + + expectedStatus: task.SWait, + expectedObj: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + + obj.Status.ObservedGeneration = 3 + obj.Status.ID = fakePDName + obj.Status.IsLeader = true + obj.Status.UpdateRevision = newRevision + obj.Status.CurrentRevision = "keep" + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.PDCondInitialized, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + Reason: "Initialized", + Message: "instance is initialized", + }, + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: "Unhealthy", + Message: "instance is not healthy", + }, + { + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }, + } + + return obj + }), + }, + { + desc: "pod is healthy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + return obj + }), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: oldRevision, + } + obj.Status.Phase = corev1.PodRunning + obj.Status.Conditions = append(obj.Status.Conditions, corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }) + return obj + }), + }, + Healthy: true, + Initialized: true, + IsLeader: true, + MemberID: fakePDName, + }, + + expectedStatus: task.SComplete, + expectedObj: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + + obj.Status.ObservedGeneration = 3 + obj.Status.ID = fakePDName + obj.Status.IsLeader = true + obj.Status.UpdateRevision = newRevision + obj.Status.CurrentRevision = oldRevision + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.PDCondInitialized, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + Reason: "Initialized", + Message: "instance is initialized", + }, + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + Reason: "Healthy", + Message: "instance is healthy", + }, + { + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }, + } + + return obj + }), + }, + { + desc: "pod is deleting", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + return obj + }), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.SetDeletionTimestamp(&now) + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: oldRevision, + } + obj.Status.Phase = corev1.PodRunning + obj.Status.Conditions = append(obj.Status.Conditions, corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }) + return obj + }), + }, + PodIsTerminating: true, + Healthy: true, + Initialized: true, + IsLeader: true, + MemberID: fakePDName, + }, + + expectedStatus: task.SRetry, + expectedObj: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + + obj.Status.ObservedGeneration = 3 + obj.Status.ID = fakePDName + obj.Status.IsLeader = true + obj.Status.UpdateRevision = newRevision + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.PDCondInitialized, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + Reason: "Initialized", + Message: "instance is initialized", + }, + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: "Unhealthy", + Message: "instance is not healthy", + }, + { + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }, + } + + return obj + }), + }, + { + desc: "not init", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + return obj + }), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.SetDeletionTimestamp(&now) + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: oldRevision, + } + obj.Status.Phase = corev1.PodRunning + obj.Status.Conditions = append(obj.Status.Conditions, corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }) + return obj + }), + }, + Healthy: true, + IsLeader: true, + MemberID: fakePDName, + }, + + expectedStatus: task.SWait, + expectedObj: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + + obj.Status.ObservedGeneration = 3 + obj.Status.ID = fakePDName + obj.Status.IsLeader = true + obj.Status.CurrentRevision = oldRevision + obj.Status.UpdateRevision = newRevision + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.PDCondInitialized, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: "Uninitialized", + Message: "instance has not been initialized yet", + }, + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionTrue, + ObservedGeneration: 3, + Reason: "Healthy", + Message: "instance is healthy", + }, + { + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }, + } + + return obj + }), + }, + { + desc: "not init and not healthy", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + return obj + }), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.SetDeletionTimestamp(&now) + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: oldRevision, + } + obj.Status.Phase = corev1.PodRunning + obj.Status.Conditions = append(obj.Status.Conditions, corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }) + return obj + }), + }, + IsLeader: true, + MemberID: fakePDName, + }, + + expectedStatus: task.SWait, + expectedObj: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + + obj.Status.ObservedGeneration = 3 + obj.Status.ID = fakePDName + obj.Status.IsLeader = true + obj.Status.UpdateRevision = newRevision + obj.Status.Conditions = []metav1.Condition{ + { + Type: v1alpha1.PDCondInitialized, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: "Uninitialized", + Message: "instance has not been initialized yet", + }, + { + Type: v1alpha1.CondHealth, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: "Unhealthy", + Message: "instance is not healthy", + }, + { + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: 3, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instace is not suspended", + }, + } + + return obj + }), + }, + { + desc: "failed to update status", + state: &ReconcileContext{ + State: &state{ + pd: fake.FakeObj(fakePDName, func(obj *v1alpha1.PD) *v1alpha1.PD { + obj.Generation = 3 + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: newRevision, + } + return obj + }), + pod: fake.FakeObj("aaa-pd-xxx", func(obj *corev1.Pod) *corev1.Pod { + obj.SetDeletionTimestamp(&now) + obj.Labels = map[string]string{ + v1alpha1.LabelKeyInstanceRevisionHash: oldRevision, + } + obj.Status.Phase = corev1.PodRunning + obj.Status.Conditions = append(obj.Status.Conditions, corev1.PodCondition{ + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }) + return obj + }), + }, + IsLeader: true, + MemberID: fakePDName, + }, + unexpectedErr: true, + + expectedStatus: task.SFail, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + var objs []client.Object + objs = append(objs, c.state.PD()) + if c.state.Pod() != nil { + objs = append(objs, c.state.Pod()) + } + fc := client.NewFakeClient(objs...) + if c.unexpectedErr { + fc.WithError("*", "*", errors.NewInternalError(fmt.Errorf("fake internal err"))) + } + + ctx := context.Background() + res, done := task.RunTask(ctx, TaskStatus(c.state, fc)) + assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), c.desc) + assert.False(tt, done, c.desc) + + // no need to check update result + if c.unexpectedErr { + return + } + + obj := &v1alpha1.PD{} + require.NoError(tt, fc.Get(ctx, client.ObjectKey{Name: fakePDName}, obj), c.desc) + conds := obj.Status.Conditions + for i := range conds { + cond := &conds[i] + cond.LastTransitionTime = metav1.Time{} + } + assert.Equal(tt, c.expectedObj, obj, c.desc) + }) + } +} diff --git a/pkg/controllers/tidb/tasks/state_test.go b/pkg/controllers/tidb/tasks/state_test.go new file mode 100644 index 0000000000..4c20668e3f --- /dev/null +++ b/pkg/controllers/tidb/tasks/state_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +func TestState(t *testing.T) { + cases := []struct { + desc string + key types.NamespacedName + objs []client.Object + + expected State + }{ + { + desc: "normal", + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + objs: []client.Object{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiDB) *v1alpha1.TiDB { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + fake.FakeObj[v1alpha1.Cluster]("aaa"), + fake.FakeObj[corev1.Pod]("aaa-tidb-xxx"), + }, + + expected: &state{ + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + tidb: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiDB) *v1alpha1.TiDB { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj[corev1.Pod]("aaa-tidb-xxx"), + }, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + fc := client.NewFakeClient(c.objs...) + + s := NewState(c.key) + + ctx := context.Background() + res, done := task.RunTask(ctx, task.Block( + common.TaskContextTiDB(s, fc), + common.TaskContextCluster(s, fc), + common.TaskContextPod(s, fc), + )) + assert.Equal(tt, task.SComplete, res.Status(), c.desc) + assert.False(tt, done, c.desc) + assert.Equal(tt, c.expected, s, c.desc) + }) + } +} diff --git a/pkg/controllers/tiflash/tasks/state_test.go b/pkg/controllers/tiflash/tasks/state_test.go new file mode 100644 index 0000000000..75db46c45b --- /dev/null +++ b/pkg/controllers/tiflash/tasks/state_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +func TestState(t *testing.T) { + cases := []struct { + desc string + key types.NamespacedName + objs []client.Object + + expected State + }{ + { + desc: "normal", + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + objs: []client.Object{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiFlash) *v1alpha1.TiFlash { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + fake.FakeObj[v1alpha1.Cluster]("aaa"), + fake.FakeObj[corev1.Pod]("aaa-tiflash-xxx"), + }, + + expected: &state{ + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + tiflash: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiFlash) *v1alpha1.TiFlash { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj[corev1.Pod]("aaa-tiflash-xxx"), + }, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + fc := client.NewFakeClient(c.objs...) + + s := NewState(c.key) + + ctx := context.Background() + res, done := task.RunTask(ctx, task.Block( + common.TaskContextTiFlash(s, fc), + common.TaskContextCluster(s, fc), + common.TaskContextPod(s, fc), + )) + assert.Equal(tt, task.SComplete, res.Status(), c.desc) + assert.False(tt, done, c.desc) + assert.Equal(tt, c.expected, s, c.desc) + }) + } +} diff --git a/pkg/controllers/tiflash/tasks/status.go b/pkg/controllers/tiflash/tasks/status.go index 9b3591f18a..56a40c17cf 100644 --- a/pkg/controllers/tiflash/tasks/status.go +++ b/pkg/controllers/tiflash/tasks/status.go @@ -44,6 +44,7 @@ func TaskStatus(state *ReconcileContext, c client.Client) task.Task { if pod != nil && statefulset.IsPodRunningAndReady(pod) && !state.PodIsTerminating && + state.Store != nil && state.Store.NodeState == v1alpha1.StoreStateServing { healthy = true } diff --git a/pkg/controllers/tikv/tasks/state_test.go b/pkg/controllers/tikv/tasks/state_test.go new file mode 100644 index 0000000000..e4ff0d0598 --- /dev/null +++ b/pkg/controllers/tikv/tasks/state_test.go @@ -0,0 +1,88 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package tasks + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/apis/core/v1alpha1" + "github.com/pingcap/tidb-operator/pkg/client" + "github.com/pingcap/tidb-operator/pkg/controllers/common" + "github.com/pingcap/tidb-operator/pkg/utils/fake" + "github.com/pingcap/tidb-operator/pkg/utils/task/v3" +) + +func TestState(t *testing.T) { + cases := []struct { + desc string + key types.NamespacedName + objs []client.Object + + expected State + }{ + { + desc: "normal", + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + objs: []client.Object{ + fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiKV) *v1alpha1.TiKV { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + fake.FakeObj[v1alpha1.Cluster]("aaa"), + fake.FakeObj[corev1.Pod]("aaa-tikv-xxx"), + }, + + expected: &state{ + key: types.NamespacedName{ + Name: "aaa-xxx", + }, + tikv: fake.FakeObj("aaa-xxx", func(obj *v1alpha1.TiKV) *v1alpha1.TiKV { + obj.Spec.Cluster.Name = "aaa" + return obj + }), + cluster: fake.FakeObj[v1alpha1.Cluster]("aaa"), + pod: fake.FakeObj[corev1.Pod]("aaa-tikv-xxx"), + }, + }, + } + + for i := range cases { + c := &cases[i] + t.Run(c.desc, func(tt *testing.T) { + tt.Parallel() + + fc := client.NewFakeClient(c.objs...) + + s := NewState(c.key) + + ctx := context.Background() + res, done := task.RunTask(ctx, task.Block( + common.TaskContextTiKV(s, fc), + common.TaskContextCluster(s, fc), + common.TaskContextPod(s, fc), + )) + assert.Equal(tt, task.SComplete, res.Status(), c.desc) + assert.False(tt, done, c.desc) + assert.Equal(tt, c.expected, s, c.desc) + }) + } +} diff --git a/pkg/controllers/tikv/tasks/status.go b/pkg/controllers/tikv/tasks/status.go index 209d59bb55..1349b9441c 100644 --- a/pkg/controllers/tikv/tasks/status.go +++ b/pkg/controllers/tikv/tasks/status.go @@ -45,6 +45,7 @@ func TaskStatus(state *ReconcileContext, c client.Client) task.Task { if pod != nil && statefulset.IsPodRunningAndReady(pod) && !state.PodIsTerminating && + state.Store != nil && state.Store.NodeState == v1alpha1.StoreStateServing { healthy = true } diff --git a/pkg/pdapi/v1/client.go b/pkg/pdapi/v1/client.go index 64c26ddb8c..3015cc042a 100644 --- a/pkg/pdapi/v1/client.go +++ b/pkg/pdapi/v1/client.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:generate ${GOBIN}/mockgen -write_command_comment=false -copyright_file ${BOILERPLATE_FILE} -destination mock_generated.go -package=pdapi ${GO_MODULE}/pkg/pdapi/v1 PDClient package pdapi import ( diff --git a/pkg/pdapi/v1/mock_generated.go b/pkg/pdapi/v1/mock_generated.go new file mode 100644 index 0000000000..c7c31d9399 --- /dev/null +++ b/pkg/pdapi/v1/mock_generated.go @@ -0,0 +1,316 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/pingcap/tidb-operator/pkg/pdapi/v1 (interfaces: PDClient) + +// Package pdapi is a generated GoMock package. +package pdapi + +import ( + context "context" + reflect "reflect" + + metapb "github.com/pingcap/kvproto/pkg/metapb" + pdpb "github.com/pingcap/kvproto/pkg/pdpb" + gomock "go.uber.org/mock/gomock" +) + +// MockPDClient is a mock of PDClient interface. +type MockPDClient struct { + ctrl *gomock.Controller + recorder *MockPDClientMockRecorder + isgomock struct{} +} + +// MockPDClientMockRecorder is the mock recorder for MockPDClient. +type MockPDClientMockRecorder struct { + mock *MockPDClient +} + +// NewMockPDClient creates a new mock instance. +func NewMockPDClient(ctrl *gomock.Controller) *MockPDClient { + mock := &MockPDClient{ctrl: ctrl} + mock.recorder = &MockPDClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPDClient) EXPECT() *MockPDClientMockRecorder { + return m.recorder +} + +// BeginEvictLeader mocks base method. +func (m *MockPDClient) BeginEvictLeader(ctx context.Context, storeID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeginEvictLeader", ctx, storeID) + ret0, _ := ret[0].(error) + return ret0 +} + +// BeginEvictLeader indicates an expected call of BeginEvictLeader. +func (mr *MockPDClientMockRecorder) BeginEvictLeader(ctx, storeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginEvictLeader", reflect.TypeOf((*MockPDClient)(nil).BeginEvictLeader), ctx, storeID) +} + +// DeleteMember mocks base method. +func (m *MockPDClient) DeleteMember(ctx context.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMember", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMember indicates an expected call of DeleteMember. +func (mr *MockPDClientMockRecorder) DeleteMember(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMember", reflect.TypeOf((*MockPDClient)(nil).DeleteMember), ctx, name) +} + +// DeleteMemberByID mocks base method. +func (m *MockPDClient) DeleteMemberByID(ctx context.Context, memberID uint64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMemberByID", ctx, memberID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMemberByID indicates an expected call of DeleteMemberByID. +func (mr *MockPDClientMockRecorder) DeleteMemberByID(ctx, memberID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMemberByID", reflect.TypeOf((*MockPDClient)(nil).DeleteMemberByID), ctx, memberID) +} + +// DeleteStore mocks base method. +func (m *MockPDClient) DeleteStore(ctx context.Context, storeID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteStore", ctx, storeID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteStore indicates an expected call of DeleteStore. +func (mr *MockPDClientMockRecorder) DeleteStore(ctx, storeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStore", reflect.TypeOf((*MockPDClient)(nil).DeleteStore), ctx, storeID) +} + +// EndEvictLeader mocks base method. +func (m *MockPDClient) EndEvictLeader(ctx context.Context, storeID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EndEvictLeader", ctx, storeID) + ret0, _ := ret[0].(error) + return ret0 +} + +// EndEvictLeader indicates an expected call of EndEvictLeader. +func (mr *MockPDClientMockRecorder) EndEvictLeader(ctx, storeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndEvictLeader", reflect.TypeOf((*MockPDClient)(nil).EndEvictLeader), ctx, storeID) +} + +// GetCluster mocks base method. +func (m *MockPDClient) GetCluster(ctx context.Context) (*metapb.Cluster, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetCluster", ctx) + ret0, _ := ret[0].(*metapb.Cluster) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCluster indicates an expected call of GetCluster. +func (mr *MockPDClientMockRecorder) GetCluster(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCluster", reflect.TypeOf((*MockPDClient)(nil).GetCluster), ctx) +} + +// GetConfig mocks base method. +func (m *MockPDClient) GetConfig(ctx context.Context) (*PDConfigFromAPI, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfig", ctx) + ret0, _ := ret[0].(*PDConfigFromAPI) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetConfig indicates an expected call of GetConfig. +func (mr *MockPDClientMockRecorder) GetConfig(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockPDClient)(nil).GetConfig), ctx) +} + +// GetEvictLeaderScheduler mocks base method. +func (m *MockPDClient) GetEvictLeaderScheduler(ctx context.Context, storeID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEvictLeaderScheduler", ctx, storeID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEvictLeaderScheduler indicates an expected call of GetEvictLeaderScheduler. +func (mr *MockPDClientMockRecorder) GetEvictLeaderScheduler(ctx, storeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvictLeaderScheduler", reflect.TypeOf((*MockPDClient)(nil).GetEvictLeaderScheduler), ctx, storeID) +} + +// GetEvictLeaderSchedulers mocks base method. +func (m *MockPDClient) GetEvictLeaderSchedulers(ctx context.Context) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetEvictLeaderSchedulers", ctx) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetEvictLeaderSchedulers indicates an expected call of GetEvictLeaderSchedulers. +func (mr *MockPDClientMockRecorder) GetEvictLeaderSchedulers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEvictLeaderSchedulers", reflect.TypeOf((*MockPDClient)(nil).GetEvictLeaderSchedulers), ctx) +} + +// GetHealth mocks base method. +func (m *MockPDClient) GetHealth(ctx context.Context) (*HealthInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHealth", ctx) + ret0, _ := ret[0].(*HealthInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHealth indicates an expected call of GetHealth. +func (mr *MockPDClientMockRecorder) GetHealth(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHealth", reflect.TypeOf((*MockPDClient)(nil).GetHealth), ctx) +} + +// GetMSMembers mocks base method. +func (m *MockPDClient) GetMSMembers(ctx context.Context, service string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMSMembers", ctx, service) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMSMembers indicates an expected call of GetMSMembers. +func (mr *MockPDClientMockRecorder) GetMSMembers(ctx, service any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMSMembers", reflect.TypeOf((*MockPDClient)(nil).GetMSMembers), ctx, service) +} + +// GetMembers mocks base method. +func (m *MockPDClient) GetMembers(ctx context.Context) (*MembersInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMembers", ctx) + ret0, _ := ret[0].(*MembersInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMembers indicates an expected call of GetMembers. +func (mr *MockPDClientMockRecorder) GetMembers(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMembers", reflect.TypeOf((*MockPDClient)(nil).GetMembers), ctx) +} + +// GetPDLeader mocks base method. +func (m *MockPDClient) GetPDLeader(ctx context.Context) (*pdpb.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPDLeader", ctx) + ret0, _ := ret[0].(*pdpb.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPDLeader indicates an expected call of GetPDLeader. +func (mr *MockPDClientMockRecorder) GetPDLeader(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPDLeader", reflect.TypeOf((*MockPDClient)(nil).GetPDLeader), ctx) +} + +// GetStore mocks base method. +func (m *MockPDClient) GetStore(ctx context.Context, storeID string) (*StoreInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStore", ctx, storeID) + ret0, _ := ret[0].(*StoreInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStore indicates an expected call of GetStore. +func (mr *MockPDClientMockRecorder) GetStore(ctx, storeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStore", reflect.TypeOf((*MockPDClient)(nil).GetStore), ctx, storeID) +} + +// GetStores mocks base method. +func (m *MockPDClient) GetStores(ctx context.Context) (*StoresInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStores", ctx) + ret0, _ := ret[0].(*StoresInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetStores indicates an expected call of GetStores. +func (mr *MockPDClientMockRecorder) GetStores(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStores", reflect.TypeOf((*MockPDClient)(nil).GetStores), ctx) +} + +// SetStoreLabels mocks base method. +func (m *MockPDClient) SetStoreLabels(ctx context.Context, storeID uint64, labels map[string]string) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetStoreLabels", ctx, storeID, labels) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SetStoreLabels indicates an expected call of SetStoreLabels. +func (mr *MockPDClientMockRecorder) SetStoreLabels(ctx, storeID, labels any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetStoreLabels", reflect.TypeOf((*MockPDClient)(nil).SetStoreLabels), ctx, storeID, labels) +} + +// TransferPDLeader mocks base method. +func (m *MockPDClient) TransferPDLeader(ctx context.Context, name string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransferPDLeader", ctx, name) + ret0, _ := ret[0].(error) + return ret0 +} + +// TransferPDLeader indicates an expected call of TransferPDLeader. +func (mr *MockPDClientMockRecorder) TransferPDLeader(ctx, name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferPDLeader", reflect.TypeOf((*MockPDClient)(nil).TransferPDLeader), ctx, name) +} + +// UpdateReplicationConfig mocks base method. +func (m *MockPDClient) UpdateReplicationConfig(ctx context.Context, config PDReplicationConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateReplicationConfig", ctx, config) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateReplicationConfig indicates an expected call of UpdateReplicationConfig. +func (mr *MockPDClientMockRecorder) UpdateReplicationConfig(ctx, config any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateReplicationConfig", reflect.TypeOf((*MockPDClient)(nil).UpdateReplicationConfig), ctx, config) +} diff --git a/pkg/timanager/pd/mock_generated.go b/pkg/timanager/pd/mock_generated.go new file mode 100644 index 0000000000..8a90ec175a --- /dev/null +++ b/pkg/timanager/pd/mock_generated.go @@ -0,0 +1,242 @@ +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/pingcap/tidb-operator/pkg/timanager/pd (interfaces: PDClient,StoreCache,MemberCache) + +// Package pd is a generated GoMock package. +package pd + +import ( + reflect "reflect" + + pdapi "github.com/pingcap/tidb-operator/pkg/pdapi/v1" + timanager "github.com/pingcap/tidb-operator/pkg/timanager" + v1 "github.com/pingcap/tidb-operator/pkg/timanager/apis/pd/v1" + gomock "go.uber.org/mock/gomock" + labels "k8s.io/apimachinery/pkg/labels" +) + +// MockPDClient is a mock of PDClient interface. +type MockPDClient struct { + ctrl *gomock.Controller + recorder *MockPDClientMockRecorder + isgomock struct{} +} + +// MockPDClientMockRecorder is the mock recorder for MockPDClient. +type MockPDClientMockRecorder struct { + mock *MockPDClient +} + +// NewMockPDClient creates a new mock instance. +func NewMockPDClient(ctrl *gomock.Controller) *MockPDClient { + mock := &MockPDClient{ctrl: ctrl} + mock.recorder = &MockPDClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPDClient) EXPECT() *MockPDClientMockRecorder { + return m.recorder +} + +// HasSynced mocks base method. +func (m *MockPDClient) HasSynced() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "HasSynced") + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasSynced indicates an expected call of HasSynced. +func (mr *MockPDClientMockRecorder) HasSynced() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasSynced", reflect.TypeOf((*MockPDClient)(nil).HasSynced)) +} + +// Members mocks base method. +func (m *MockPDClient) Members() timanager.RefreshableCacheLister[v1.Member, *v1.Member] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Members") + ret0, _ := ret[0].(timanager.RefreshableCacheLister[v1.Member, *v1.Member]) + return ret0 +} + +// Members indicates an expected call of Members. +func (mr *MockPDClientMockRecorder) Members() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Members", reflect.TypeOf((*MockPDClient)(nil).Members)) +} + +// Stores mocks base method. +func (m *MockPDClient) Stores() timanager.RefreshableCacheLister[v1.Store, *v1.Store] { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Stores") + ret0, _ := ret[0].(timanager.RefreshableCacheLister[v1.Store, *v1.Store]) + return ret0 +} + +// Stores indicates an expected call of Stores. +func (mr *MockPDClientMockRecorder) Stores() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stores", reflect.TypeOf((*MockPDClient)(nil).Stores)) +} + +// Underlay mocks base method. +func (m *MockPDClient) Underlay() pdapi.PDClient { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Underlay") + ret0, _ := ret[0].(pdapi.PDClient) + return ret0 +} + +// Underlay indicates an expected call of Underlay. +func (mr *MockPDClientMockRecorder) Underlay() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Underlay", reflect.TypeOf((*MockPDClient)(nil).Underlay)) +} + +// MockStoreCache is a mock of StoreCache interface. +type MockStoreCache[T any, PT timanager.Object[T]] struct { + ctrl *gomock.Controller + recorder *MockStoreCacheMockRecorder[T, PT] + isgomock struct{} +} + +// MockStoreCacheMockRecorder is the mock recorder for MockStoreCache. +type MockStoreCacheMockRecorder[T any, PT timanager.Object[T]] struct { + mock *MockStoreCache[T, PT] +} + +// NewMockStoreCache creates a new mock instance. +func NewMockStoreCache[T any, PT timanager.Object[T]](ctrl *gomock.Controller) *MockStoreCache[T, PT] { + mock := &MockStoreCache[T, PT]{ctrl: ctrl} + mock.recorder = &MockStoreCacheMockRecorder[T, PT]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStoreCache[T, PT]) EXPECT() *MockStoreCacheMockRecorder[T, PT] { + return m.recorder +} + +// Get mocks base method. +func (m *MockStoreCache[T, PT]) Get(name string) (*v1.Store, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", name) + ret0, _ := ret[0].(*v1.Store) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStoreCacheMockRecorder[T, PT]) Get(name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStoreCache[T, PT])(nil).Get), name) +} + +// List mocks base method. +func (m *MockStoreCache[T, PT]) List(selector labels.Selector) ([]*v1.Store, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", selector) + ret0, _ := ret[0].([]*v1.Store) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockStoreCacheMockRecorder[T, PT]) List(selector any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStoreCache[T, PT])(nil).List), selector) +} + +// Refresh mocks base method. +func (m *MockStoreCache[T, PT]) Refresh() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Refresh") +} + +// Refresh indicates an expected call of Refresh. +func (mr *MockStoreCacheMockRecorder[T, PT]) Refresh() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockStoreCache[T, PT])(nil).Refresh)) +} + +// MockMemberCache is a mock of MemberCache interface. +type MockMemberCache[T any, PT timanager.Object[T]] struct { + ctrl *gomock.Controller + recorder *MockMemberCacheMockRecorder[T, PT] + isgomock struct{} +} + +// MockMemberCacheMockRecorder is the mock recorder for MockMemberCache. +type MockMemberCacheMockRecorder[T any, PT timanager.Object[T]] struct { + mock *MockMemberCache[T, PT] +} + +// NewMockMemberCache creates a new mock instance. +func NewMockMemberCache[T any, PT timanager.Object[T]](ctrl *gomock.Controller) *MockMemberCache[T, PT] { + mock := &MockMemberCache[T, PT]{ctrl: ctrl} + mock.recorder = &MockMemberCacheMockRecorder[T, PT]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockMemberCache[T, PT]) EXPECT() *MockMemberCacheMockRecorder[T, PT] { + return m.recorder +} + +// Get mocks base method. +func (m *MockMemberCache[T, PT]) Get(name string) (*v1.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", name) + ret0, _ := ret[0].(*v1.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockMemberCacheMockRecorder[T, PT]) Get(name any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockMemberCache[T, PT])(nil).Get), name) +} + +// List mocks base method. +func (m *MockMemberCache[T, PT]) List(selector labels.Selector) ([]*v1.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", selector) + ret0, _ := ret[0].([]*v1.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockMemberCacheMockRecorder[T, PT]) List(selector any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockMemberCache[T, PT])(nil).List), selector) +} + +// Refresh mocks base method. +func (m *MockMemberCache[T, PT]) Refresh() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Refresh") +} + +// Refresh indicates an expected call of Refresh. +func (mr *MockMemberCacheMockRecorder[T, PT]) Refresh() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Refresh", reflect.TypeOf((*MockMemberCache[T, PT])(nil).Refresh)) +} diff --git a/pkg/timanager/pd/pd.go b/pkg/timanager/pd/pd.go index c33fffd5ee..8a40d39df6 100644 --- a/pkg/timanager/pd/pd.go +++ b/pkg/timanager/pd/pd.go @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:generate ${GOBIN}/mockgen -write_command_comment=false -copyright_file ${BOILERPLATE_FILE} -destination mock_generated.go -package=pd ${GO_MODULE}/pkg/timanager/pd PDClient,StoreCache,MemberCache package pd import ( diff --git a/pkg/utils/k8s/pod.go b/pkg/utils/k8s/pod.go index 0fee81e8f1..ed4b25d78e 100644 --- a/pkg/utils/k8s/pod.go +++ b/pkg/utils/k8s/pod.go @@ -85,6 +85,9 @@ func ComparePods(current, expected *corev1.Pod) CompareResult { } func GetResourceRequirements(req v1alpha1.ResourceRequirements) corev1.ResourceRequirements { + if req.CPU == nil && req.Memory == nil { + return corev1.ResourceRequirements{} + } ret := corev1.ResourceRequirements{ Limits: map[corev1.ResourceName]resource.Quantity{}, Requests: map[corev1.ResourceName]resource.Quantity{}, diff --git a/pkg/volumes/mock.go b/pkg/volumes/mock_generated.go similarity index 81% rename from pkg/volumes/mock.go rename to pkg/volumes/mock_generated.go index 5bf488279b..979d7326be 100644 --- a/pkg/volumes/mock.go +++ b/pkg/volumes/mock_generated.go @@ -1,11 +1,21 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/pingcap/tidb-operator/pkg/volumes (interfaces: Modifier) +// Copyright 2024 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Generated by this command: +// http://www.apache.org/licenses/LICENSE-2.0 // -// mockgen -destination mock.go -package=volumes github.com/pingcap/tidb-operator/pkg/volumes Modifier +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/pingcap/tidb-operator/pkg/volumes (interfaces: Modifier) + // Package volumes is a generated GoMock package. package volumes diff --git a/pkg/volumes/types.go b/pkg/volumes/types.go index c016b8c2cd..ba8d48affc 100644 --- a/pkg/volumes/types.go +++ b/pkg/volumes/types.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -//go:generate ${GOBIN}/mockgen -destination mock.go -package=volumes ${GO_MODULE}/pkg/volumes Modifier +//go:generate ${GOBIN}/mockgen -write_command_comment=false -copyright_file ${BOILERPLATE_FILE} -destination mock_generated.go -package=volumes ${GO_MODULE}/pkg/volumes Modifier package volumes import ( diff --git a/pkg/volumes/utils.go b/pkg/volumes/utils.go index 50077ad6fb..b98bfcc187 100644 --- a/pkg/volumes/utils.go +++ b/pkg/volumes/utils.go @@ -24,6 +24,7 @@ import ( "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" @@ -230,7 +231,7 @@ func SyncPVCs(ctx context.Context, cli client.Client, for _, expectPVC := range expectPVCs { var actualPVC corev1.PersistentVolumeClaim if err := cli.Get(ctx, client.ObjectKey{Namespace: expectPVC.Namespace, Name: expectPVC.Name}, &actualPVC); err != nil { - if client.IgnoreNotFound(err) != nil { + if !errors.IsNotFound(err) { return false, fmt.Errorf("can't get expectPVC %s/%s: %w", expectPVC.Namespace, expectPVC.Name, err) } diff --git a/pkg/volumes/utils_test.go b/pkg/volumes/utils_test.go index 53f38049dd..7459e53059 100644 --- a/pkg/volumes/utils_test.go +++ b/pkg/volumes/utils_test.go @@ -96,7 +96,7 @@ func TestSyncPVCs(t *testing.T) { fake.FakeObj[corev1.PersistentVolumeClaim]("pvc-0"), }, expectPVCs: []*corev1.PersistentVolumeClaim{ - fake.FakeObj[corev1.PersistentVolumeClaim]("pvc-0", fake.Label[corev1.PersistentVolumeClaim]("foo", "bar")), + fake.FakeObj("pvc-0", fake.Label[corev1.PersistentVolumeClaim]("foo", "bar")), }, expectFunc: func(g *WithT, cli client.Client) { var pvc corev1.PersistentVolumeClaim @@ -107,13 +107,13 @@ func TestSyncPVCs(t *testing.T) { { name: "did not change PVC", existingObjs: []client.Object{ - fake.FakeObj[corev1.PersistentVolumeClaim]("pvc-0", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { + fake.FakeObj("pvc-0", func(obj *corev1.PersistentVolumeClaim) *corev1.PersistentVolumeClaim { obj.Status.Phase = corev1.ClaimBound return obj }), }, expectPVCs: []*corev1.PersistentVolumeClaim{ - fake.FakeObj[corev1.PersistentVolumeClaim]("pvc-0", fake.Label[corev1.PersistentVolumeClaim]("foo", "bar")), + fake.FakeObj("pvc-0", fake.Label[corev1.PersistentVolumeClaim]("foo", "bar")), }, setup: func(vm *MockModifier) { vm.EXPECT().GetActualVolume(gomock.Any(), gomock.Any(), gomock.Any()).Return(&ActualVolume{