diff --git a/docs/api-references/docs.md b/docs/api-references/docs.md index 8466de30c8..fe1a033756 100644 --- a/docs/api-references/docs.md +++ b/docs/api-references/docs.md @@ -17840,6 +17840,21 @@ string
tidbAuth
+
+
+TiCITiDBAuth
+
+
+TiDBAuth configures the TiDB auth used by TiCI meta. +If it is set, passwordSecret must reference the Secret key that stores the TiDB auth data.
+storageClassName
string
@@ -18250,6 +18265,50 @@ string
+(Appears on: +TiCIMetaSpec) +
++
TiCITiDBAuth contains TiDB auth settings used by TiCI.
+ +| Field | +Description | +
|---|---|
+user
+
+string
+
+ |
+
+(Optional)
+ User is the TiDB user used by TiCI meta. Defaults to root. + |
+
+passwordSecret
+
+
+Kubernetes core/v1.SecretKeySelector
+
+
+ |
+
+ PasswordSecret selects the Secret key that stores the TiDB auth data. +The referenced Secret key is required; optional must not be true. + |
+
(Appears on: diff --git a/manifests/crd.yaml b/manifests/crd.yaml index 4948e6a384..aee109a8f0 100644 --- a/manifests/crd.yaml +++ b/manifests/crd.yaml @@ -43457,6 +43457,29 @@ spec: terminationGracePeriodSeconds: format: int64 type: integer + tidbAuth: + properties: + passwordSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: optional must be false because TiCI meta TiDB + auth Secret is required + rule: '!has(self.optional) || self.optional == false' + user: + type: string + required: + - passwordSecret + type: object tolerations: items: properties: diff --git a/manifests/crd/v1/pingcap.com_tidbclusters.yaml b/manifests/crd/v1/pingcap.com_tidbclusters.yaml index aef36b25f3..789fa54dd9 100644 --- a/manifests/crd/v1/pingcap.com_tidbclusters.yaml +++ b/manifests/crd/v1/pingcap.com_tidbclusters.yaml @@ -17801,6 +17801,29 @@ spec: terminationGracePeriodSeconds: format: int64 type: integer + tidbAuth: + properties: + passwordSecret: + properties: + key: + type: string + name: + type: string + optional: + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + x-kubernetes-validations: + - message: optional must be false because TiCI meta TiDB + auth Secret is required + rule: '!has(self.optional) || self.optional == false' + user: + type: string + required: + - passwordSecret + type: object tolerations: items: properties: diff --git a/pkg/apis/pingcap/v1alpha1/openapi_generated.go b/pkg/apis/pingcap/v1alpha1/openapi_generated.go index 49638c8f45..aee06ce771 100644 --- a/pkg/apis/pingcap/v1alpha1/openapi_generated.go +++ b/pkg/apis/pingcap/v1alpha1/openapi_generated.go @@ -126,6 +126,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCIReaderSpec": schema_pkg_apis_pingcap_v1alpha1_TiCIReaderSpec(ref), "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCIS3Spec": schema_pkg_apis_pingcap_v1alpha1_TiCIS3Spec(ref), "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCISpec": schema_pkg_apis_pingcap_v1alpha1_TiCISpec(ref), + "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCITiDBAuth": schema_pkg_apis_pingcap_v1alpha1_TiCITiDBAuth(ref), "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCIWorkerSpec": schema_pkg_apis_pingcap_v1alpha1_TiCIWorkerSpec(ref), "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiDBAccessConfig": schema_pkg_apis_pingcap_v1alpha1_TiDBAccessConfig(ref), "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiDBConfig": schema_pkg_apis_pingcap_v1alpha1_TiDBConfig(ref), @@ -10169,6 +10170,12 @@ func schema_pkg_apis_pingcap_v1alpha1_TiCIMetaSpec(ref common.ReferenceCallback) Format: "", }, }, + "tidbAuth": { + SchemaProps: spec.SchemaProps{ + Description: "TiDBAuth configures the TiDB auth used by TiCI meta. If it is set, passwordSecret must reference the Secret key that stores the TiDB auth data.", + Ref: ref("github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCITiDBAuth"), + }, + }, "storageClassName": { SchemaProps: spec.SchemaProps{ Description: "The storageClassName of the persistent volume for TiCI meta data storage. Defaults to Kubernetes default storage class.", @@ -10188,7 +10195,7 @@ func schema_pkg_apis_pingcap_v1alpha1_TiCIMetaSpec(ref common.ReferenceCallback) }, }, Dependencies: []string{ - "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.Probe", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.StorageVolume", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.SuspendAction", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TopologySpreadConstraint", "k8s.io/api/core/v1.Affinity", "k8s.io/api/core/v1.Container", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.LocalObjectReference", "k8s.io/api/core/v1.PodDNSConfig", "k8s.io/api/core/v1.PodSecurityContext", "k8s.io/api/core/v1.ResourceClaim", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.Toleration", "k8s.io/api/core/v1.Volume", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/api/resource.Quantity"}, + "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.Probe", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.StorageVolume", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.SuspendAction", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TiCITiDBAuth", "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1.TopologySpreadConstraint", "k8s.io/api/core/v1.Affinity", "k8s.io/api/core/v1.Container", "k8s.io/api/core/v1.EnvFromSource", "k8s.io/api/core/v1.EnvVar", "k8s.io/api/core/v1.LocalObjectReference", "k8s.io/api/core/v1.PodDNSConfig", "k8s.io/api/core/v1.PodSecurityContext", "k8s.io/api/core/v1.ResourceClaim", "k8s.io/api/core/v1.SecurityContext", "k8s.io/api/core/v1.Toleration", "k8s.io/api/core/v1.Volume", "k8s.io/api/core/v1.VolumeMount", "k8s.io/apimachinery/pkg/api/resource.Quantity"}, } } @@ -10343,6 +10350,35 @@ func schema_pkg_apis_pingcap_v1alpha1_TiCISpec(ref common.ReferenceCallback) com } } +func schema_pkg_apis_pingcap_v1alpha1_TiCITiDBAuth(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TiCITiDBAuth contains TiDB auth settings used by TiCI.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "user": { + SchemaProps: spec.SchemaProps{ + Description: "User is the TiDB user used by TiCI meta. Defaults to root.", + Type: []string{"string"}, + Format: "", + }, + }, + "passwordSecret": { + SchemaProps: spec.SchemaProps{ + Description: "PasswordSecret selects the Secret key that stores the TiDB auth data. The referenced Secret key is required; optional must not be true.", + Ref: ref("k8s.io/api/core/v1.SecretKeySelector"), + }, + }, + }, + Required: []string{"passwordSecret"}, + }, + }, + Dependencies: []string{ + "k8s.io/api/core/v1.SecretKeySelector"}, + } +} + func schema_pkg_apis_pingcap_v1alpha1_TiCIWorkerSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/pkg/apis/pingcap/v1alpha1/types.go b/pkg/apis/pingcap/v1alpha1/types.go index ba6617413c..a32955a2b5 100644 --- a/pkg/apis/pingcap/v1alpha1/types.go +++ b/pkg/apis/pingcap/v1alpha1/types.go @@ -1000,6 +1000,11 @@ type TiCIMetaSpec struct { // +optional Config string `json:"config,omitempty"` + // TiDBAuth configures the TiDB auth used by TiCI meta. + // If it is set, passwordSecret must reference the Secret key that stores the TiDB auth data. + // +optional + TiDBAuth *TiCITiDBAuth `json:"tidbAuth,omitempty"` + // The storageClassName of the persistent volume for TiCI meta data storage. // Defaults to Kubernetes default storage class. // +optional @@ -1011,6 +1016,20 @@ type TiCIMetaSpec struct { VolumeAttributesClassName *string `json:"volumeAttributesClassName,omitempty"` } +// TiCITiDBAuth contains TiDB auth settings used by TiCI. +// +k8s:openapi-gen=true +type TiCITiDBAuth struct { + // User is the TiDB user used by TiCI meta. Defaults to root. + // +optional + User string `json:"user,omitempty"` + + // PasswordSecret selects the Secret key that stores the TiDB auth data. + // The referenced Secret key is required; optional must not be true. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="!has(self.optional) || self.optional == false",message="optional must be false because TiCI meta TiDB auth Secret is required" + PasswordSecret *corev1.SecretKeySelector `json:"passwordSecret"` +} + // TiCIWorkerSpec contains details of TiCI worker members // +k8s:openapi-gen=true type TiCIWorkerSpec struct { diff --git a/pkg/apis/pingcap/v1alpha1/validation/validation.go b/pkg/apis/pingcap/v1alpha1/validation/validation.go index 3aaae8915a..efa4edf300 100644 --- a/pkg/apis/pingcap/v1alpha1/validation/validation.go +++ b/pkg/apis/pingcap/v1alpha1/validation/validation.go @@ -171,6 +171,9 @@ func validateTiCISpec(spec *v1alpha1.TiCISpec, fldPath *field.Path) field.ErrorL if len(spec.Meta.StorageVolumes) > 0 { allErrs = append(allErrs, validateStorageVolumes(spec.Meta.StorageVolumes, fldPath.Child("meta", "storageVolumes"))...) } + if spec.Meta.TiDBAuth != nil { + allErrs = append(allErrs, validateTiCITiDBAuth(spec.Meta.TiDBAuth, fldPath.Child("meta", "tidbAuth"))...) + } } if spec.Worker == nil { allErrs = append(allErrs, field.Required(fldPath.Child("worker"), "worker is required when tici is enabled")) @@ -281,6 +284,21 @@ func validateTiKVSpec(spec *v1alpha1.TiKVSpec, fldPath *field.Path) field.ErrorL return allErrs } +func validateTiCITiDBAuth(auth *v1alpha1.TiCITiDBAuth, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if auth.PasswordSecret == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("passwordSecret"), "passwordSecret is required when tidbAuth is set")) + } else { + allErrs = append(allErrs, validateSecretKeySelector(auth.PasswordSecret, fldPath.Child("passwordSecret"))...) + if auth.PasswordSecret.Optional != nil && *auth.PasswordSecret.Optional { + allErrs = append(allErrs, field.Invalid(fldPath.Child("passwordSecret", "optional"), *auth.PasswordSecret.Optional, "optional must be false because TiCI meta TiDB auth Secret is required")) + } + } + + return allErrs +} + func validateTiFlashSpec(spec *v1alpha1.TiFlashSpec, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} allErrs = append(allErrs, validateComponentSpec(&spec.ComponentSpec, fldPath)...) diff --git a/pkg/apis/pingcap/v1alpha1/validation/validation_test.go b/pkg/apis/pingcap/v1alpha1/validation/validation_test.go index 31046ad542..f35622ed8b 100644 --- a/pkg/apis/pingcap/v1alpha1/validation/validation_test.go +++ b/pkg/apis/pingcap/v1alpha1/validation/validation_test.go @@ -343,6 +343,64 @@ func TestValidateDMAnnotations(t *testing.T) { } } +func TestValidateTiCITiDBAuth(t *testing.T) { + tests := []struct { + name string + auth *v1alpha1.TiCITiDBAuth + expectedErrs int + expectedField string + }{ + { + name: "valid password secret", + auth: &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + }, + }, + { + name: "optional false is valid", + auth: &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + Optional: pointer.BoolPtr(false), + }, + }, + }, + { + name: "password secret is required", + auth: &v1alpha1.TiCITiDBAuth{}, + expectedErrs: 1, + expectedField: "spec.tici.meta.tidbAuth.passwordSecret", + }, + { + name: "optional true is forbidden", + auth: &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + Optional: pointer.BoolPtr(true), + }, + }, + expectedErrs: 1, + expectedField: "spec.tici.meta.tidbAuth.passwordSecret.optional", + }, + } + + for _, tt := range tests { + errs := validateTiCITiDBAuth(tt.auth, field.NewPath("spec", "tici", "meta", "tidbAuth")) + if len(errs) != tt.expectedErrs { + t.Errorf("[%s]: expected %d failures, got %d failures: %v", tt.name, tt.expectedErrs, len(errs), errs) + continue + } + if tt.expectedField != "" && errs[0].Field != tt.expectedField { + t.Errorf("[%s]: expected error field %q, got %q", tt.name, tt.expectedField, errs[0].Field) + } + } +} + func TestValidatePumpSpec(t *testing.T) { g := NewGomegaWithT(t) tests := []struct { diff --git a/pkg/apis/pingcap/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/pingcap/v1alpha1/zz_generated.deepcopy.go index a4e350c3be..a14af61d85 100644 --- a/pkg/apis/pingcap/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/pingcap/v1alpha1/zz_generated.deepcopy.go @@ -5908,6 +5908,11 @@ func (in *TiCIMetaSpec) DeepCopyInto(out *TiCIMetaSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TiDBAuth != nil { + in, out := &in.TiDBAuth, &out.TiDBAuth + *out = new(TiCITiDBAuth) + (*in).DeepCopyInto(*out) + } if in.StorageClassName != nil { in, out := &in.StorageClassName, &out.StorageClassName *out = new(string) @@ -6088,6 +6093,27 @@ func (in *TiCIStatus) DeepCopy() *TiCIStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TiCITiDBAuth) DeepCopyInto(out *TiCITiDBAuth) { + *out = *in + if in.PasswordSecret != nil { + in, out := &in.PasswordSecret, &out.PasswordSecret + *out = new(v1.SecretKeySelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TiCITiDBAuth. +func (in *TiCITiDBAuth) DeepCopy() *TiCITiDBAuth { + if in == nil { + return nil + } + out := new(TiCITiDBAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TiCIWorkerSpec) DeepCopyInto(out *TiCIWorkerSpec) { *out = *in diff --git a/pkg/controller/tidbcluster/tidb_cluster_controller.go b/pkg/controller/tidbcluster/tidb_cluster_controller.go index 7197d3f3b4..eff858ba7f 100644 --- a/pkg/controller/tidbcluster/tidb_cluster_controller.go +++ b/pkg/controller/tidbcluster/tidb_cluster_controller.go @@ -18,8 +18,10 @@ import ( "time" apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/cache" @@ -83,6 +85,7 @@ func NewController(deps *controller.Dependencies) *Controller { tidbClusterInformer := deps.InformerFactory.Pingcap().V1alpha1().TidbClusters() statefulsetInformer := deps.KubeInformerFactory.Apps().V1().StatefulSets() + secretInformer := deps.KubeInformerFactory.Core().V1().Secrets() tidbClusterInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: c.enqueueTidbCluster, UpdateFunc: func(old, cur interface{}) { @@ -97,6 +100,18 @@ func NewController(deps *controller.Dependencies) *Controller { }, DeleteFunc: c.deleteStatefulSet, }) + secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.enqueueTidbClustersForTiCIMetaAuthSecret, + UpdateFunc: func(old, cur interface{}) { + oldSecret := old.(*corev1.Secret) + curSecret := cur.(*corev1.Secret) + if curSecret.ResourceVersion == oldSecret.ResourceVersion { + return + } + c.enqueueTidbClustersForTiCIMetaAuthSecret(cur) + }, + DeleteFunc: c.enqueueTidbClustersForTiCIMetaAuthSecret, + }) return c } @@ -275,6 +290,38 @@ func (c *Controller) deleteStatefulSet(obj interface{}) { c.enqueueTidbCluster(tc) } +func (c *Controller) enqueueTidbClustersForTiCIMetaAuthSecret(obj interface{}) { + secret, ok := obj.(*corev1.Secret) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %+v", obj)) + return + } + secret, ok = tombstone.Obj.(*corev1.Secret) + if !ok { + utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a secret %+v", obj)) + return + } + } + + tcs, err := c.deps.TiDBClusterLister.TidbClusters(secret.Namespace).List(labels.Everything()) + if err != nil { + utilruntime.HandleError(fmt.Errorf("couldn't list TidbClusters for TiCI meta TiDB auth secret %s/%s: %v", secret.Namespace, secret.Name, err)) + return + } + for _, tc := range tcs { + if tc.Spec.TiCI == nil || tc.Spec.TiCI.Meta == nil || tc.Spec.TiCI.Meta.TiDBAuth == nil || tc.Spec.TiCI.Meta.TiDBAuth.PasswordSecret == nil { + continue + } + if tc.Spec.TiCI.Meta.TiDBAuth.PasswordSecret.Name != secret.Name { + continue + } + klog.V(4).Infof("TiCI meta TiDB auth secret %s/%s changed, TidbCluster: %s/%s", secret.Namespace, secret.Name, tc.Namespace, tc.Name) + c.enqueueTidbCluster(tc) + } +} + // resolveTidbClusterFromSet returns the TidbCluster by a StatefulSet, // or nil if the StatefulSet could not be resolved to a matching TidbCluster // of the correct Kind. diff --git a/pkg/controller/tidbcluster/tidb_cluster_controller_test.go b/pkg/controller/tidbcluster/tidb_cluster_controller_test.go index 3eacfe8333..e3fb26025d 100644 --- a/pkg/controller/tidbcluster/tidb_cluster_controller_test.go +++ b/pkg/controller/tidbcluster/tidb_cluster_controller_test.go @@ -194,6 +194,75 @@ func TestTidbClusterControllerUpdateStatefulSet(t *testing.T) { } } +func TestTidbClusterControllerEnqueueTiCIMetaAuthSecret(t *testing.T) { + g := NewGomegaWithT(t) + type testcase struct { + name string + secretName string + useTombstone bool + expectedLen int + } + + testFn := func(test *testcase, t *testing.T) { + t.Log("test: ", test.name) + + tc := newTidbCluster() + tc.Spec.TiCI = &v1alpha1.TiCISpec{ + Meta: &v1alpha1.TiCIMetaSpec{ + TiDBAuth: &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + }, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: test.secretName, + Namespace: tc.Namespace, + }, + } + + fakeDeps := controller.NewFakeDependencies() + tcc := NewController(fakeDeps) + tcc.control = NewFakeTidbClusterControlInterface() + tcIndexer := fakeDeps.InformerFactory.Pingcap().V1alpha1().TidbClusters().Informer().GetIndexer() + err := tcIndexer.Add(tc) + g.Expect(err).NotTo(HaveOccurred()) + + var obj interface{} = secret + if test.useTombstone { + obj = cache.DeletedFinalStateUnknown{Obj: secret} + } + tcc.enqueueTidbClustersForTiCIMetaAuthSecret(obj) + g.Expect(tcc.queue.Len()).To(Equal(test.expectedLen)) + } + + tests := []testcase{ + { + name: "referenced auth secret", + secretName: "tidb-auth", + expectedLen: 1, + }, + { + name: "unrelated secret", + secretName: "other-auth", + expectedLen: 0, + }, + { + name: "referenced auth secret tombstone", + secretName: "tidb-auth", + useTombstone: true, + expectedLen: 1, + }, + } + + for i := range tests { + testFn(&tests[i], t) + } +} + func TestTidbClusterControllerSync(t *testing.T) { g := NewGomegaWithT(t) type testcase struct { diff --git a/pkg/manager/member/tici_member_manager.go b/pkg/manager/member/tici_member_manager.go index 809fa32782..ea161d68df 100644 --- a/pkg/manager/member/tici_member_manager.go +++ b/pkg/manager/member/tici_member_manager.go @@ -14,6 +14,7 @@ package member import ( + "encoding/json" "fmt" "strings" @@ -29,6 +30,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -36,6 +38,25 @@ import ( "k8s.io/utils/pointer" ) +const ( + ticiMetaTiDBAuthHashAnnotation = "tidb.pingcap.com/tici-meta-tidb-auth-hash" + ticiMetaConfigMountPath = "/etc/tici" + ticiMetaConfigTemplateMountPath = "/etc/tici-template" + ticiMetaTiDBAuthMountPath = "/etc/tici-tidb-auth" + ticiMetaTiDBAuthFileName = "tidb-auth" + ticiMetaTiDBAuthDefaultUser = "root" + ticiMetaTiDBAuthVolumeName = "tidb-auth" + ticiMetaTiDBAuthRefNotFound = "TiCIMetaTiDBAuthRefNotFound" + ticiMetaTiDBAuthRefKeyNotFound = "TiCIMetaTiDBAuthRefKeyNotFound" +) + +type tiCIMetaTiDBAuth struct { + User string + SecretName string + SecretKey string + Hash string +} + // ticiMemberManager implements manager.Manager. type ticiMemberManager struct { deps *controller.Dependencies @@ -208,7 +229,12 @@ func (m *ticiMemberManager) syncTiCIMetaStatefulSet(tc *v1alpha1.TidbCluster) er return err } - newSts, err := getNewTiCIMetaStatefulSet(tc, cm) + tidbAuth, err := m.getTiCIMetaTiDBAuth(tc) + if err != nil { + return err + } + + newSts, err := getNewTiCIMetaStatefulSet(tc, cm, tidbAuth) if err != nil { return err } @@ -284,12 +310,25 @@ func prepareTiCIRollingUpgrade(newSet, oldSet *appsv1.StatefulSet) { if newSet.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { return } - needsUpgrade := !templateEqual(newSet, oldSet) || oldSet.Status.CurrentRevision != oldSet.Status.UpdateRevision + needsUpgrade := !ticiTemplateEqual(newSet, oldSet) || oldSet.Status.CurrentRevision != oldSet.Status.UpdateRevision if needsUpgrade { mngerutils.SetUpgradePartition(newSet, 0) } } +func ticiTemplateEqual(newSet, oldSet *appsv1.StatefulSet) bool { + oldStsSpec := appsv1.StatefulSetSpec{} + lastAppliedConfig, ok := oldSet.Annotations[LastAppliedConfigAnnotation] + if !ok { + return false + } + if err := json.Unmarshal([]byte(lastAppliedConfig), &oldStsSpec); err != nil { + return false + } + return apiequality.Semantic.DeepEqual(oldStsSpec.Template.Spec, newSet.Spec.Template.Spec) && + apiequality.Semantic.DeepEqual(oldStsSpec.Template.Annotations, newSet.Spec.Template.Annotations) +} + func (m *ticiMemberManager) syncTiCIMetaStatus(tc *v1alpha1.TidbCluster, sts *appsv1.StatefulSet) error { if sts == nil { tc.Status.TiCIMeta.StatefulSet = &appsv1.StatefulSetStatus{} @@ -399,6 +438,58 @@ func getTiCIMetaConfigMap(tc *v1alpha1.TidbCluster) (*corev1.ConfigMap, error) { return cm, nil } +func (m *ticiMemberManager) getTiCIMetaTiDBAuth(tc *v1alpha1.TidbCluster) (*tiCIMetaTiDBAuth, error) { + if tc.Spec.TiCI == nil || tc.Spec.TiCI.Meta == nil || tc.Spec.TiCI.Meta.TiDBAuth == nil { + return nil, nil + } + + authSpec := tc.Spec.TiCI.Meta.TiDBAuth + secretRef := authSpec.PasswordSecret + if secretRef == nil { + return nil, fmt.Errorf("spec.tici.meta.tidbAuth.passwordSecret is required when tidbAuth is set") + } + + ns := tc.GetNamespace() + user := authSpec.User + if user == "" { + user = ticiMetaTiDBAuthDefaultUser + } + + secret, err := m.deps.SecretLister.Secrets(ns).Get(secretRef.Name) + if err != nil { + msg := fmt.Sprintf("TiCI meta TiDB auth secret %s/%s is not found", ns, secretRef.Name) + if errors.IsNotFound(err) { + m.recordTiCIMetaTiDBAuthWarning(tc, ticiMetaTiDBAuthRefNotFound, msg) + return nil, controller.RequeueErrorf("%s", msg) + } + return nil, fmt.Errorf("%s: %v", msg, err) + } + + authData, ok := secret.Data[secretRef.Key] + if !ok { + msg := fmt.Sprintf("TiCI meta TiDB auth secret %s/%s does not contain key %q", ns, secretRef.Name, secretRef.Key) + m.recordTiCIMetaTiDBAuthWarning(tc, ticiMetaTiDBAuthRefKeyNotFound, msg) + return nil, fmt.Errorf("%s", msg) + } + authHash, err := mngerutils.Sha256Sum(map[string]string{secretRef.Key: string(authData)}) + if err != nil { + return nil, err + } + + return &tiCIMetaTiDBAuth{ + User: user, + SecretName: secretRef.Name, + SecretKey: secretRef.Key, + Hash: authHash, + }, nil +} + +func (m *ticiMemberManager) recordTiCIMetaTiDBAuthWarning(tc *v1alpha1.TidbCluster, reason, msg string) { + if m.deps != nil && m.deps.Recorder != nil { + m.deps.Recorder.Event(tc, corev1.EventTypeWarning, reason, msg) + } +} + func getTiCIWorkerConfigMap(tc *v1alpha1.TidbCluster) (*corev1.ConfigMap, error) { configText, err := buildTiCIWorkerConfig(tc) if err != nil { @@ -420,7 +511,7 @@ func getTiCIWorkerConfigMap(tc *v1alpha1.TidbCluster) (*corev1.ConfigMap, error) return cm, nil } -func getNewTiCIMetaStatefulSet(tc *v1alpha1.TidbCluster, cm *corev1.ConfigMap) (*appsv1.StatefulSet, error) { +func getNewTiCIMetaStatefulSet(tc *v1alpha1.TidbCluster, cm *corev1.ConfigMap, tidbAuth *tiCIMetaTiDBAuth) (*appsv1.StatefulSet, error) { if tc.Spec.TiCI == nil || tc.Spec.TiCI.Meta == nil { return nil, nil } @@ -433,6 +524,9 @@ func getNewTiCIMetaStatefulSet(tc *v1alpha1.TidbCluster, cm *corev1.ConfigMap) ( stsName := controller.TiCIMetaMemberName(tcName) podLabels := util.CombineStringMap(stsLabels, baseSpec.Labels()) podAnnotations := util.CombineStringMap(baseSpec.Annotations(), controller.AnnProm(v1alpha1.DefaultTiCIMetaStatusPort, "/metrics")) + if tidbAuth != nil && tidbAuth.Hash != "" { + podAnnotations[ticiMetaTiDBAuthHashAnnotation] = tidbAuth.Hash + } stsAnnotations := getStsAnnotations(tc.Annotations, label.TiCIMetaLabelVal) headlessSvcName := controller.TiCIMetaPeerMemberName(tcName) @@ -442,16 +536,28 @@ func getNewTiCIMetaStatefulSet(tc *v1alpha1.TidbCluster, cm *corev1.ConfigMap) ( volMounts = append(volMounts, storageVolMounts...) volMounts = append(volMounts, spec.AdditionalVolumeMounts...) - configMountPath := "/etc/tici" if cm != nil { - volMounts = append(volMounts, corev1.VolumeMount{Name: "config", MountPath: configMountPath}) - vols = append(vols, corev1.Volume{Name: "config", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name}, - Items: []corev1.KeyToPath{{Key: "config-file", Path: "tici.toml"}}, - }}}) + volMounts = append(volMounts, + corev1.VolumeMount{Name: "config", MountPath: ticiMetaConfigTemplateMountPath, ReadOnly: true}, + corev1.VolumeMount{Name: "runtime-config", MountPath: ticiMetaConfigMountPath}, + ) + vols = append(vols, + corev1.Volume{Name: "config", VolumeSource: corev1.VolumeSource{ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cm.Name}, + Items: []corev1.KeyToPath{{Key: "config-file", Path: "tici.toml"}}, + }}}, + corev1.Volume{Name: "runtime-config", VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}}}, + ) + if tidbAuth != nil { + volMounts = append(volMounts, corev1.VolumeMount{Name: ticiMetaTiDBAuthVolumeName, MountPath: ticiMetaTiDBAuthMountPath, ReadOnly: true}) + vols = append(vols, corev1.Volume{Name: ticiMetaTiDBAuthVolumeName, VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{ + SecretName: tidbAuth.SecretName, + Items: []corev1.KeyToPath{{Key: tidbAuth.SecretKey, Path: ticiMetaTiDBAuthFileName}}, + }}}) + } } - args := renderTiCIStartArgs(tc, v1alpha1.TiCIMetaMemberType, headlessSvcName) + args := renderTiCIMetaStartScript(tc, headlessSvcName, tidbAuth) envs := []corev1.EnvVar{ {Name: "POD_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "metadata.name"}}}, @@ -690,6 +796,36 @@ func renderTiCIStartArgs(tc *v1alpha1.TidbCluster, memberType v1alpha1.MemberTyp return strings.Join(args, " ") } +func renderTiCIMetaStartScript(tc *v1alpha1.TidbCluster, headlessSvcName string, tidbAuth *tiCIMetaTiDBAuth) string { + configTemplatePath := fmt.Sprintf("%s/tici.toml", ticiMetaConfigTemplateMountPath) + configPath := fmt.Sprintf("%s/tici.toml", ticiMetaConfigMountPath) + + lines := []string{ + "set -e", + fmt.Sprintf("config_template=%q", configTemplatePath), + fmt.Sprintf("config_file=%q", configPath), + } + if tidbAuth != nil { + authPath := fmt.Sprintf("%s/%s", ticiMetaTiDBAuthMountPath, ticiMetaTiDBAuthFileName) + lines = append(lines, + fmt.Sprintf("tidb_auth_user=%q", tidbAuth.User), + fmt.Sprintf("tidb_auth_file=%q", authPath), + `if [ ! -f "${tidb_auth_file}" ]; then`, + ` echo "TiCI TiDB auth file ${tidb_auth_file} is missing" >&2`, + ` exit 1`, + `fi`, + `encoded_tidb_auth=$(od -An -tx1 -v "${tidb_auth_file}" | tr -d ' \n' | sed 's/../%&/g')`, + `sed "s#mysql://${tidb_auth_user}@#mysql://${tidb_auth_user}:${encoded_tidb_auth}@#" "${config_template}" > "${config_file}"`, + ) + } else { + lines = append(lines, + `cp "${config_template}" "${config_file}"`, + ) + } + lines = append(lines, renderTiCIStartArgs(tc, v1alpha1.TiCIMetaMemberType, headlessSvcName)) + return strings.Join(lines, "\n") +} + func buildTiCIMetaConfig(tc *v1alpha1.TidbCluster) (string, error) { s3, err := buildTiCIS3Config(tc) if err != nil { @@ -701,9 +837,13 @@ func buildTiCIMetaConfig(tc *v1alpha1.TidbCluster) (string, error) { tidbHost := controller.TiDBMemberName(tc.Name) tidbPort := tc.Spec.TiDB.GetServicePort() pdAddr := fmt.Sprintf("%s:%d", controller.PDMemberName(tc.Name), v1alpha1.DefaultPDClientPort) + tidbUser := ticiMetaTiDBAuthDefaultUser + if tc.Spec.TiCI != nil && tc.Spec.TiCI.Meta != nil && tc.Spec.TiCI.Meta.TiDBAuth != nil && tc.Spec.TiCI.Meta.TiDBAuth.User != "" { + tidbUser = tc.Spec.TiCI.Meta.TiDBAuth.User + } baseConfig := fmt.Sprintf(`[tidb-server] -dsns = ["mysql://root@%s:%d"] +dsns = ["mysql://%s@%s:%d"] [server] pd-addr = "%s" @@ -716,7 +856,7 @@ secret-key = "%s" use-path-style = %t bucket = "%s" prefix = "%s" -`, tidbHost, tidbPort, pdAddr, s3.Endpoint, s3.Region, s3.AccessKey, s3.SecretKey, s3.UsePathStyle, s3.Bucket, s3.Prefix) +`, tidbUser, tidbHost, tidbPort, pdAddr, s3.Endpoint, s3.Region, s3.AccessKey, s3.SecretKey, s3.UsePathStyle, s3.Bucket, s3.Prefix) if tc.Spec.TiCI != nil && tc.Spec.TiCI.Meta != nil { return appendTiCICustomConfig(baseConfig, tc.Spec.TiCI.Meta.Config) } diff --git a/pkg/manager/member/tici_member_manager_test.go b/pkg/manager/member/tici_member_manager_test.go index af088f7f8a..e3c2c57206 100644 --- a/pkg/manager/member/tici_member_manager_test.go +++ b/pkg/manager/member/tici_member_manager_test.go @@ -20,10 +20,12 @@ import ( "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1" tcconfig "github.com/pingcap/tidb-operator/pkg/apis/util/config" + "github.com/pingcap/tidb-operator/pkg/controller" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/record" ) func TestBuildTiCIWorkerConfigWithCustomConfig(t *testing.T) { @@ -78,6 +80,60 @@ data-dir = "/data/tici-meta"` } } +func TestRenderTiCIMetaStartScriptInjectsTiDBAuthFromSecretVolume(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tidbAuth := &tiCIMetaTiDBAuth{User: "root"} + + script := renderTiCIMetaStartScript(tc, "tici-test-tici-meta-peer", tidbAuth) + for _, expected := range []string{ + `tidb_auth_user="root"`, + `tidb_auth_file="/etc/tici-tidb-auth/tidb-auth"`, + `od -An -tx1 -v "${tidb_auth_file}"`, + `sed "s#mysql://${tidb_auth_user}@#mysql://${tidb_auth_user}:${encoded_tidb_auth}@#"`, + `exec /tici-server meta --config=/etc/tici/tici.toml`, + } { + if !strings.Contains(script, expected) { + t.Fatalf("expected meta start script to contain %q, got: %s", expected, script) + } + } +} + +func TestRenderTiCIMetaStartScriptWithoutTiDBAuthCopiesConfig(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + + script := renderTiCIMetaStartScript(tc, "tici-test-tici-meta-peer", nil) + for _, expected := range []string{ + `cp "${config_template}" "${config_file}"`, + `exec /tici-server meta --config=/etc/tici/tici.toml`, + } { + if !strings.Contains(script, expected) { + t.Fatalf("expected meta start script to contain %q, got: %s", expected, script) + } + } + if strings.Contains(script, "tidb_auth_file") { + t.Fatalf("expected meta start script without TiDB auth to avoid auth file handling, got: %s", script) + } +} + +func TestBuildTiCIMetaConfigUsesTiDBAuthUser(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tc.Spec.TiCI.Meta.TiDBAuth = &v1alpha1.TiCITiDBAuth{ + User: "tici", + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + } + + cfg, err := buildTiCIMetaConfig(tc) + if err != nil { + t.Fatalf("build meta config failed: %v", err) + } + if !strings.Contains(cfg, `mysql://tici@tici-test-tidb:4000`) { + t.Fatalf("expected meta config to use configured TiDB auth user without credential data, got: %s", cfg) + } +} + func TestBuildTiCIConfigIncludesS3Prefix(t *testing.T) { tc := newTidbClusterForTiCIConfig() tc.Spec.TiCI.S3.Prefix = "custom_prefix" @@ -176,6 +232,237 @@ func TestPrepareTiCIRollingUpgrade(t *testing.T) { } } +func TestPrepareTiCIRollingUpgradeWithTemplateAnnotationChange(t *testing.T) { + partition := int32(1) + oldSet := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tc-tici-meta", + Namespace: "default", + Annotations: map[string]string{}, + }, + Spec: appsv1.StatefulSetSpec{ + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{Partition: &partition}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + ticiMetaTiDBAuthHashAnnotation: "old", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "tici-meta", Image: "tici:v1"}}, + }, + }, + }, + Status: appsv1.StatefulSetStatus{ + CurrentRevision: "tc-tici-meta-current", + UpdateRevision: "tc-tici-meta-current", + }, + } + specData, err := json.Marshal(appsv1.StatefulSetSpec{Template: oldSet.Spec.Template}) + if err != nil { + t.Fatalf("marshal spec failed: %v", err) + } + oldSet.Annotations[LastAppliedConfigAnnotation] = string(specData) + + newSet := oldSet.DeepCopy() + newSet.Spec.Template.Annotations[ticiMetaTiDBAuthHashAnnotation] = "new" + + prepareTiCIRollingUpgrade(newSet, oldSet) + if newSet.Spec.UpdateStrategy.RollingUpdate == nil || newSet.Spec.UpdateStrategy.RollingUpdate.Partition == nil { + t.Fatalf("rolling update partition should be set") + } + if got := *newSet.Spec.UpdateStrategy.RollingUpdate.Partition; got != 0 { + t.Fatalf("expected partition 0 when TiCI template annotation changes, got: %d", got) + } +} + +func TestGetNewTiCIMetaStatefulSetUsesConfigMapAndTiDBAuthSecret(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tidbAuth := &tiCIMetaTiDBAuth{ + User: "root", + SecretName: "tidb-auth", + SecretKey: "auth", + Hash: "tidb-auth-hash", + } + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tici-test-tici-meta", + Namespace: "test-ns", + }, + Data: map[string]string{"config-file": "[server]\npd-addr = \"x\"\n"}, + } + + sts, err := getNewTiCIMetaStatefulSet(tc, cm, tidbAuth) + if err != nil { + t.Fatalf("build TiCI meta statefulset failed: %v", err) + } + if sts == nil { + t.Fatal("expected non-nil TiCI meta statefulset") + return + } + + foundConfigVolume := false + foundRuntimeConfigVolume := false + foundTiDBAuthVolume := false + for _, volume := range sts.Spec.Template.Spec.Volumes { + if volume.Name == "config" && volume.ConfigMap != nil && volume.ConfigMap.Name == cm.Name { + foundConfigVolume = true + } + if volume.Name == "runtime-config" && volume.EmptyDir != nil { + foundRuntimeConfigVolume = true + } + if volume.Name == "tidb-auth" && volume.Secret != nil && volume.Secret.SecretName == "tidb-auth" && + len(volume.Secret.Items) == 1 && volume.Secret.Items[0].Key == "auth" && volume.Secret.Items[0].Path == "tidb-auth" && + volume.Secret.Optional == nil { + foundTiDBAuthVolume = true + } + } + if !foundConfigVolume { + t.Fatalf("expected TiCI meta statefulset to mount config from configmap, got volumes: %+v", sts.Spec.Template.Spec.Volumes) + } + if !foundRuntimeConfigVolume { + t.Fatalf("expected TiCI meta statefulset to include writable runtime config volume, got volumes: %+v", sts.Spec.Template.Spec.Volumes) + } + if !foundTiDBAuthVolume { + t.Fatalf("expected TiCI meta statefulset to mount TiDB auth secret, got volumes: %+v", sts.Spec.Template.Spec.Volumes) + } + if sts.Spec.Template.Annotations[ticiMetaTiDBAuthHashAnnotation] != "tidb-auth-hash" { + t.Fatalf("expected TiCI meta statefulset to include TiDB auth hash annotation, got: %s", sts.Spec.Template.Annotations[ticiMetaTiDBAuthHashAnnotation]) + } +} + +func TestGetNewTiCIMetaStatefulSetWithoutTiDBAuthDoesNotMountAuthSecret(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tici-test-tici-meta", + Namespace: "test-ns", + }, + Data: map[string]string{"config-file": "[server]\npd-addr = \"x\"\n"}, + } + + sts, err := getNewTiCIMetaStatefulSet(tc, cm, nil) + if err != nil { + t.Fatalf("build TiCI meta statefulset failed: %v", err) + } + if sts == nil { + t.Fatal("expected non-nil TiCI meta statefulset") + return + } + for _, volume := range sts.Spec.Template.Spec.Volumes { + if volume.Name == "tidb-auth" { + t.Fatalf("expected TiCI meta statefulset without TiDB auth to avoid auth secret volume, got volumes: %+v", sts.Spec.Template.Spec.Volumes) + } + } + if _, ok := sts.Spec.Template.Annotations[ticiMetaTiDBAuthHashAnnotation]; ok { + t.Fatalf("expected TiCI meta statefulset without TiDB auth to avoid TiDB auth hash annotation, got: %+v", sts.Spec.Template.Annotations) + } +} + +func TestGetTiCIMetaTiDBAuthFromExplicitSecret(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tc.Spec.TiCI.Meta.TiDBAuth = &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + } + deps := controller.NewFakeDependencies() + secretIndexer := deps.KubeInformerFactory.Core().V1().Secrets().Informer().GetIndexer() + if err := secretIndexer.Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "tidb-auth", Namespace: "test-ns"}, + Data: map[string][]byte{"auth": []byte("secret-data")}, + }); err != nil { + t.Fatalf("add secret failed: %v", err) + } + m := &ticiMemberManager{deps: deps} + + auth, err := m.getTiCIMetaTiDBAuth(tc) + if err != nil { + t.Fatalf("get TiCI meta TiDB auth failed: %v", err) + } + if auth == nil { + t.Fatal("expected TiCI meta TiDB auth") + return + } + if auth.User != "root" || auth.SecretName != "tidb-auth" || auth.SecretKey != "auth" || auth.Hash == "" { + t.Fatalf("unexpected TiCI meta TiDB auth: %+v", auth) + } +} + +func TestGetTiCIMetaTiDBAuthMissingSecretWarnsAndRequeues(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tc.Spec.TiCI.Meta.TiDBAuth = &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + } + deps := controller.NewFakeDependencies() + m := &ticiMemberManager{deps: deps} + + auth, err := m.getTiCIMetaTiDBAuth(tc) + if auth != nil { + t.Fatalf("expected no TiCI meta TiDB auth when secret is missing, got: %+v", auth) + } + if !controller.IsRequeueError(err) { + t.Fatalf("expected missing TiCI meta TiDB auth secret to requeue, got: %v", err) + } + event := readFakeRecorderEvent(t, deps) + if !strings.Contains(event, corev1.EventTypeWarning) || !strings.Contains(event, ticiMetaTiDBAuthRefNotFound) || !strings.Contains(event, "test-ns/tidb-auth") { + t.Fatalf("unexpected event for missing TiCI meta TiDB auth secret: %s", event) + } +} + +func TestGetTiCIMetaTiDBAuthMissingSecretKeyWarnsAndErrors(t *testing.T) { + tc := newTidbClusterForTiCIConfig() + tc.Spec.TiCI.Meta.TiDBAuth = &v1alpha1.TiCITiDBAuth{ + PasswordSecret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: "tidb-auth"}, + Key: "auth", + }, + } + deps := controller.NewFakeDependencies() + secretIndexer := deps.KubeInformerFactory.Core().V1().Secrets().Informer().GetIndexer() + if err := secretIndexer.Add(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "tidb-auth", Namespace: "test-ns"}, + Data: map[string][]byte{"other": []byte("secret-data")}, + }); err != nil { + t.Fatalf("add secret failed: %v", err) + } + m := &ticiMemberManager{deps: deps} + + auth, err := m.getTiCIMetaTiDBAuth(tc) + if auth != nil { + t.Fatalf("expected no TiCI meta TiDB auth when secret key is missing, got: %+v", auth) + } + if err == nil || controller.IsRequeueError(err) || !strings.Contains(err.Error(), `key "auth"`) { + t.Fatalf("expected missing TiCI meta TiDB auth secret key to return a clear non-requeue error, got: %v", err) + } + event := readFakeRecorderEvent(t, deps) + if !strings.Contains(event, corev1.EventTypeWarning) || !strings.Contains(event, ticiMetaTiDBAuthRefKeyNotFound) || !strings.Contains(event, `key "auth"`) { + t.Fatalf("unexpected event for missing TiCI meta TiDB auth secret key: %s", event) + } +} + +func readFakeRecorderEvent(t *testing.T, deps *controller.Dependencies) string { + t.Helper() + fakeRecorder, ok := deps.Recorder.(*record.FakeRecorder) + if !ok { + t.Fatalf("expected fake recorder, got %T", deps.Recorder) + } + select { + case event := <-fakeRecorder.Events: + return event + default: + t.Fatal("expected event to be recorded") + return "" + } +} + func newTidbClusterForTiCIConfig() *v1alpha1.TidbCluster { return &v1alpha1.TidbCluster{ ObjectMeta: metav1.ObjectMeta{