Skip to content

Commit 4c25139

Browse files
authored
feat(cluster): add feature gates (#6076)
1 parent 74f20e4 commit 4c25139

38 files changed

+679
-86
lines changed

api/core/v1alpha1/cluster_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package v1alpha1
1717
import (
1818
corev1 "k8s.io/api/core/v1"
1919
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
21+
meta "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1"
2022
)
2123

2224
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -74,6 +76,8 @@ type ClusterSpec struct {
7476
// The default value is 10.
7577
// +kubebuilder:validation:Minimum=0
7678
RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty"`
79+
80+
FeatureGates []meta.FeatureGate `json:"featureGates,omitempty"`
7781
}
7882

7983
type SuspendAction struct {
@@ -142,6 +146,9 @@ type ClusterStatus struct {
142146
// PD means url of the pd service, it's prepared for internal use
143147
// e.g. https://pd:2379
144148
PD string `json:"pd,omitempty"`
149+
150+
// FeatureGates of this cluster
151+
FeatureGates []meta.FeatureGateStatus `json:"featureGates,omitempty"`
145152
}
146153

147154
type ComponentKind string

api/core/v1alpha1/zz_generated.deepcopy.go

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/meta/v1alpha1/feature.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2024 PingCAP, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package v1alpha1
16+
17+
type Feature string
18+
19+
type FeatureStage string
20+
21+
const (
22+
FeatureStageAlpha FeatureStage = "ALPHA"
23+
FeatureStageBeta FeatureStage = "BETA"
24+
FeatureStageStable FeatureStage = "STABLE"
25+
FeatureStageDeprecated FeatureStage = "DEPRECATED"
26+
)
27+
28+
type FeatureGate struct {
29+
Name Feature `json:"name"`
30+
}
31+
32+
type FeatureGateStatus struct {
33+
FeatureGate `json:",inline"`
34+
Stage FeatureStage `json:"stage"`
35+
}
36+
37+
const (
38+
// Support modify volume by VolumeAttributeClass
39+
VolumeAttributeClass Feature = "VolumeAttributeClass"
40+
VolumeAttributeClassStage FeatureStage = FeatureStageAlpha
41+
)

cmd/operator/main.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,7 @@ func setup(ctx context.Context, mgr ctrl.Manager) error {
147147
pdcm := pdm.NewPDClientManager(mgr.GetLogger(), c)
148148

149149
logger.Info("setup volume modifier")
150-
vm, err := volumes.NewModifier(ctx, mgr.GetLogger().WithName("VolumeModifier"), c)
151-
if err != nil {
152-
return fmt.Errorf("failed to create volume modifier: %w", err)
153-
}
150+
vm := volumes.NewModifierFactory(mgr.GetLogger().WithName("VolumeModifier"), c)
154151

155152
setupLog.Info("setup controllers")
156153
if err := setupControllers(mgr, c, pdcm, vm); err != nil {
@@ -208,7 +205,7 @@ func addIndexer(ctx context.Context, mgr ctrl.Manager) error {
208205
return nil
209206
}
210207

211-
func setupControllers(mgr ctrl.Manager, c client.Client, pdcm pdm.PDClientManager, vm volumes.Modifier) error {
208+
func setupControllers(mgr ctrl.Manager, c client.Client, pdcm pdm.PDClientManager, vm volumes.ModifierFactory) error {
212209
if err := cluster.Setup(mgr, c, pdcm); err != nil {
213210
return fmt.Errorf("unable to create controller Cluster: %w", err)
214211
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ require (
77
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1
88
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
99
github.com/Masterminds/semver/v3 v3.3.0
10-
github.com/aws/aws-sdk-go-v2 v1.30.5
1110
github.com/aws/aws-sdk-go-v2/config v1.27.35
1211
github.com/aws/aws-sdk-go-v2/service/ec2 v1.177.3
1312
github.com/aws/smithy-go v1.20.4
@@ -60,6 +59,7 @@ require (
6059
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
6160
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
6261
github.com/MakeNowJust/heredoc v1.0.0 // indirect
62+
github.com/aws/aws-sdk-go-v2 v1.30.5 // indirect
6363
github.com/aws/aws-sdk-go-v2/credentials v1.17.33 // indirect
6464
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
6565
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect

manifests/crd/core.pingcap.com_clusters.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,15 @@ spec:
7676
type: string
7777
type: object
7878
x-kubernetes-map-type: atomic
79+
featureGates:
80+
items:
81+
properties:
82+
name:
83+
type: string
84+
required:
85+
- name
86+
type: object
87+
type: array
7988
paused:
8089
description: Paused specifies whether to pause the reconciliation
8190
loop for all components of the cluster.
@@ -215,6 +224,19 @@ spec:
215224
x-kubernetes-list-map-keys:
216225
- type
217226
x-kubernetes-list-type: map
227+
featureGates:
228+
description: FeatureGates of this cluster
229+
items:
230+
properties:
231+
name:
232+
type: string
233+
stage:
234+
type: string
235+
required:
236+
- name
237+
- stage
238+
type: object
239+
type: array
218240
id:
219241
description: ID is the cluster id.
220242
type: string

pkg/controllers/cluster/tasks/ctx.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
2525
coreutil "github.com/pingcap/tidb-operator/pkg/apiutil/core/v1alpha1"
2626
"github.com/pingcap/tidb-operator/pkg/client"
27+
"github.com/pingcap/tidb-operator/pkg/features"
2728
"github.com/pingcap/tidb-operator/pkg/utils/task"
2829
)
2930

@@ -67,10 +68,12 @@ func (t *TaskContext) Sync(ctx task.Context[ReconcileContext]) task.Result {
6768
if !errors.IsNotFound(err) {
6869
return task.Fail().With("can't get tidb cluster: %w", err)
6970
}
71+
features.Deregister(rtx.Key.Namespace, rtx.Key.Name)
7072

7173
return task.Complete().Break().With("tidb cluster has been deleted")
7274
}
7375
rtx.Cluster = &cluster
76+
features.Register(rtx.Cluster)
7477

7578
if coreutil.ShouldPauseReconcile(rtx.Cluster) {
7679
return task.Complete().Break().With("cluster reconciliation is paused")

pkg/controllers/common/interfaces_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ func FakeGroupAndInstanceSliceState[
158158
RI runtime.Instance,
159159
](g RG, s ...RI) GroupAndInstanceSliceState[RG, RI] {
160160
return &fakeGroupAndInstanceSliceState[RG, RI]{
161-
GroupState: FakeGroupState[RG](g),
162-
InstanceSliceState: FakeInstanceSliceState[RI](s),
161+
GroupState: FakeGroupState(g),
162+
InstanceSliceState: FakeInstanceSliceState(s),
163163
}
164164
}
165165

@@ -207,8 +207,8 @@ func FakeGroupAndInstanceSliceAndRevisionState[
207207
s ...RI,
208208
) GroupAndInstanceSliceAndRevisionState[RG, RI] {
209209
return &fakeGroupAndInstanceSliceAndRevisionState[RG, RI]{
210-
GroupState: FakeGroupState[RG](g),
211-
InstanceSliceState: FakeInstanceSliceState[RI](s),
210+
GroupState: FakeGroupState(g),
211+
InstanceSliceState: FakeInstanceSliceState(s),
212212
RevisionState: FakeRevisionState(update, current, collisionCount),
213213
}
214214
}

pkg/controllers/common/task.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828

2929
"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
3030
"github.com/pingcap/tidb-operator/pkg/client"
31+
"github.com/pingcap/tidb-operator/pkg/features"
3132
"github.com/pingcap/tidb-operator/pkg/utils/task/v3"
3233
)
3334

@@ -182,3 +183,13 @@ func TaskSuspendPod(state PodState, c client.Client) task.Task {
182183
return task.Retry(task.DefaultRequeueAfter).With("pod is deleting")
183184
})
184185
}
186+
187+
func TaskFeatureGates(state ClusterState) task.Task {
188+
return task.NameTaskFunc("FeatureGates", func(context.Context) task.Result {
189+
if err := features.Verify(state.Cluster()); err != nil {
190+
return task.Fail().With("feature gates are not up to date: %v", err)
191+
}
192+
193+
return task.Complete().With("feature gates are initialized")
194+
})
195+
}

pkg/controllers/common/task_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/pingcap/tidb-operator/api/v2/core/v1alpha1"
2828
"github.com/pingcap/tidb-operator/pkg/client"
29+
"github.com/pingcap/tidb-operator/pkg/features"
2930
"github.com/pingcap/tidb-operator/pkg/utils/fake"
3031
"github.com/pingcap/tidb-operator/pkg/utils/task/v3"
3132
)
@@ -395,3 +396,67 @@ func TestTaskSuspendPod(t *testing.T) {
395396
})
396397
}
397398
}
399+
400+
func TestFeatureGates(t *testing.T) {
401+
cases := []struct {
402+
desc string
403+
state *fakeState[v1alpha1.Cluster]
404+
objs []client.Object
405+
unexpectedErr bool
406+
407+
expectedResult task.Status
408+
expectedObj *corev1.Pod
409+
}{
410+
{
411+
desc: "up to date",
412+
state: &fakeState[v1alpha1.Cluster]{
413+
name: "xxx",
414+
obj: fake.FakeObj("xxx", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster {
415+
obj.Generation = 1
416+
return obj
417+
}),
418+
},
419+
expectedResult: task.SComplete,
420+
},
421+
{
422+
desc: "generation is changed",
423+
state: &fakeState[v1alpha1.Cluster]{
424+
name: "xxx",
425+
obj: fake.FakeObj("xxx", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster {
426+
obj.Generation = 2
427+
return obj
428+
}),
429+
},
430+
expectedResult: task.SFail,
431+
},
432+
{
433+
desc: "uid is changed",
434+
state: &fakeState[v1alpha1.Cluster]{
435+
name: "xxx",
436+
obj: fake.FakeObj("xxx", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster {
437+
obj.Generation = 1
438+
obj.UID = "newuid"
439+
return obj
440+
}),
441+
},
442+
expectedResult: task.SFail,
443+
},
444+
}
445+
446+
for i := range cases {
447+
c := &cases[i]
448+
t.Run(c.desc, func(tt *testing.T) {
449+
tt.Parallel()
450+
451+
features.Register(fake.FakeObj("xxx", func(obj *v1alpha1.Cluster) *v1alpha1.Cluster {
452+
obj.Generation = 1
453+
return obj
454+
}))
455+
456+
s := &fakeClusterState{s: c.state}
457+
res, done := task.RunTask(context.Background(), TaskFeatureGates(s))
458+
assert.Equal(tt, c.expectedResult, res.Status(), c.desc)
459+
assert.False(tt, done, c.desc)
460+
})
461+
}
462+
}

pkg/controllers/pd/builder.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.Task
3030

3131
// get cluster
3232
common.TaskContextCluster(state, r.Client),
33+
common.TaskFeatureGates(state),
3334
// if it's paused just return
3435
task.IfBreak(common.CondClusterIsPaused(state)),
3536

@@ -51,7 +52,7 @@ func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.Task
5152

5253
common.TaskContextPDSlice(state, r.Client),
5354
tasks.TaskConfigMap(state, r.Client),
54-
tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifier),
55+
tasks.TaskPVC(state, r.Logger, r.Client, r.VolumeModifierFactory),
5556
tasks.TaskPod(state, r.Client),
5657
// If pd client has not been registered yet, do not update status of the pd
5758
task.IfBreak(tasks.CondPDClientIsNotRegisterred(state),

pkg/controllers/pd/controller.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,18 @@ import (
3737
)
3838

3939
type Reconciler struct {
40-
Logger logr.Logger
41-
Client client.Client
42-
PDClientManager pdm.PDClientManager
43-
VolumeModifier volumes.Modifier
40+
Logger logr.Logger
41+
Client client.Client
42+
PDClientManager pdm.PDClientManager
43+
VolumeModifierFactory volumes.ModifierFactory
4444
}
4545

46-
func Setup(mgr manager.Manager, c client.Client, pdcm pdm.PDClientManager, vm volumes.Modifier) error {
46+
func Setup(mgr manager.Manager, c client.Client, pdcm pdm.PDClientManager, vm volumes.ModifierFactory) error {
4747
r := &Reconciler{
48-
Logger: mgr.GetLogger().WithName("PD"),
49-
Client: c,
50-
PDClientManager: pdcm,
51-
VolumeModifier: vm,
48+
Logger: mgr.GetLogger().WithName("PD"),
49+
Client: c,
50+
PDClientManager: pdcm,
51+
VolumeModifierFactory: vm,
5252
}
5353
return ctrl.NewControllerManagedBy(mgr).For(&v1alpha1.PD{}).
5454
Owns(&corev1.Pod{}).

pkg/controllers/pd/tasks/pvc.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,11 @@ import (
3131
"github.com/pingcap/tidb-operator/pkg/volumes"
3232
)
3333

34-
func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.Modifier) task.Task {
34+
func TaskPVC(state *ReconcileContext, logger logr.Logger, c client.Client, vm volumes.ModifierFactory) task.Task {
3535
return task.NameTaskFunc("PVC", func(ctx context.Context) task.Result {
36+
ck := state.Cluster()
3637
pvcs := newPVCs(state)
37-
if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm, logger); err != nil {
38+
if wait, err := volumes.SyncPVCs(ctx, c, pvcs, vm.New(ck.Namespace, ck.Name), logger); err != nil {
3839
return task.Fail().With("failed to sync pvcs: %v", err)
3940
} else if wait {
4041
return task.Wait().With("waiting for pvcs to be synced")

pkg/controllers/pd/tasks/pvc_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ func TestTaskPVC(t *testing.T) {
143143

144144
ctrl := gomock.NewController(tt)
145145
vm := volumes.NewMockModifier(ctrl)
146+
vf := volumes.NewMockModifierFactory(ctrl)
147+
vf.EXPECT().New("", "aaa").Return(vm)
146148
expectedPVCs := newPVCs(c.state)
147149
for _, expected := range expectedPVCs {
148150
for _, current := range c.pvcs {
@@ -164,7 +166,7 @@ func TestTaskPVC(t *testing.T) {
164166
fc.WithError("patch", "*", errors.NewInternalError(fmt.Errorf("fake internal err")))
165167
}
166168

167-
res, done := task.RunTask(ctx, TaskPVC(c.state, logr.Discard(), fc, vm))
169+
res, done := task.RunTask(ctx, TaskPVC(c.state, logr.Discard(), fc, vf))
168170
assert.Equal(tt, c.expectedStatus.String(), res.Status().String(), res.Message())
169171
assert.False(tt, done, c.desc)
170172

0 commit comments

Comments
 (0)