diff --git a/api/core/v1alpha1/common_types.go b/api/core/v1alpha1/common_types.go index fb07e0bff8b..9c86850bd3f 100644 --- a/api/core/v1alpha1/common_types.go +++ b/api/core/v1alpha1/common_types.go @@ -137,6 +137,8 @@ const ( LabelValComponentRouter = string(meta.ComponentRouter) LabelValComponentResourceManager = string(meta.ComponentResourceManager) LabelValComponentTiProxy = string(meta.ComponentTiProxy) + LabelValComponentDMMaster = string(meta.ComponentDMMaster) + LabelValComponentDMWorker = string(meta.ComponentDMWorker) // Deprecated: use LabelValComponentScheduling LabelValComponentScheduler = string(meta.ComponentScheduler) diff --git a/api/core/v1alpha1/dm_types.go b/api/core/v1alpha1/dm_types.go new file mode 100644 index 00000000000..a6b77d43aa6 --- /dev/null +++ b/api/core/v1alpha1/dm_types.go @@ -0,0 +1,220 @@ +// 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + meta "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" +) + +const ( + DMPortName = "dm-master" + DefaultDMPort = 8261 + DMPeerPortName = "dm-master-peer" + DefaultDMPeerPort = 8291 + + DefaultDMMinReadySeconds = 5 + + VolumeMountTypeDMData VolumeMountType = "data" + VolumeMountDMDataDefaultPath = "/var/lib/dm" +) + +const ( + DMGroupCondAvailable = "Available" + DMGroupAvailableReason = "DMGroupAvailable" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// DMGroupList defines a list of DM master groups +type DMGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DMGroup `json:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector +// +kubebuilder:resource:categories=group,shortName=dmg +// +kubebuilder:selectablefield:JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Desired",type=string,JSONPath=`.spec.replicas` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.readyReplicas` +// +kubebuilder:printcolumn:name="Updated",type=string,JSONPath=`.status.updatedReplicas` +// +kubebuilder:printcolumn:name="UpdateRevision",type=string,JSONPath=`.status.updateRevision` +// +kubebuilder:printcolumn:name="CurrentRevision",type=string,JSONPath=`.status.currentRevision` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// DMGroup defines a group of similar DM master instances. +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 40",message="name must not exceed 40 characters" +type DMGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DMGroupSpec `json:"spec,omitempty"` + Status DMGroupStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// DMList defines a list of DM master instances +type DMList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DM `json:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories=instance +// +kubebuilder:selectablefield:JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Synced",type=string,JSONPath=`.status.conditions[?(@.type=="Synced")].status` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// DM defines a DM master instance +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 47",message="name must not exceed 47 characters" +type DM struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DMSpec `json:"spec,omitempty"` + Status DMStatus `json:"status,omitempty"` +} + +// DMGroupSpec describes the common attributes of a DMGroup +type DMGroupSpec struct { + Cluster ClusterReference `json:"cluster"` + // Features are enabled features + Features []meta.Feature `json:"features,omitempty"` + + // +kubebuilder:validation:Minimum=0 + Replicas *int32 `json:"replicas"` + + // +listType=map + // +listMapKey=type + SchedulePolicies []SchedulePolicy `json:"schedulePolicies,omitempty"` + + // MinReadySeconds specifies the minimum number of seconds for which a newly created pod + // must be ready without any containers crashing for it to be considered available. + // +kubebuilder:validation:Minimum=0 + // +optional + MinReadySeconds *int64 `json:"minReadySeconds,omitempty"` + + Template DMTemplate `json:"template"` +} + +// DMTemplate defines the template for DM master instances +type DMTemplate struct { + ObjectMeta `json:"metadata,omitempty"` + Spec DMTemplateSpec `json:"spec"` +} + +// DMTemplateSpec can only be specified in DMGroup +// +kubebuilder:validation:XValidation:rule="!has(self.overlay) || !has(self.overlay.volumeClaims) || (has(self.volumes) && self.overlay.volumeClaims.all(vc, vc.name in self.volumes.map(v, v.name)))",message="overlay volumeClaims names must exist in volumes" +type DMTemplateSpec struct { + // Version must be a semantic version. + // It can have a v prefix or not. + // +kubebuilder:validation:Pattern=`^(v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` + Version string `json:"version"` + // Image is the DM master image. + // If tag is omitted, version will be used as the image tag. + // Default is pingcap/dm + Image *string `json:"image,omitempty"` + // Server defines server config for DM master + Server DMServer `json:"server,omitempty"` + Resources ResourceRequirements `json:"resources,omitempty"` + // UpdateStrategy defines the update strategy for DM master + UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` + // Config defines the config file of DM master + Config ConfigFile `json:"config,omitempty"` + + // Security defines security config + Security *Security `json:"security,omitempty"` + + // DataVolume defines the persistent volume for DM master's embedded etcd data. + // This volume is required for dm-master to persist cluster state. + DataVolume Volume `json:"dataVolume"` + + // Volumes defines additional persistent volumes, it is optional. + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=256 + Volumes []Volume `json:"volumes,omitempty"` + // Overlay defines a k8s native resource template patch. + // All resources (pod, pvcs, ...) managed by DM master can be overlaid by this field. + Overlay *Overlay `json:"overlay,omitempty"` +} + +// DMServer defines server config for DM master +type DMServer struct { + // Ports defines all ports listened by DM master + Ports DMPorts `json:"ports,omitempty"` +} + +// DMPorts defines the ports for DM master +type DMPorts struct { + // Port defines the main API/gRPC port for DM master. Default is 8261. + Port *Port `json:"port,omitempty"` + // PeerPort defines the etcd peer port for DM master. Default is 8291. + PeerPort *Port `json:"peerPort,omitempty"` +} + +// DMGroupStatus defines the observed state of a DMGroup +type DMGroupStatus struct { + CommonStatus `json:",inline"` + GroupStatus `json:",inline"` +} + +// DMSpec describes the attributes of a DM master instance +// +kubebuilder:validation:XValidation:rule="(!has(oldSelf.topology) && !has(self.topology)) || (has(oldSelf.topology) && has(self.topology))",fieldPath=".topology",message="topology can only be set when creating" +type DMSpec struct { + // Cluster is a reference of tidb cluster + Cluster ClusterReference `json:"cluster"` + // Features are enabled features + Features []meta.Feature `json:"features,omitempty"` + + // Topology defines the topology domain of this DM master instance. + // It will be translated into a node affinity config. + // Topology cannot be changed. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="topology is immutable" + Topology Topology `json:"topology,omitempty"` + + // Subdomain means the subdomain of the exported DM master DNS. + // All DM master instances in the same group use the same subdomain. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="subdomain is immutable" + Subdomain string `json:"subdomain"` + + // DMTemplateSpec embeds fields managed by DMGroup + DMTemplateSpec `json:",inline"` +} + +// DMStatus defines the observed state of a DM master instance +type DMStatus struct { + CommonStatus `json:",inline"` + + // MemberID is the etcd member ID of this DM master instance + MemberID string `json:"memberID,omitempty"` +} diff --git a/api/core/v1alpha1/dmworker_types.go b/api/core/v1alpha1/dmworker_types.go new file mode 100644 index 00000000000..755b8e39576 --- /dev/null +++ b/api/core/v1alpha1/dmworker_types.go @@ -0,0 +1,226 @@ +// 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + meta "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" +) + +const ( + DMWorkerPortName = "dm-worker" + DefaultDMWorkerPort = 8262 + + DefaultDMWorkerMinReadySeconds = 5 + + VolumeMountTypeDMWorkerRelay VolumeMountType = "relay-dir" + VolumeMountDMWorkerRelayDefaultPath = "/var/lib/dm-worker/relay" +) + +const ( + DMWorkerGroupCondAvailable = "Available" + DMWorkerGroupAvailableReason = "DMWorkerGroupAvailable" +) + +// DMGroupReference is a reference to a DMGroup in the same namespace. +type DMGroupReference struct { + // Name is the name of the DMGroup. + Name string `json:"name"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// DMWorkerGroupList defines a list of DM worker groups +type DMWorkerGroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DMWorkerGroup `json:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector +// +kubebuilder:resource:categories=group,shortName=dmwg +// +kubebuilder:selectablefield:JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="DMGroup",type=string,JSONPath=`.spec.dmGroupRef.name` +// +kubebuilder:printcolumn:name="Desired",type=string,JSONPath=`.spec.replicas` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.readyReplicas` +// +kubebuilder:printcolumn:name="Updated",type=string,JSONPath=`.status.updatedReplicas` +// +kubebuilder:printcolumn:name="UpdateRevision",type=string,JSONPath=`.status.updateRevision` +// +kubebuilder:printcolumn:name="CurrentRevision",type=string,JSONPath=`.status.currentRevision` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// DMWorkerGroup defines a group of similar DM worker instances. +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 40",message="name must not exceed 40 characters" +type DMWorkerGroup struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DMWorkerGroupSpec `json:"spec,omitempty"` + Status DMWorkerGroupStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// DMWorkerList defines a list of DM worker instances +type DMWorkerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []DMWorker `json:"items"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories=instance +// +kubebuilder:selectablefield:JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Cluster",type=string,JSONPath=`.spec.cluster.name` +// +kubebuilder:printcolumn:name="Synced",type=string,JSONPath=`.status.conditions[?(@.type=="Synced")].status` +// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// DMWorker defines a DM worker instance +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 47",message="name must not exceed 47 characters" +type DMWorker struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DMWorkerSpec `json:"spec,omitempty"` + Status DMWorkerStatus `json:"status,omitempty"` +} + +// DMWorkerGroupSpec describes the common attributes of a DMWorkerGroup +type DMWorkerGroupSpec struct { + Cluster ClusterReference `json:"cluster"` + // DMGroupRef is a reference to the DMGroup (dm-master cluster) that workers join. + DMGroupRef DMGroupReference `json:"dmGroupRef"` + // Features are enabled features + Features []meta.Feature `json:"features,omitempty"` + + // +kubebuilder:validation:Minimum=0 + Replicas *int32 `json:"replicas"` + + // +listType=map + // +listMapKey=type + SchedulePolicies []SchedulePolicy `json:"schedulePolicies,omitempty"` + + // MinReadySeconds specifies the minimum number of seconds for which a newly created pod + // must be ready without any containers crashing for it to be considered available. + // +kubebuilder:validation:Minimum=0 + // +optional + MinReadySeconds *int64 `json:"minReadySeconds,omitempty"` + + Template DMWorkerTemplate `json:"template"` +} + +// DMWorkerTemplate defines the template for DM worker instances +type DMWorkerTemplate struct { + ObjectMeta `json:"metadata,omitempty"` + Spec DMWorkerTemplateSpec `json:"spec"` +} + +// DMWorkerTemplateSpec can only be specified in DMWorkerGroup +// +kubebuilder:validation:XValidation:rule="!has(self.overlay) || !has(self.overlay.volumeClaims) || (has(self.volumes) && self.overlay.volumeClaims.all(vc, vc.name in self.volumes.map(v, v.name)))",message="overlay volumeClaims names must exist in volumes" +type DMWorkerTemplateSpec struct { + // Version must be a semantic version. + // It can have a v prefix or not. + // +kubebuilder:validation:Pattern=`^(v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$` + Version string `json:"version"` + // Image is the DM worker image. + // If tag is omitted, version will be used as the image tag. + // Default is pingcap/dm + Image *string `json:"image,omitempty"` + // Server defines server config for DM worker + Server DMWorkerServer `json:"server,omitempty"` + Resources ResourceRequirements `json:"resources,omitempty"` + // UpdateStrategy defines the update strategy for DM worker + UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` + // Config defines the config file of DM worker + Config ConfigFile `json:"config,omitempty"` + + // Security defines security config + Security *Security `json:"security,omitempty"` + + // RelayVolume defines the persistent volume for DM worker's relay log storage. + // This volume is required for dm-worker to persist relay logs. + RelayVolume Volume `json:"relayVolume"` + + // Volumes defines additional persistent volumes, it is optional. + // +listType=map + // +listMapKey=name + // +kubebuilder:validation:MaxItems=256 + Volumes []Volume `json:"volumes,omitempty"` + // Overlay defines a k8s native resource template patch. + // All resources (pod, pvcs, ...) managed by DM worker can be overlaid by this field. + // Use this to mount upstream/downstream TLS cert Secrets into the dm-worker pod. + Overlay *Overlay `json:"overlay,omitempty"` +} + +// DMWorkerServer defines server config for DM worker +type DMWorkerServer struct { + // Ports defines all ports listened by DM worker + Ports DMWorkerPorts `json:"ports,omitempty"` +} + +// DMWorkerPorts defines the ports for DM worker +type DMWorkerPorts struct { + // Port defines the main port for DM worker. Default is 8262. + Port *Port `json:"port,omitempty"` +} + +// DMWorkerGroupStatus defines the observed state of a DMWorkerGroup +type DMWorkerGroupStatus struct { + CommonStatus `json:",inline"` + GroupStatus `json:",inline"` +} + +// DMWorkerSpec describes the attributes of a DM worker instance +// +kubebuilder:validation:XValidation:rule="(!has(oldSelf.topology) && !has(self.topology)) || (has(oldSelf.topology) && has(self.topology))",fieldPath=".topology",message="topology can only be set when creating" +type DMWorkerSpec struct { + // Cluster is a reference of tidb cluster + Cluster ClusterReference `json:"cluster"` + // DMGroupRef is a reference to the DMGroup (dm-master cluster) that this worker joins. + // The controller resolves the dm-master client Service address from this reference. + DMGroupRef DMGroupReference `json:"dmGroupRef"` + // Features are enabled features + Features []meta.Feature `json:"features,omitempty"` + + // Topology defines the topology domain of this DM worker instance. + // It will be translated into a node affinity config. + // Topology cannot be changed. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="topology is immutable" + Topology Topology `json:"topology,omitempty"` + + // Subdomain means the subdomain of the exported DM worker DNS. + // All DM worker instances in the same group use the same subdomain. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="subdomain is immutable" + Subdomain string `json:"subdomain"` + + // DMWorkerTemplateSpec embeds fields managed by DMWorkerGroup + DMWorkerTemplateSpec `json:",inline"` +} + +// DMWorkerStatus defines the observed state of a DM worker instance +type DMWorkerStatus struct { + CommonStatus `json:",inline"` +} diff --git a/api/core/v1alpha1/names.go b/api/core/v1alpha1/names.go index 5b77953e54a..9b9ab027c16 100644 --- a/api/core/v1alpha1/names.go +++ b/api/core/v1alpha1/names.go @@ -67,6 +67,8 @@ const ( ContainerNameRouter = "router" ContainerNameResourceManager = "resource-manager" ContainerNameTiProxy = "tiproxy" + ContainerNameDMMaster = "dm-master" + ContainerNameDMWorker = "dm-worker" // Deprecated: use ContainerNameScheduling ContainerNameScheduler = "scheduler" @@ -105,6 +107,8 @@ const ( DirPathConfigRouter = "/etc/router" DirPathConfigResourceManager = "/etc/resource-manager" DirPathConfigTiProxy = "/etc/tiproxy" + DirPathConfigDMMaster = "/etc/dm-master" + DirPathConfigDMWorker = "/etc/dm-worker" // Deprecated: use DirPathConfigScheduling DirPathConfigScheduler = "/etc/scheduler" @@ -136,6 +140,8 @@ const ( DirPathClusterTLSRouter = "/var/lib/router-tls" DirPathClusterTLSResourceManager = "/var/lib/resource-manager-tls" DirPathClusterTLSTiProxy = "/var/lib/tiproxy-tls" + DirPathClusterTLSDMMaster = "/var/lib/dm-master-tls" + DirPathClusterTLSDMWorker = "/var/lib/dm-worker-tls" // Deprecated: use DirPathClusterTLSScheduling DirPathClusterTLSScheduler = "/var/lib/scheduler-tls" diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go index 48f5e451113..ef9056076b2 100644 --- a/api/core/v1alpha1/zz_generated.deepcopy.go +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -385,6 +385,679 @@ func (in *CoprocessorReference) DeepCopy() *CoprocessorReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DM) DeepCopyInto(out *DM) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DM. +func (in *DM) DeepCopy() *DM { + if in == nil { + return nil + } + out := new(DM) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DM) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMGroup) DeepCopyInto(out *DMGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMGroup. +func (in *DMGroup) DeepCopy() *DMGroup { + if in == nil { + return nil + } + out := new(DMGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMGroupList) DeepCopyInto(out *DMGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DMGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMGroupList. +func (in *DMGroupList) DeepCopy() *DMGroupList { + if in == nil { + return nil + } + out := new(DMGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMGroupReference) DeepCopyInto(out *DMGroupReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMGroupReference. +func (in *DMGroupReference) DeepCopy() *DMGroupReference { + if in == nil { + return nil + } + out := new(DMGroupReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMGroupSpec) DeepCopyInto(out *DMGroupSpec) { + *out = *in + out.Cluster = in.Cluster + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make([]metav1alpha1.Feature, len(*in)) + copy(*out, *in) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.SchedulePolicies != nil { + in, out := &in.SchedulePolicies, &out.SchedulePolicies + *out = make([]SchedulePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MinReadySeconds != nil { + in, out := &in.MinReadySeconds, &out.MinReadySeconds + *out = new(int64) + **out = **in + } + in.Template.DeepCopyInto(&out.Template) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMGroupSpec. +func (in *DMGroupSpec) DeepCopy() *DMGroupSpec { + if in == nil { + return nil + } + out := new(DMGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMGroupStatus) DeepCopyInto(out *DMGroupStatus) { + *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) + out.GroupStatus = in.GroupStatus + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMGroupStatus. +func (in *DMGroupStatus) DeepCopy() *DMGroupStatus { + if in == nil { + return nil + } + out := new(DMGroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMList) DeepCopyInto(out *DMList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DM, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMList. +func (in *DMList) DeepCopy() *DMList { + if in == nil { + return nil + } + out := new(DMList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMPorts) DeepCopyInto(out *DMPorts) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(Port) + **out = **in + } + if in.PeerPort != nil { + in, out := &in.PeerPort, &out.PeerPort + *out = new(Port) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMPorts. +func (in *DMPorts) DeepCopy() *DMPorts { + if in == nil { + return nil + } + out := new(DMPorts) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMServer) DeepCopyInto(out *DMServer) { + *out = *in + in.Ports.DeepCopyInto(&out.Ports) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMServer. +func (in *DMServer) DeepCopy() *DMServer { + if in == nil { + return nil + } + out := new(DMServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMSpec) DeepCopyInto(out *DMSpec) { + *out = *in + out.Cluster = in.Cluster + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make([]metav1alpha1.Feature, len(*in)) + copy(*out, *in) + } + if in.Topology != nil { + in, out := &in.Topology, &out.Topology + *out = make(Topology, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.DMTemplateSpec.DeepCopyInto(&out.DMTemplateSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMSpec. +func (in *DMSpec) DeepCopy() *DMSpec { + if in == nil { + return nil + } + out := new(DMSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMStatus) DeepCopyInto(out *DMStatus) { + *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMStatus. +func (in *DMStatus) DeepCopy() *DMStatus { + if in == nil { + return nil + } + out := new(DMStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMTemplate) DeepCopyInto(out *DMTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMTemplate. +func (in *DMTemplate) DeepCopy() *DMTemplate { + if in == nil { + return nil + } + out := new(DMTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMTemplateSpec) DeepCopyInto(out *DMTemplateSpec) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + in.Server.DeepCopyInto(&out.Server) + in.Resources.DeepCopyInto(&out.Resources) + out.UpdateStrategy = in.UpdateStrategy + if in.Security != nil { + in, out := &in.Security, &out.Security + *out = new(Security) + (*in).DeepCopyInto(*out) + } + in.DataVolume.DeepCopyInto(&out.DataVolume) + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Overlay != nil { + in, out := &in.Overlay, &out.Overlay + *out = new(Overlay) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMTemplateSpec. +func (in *DMTemplateSpec) DeepCopy() *DMTemplateSpec { + if in == nil { + return nil + } + out := new(DMTemplateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorker) DeepCopyInto(out *DMWorker) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorker. +func (in *DMWorker) DeepCopy() *DMWorker { + if in == nil { + return nil + } + out := new(DMWorker) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMWorker) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerGroup) DeepCopyInto(out *DMWorkerGroup) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerGroup. +func (in *DMWorkerGroup) DeepCopy() *DMWorkerGroup { + if in == nil { + return nil + } + out := new(DMWorkerGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMWorkerGroup) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerGroupList) DeepCopyInto(out *DMWorkerGroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DMWorkerGroup, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerGroupList. +func (in *DMWorkerGroupList) DeepCopy() *DMWorkerGroupList { + if in == nil { + return nil + } + out := new(DMWorkerGroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMWorkerGroupList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerGroupSpec) DeepCopyInto(out *DMWorkerGroupSpec) { + *out = *in + out.Cluster = in.Cluster + out.DMGroupRef = in.DMGroupRef + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make([]metav1alpha1.Feature, len(*in)) + copy(*out, *in) + } + if in.Replicas != nil { + in, out := &in.Replicas, &out.Replicas + *out = new(int32) + **out = **in + } + if in.SchedulePolicies != nil { + in, out := &in.SchedulePolicies, &out.SchedulePolicies + *out = make([]SchedulePolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MinReadySeconds != nil { + in, out := &in.MinReadySeconds, &out.MinReadySeconds + *out = new(int64) + **out = **in + } + in.Template.DeepCopyInto(&out.Template) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerGroupSpec. +func (in *DMWorkerGroupSpec) DeepCopy() *DMWorkerGroupSpec { + if in == nil { + return nil + } + out := new(DMWorkerGroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerGroupStatus) DeepCopyInto(out *DMWorkerGroupStatus) { + *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) + out.GroupStatus = in.GroupStatus + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerGroupStatus. +func (in *DMWorkerGroupStatus) DeepCopy() *DMWorkerGroupStatus { + if in == nil { + return nil + } + out := new(DMWorkerGroupStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerList) DeepCopyInto(out *DMWorkerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DMWorker, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerList. +func (in *DMWorkerList) DeepCopy() *DMWorkerList { + if in == nil { + return nil + } + out := new(DMWorkerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DMWorkerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerPorts) DeepCopyInto(out *DMWorkerPorts) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(Port) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerPorts. +func (in *DMWorkerPorts) DeepCopy() *DMWorkerPorts { + if in == nil { + return nil + } + out := new(DMWorkerPorts) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerServer) DeepCopyInto(out *DMWorkerServer) { + *out = *in + in.Ports.DeepCopyInto(&out.Ports) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerServer. +func (in *DMWorkerServer) DeepCopy() *DMWorkerServer { + if in == nil { + return nil + } + out := new(DMWorkerServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerSpec) DeepCopyInto(out *DMWorkerSpec) { + *out = *in + out.Cluster = in.Cluster + out.DMGroupRef = in.DMGroupRef + if in.Features != nil { + in, out := &in.Features, &out.Features + *out = make([]metav1alpha1.Feature, len(*in)) + copy(*out, *in) + } + if in.Topology != nil { + in, out := &in.Topology, &out.Topology + *out = make(Topology, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.DMWorkerTemplateSpec.DeepCopyInto(&out.DMWorkerTemplateSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerSpec. +func (in *DMWorkerSpec) DeepCopy() *DMWorkerSpec { + if in == nil { + return nil + } + out := new(DMWorkerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerStatus) DeepCopyInto(out *DMWorkerStatus) { + *out = *in + in.CommonStatus.DeepCopyInto(&out.CommonStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerStatus. +func (in *DMWorkerStatus) DeepCopy() *DMWorkerStatus { + if in == nil { + return nil + } + out := new(DMWorkerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerTemplate) DeepCopyInto(out *DMWorkerTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerTemplate. +func (in *DMWorkerTemplate) DeepCopy() *DMWorkerTemplate { + if in == nil { + return nil + } + out := new(DMWorkerTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DMWorkerTemplateSpec) DeepCopyInto(out *DMWorkerTemplateSpec) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + in.Server.DeepCopyInto(&out.Server) + in.Resources.DeepCopyInto(&out.Resources) + out.UpdateStrategy = in.UpdateStrategy + if in.Security != nil { + in, out := &in.Security, &out.Security + *out = new(Security) + (*in).DeepCopyInto(*out) + } + in.RelayVolume.DeepCopyInto(&out.RelayVolume) + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make([]Volume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Overlay != nil { + in, out := &in.Overlay, &out.Overlay + *out = new(Overlay) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DMWorkerTemplateSpec. +func (in *DMWorkerTemplateSpec) DeepCopy() *DMWorkerTemplateSpec { + if in == nil { + return nil + } + out := new(DMWorkerTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DNS) DeepCopyInto(out *DNS) { *out = *in diff --git a/api/core/v1alpha1/zz_generated.register.go b/api/core/v1alpha1/zz_generated.register.go index 80791b293f4..6c92544080f 100644 --- a/api/core/v1alpha1/zz_generated.register.go +++ b/api/core/v1alpha1/zz_generated.register.go @@ -61,6 +61,14 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &Cluster{}, &ClusterList{}, + &DM{}, + &DMGroup{}, + &DMGroupList{}, + &DMList{}, + &DMWorker{}, + &DMWorkerGroup{}, + &DMWorkerGroupList{}, + &DMWorkerList{}, &PD{}, &PDGroup{}, &PDGroupList{}, diff --git a/api/meta/v1alpha1/types.go b/api/meta/v1alpha1/types.go index f3bb4bcb60c..e565384bbb1 100644 --- a/api/meta/v1alpha1/types.go +++ b/api/meta/v1alpha1/types.go @@ -45,6 +45,8 @@ const ( ComponentRouter Component = "router" ComponentResourceManager Component = "resource-manager" ComponentTiProxy Component = "tiproxy" + ComponentDMMaster Component = "dm-master" + ComponentDMWorker Component = "dm-worker" // Deprecated: use ComponentScheduling ComponentScheduler Component = "scheduler" ) diff --git a/cmd/tidb-operator/main.go b/cmd/tidb-operator/main.go index f6eaa40d947..d53d264fa66 100644 --- a/cmd/tidb-operator/main.go +++ b/cmd/tidb-operator/main.go @@ -53,6 +53,10 @@ import ( "github.com/pingcap/tidb-operator/v2/pkg/controllers/br/tibr" "github.com/pingcap/tidb-operator/v2/pkg/controllers/br/tibrgc" "github.com/pingcap/tidb-operator/v2/pkg/controllers/cluster" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dm" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmgroup" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworker" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworkergroup" "github.com/pingcap/tidb-operator/v2/pkg/controllers/pd" "github.com/pingcap/tidb-operator/v2/pkg/controllers/pdgroup" "github.com/pingcap/tidb-operator/v2/pkg/controllers/resourcemanager" @@ -406,6 +410,22 @@ func addIndexer(ctx context.Context, mgr ctrl.Manager) error { return err } + if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.DMGroup{}, "spec.cluster.name", + func(obj client.Object) []string { + dmg := obj.(*v1alpha1.DMGroup) + return []string{dmg.Spec.Cluster.Name} + }); err != nil { + return err + } + + if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.DMWorkerGroup{}, "spec.cluster.name", + func(obj client.Object) []string { + dwg := obj.(*v1alpha1.DMWorkerGroup) + return []string{dwg.Spec.Cluster.Name} + }); err != nil { + return err + } + return nil } @@ -576,6 +596,30 @@ func setupControllers( return tikvworker.Setup(mgr, c, pdcm, vm, tf.Tracker("tikvworker")) }, }, + { + name: "DMGroup", + setupFunc: func() error { + return dmgroup.Setup(mgr, c, tf.AllocateFactory("dm")) + }, + }, + { + name: "DM", + setupFunc: func() error { + return dm.Setup(mgr, c, vm, tf.Tracker("dm")) + }, + }, + { + name: "DMWorkerGroup", + setupFunc: func() error { + return dmworkergroup.Setup(mgr, c, tf.AllocateFactory("dmworker")) + }, + }, + { + name: "DMWorker", + setupFunc: func() error { + return dmworker.Setup(mgr, c, vm, tf.Tracker("dmworker")) + }, + }, { name: "TiBR", setupFunc: func() error { @@ -693,6 +737,18 @@ func BuildCacheByObject() map[client.Object]cache.ByObject { &v1alpha1.Scheduler{}: { Label: labels.Everything(), }, + &v1alpha1.DMGroup{}: { + Label: labels.Everything(), + }, + &v1alpha1.DM{}: { + Label: labels.Everything(), + }, + &v1alpha1.DMWorkerGroup{}: { + Label: labels.Everything(), + }, + &v1alpha1.DMWorker{}: { + Label: labels.Everything(), + }, &corev1.Secret{}: { // TLS secrets managed by cert-manager or user Label: labels.Everything(), diff --git a/examples/basic/07-dm.yaml b/examples/basic/07-dm.yaml new file mode 100644 index 00000000000..188c2ef0fa6 --- /dev/null +++ b/examples/basic/07-dm.yaml @@ -0,0 +1,29 @@ +apiVersion: core.pingcap.com/v1alpha1 +kind: DMGroup +metadata: + name: dm + labels: + pingcap.com/group: dm + pingcap.com/component: dm-master + pingcap.com/cluster: basic +spec: + cluster: + name: basic + replicas: 1 + template: + metadata: + annotations: + author: pingcap + spec: + version: v8.5.2 + resources: + cpu: "1" + memory: 2Gi + config: | + [log] + level = "info" + dataVolume: + name: data + storage: 10Gi + mounts: + - type: data diff --git a/examples/basic/08-dmworker.yaml b/examples/basic/08-dmworker.yaml new file mode 100644 index 00000000000..eb50c139c3d --- /dev/null +++ b/examples/basic/08-dmworker.yaml @@ -0,0 +1,31 @@ +apiVersion: core.pingcap.com/v1alpha1 +kind: DMWorkerGroup +metadata: + name: dmworker + labels: + pingcap.com/group: dmworker + pingcap.com/component: dm-worker + pingcap.com/cluster: basic +spec: + cluster: + name: basic + dmGroupRef: + name: dm + replicas: 1 + template: + metadata: + annotations: + author: pingcap + spec: + version: v8.5.2 + resources: + cpu: "1" + memory: 2Gi + config: | + [log] + level = "info" + relayVolume: + name: relay + storage: 20Gi + mounts: + - type: relay-dir diff --git a/pkg/apiutil/core/v1alpha1/dm.go b/pkg/apiutil/core/v1alpha1/dm.go new file mode 100644 index 00000000000..1a8e91703e5 --- /dev/null +++ b/pkg/apiutil/core/v1alpha1/dm.go @@ -0,0 +1,61 @@ +// 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 coreutil + +import ( + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" +) + +func DMGroupPort(dmg *v1alpha1.DMGroup) int32 { + if dmg.Spec.Template.Spec.Server.Ports.Port != nil { + return dmg.Spec.Template.Spec.Server.Ports.Port.Port + } + return v1alpha1.DefaultDMPort +} + +func DMGroupPeerPort(dmg *v1alpha1.DMGroup) int32 { + if dmg.Spec.Template.Spec.Server.Ports.PeerPort != nil { + return dmg.Spec.Template.Spec.Server.Ports.PeerPort.Port + } + return v1alpha1.DefaultDMPeerPort +} + +func DMPort(dm *v1alpha1.DM) int32 { + if dm.Spec.Server.Ports.Port != nil { + return dm.Spec.Server.Ports.Port.Port + } + return v1alpha1.DefaultDMPort +} + +func DMPeerPort(dm *v1alpha1.DM) int32 { + if dm.Spec.Server.Ports.PeerPort != nil { + return dm.Spec.Server.Ports.PeerPort.Port + } + return v1alpha1.DefaultDMPeerPort +} + +func DMWorkerGroupPort(dwg *v1alpha1.DMWorkerGroup) int32 { + if dwg.Spec.Template.Spec.Server.Ports.Port != nil { + return dwg.Spec.Template.Spec.Server.Ports.Port.Port + } + return v1alpha1.DefaultDMWorkerPort +} + +func DMWorkerPort(dw *v1alpha1.DMWorker) int32 { + if dw.Spec.Server.Ports.Port != nil { + return dw.Spec.Server.Ports.Port.Port + } + return v1alpha1.DefaultDMWorkerPort +} diff --git a/pkg/apiutil/core/v1alpha1/util.go b/pkg/apiutil/core/v1alpha1/util.go index 00ceb6a7041..0d18c8aa6b1 100644 --- a/pkg/apiutil/core/v1alpha1/util.go +++ b/pkg/apiutil/core/v1alpha1/util.go @@ -51,6 +51,8 @@ var allMainContainers = map[string]struct{}{ v1alpha1.ContainerNameScheduling: {}, v1alpha1.ContainerNameTiProxy: {}, v1alpha1.ContainerNameTiKVWorker: {}, + v1alpha1.ContainerNameDMMaster: {}, + v1alpha1.ContainerNameDMWorker: {}, } // IsMainContainer checks whether the container is a main container diff --git a/pkg/configs/dm/config.go b/pkg/configs/dm/config.go new file mode 100644 index 00000000000..11d2d9a8491 --- /dev/null +++ b/pkg/configs/dm/config.go @@ -0,0 +1,174 @@ +// 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 dm + +import ( + "fmt" + "path" + "strconv" + "strings" + + corev1 "k8s.io/api/core/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" +) + +const ( + InitialClusterStateNew = "new" + InitialClusterStateExisting = "existing" +) + +// Config is a subset config of dm-master. +// Only operator-managed fields are defined here. +type Config struct { + Name string `toml:"name"` + DataDir string `toml:"data-dir"` + + MasterAddr string `toml:"master-addr"` + AdvertiseAddr string `toml:"advertise-addr"` + + PeerUrls string `toml:"peer-urls"` + AdvertisePeerUrls string `toml:"advertise-peer-urls"` + + InitialCluster string `toml:"initial-cluster"` + InitialClusterState string `toml:"initial-cluster-state"` + Join string `toml:"join"` + + // SSL fields are top-level in DM master config (not nested under [security]) + SSLCA string `toml:"ssl-ca"` + SSLCert string `toml:"ssl-cert"` + SSLKey string `toml:"ssl-key"` +} + +// Overlay fills in operator-managed config fields. +// peers is the list of all dm-master instances in the same group (used for initial-cluster). +// join is the existing dm-master client address list for scale-out; empty means bootstrap. +func (c *Config) Overlay(cluster *v1alpha1.Cluster, dm *v1alpha1.DM, peers []*v1alpha1.DM, join string) error { + if err := c.Validate(); err != nil { + return err + } + + if coreutil.IsTLSClusterEnabled(cluster) { + c.SSLCA = path.Join(v1alpha1.DirPathClusterTLSDMMaster, corev1.ServiceAccountRootCAKey) + c.SSLCert = path.Join(v1alpha1.DirPathClusterTLSDMMaster, corev1.TLSCertKey) + c.SSLKey = path.Join(v1alpha1.DirPathClusterTLSDMMaster, corev1.TLSPrivateKeyKey) + } + + c.Name = dm.Name + c.MasterAddr = coreutil.ListenAddress(coreutil.DMPort(dm)) + c.AdvertiseAddr = coreutil.InstanceAdvertiseAddress[scope.DM](cluster, dm, coreutil.DMPort(dm)) + c.PeerUrls = coreutil.ListenURL(cluster, coreutil.DMPeerPort(dm)) + c.AdvertisePeerUrls = coreutil.InstanceAdvertiseURL[scope.DM](cluster, dm, coreutil.DMPeerPort(dm)) + + // data dir from DataVolume mounts + for k := range dm.Spec.DataVolume.Mounts { + mount := &dm.Spec.DataVolume.Mounts[k] + if mount.Type == v1alpha1.VolumeMountTypeDMData { + c.DataDir = mount.MountPath + } + } + if c.DataDir == "" { + c.DataDir = v1alpha1.VolumeMountDMDataDefaultPath + } + + initialClusterNum, ok := dm.Annotations[v1alpha1.AnnoKeyInitialClusterNum] + if !ok { + // joining an existing cluster + c.Join = join + } + + if c.Join == "" { + num, err := strconv.ParseInt(initialClusterNum, 10, 32) + if err != nil { + return fmt.Errorf("cannot parse initial cluster num %v: %w", initialClusterNum, err) + } + peers = filterBootstrappingPeers(peers) + if num != int64(len(peers)) { + return fmt.Errorf("unexpected number of replicas, expected is %v, current is %v", num, len(peers)) + } + c.InitialCluster = getInitialCluster(cluster, peers) + c.InitialClusterState = InitialClusterStateNew + } + + return nil +} + +func filterBootstrappingPeers(peers []*v1alpha1.DM) []*v1alpha1.DM { + var boot []*v1alpha1.DM + for _, peer := range peers { + if _, ok := peer.Annotations[v1alpha1.AnnoKeyInitialClusterNum]; ok { + boot = append(boot, peer) + } + } + return boot +} + +func (c *Config) Validate() error { + var fields []string + + if c.Name != "" { + fields = append(fields, "name") + } + if c.DataDir != "" { + fields = append(fields, "data-dir") + } + if c.MasterAddr != "" { + fields = append(fields, "master-addr") + } + if c.AdvertiseAddr != "" { + fields = append(fields, "advertise-addr") + } + if c.PeerUrls != "" { + fields = append(fields, "peer-urls") + } + if c.AdvertisePeerUrls != "" { + fields = append(fields, "advertise-peer-urls") + } + if c.InitialCluster != "" { + fields = append(fields, "initial-cluster") + } + if c.InitialClusterState != "" { + fields = append(fields, "initial-cluster-state") + } + if c.Join != "" { + fields = append(fields, "join") + } + if c.SSLCA != "" { + fields = append(fields, "ssl-ca") + } + if c.SSLCert != "" { + fields = append(fields, "ssl-cert") + } + if c.SSLKey != "" { + fields = append(fields, "ssl-key") + } + + if len(fields) == 0 { + return nil + } + + return fmt.Errorf("%v: %w", fields, v1alpha1.ErrFieldIsManagedByOperator) +} + +func getInitialCluster(c *v1alpha1.Cluster, peers []*v1alpha1.DM) string { + var urls []string + for _, peer := range peers { + url := coreutil.InstanceAdvertiseURL[scope.DM](c, peer, coreutil.DMPeerPort(peer)) + urls = append(urls, peer.Name+"="+url) + } + return strings.Join(urls, ",") +} diff --git a/pkg/configs/dmworker/config.go b/pkg/configs/dmworker/config.go new file mode 100644 index 00000000000..f30d44a100f --- /dev/null +++ b/pkg/configs/dmworker/config.go @@ -0,0 +1,109 @@ +// 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 dmworker + +import ( + "fmt" + "path" + + corev1 "k8s.io/api/core/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" +) + +// Config is a subset config of dm-worker. +// Only operator-managed fields are defined here. +type Config struct { + Name string `toml:"name"` + WorkerAddr string `toml:"worker-addr"` + AdvertiseAddr string `toml:"advertise-addr"` + // Join is the dm-master client address list (e.g., "host1:8261,host2:8261") + Join string `toml:"join"` + RelayDir string `toml:"relay-dir"` + + // SSL fields are top-level in DM worker config (not nested under [security]) + SSLCA string `toml:"ssl-ca"` + SSLCert string `toml:"ssl-cert"` + SSLKey string `toml:"ssl-key"` +} + +// Overlay fills in operator-managed config fields. +// dmMasterAddr is the dm-master client service address (e.g., "host:8261") used as the join target. +func (c *Config) Overlay(cluster *v1alpha1.Cluster, dw *v1alpha1.DMWorker, dmMasterAddr string) error { + if err := c.Validate(); err != nil { + return err + } + + if coreutil.IsTLSClusterEnabled(cluster) { + c.SSLCA = path.Join(v1alpha1.DirPathClusterTLSDMWorker, corev1.ServiceAccountRootCAKey) + c.SSLCert = path.Join(v1alpha1.DirPathClusterTLSDMWorker, corev1.TLSCertKey) + c.SSLKey = path.Join(v1alpha1.DirPathClusterTLSDMWorker, corev1.TLSPrivateKeyKey) + } + + c.Name = dw.Name + c.WorkerAddr = coreutil.ListenAddress(coreutil.DMWorkerPort(dw)) + c.AdvertiseAddr = coreutil.InstanceAdvertiseAddress[scope.DMWorker](cluster, dw, coreutil.DMWorkerPort(dw)) + c.Join = dmMasterAddr + + // relay dir from RelayVolume mounts + for k := range dw.Spec.RelayVolume.Mounts { + mount := &dw.Spec.RelayVolume.Mounts[k] + if mount.Type == v1alpha1.VolumeMountTypeDMWorkerRelay { + c.RelayDir = mount.MountPath + } + } + if c.RelayDir == "" { + c.RelayDir = v1alpha1.VolumeMountDMWorkerRelayDefaultPath + } + + return nil +} + +func (c *Config) Validate() error { + var fields []string + + if c.Name != "" { + fields = append(fields, "name") + } + if c.WorkerAddr != "" { + fields = append(fields, "worker-addr") + } + if c.AdvertiseAddr != "" { + fields = append(fields, "advertise-addr") + } + if c.Join != "" { + fields = append(fields, "join") + } + if c.RelayDir != "" { + fields = append(fields, "relay-dir") + } + if c.SSLCA != "" { + fields = append(fields, "ssl-ca") + } + if c.SSLCert != "" { + fields = append(fields, "ssl-cert") + } + if c.SSLKey != "" { + fields = append(fields, "ssl-key") + } + + if len(fields) == 0 { + return nil + } + + return fmt.Errorf("%v: %w", fields, v1alpha1.ErrFieldIsManagedByOperator) +} diff --git a/pkg/controllers/common/interfaces.go b/pkg/controllers/common/interfaces.go index b1eea1a6e1e..e6490fa4f95 100644 --- a/pkg/controllers/common/interfaces.go +++ b/pkg/controllers/common/interfaces.go @@ -41,6 +41,8 @@ type ( TiFlashSliceInitializer = ResourceSliceInitializer[v1alpha1.TiFlash] TiCDCSliceInitializer = ResourceSliceInitializer[v1alpha1.TiCDC] TiProxySliceInitializer = ResourceSliceInitializer[v1alpha1.TiProxy] + DMSliceInitializer = ResourceSliceInitializer[v1alpha1.DM] + DMWorkerSliceInitializer = ResourceSliceInitializer[v1alpha1.DMWorker] ) type GroupState[G runtime.Group] interface { @@ -203,6 +205,36 @@ type ( } ) +type ( + DMGroupState interface { + DMGroup() *v1alpha1.DMGroup + } + DMState interface { + DM() *v1alpha1.DM + } + DMSliceStateInitializer interface { + DMSliceInitializer() DMSliceInitializer + } + DMSliceState interface { + DMSlice() []*v1alpha1.DM + } +) + +type ( + DMWorkerGroupState interface { + DMWorkerGroup() *v1alpha1.DMWorkerGroup + } + DMWorkerState interface { + DMWorker() *v1alpha1.DMWorker + } + DMWorkerSliceStateInitializer interface { + DMWorkerSliceInitializer() DMWorkerSliceInitializer + } + DMWorkerSliceState interface { + DMWorkerSlice() []*v1alpha1.DMWorker + } +) + type ObjectState[ F client.Object, ] interface { diff --git a/pkg/controllers/dm/builder.go b/pkg/controllers/dm/builder.go new file mode 100644 index 00000000000..a16d8cd3086 --- /dev/null +++ b/pkg/controllers/dm/builder.go @@ -0,0 +1,79 @@ +// 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 dm + +import ( + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dm/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { + runner := task.NewTaskRunner(reporter, + // get DM instance + common.TaskContextObject[scope.DM](state, r.Client), + common.TaskTrack[scope.DM](state, r.Tracker), + // refresh the abnormal_instance gauge, or clear it if the CR is gone + common.TaskObserveInstance[scope.DM](state), + // if it's deleted just return + task.IfBreak(common.CondObjectHasBeenDeleted[scope.DM](state)), + + // get cluster info + common.TaskContextCluster[scope.DM](state, r.Client), + // if it's paused just return + task.IfBreak(common.CondClusterIsPaused(state)), + // if the cluster is deleting, del all subresources and remove the finalizer directly + task.IfBreak(common.CondClusterIsDeleting(state), + common.TaskInstanceFinalizerDel[scope.DM](state, r.Client, common.DefaultInstanceSubresourceLister), + ), + + task.IfBreak(common.CondObjectIsDeleting[scope.DM](state), + common.TaskInstanceFinalizerDel[scope.DM](state, r.Client, common.DefaultInstanceSubresourceLister), + common.TaskInstanceConditionSynced[scope.DM](state), + common.TaskInstanceConditionReady[scope.DM](state), + common.TaskInstanceConditionRunning[scope.DM](state), + common.TaskStatusPersister[scope.DM](state, r.Client), + ), + common.TaskFinalizerAdd[scope.DM](state, r.Client), + + // get pod and check whether the cluster is suspending + common.TaskContextPod[scope.DM](state, r.Client), + task.IfBreak(common.CondClusterIsSuspending(state), + common.TaskSuspendPod(state, r.Client), + common.TaskInstanceConditionSuspended[scope.DM](state), + common.TaskInstanceConditionSynced[scope.DM](state), + common.TaskInstanceConditionReady[scope.DM](state), + common.TaskInstanceConditionRunning[scope.DM](state), + common.TaskStatusPersister[scope.DM](state, r.Client), + ), + + // list peer DM instances (for config generation) + common.TaskContextPeerSlice[scope.DM](state, r.Client), + // health check and member ID + tasks.TaskContextInfoFromDM(state, r.Client), + + // normal process + tasks.TaskConfigMap(state, r.Client), + common.TaskPVC[scope.DM](state, r.Client, r.VolumeModifierFactory, tasks.PVCNewer()), + tasks.TaskPod(state, r.Client), + common.TaskInstanceConditionSynced[scope.DM](state), + common.TaskInstanceConditionReady[scope.DM](state), + common.TaskInstanceConditionRunning[scope.DM](state), + tasks.TaskStatus(state, r.Client), + ) + + return runner +} diff --git a/pkg/controllers/dm/controller.go b/pkg/controllers/dm/controller.go new file mode 100644 index 00000000000..610293c0ca9 --- /dev/null +++ b/pkg/controllers/dm/controller.go @@ -0,0 +1,84 @@ +// 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 dm + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dm/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" + "github.com/pingcap/tidb-operator/v2/pkg/volumes" +) + +type Reconciler struct { + Logger logr.Logger + Client client.Client + VolumeModifierFactory volumes.ModifierFactory + Tracker tracker.Tracker +} + +func Setup(mgr manager.Manager, c client.Client, vm volumes.ModifierFactory, t tracker.Tracker) error { + r := &Reconciler{ + Logger: mgr.GetLogger().WithName("DM"), + Client: c, + VolumeModifierFactory: vm, + Tracker: t, + } + return ctrl.NewControllerManagedBy(mgr).For(&v1alpha1.DM{}). + Owns(&corev1.Pod{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.PersistentVolumeClaim{}). + Watches(&v1alpha1.Cluster{}, r.ClusterEventHandler(), + builder.WithPredicates(predicate.GenerationChangedPredicate{})). + WithOptions(controller.Options{RateLimiter: k8s.RateLimiter}). + Complete(r) +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logr.FromContextOrDiscard(ctx) + reporter := task.NewTableTaskReporter(uuid.NewString()) + + startTime := time.Now() + logger.Info("start reconcile") + defer func() { + dur := time.Since(startTime) + logger.Info("end reconcile", "duration", dur) + summary := fmt.Sprintf("summary for %v\n%s", req.NamespacedName, reporter.Summary()) + logger.Info(summary) + }() + + rtx := &tasks.ReconcileContext{ + State: tasks.NewState(req.NamespacedName), + } + + runner := r.NewRunner(rtx, reporter) + + return runner.Run(ctx) +} diff --git a/pkg/controllers/dm/handler.go b/pkg/controllers/dm/handler.go new file mode 100644 index 00000000000..e4456e7e8d2 --- /dev/null +++ b/pkg/controllers/dm/handler.go @@ -0,0 +1,73 @@ +// 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 dm + +import ( + "context" + "reflect" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" +) + +func (r *Reconciler) ClusterEventHandler() handler.TypedEventHandler[client.Object, reconcile.Request] { + return handler.TypedFuncs[client.Object, reconcile.Request]{ + UpdateFunc: func(ctx context.Context, event event.TypedUpdateEvent[client.Object], + queue workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + oldObj := event.ObjectOld.(*v1alpha1.Cluster) + newObj := event.ObjectNew.(*v1alpha1.Cluster) + + if newObj.Status.PD != oldObj.Status.PD { + r.Logger.Info("pd url is updating", "from", oldObj.Status.PD, "to", newObj.Status.PD) + } else if newObj.Status.ID != oldObj.Status.ID { + r.Logger.Info("cluster id is updating", "from", oldObj.Status.ID, "to", newObj.Status.ID) + } else if !reflect.DeepEqual(oldObj.Spec.SuspendAction, newObj.Spec.SuspendAction) { + r.Logger.Info("suspend action is updating", "from", oldObj.Spec.SuspendAction, "to", newObj.Spec.SuspendAction) + } else if oldObj.Spec.Paused != newObj.Spec.Paused { + r.Logger.Info("cluster paused is updating", "from", oldObj.Spec.Paused, "to", newObj.Spec.Paused) + } else { + return + } + + var dml v1alpha1.DMList + if err := r.Client.List(ctx, &dml, client.MatchingLabels{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyCluster: newObj.Name, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + }, client.InNamespace(newObj.Namespace)); err != nil { + r.Logger.Error(err, "cannot list all dm instances", "ns", newObj.Namespace, "cluster", newObj.Name) + return + } + + for i := range dml.Items { + dm := &dml.Items[i] + r.Logger.Info("queue add", "reason", "cluster change", "namespace", dm.Namespace, "cluster", dm.Spec.Cluster, "name", dm.Name) + queue.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: dm.Name, + Namespace: dm.Namespace, + }, + }) + } + }, + } +} diff --git a/pkg/controllers/dm/tasks/cm.go b/pkg/controllers/dm/tasks/cm.go new file mode 100644 index 00000000000..72c435650f2 --- /dev/null +++ b/pkg/controllers/dm/tasks/cm.go @@ -0,0 +1,75 @@ +// 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" + "strconv" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + dmcfg "github.com/pingcap/tidb-operator/v2/pkg/configs/dm" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/toml" +) + +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { + cfg := dmcfg.Config{} + decoder, encoder := toml.Codec[dmcfg.Config]() + if err := decoder.Decode([]byte(state.DM().Spec.Config), &cfg); err != nil { + return task.Fail().With("dm config cannot be decoded: %v", err) + } + + joinAddr := coreutil.ServiceHost(state.Cluster(), state.DM().Spec.Subdomain) + + ":" + strconv.Itoa(int(coreutil.DMPort(state.DM()))) + + if err := cfg.Overlay(state.Cluster(), state.DM(), state.DMSlice(), joinAddr); err != nil { + return task.Fail().With("cannot generate dm config: %v", err) + } + + data, err := encoder.Encode(&cfg) + if err != nil { + return task.Fail().With("dm config cannot be encoded: %v", err) + } + + expected := newConfigMap(state.DM(), data) + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't create/update the cm of dm: %v", err) + } + return task.Complete().With("cm is synced") + }) +} + +func newConfigMap(dm *v1alpha1.DM, data []byte) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.PodName[scope.DM](dm), + Namespace: dm.Namespace, + Labels: coreutil.ConfigMapLabels[scope.DM](dm), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dm, v1alpha1.SchemeGroupVersion.WithKind("DM")), + }, + }, + Data: map[string]string{ + v1alpha1.FileNameConfig: string(data), + }, + } +} diff --git a/pkg/controllers/dm/tasks/ctx.go b/pkg/controllers/dm/tasks/ctx.go new file mode 100644 index 00000000000..b81bae5ce67 --- /dev/null +++ b/pkg/controllers/dm/tasks/ctx.go @@ -0,0 +1,94 @@ +// 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" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/pingcap/tidb-operator/v2/pkg/apicall" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +type ReconcileContext struct { + State + + MemberID string +} + +func TaskContextInfoFromDM(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ContextInfoFromDM", func(ctx context.Context) task.Result { + cluster := state.Cluster() + dm := state.DM() + + addr := coreutil.InstanceAdvertiseAddress[scope.DM](cluster, dm, coreutil.DMPort(dm)) + scheme := "http" + + var httpClient *http.Client + if coreutil.IsTLSClusterEnabled(cluster) { + tlsConfig, err := apicall.GetClientTLSConfig(ctx, c, cluster) + if err != nil { + return task.Fail().With("cannot get tls config from secret: %w", err) + } + httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + scheme = "https" + } else { + httpClient = http.DefaultClient + } + + url := fmt.Sprintf("%s://%s/api/v1/cluster/info", scheme, addr) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return task.Complete().With("cannot build request for dm-master cluster info: %v", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return task.Complete().With("context without health info is completed, dm-master can't be reached: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return task.Complete().With("dm-master returned non-200 status: %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return task.Complete().With("cannot read dm-master cluster info response: %v", err) + } + + var info struct { + Name string `json:"name"` + } + if err := json.Unmarshal(body, &info); err != nil { + return task.Complete().With("cannot parse dm-master cluster info response: %v", err) + } + + state.SetHealthy() + state.MemberID = info.Name + + return task.Complete().With("get info from dm-master") + }) +} diff --git a/pkg/controllers/dm/tasks/pod.go b/pkg/controllers/dm/tasks/pod.go new file mode 100644 index 00000000000..a69d7bd92b9 --- /dev/null +++ b/pkg/controllers/dm/tasks/pod.go @@ -0,0 +1,198 @@ +// 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" + "path/filepath" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/image" + "github.com/pingcap/tidb-operator/v2/pkg/overlay" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + maputil "github.com/pingcap/tidb-operator/v2/pkg/utils/map" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +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.DM()) + pod := state.Pod() + if pod == nil { + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't create pod of dm: %v", err) + } + state.SetPod(expected) + return task.Complete().With("pod is created") + } + + logger.Info("will update the pod in place") + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of dm: %v", err) + } + + state.SetPod(expected) + return task.Complete().With("pod is synced") + }) +} + +func newPod(cluster *v1alpha1.Cluster, dm *v1alpha1.DM) *corev1.Pod { + vols := []corev1.Volume{ + { + Name: v1alpha1.VolumeNameConfig, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: coreutil.PodName[scope.DM](dm), + }, + }, + }, + }, + } + + mounts := []corev1.VolumeMount{ + { + Name: v1alpha1.VolumeNameConfig, + MountPath: v1alpha1.DirPathConfigDMMaster, + }, + } + + // DataVolume mount + dataVolName := dmVolumeName(dm.Spec.DataVolume.Name) + dataClaimName := coreutil.PersistentVolumeClaimName[scope.DM](dm, dm.Spec.DataVolume.Name) + vols = append(vols, corev1.Volume{ + Name: dataVolName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: dataClaimName, + }, + }, + }) + + dataMountPath := v1alpha1.VolumeMountDMDataDefaultPath + for i := range dm.Spec.DataVolume.Mounts { + mount := &dm.Spec.DataVolume.Mounts[i] + if mount.Type == v1alpha1.VolumeMountTypeDMData && mount.MountPath != "" { + dataMountPath = mount.MountPath + break + } + } + mounts = append(mounts, corev1.VolumeMount{ + Name: dataVolName, + MountPath: dataMountPath, + }) + + // Additional volumes from dm.Spec.Volumes + for i := range dm.Spec.Volumes { + vol := &dm.Spec.Volumes[i] + name := dmVolumeName(vol.Name) + vols = append(vols, corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: coreutil.PersistentVolumeClaimName[scope.DM](dm, vol.Name), + }, + }, + }) + for j := range vol.Mounts { + mount := &vol.Mounts[j] + mounts = append(mounts, corev1.VolumeMount{ + Name: name, + MountPath: mount.MountPath, + SubPath: mount.SubPath, + }) + } + } + + // TLS volume + if coreutil.IsTLSClusterEnabled(cluster) { + vols = append(vols, *coreutil.ClusterTLSVolume[scope.DM](dm)) + mounts = append(mounts, corev1.VolumeMount{ + Name: v1alpha1.VolumeNameClusterTLS, + MountPath: v1alpha1.DirPathClusterTLSDMMaster, + ReadOnly: true, + }) + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dm.Namespace, + Name: coreutil.PodName[scope.DM](dm), + Labels: maputil.Merge( + coreutil.PodLabels[scope.DM](dm), + // legacy labels in v1 + map[string]string{ + v1alpha1.LabelKeyClusterID: cluster.Status.ID, + }, + // TODO: remove it + k8s.LabelsK8sApp(cluster.Name, v1alpha1.LabelValComponentDMMaster), + ), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dm, v1alpha1.SchemeGroupVersion.WithKind("DM")), + }, + }, + Spec: corev1.PodSpec{ + Hostname: coreutil.PodName[scope.DM](dm), + Subdomain: dm.Spec.Subdomain, + NodeSelector: dm.Spec.Topology, + Containers: []corev1.Container{ + { + Name: v1alpha1.ContainerNameDMMaster, + Image: image.DM.Image(dm.Spec.Image, dm.Spec.Version), + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/dm-master", + "--config", + filepath.Join(v1alpha1.DirPathConfigDMMaster, v1alpha1.FileNameConfig), + }, + Ports: []corev1.ContainerPort{ + { + Name: v1alpha1.DMPortName, + ContainerPort: coreutil.DMPort(dm), + }, + { + Name: v1alpha1.DMPeerPortName, + ContainerPort: coreutil.DMPeerPort(dm), + }, + }, + VolumeMounts: mounts, + Resources: k8s.GetResourceRequirements(dm.Spec.Resources), + }, + }, + Volumes: vols, + }, + } + + if dm.Spec.Overlay != nil { + overlay.OverlayPod(pod, dm.Spec.Overlay.Pod) + } + + return pod +} + +// dmVolumeName returns the volume name for a DM volume +func dmVolumeName(volName string) string { + return metav1alpha1.VolNamePrefix + volName +} diff --git a/pkg/controllers/dm/tasks/pvc.go b/pkg/controllers/dm/tasks/pvc.go new file mode 100644 index 00000000000..45d88d857ff --- /dev/null +++ b/pkg/controllers/dm/tasks/pvc.go @@ -0,0 +1,82 @@ +// 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 ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + meta "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/features" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" +) + +func PVCNewer() common.PVCNewer[*v1alpha1.DM] { + return common.PVCNewerFunc[*v1alpha1.DM]( + func(cluster *v1alpha1.Cluster, dm *v1alpha1.DM, fg features.Gates) []*corev1.PersistentVolumeClaim { + // DataVolume PVC (the primary etcd data volume for dm-master) + dataVol := &dm.Spec.DataVolume + dataPVC := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.PersistentVolumeClaimName[scope.DM](dm, dataVol.Name), + Namespace: dm.Namespace, + Labels: coreutil.PersistentVolumeClaimLabels[scope.DM](dm, dataVol.Name), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dm, v1alpha1.SchemeGroupVersion.WithKind("DM")), + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: dataVol.Storage, + }, + }, + StorageClassName: dataVol.StorageClassName, + }, + } + if fg.Enabled(meta.VolumeAttributesClass) { + dataPVC.Spec.VolumeAttributesClassName = dataVol.VolumeAttributesClassName + } + // legacy labels in v1 + if cluster.Status.ID != "" { + dataPVC.Labels[v1alpha1.LabelKeyClusterID] = cluster.Status.ID + } + + pvcs := []*corev1.PersistentVolumeClaim{dataPVC} + + // Additional volumes + additionalPVCs := coreutil.PVCs[scope.DM]( + cluster, + dm, + coreutil.EnableVAC(fg.Enabled(meta.VolumeAttributesClass)), + coreutil.PVCPatchFunc(func(_ *v1alpha1.Volume, pvc *corev1.PersistentVolumeClaim) { + // legacy labels in v1 + if cluster.Status.ID != "" { + pvc.Labels[v1alpha1.LabelKeyClusterID] = cluster.Status.ID + } + }), + ) + pvcs = append(pvcs, additionalPVCs...) + + return pvcs + }, + ) +} diff --git a/pkg/controllers/dm/tasks/state.go b/pkg/controllers/dm/tasks/state.go new file mode 100644 index 00000000000..4057b352cfa --- /dev/null +++ b/pkg/controllers/dm/tasks/state.go @@ -0,0 +1,149 @@ +// 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 ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + stateutil "github.com/pingcap/tidb-operator/v2/pkg/state" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + dm *v1alpha1.DM + pod *corev1.Pod + dms []*v1alpha1.DM + + // Pod cannot be updated when call DELETE API, so we have to set this field to indicate + // the underlay pod has been deleting + isPodTerminating bool + + statusChanged bool + + healthy bool + + stateutil.IFeatureGates +} + +type State interface { + common.DMState + common.ClusterState + common.DMSliceState + + common.PodState + common.PodStateUpdater + + common.InstanceState[*runtime.DM] + + common.ContextClusterNewer[*v1alpha1.DM] + common.ContextObjectNewer[*v1alpha1.DM] + common.ContextSliceNewer[*v1alpha1.DM, *v1alpha1.DM] + + common.StatusUpdater + common.StatusPersister[*v1alpha1.DM] + + common.HealthyState + common.HealthyStateUpdater + + stateutil.IFeatureGates +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + s.IFeatureGates = stateutil.NewFeatureGates[scope.DM](s) + + return s +} + +func (s *state) Key() types.NamespacedName { + return s.key +} + +func (s *state) Object() *v1alpha1.DM { + return s.dm +} + +func (s *state) SetObject(dm *v1alpha1.DM) { + s.dm = dm +} + +func (s *state) DM() *v1alpha1.DM { + return s.dm +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) Pod() *corev1.Pod { + return s.pod +} + +func (s *state) IsPodTerminating() bool { + return s.isPodTerminating +} + +func (s *state) Instance() *runtime.DM { + return runtime.FromDM(s.dm) +} + +func (s *state) SetPod(pod *corev1.Pod) { + s.pod = pod + if pod != nil && !pod.GetDeletionTimestamp().IsZero() { + s.isPodTerminating = true + } +} + +func (s *state) DeletePod(pod *corev1.Pod) { + s.isPodTerminating = true + s.pod = pod +} + +func (s *state) SetCluster(cluster *v1alpha1.Cluster) { + s.cluster = cluster +} + +func (s *state) IsStatusChanged() bool { + return s.statusChanged +} + +func (s *state) SetStatusChanged() { + s.statusChanged = true +} + +func (s *state) DMSlice() []*v1alpha1.DM { + return s.dms +} + +func (s *state) SetInstanceSlice(dms []*v1alpha1.DM) { + s.dms = dms +} + +func (s *state) IsHealthy() bool { + return s.healthy +} + +func (s *state) SetHealthy() { + s.healthy = true +} diff --git a/pkg/controllers/dm/tasks/status.go b/pkg/controllers/dm/tasks/status.go new file mode 100644 index 00000000000..1f65ab6e644 --- /dev/null +++ b/pkg/controllers/dm/tasks/status.go @@ -0,0 +1,85 @@ +// 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" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/compare" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +const ( + defaultTaskWaitDuration = 5 * time.Second +) + +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { + needUpdate := state.IsStatusChanged() + dm := state.DM() + pod := state.Pod() + ready := coreutil.IsReady[scope.DM](dm) + + needUpdate = syncSuspendCond(dm) || needUpdate + + needUpdate = compare.SetIfNotEmptyAndChanged(&dm.Status.MemberID, state.MemberID) || needUpdate + needUpdate = compare.SetIfChanged(&dm.Status.ObservedGeneration, dm.Generation) || needUpdate + needUpdate = compare.SetIfNotEmptyAndChanged( + &dm.Status.UpdateRevision, + dm.Labels[v1alpha1.LabelKeyInstanceRevisionHash], + ) || needUpdate + + if ready && pod != nil { + needUpdate = compare.SetIfNotEmptyAndChanged( + &dm.Status.CurrentRevision, + pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash], + ) || needUpdate + } + + if needUpdate { + if err := c.Status().Update(ctx, dm); err != nil { + return task.Fail().With("cannot update status: %v", err) + } + } + + if !ready { + if state.IsPodTerminating() { + return task.Retry(defaultTaskWaitDuration).With("pod may be terminating, requeue to retry") + } + return task.Retry(defaultTaskWaitDuration).With("dm-master is not ready, requeue to retry") + } + + return task.Complete().With("status is synced") + }) +} + +func syncSuspendCond(dm *v1alpha1.DM) bool { + // always set it as unsuspended + return meta.SetStatusCondition(&dm.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: dm.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instance is not suspended", + }) +} diff --git a/pkg/controllers/dmgroup/builder.go b/pkg/controllers/dmgroup/builder.go new file mode 100644 index 00000000000..4b75c1bc024 --- /dev/null +++ b/pkg/controllers/dmgroup/builder.go @@ -0,0 +1,71 @@ +// 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 dmgroup + +import ( + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmgroup/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { + runner := task.NewTaskRunner(reporter, + // get dmgroup + common.TaskContextObject[scope.DMGroup](state, r.Client), + // if it's gone just return + task.IfBreak(common.CondObjectHasBeenDeleted[scope.DMGroup](state)), + + // get cluster + common.TaskContextCluster[scope.DMGroup](state, r.Client), + // if it's paused just return + task.IfBreak(common.CondClusterIsPaused(state)), + task.IfBreak(common.CondFeatureGatesIsNotSynced[scope.DMGroup](state)), + + // get all dm masters + common.TaskContextSlice[scope.DMGroup](state, r.Client), + + task.IfBreak(common.CondObjectIsDeleting[scope.DMGroup](state), + tasks.TaskFinalizerDel(state, r.Client), + common.TaskGroupConditionReady[scope.DMGroup](state), + common.TaskGroupConditionSynced[scope.DMGroup](state), + common.TaskStatusRevisionAndReplicas[scope.DMGroup](state), + common.TaskStatusPersister[scope.DMGroup](state, r.Client), + ), + common.TaskFinalizerAdd[scope.DMGroup](state, r.Client), + + common.TaskRevision[runtime.DMGroupTuple](state, r.Client), + + task.IfBreak( + common.CondClusterIsSuspending(state), + common.TaskGroupConditionSuspended[scope.DMGroup](state), + common.TaskGroupConditionReady[scope.DMGroup](state), + common.TaskGroupConditionSynced[scope.DMGroup](state), + common.TaskStatusPersister[scope.DMGroup](state, r.Client), + ), + tasks.TaskBoot(state, r.Client), + tasks.TaskService(state, r.Client), + tasks.TaskUpdater(state, r.Client, r.AllocateFactory), + common.TaskGroupStatusSelector[scope.DMGroup](state), + common.TaskGroupConditionSuspended[scope.DMGroup](state), + common.TaskGroupConditionReady[scope.DMGroup](state), + common.TaskGroupConditionSynced[scope.DMGroup](state), + common.TaskStatusRevisionAndReplicas[scope.DMGroup](state), + common.TaskStatusPersister[scope.DMGroup](state, r.Client), + ) + + return runner +} diff --git a/pkg/controllers/dmgroup/controller.go b/pkg/controllers/dmgroup/controller.go new file mode 100644 index 00000000000..b179970f4cb --- /dev/null +++ b/pkg/controllers/dmgroup/controller.go @@ -0,0 +1,113 @@ +// 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 dmgroup + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmgroup/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" +) + +type Reconciler struct { + Logger logr.Logger + Client client.Client + AllocateFactory tracker.AllocateFactory +} + +func Setup(mgr manager.Manager, c client.Client, af tracker.AllocateFactory) error { + r := &Reconciler{ + Logger: mgr.GetLogger().WithName("DMGroup"), + Client: c, + AllocateFactory: af, + } + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.DMGroup{}). + Owns(&v1alpha1.DM{}). + Watches(&v1alpha1.Cluster{}, r.ClusterEventHandler(), builder.WithPredicates(predicate.GenerationChangedPredicate{})). + WithOptions(controller.Options{RateLimiter: k8s.RateLimiter}). + Complete(r) +} + +func (r *Reconciler) ClusterEventHandler() handler.TypedEventHandler[client.Object, reconcile.Request] { + return handler.TypedFuncs[client.Object, reconcile.Request]{ + UpdateFunc: func(ctx context.Context, event event.TypedUpdateEvent[client.Object], + queue workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + cluster := event.ObjectNew.(*v1alpha1.Cluster) + + var list v1alpha1.DMGroupList + if err := r.Client.List(ctx, &list, client.InNamespace(cluster.Namespace), + client.MatchingFields{"spec.cluster.name": cluster.Name}); err != nil { + if !errors.IsNotFound(err) { + r.Logger.Error(err, "cannot list all dm groups", "ns", cluster.Namespace, "cluster", cluster.Name) + } + return + } + + for i := range list.Items { + dmg := &list.Items[i] + queue.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: dmg.Name, + Namespace: dmg.Namespace, + }, + }) + } + }, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logr.FromContextOrDiscard(ctx) + reporter := task.NewTableTaskReporter(uuid.NewString()) + + startTime := time.Now() + logger.Info("start reconcile") + defer func() { + dur := time.Since(startTime) + logger.Info("end reconcile", "duration", dur) + summary := fmt.Sprintf("summary for %v\n%s", req.NamespacedName, reporter.Summary()) + logger.Info(summary) + }() + + rtx := &tasks.ReconcileContext{ + State: tasks.NewState(req.NamespacedName), + } + + runner := r.NewRunner(rtx, reporter) + + return runner.Run(ctx) +} diff --git a/pkg/controllers/dmgroup/tasks/boot.go b/pkg/controllers/dmgroup/tasks/boot.go new file mode 100644 index 00000000000..8b01273f09e --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/boot.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" + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func TaskBoot(state State, c client.Client) task.Task { + return task.NameTaskFunc("Boot", func(ctx context.Context) task.Result { + var unready []string + dms := state.InstanceSlice() + for _, dm := range dms { + if _, ok := dm.Annotations[v1alpha1.AnnoKeyInitialClusterNum]; !ok { + continue + } + if !coreutil.IsReady[scope.DM](dm) { + unready = append(unready, dm.Name) + } + } + if len(unready) > 0 { + return task.Wait().With("wait until dm masters %v are ready", unready) + } + + for _, dm := range dms { + if _, ok := dm.Annotations[v1alpha1.AnnoKeyInitialClusterNum]; !ok { + continue + } + if err := delBootAnnotation(ctx, c, dm); err != nil { + return task.Fail().With("cannot del boot annotation after dm %s is available: %v", dm.Name, err) + } + } + + return task.Complete().With("dm-master cluster is bootstrapped") + }) +} + +type Patch struct { + Metadata Metadata `json:"metadata"` +} + +type Metadata struct { + ResourceVersion string `json:"resourceVersion"` + Annotations map[string]*string `json:"annotations"` +} + +func delBootAnnotation(ctx context.Context, c client.Client, dm *v1alpha1.DM) error { + p := Patch{ + Metadata: Metadata{ + ResourceVersion: dm.GetResourceVersion(), + Annotations: map[string]*string{ + v1alpha1.AnnoKeyInitialClusterNum: nil, + }, + }, + } + data, err := json.Marshal(&p) + if err != nil { + return fmt.Errorf("invalid patch: %w", err) + } + + if err := c.Patch(ctx, dm, client.RawPatch(types.MergePatchType, data)); err != nil { + return fmt.Errorf("cannot del boot annotation for %s/%s: %w", dm.GetNamespace(), dm.GetName(), err) + } + + return nil +} diff --git a/pkg/controllers/dmgroup/tasks/ctx.go b/pkg/controllers/dmgroup/tasks/ctx.go new file mode 100644 index 00000000000..eb9c98a8d91 --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/ctx.go @@ -0,0 +1,19 @@ +// 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 + +type ReconcileContext struct { + State +} diff --git a/pkg/controllers/dmgroup/tasks/finalizer.go b/pkg/controllers/dmgroup/tasks/finalizer.go new file mode 100644 index 00000000000..aeb38760a31 --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/finalizer.go @@ -0,0 +1,71 @@ +// 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" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + utilerr "k8s.io/apimachinery/pkg/util/errors" + + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func TaskFinalizerDel(state State, c client.Client) task.Task { + return task.NameTaskFunc("FinalizerDel", func(ctx context.Context) task.Result { + var errList []error + for _, dm := range state.DMSlice() { + if dm.GetDeletionTimestamp().IsZero() { + if err := c.Delete(ctx, dm); err != nil { + if errors.IsNotFound(err) { + continue + } + errList = append(errList, err) + continue + } + } + + if err := k8s.RemoveFinalizer(ctx, c, dm); err != nil { + errList = append(errList, err) + } + } + + if len(errList) != 0 { + return task.Fail().With("failed to delete all dm-master instances: %v", utilerr.NewAggregate(errList)) + } + + if len(state.DMSlice()) != 0 { + return task.Wait().With("wait for all dm-master instances being removed, %v still exists", len(state.DMSlice())) + } + + wait, err := k8s.DeleteGroupSubresource(ctx, c, runtime.FromDMGroup(state.DMGroup()), &corev1.ServiceList{}) + if err != nil { + return task.Fail().With("cannot delete subresources: %w", err) + } + if wait { + return task.Retry(task.DefaultRequeueAfter).With("wait all subresources deleted") + } + + if err := k8s.RemoveFinalizer(ctx, c, state.DMGroup()); err != nil { + return task.Fail().With("failed to ensure finalizer has been removed: %w", err) + } + + return task.Complete().With("finalizer has been removed") + }) +} diff --git a/pkg/controllers/dmgroup/tasks/state.go b/pkg/controllers/dmgroup/tasks/state.go new file mode 100644 index 00000000000..d90844245c4 --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/state.go @@ -0,0 +1,167 @@ +// 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 ( + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + stateutil "github.com/pingcap/tidb-operator/v2/pkg/state" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + dmg *v1alpha1.DMGroup + dms []*v1alpha1.DM + + updateRevision string + currentRevision string + collisionCount int32 + + statusChanged bool + + stateutil.IFeatureGates +} + +type State interface { + common.DMSliceStateInitializer + common.RevisionStateInitializer[*runtime.DMGroup] + + common.DMGroupState + common.ClusterState + common.DMSliceState + common.RevisionState + + common.GroupState[*runtime.DMGroup] + + common.ContextClusterNewer[*v1alpha1.DMGroup] + common.ContextObjectNewer[*v1alpha1.DMGroup] + common.ContextSliceNewer[*v1alpha1.DMGroup, *v1alpha1.DM] + + common.InstanceSliceState[*runtime.DM] + common.SliceState[*v1alpha1.DM] + + common.StatusUpdater + common.StatusPersister[*v1alpha1.DMGroup] + + stateutil.IFeatureGates +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + s.IFeatureGates = stateutil.NewFeatureGates[scope.DMGroup](s) + return s +} + +func (s *state) Key() types.NamespacedName { + return s.key +} + +func (s *state) Object() *v1alpha1.DMGroup { + return s.dmg +} + +func (s *state) SetObject(dmg *v1alpha1.DMGroup) { + s.dmg = dmg +} + +func (s *state) DMGroup() *v1alpha1.DMGroup { + return s.dmg +} + +func (s *state) Group() *runtime.DMGroup { + return runtime.FromDMGroup(s.dmg) +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) DMSlice() []*v1alpha1.DM { + return s.dms +} + +func (s *state) Slice() []*runtime.DM { + return runtime.FromDMSlice(s.dms) +} + +func (s *state) InstanceSlice() []*v1alpha1.DM { + return s.dms +} + +func (s *state) SetInstanceSlice(dms []*v1alpha1.DM) { + s.dms = dms +} + +func (s *state) SetCluster(cluster *v1alpha1.Cluster) { + s.cluster = cluster +} + +func (s *state) IsStatusChanged() bool { + return s.statusChanged +} + +func (s *state) SetStatusChanged() { + s.statusChanged = true +} + +func (s *state) DMSliceInitializer() common.DMSliceInitializer { + return common.NewResourceSlice(func(dms []*v1alpha1.DM) { s.dms = dms }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithLabels(s.Labels()). + Initializer() +} + +func (s *state) RevisionInitializer() common.RevisionInitializer[*runtime.DMGroup] { + return common.NewRevision[*runtime.DMGroup]( + common.RevisionSetterFunc(func(update, current string, collisionCount int32) { + s.updateRevision = update + s.currentRevision = current + s.collisionCount = collisionCount + })). + WithCurrentRevision(common.Lazy[string](func() string { + return s.dmg.Status.CurrentRevision + })). + WithCollisionCount(common.Lazy[*int32](func() *int32 { + return s.dmg.Status.CollisionCount + })). + WithParent(common.Lazy[*runtime.DMGroup](func() *runtime.DMGroup { + return runtime.FromDMGroup(s.dmg) + })). + WithLabels(s.Labels()). + Initializer() +} + +func (s *state) Revision() (update, current string, collisionCount int32) { + return s.updateRevision, s.currentRevision, s.collisionCount +} + +func (s *state) Labels() common.LabelsOption { + return common.Lazy[map[string]string](func() map[string]string { + return map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + v1alpha1.LabelKeyCluster: s.cluster.Name, + v1alpha1.LabelKeyGroup: s.dmg.Name, + } + }) +} diff --git a/pkg/controllers/dmgroup/tasks/svc.go b/pkg/controllers/dmgroup/tasks/svc.go new file mode 100644 index 00000000000..20a4810c388 --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/svc.go @@ -0,0 +1,136 @@ +// 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" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func TaskService(state State, c client.Client) task.Task { + return task.NameTaskFunc("Service", func(ctx context.Context) task.Result { + dmg := state.DMGroup() + + headless := newHeadlessService(dmg) + if err := c.Apply(ctx, headless); err != nil { + return task.Fail().With(fmt.Sprintf("can't create headless service of dm-master: %v", err)) + } + + svc := newInternalService(dmg) + if err := c.Apply(ctx, svc); err != nil { + return task.Fail().With(fmt.Sprintf("can't create internal service of dm-master: %v", err)) + } + + return task.Complete().With("services of dm-master have been applied") + }) +} + +func newHeadlessService(dmg *v1alpha1.DMGroup) *corev1.Service { + ipFamilyPolicy := corev1.IPFamilyPolicyPreferDualStack + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.HeadlessServiceName[scope.DMGroup](dmg), + Namespace: dmg.Namespace, + Labels: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + v1alpha1.LabelKeyCluster: dmg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dmg.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dmg, v1alpha1.SchemeGroupVersion.WithKind("DMGroup")), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + IPFamilyPolicy: &ipFamilyPolicy, + Selector: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + v1alpha1.LabelKeyCluster: dmg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dmg.Name, + }, + Ports: []corev1.ServicePort{ + { + Name: v1alpha1.DMPortName, + Port: coreutil.DMGroupPort(dmg), + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString(v1alpha1.DMPortName), + }, + { + Name: v1alpha1.DMPeerPortName, + Port: coreutil.DMGroupPeerPort(dmg), + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString(v1alpha1.DMPeerPortName), + }, + }, + PublishNotReadyAddresses: true, + }, + } +} + +func newInternalService(dmg *v1alpha1.DMGroup) *corev1.Service { + ipFamilyPolicy := corev1.IPFamilyPolicyPreferDualStack + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: InternalServiceName(dmg.Name), + Namespace: dmg.Namespace, + Labels: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + v1alpha1.LabelKeyCluster: dmg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dmg.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dmg, v1alpha1.SchemeGroupVersion.WithKind("DMGroup")), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + IPFamilyPolicy: &ipFamilyPolicy, + Selector: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMMaster, + v1alpha1.LabelKeyCluster: dmg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dmg.Name, + }, + Ports: []corev1.ServicePort{ + { + Name: v1alpha1.DMPortName, + Port: coreutil.DMGroupPort(dmg), + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString(v1alpha1.DMPortName), + }, + { + Name: v1alpha1.DMPeerPortName, + Port: coreutil.DMGroupPeerPort(dmg), + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString(v1alpha1.DMPeerPortName), + }, + }, + }, + } +} diff --git a/pkg/controllers/dmgroup/tasks/updater.go b/pkg/controllers/dmgroup/tasks/updater.go new file mode 100644 index 00000000000..62a74a45e7b --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/updater.go @@ -0,0 +1,164 @@ +// 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" + "strconv" + "time" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/action" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/updater" + "github.com/pingcap/tidb-operator/v2/pkg/updater/policy" + maputil "github.com/pingcap/tidb-operator/v2/pkg/utils/map" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" +) + +const ( + defaultUpdateWaitTime = time.Second * 30 +) + +func TaskUpdater(state *ReconcileContext, c client.Client, af tracker.AllocateFactory) task.Task { + return task.NameTaskFunc("Updater", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + dmg := state.DMGroup() + + checker := action.NewUpgradeChecker[scope.DMGroup](c, state.Cluster(), logger) + + if needVersionUpgrade(dmg) && !checker.CanUpgrade(ctx, dmg) { + return task.Retry(defaultUpdateWaitTime).With("wait until preconditions of upgrading is met") + } + + retryAfter := coreutil.RetryIfInstancesReadyButNotAvailable[scope.DM](state.InstanceSlice(), coreutil.MinReadySeconds[scope.DMGroup](dmg)) + if retryAfter != 0 { + return task.Retry(retryAfter).With("wait until no instances is ready but not available") + } + + var topos []v1alpha1.ScheduleTopology + for _, p := range dmg.Spec.SchedulePolicies { + switch p.Type { + case v1alpha1.SchedulePolicyTypeEvenlySpread: + topos = p.EvenlySpread.Topologies + default: + } + } + + updateRevision, _, _ := state.Revision() + + dms := state.Slice() + topoPolicy, err := policy.NewTopologyPolicy(topos, updateRevision, dms...) + if err != nil { + return task.Fail().With("invalid topo policy, it should be validated: %w", err) + } + + var instances []string + for _, in := range dms { + instances = append(instances, in.Name) + } + + allocator := af.New(dmg.Namespace, dmg.Name, instances...) + + wait, err := updater.New[runtime.DMTuple](). + WithInstances(dms...). + WithDesired(int(state.Group().Replicas())). + WithClient(c). + WithMaxSurge(0). + WithMaxUnavailable(1). + WithRevision(updateRevision). + WithNewFactory(DMNewer(dmg, updateRevision, state.InstanceSlice())). + WithAddHooks( + updater.AllocateName[*runtime.DM](allocator), + topoPolicy, + ). + WithDelHooks(topoPolicy). + WithUpdateHooks(topoPolicy). + WithScaleInPreferPolicy(topoPolicy.PolicyScaleIn()). + WithMinReadySeconds(coreutil.MinReadySeconds[scope.DMGroup](dmg)). + Build(). + Do(ctx) + if err != nil { + return task.Fail().With("cannot update instances: %w", err) + } + if wait { + return task.Wait().With("wait for all instances ready") + } + return task.Complete().With("all instances are synced") + }) +} + +func needVersionUpgrade(dmg *v1alpha1.DMGroup) bool { + return dmg.Spec.Template.Spec.Version != dmg.Status.Version && dmg.Status.Version != "" +} + +func DMNewer(dmg *v1alpha1.DMGroup, rev string, currentDMs []*v1alpha1.DM) updater.NewFactory[*runtime.DM] { + // Derive bootstrap state: if any existing instance lacks the boot annotation, + // the cluster has already bootstrapped and new instances should use join. + alreadyBootstrapped := false + for _, dm := range currentDMs { + if _, ok := dm.Annotations[v1alpha1.AnnoKeyInitialClusterNum]; !ok { + alreadyBootstrapped = true + break + } + } + + return updater.NewFunc[*runtime.DM](func() *runtime.DM { + spec := dmg.Spec.Template.Spec.DeepCopy() + + var bootAnno map[string]string + if !alreadyBootstrapped { + replicas := int64(1) + if dmg.Spec.Replicas != nil { + replicas = int64(*dmg.Spec.Replicas) + } + initialNum := strconv.FormatInt(replicas, 10) + bootAnno = map[string]string{ + v1alpha1.AnnoKeyInitialClusterNum: initialNum, + } + } + + dm := &v1alpha1.DM{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dmg.Namespace, + // Name will be allocated by updater.AllocateName + Labels: coreutil.InstanceLabels[scope.DMGroup](dmg, rev), + Annotations: coreutil.InstanceAnnotations[scope.DMGroup](dmg), + Finalizers: []string{metav1alpha1.Finalizer}, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dmg, v1alpha1.SchemeGroupVersion.WithKind("DMGroup")), + }, + }, + Spec: v1alpha1.DMSpec{ + Cluster: dmg.Spec.Cluster, + Features: dmg.Spec.Features, + Subdomain: coreutil.HeadlessServiceName[scope.DMGroup](dmg), + DMTemplateSpec: *spec, + }, + } + + dm.Annotations = maputil.MergeTo(dm.Annotations, bootAnno) + + return runtime.FromDM(dm) + }) +} diff --git a/pkg/controllers/dmgroup/tasks/util.go b/pkg/controllers/dmgroup/tasks/util.go new file mode 100644 index 00000000000..606c500cbba --- /dev/null +++ b/pkg/controllers/dmgroup/tasks/util.go @@ -0,0 +1,21 @@ +// 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 "fmt" + +func InternalServiceName(groupName string) string { + return fmt.Sprintf("%s-dm-master", groupName) +} diff --git a/pkg/controllers/dmworker/builder.go b/pkg/controllers/dmworker/builder.go new file mode 100644 index 00000000000..bd16b8a4703 --- /dev/null +++ b/pkg/controllers/dmworker/builder.go @@ -0,0 +1,76 @@ +// 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 dmworker + +import ( + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworker/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { + runner := task.NewTaskRunner(reporter, + // get DMWorker instance + common.TaskContextObject[scope.DMWorker](state, r.Client), + common.TaskTrack[scope.DMWorker](state, r.Tracker), + common.TaskObserveInstance[scope.DMWorker](state), + // if it's deleted just return + task.IfBreak(common.CondObjectHasBeenDeleted[scope.DMWorker](state)), + + // get cluster info + common.TaskContextCluster[scope.DMWorker](state, r.Client), + // if it's paused just return + task.IfBreak(common.CondClusterIsPaused(state)), + // if the cluster is deleting, del all subresources and remove the finalizer directly + task.IfBreak(common.CondClusterIsDeleting(state), + common.TaskInstanceFinalizerDel[scope.DMWorker](state, r.Client, common.DefaultInstanceSubresourceLister), + ), + + task.IfBreak(common.CondObjectIsDeleting[scope.DMWorker](state), + common.TaskInstanceFinalizerDel[scope.DMWorker](state, r.Client, common.DefaultInstanceSubresourceLister), + common.TaskInstanceConditionSynced[scope.DMWorker](state), + common.TaskInstanceConditionReady[scope.DMWorker](state), + common.TaskInstanceConditionRunning[scope.DMWorker](state), + common.TaskStatusPersister[scope.DMWorker](state, r.Client), + ), + common.TaskFinalizerAdd[scope.DMWorker](state, r.Client), + + // get pod and check whether the cluster is suspending + common.TaskContextPod[scope.DMWorker](state, r.Client), + task.IfBreak(common.CondClusterIsSuspending(state), + common.TaskSuspendPod(state, r.Client), + common.TaskInstanceConditionSuspended[scope.DMWorker](state), + common.TaskInstanceConditionSynced[scope.DMWorker](state), + common.TaskInstanceConditionReady[scope.DMWorker](state), + common.TaskInstanceConditionRunning[scope.DMWorker](state), + common.TaskStatusPersister[scope.DMWorker](state, r.Client), + ), + + // health check + tasks.TaskContextHealthFromDMWorker(state, r.Client), + + // normal process + tasks.TaskConfigMap(state, r.Client), + common.TaskPVC[scope.DMWorker](state, r.Client, r.VolumeModifierFactory, tasks.PVCNewer()), + tasks.TaskPod(state, r.Client), + common.TaskInstanceConditionSynced[scope.DMWorker](state), + common.TaskInstanceConditionReady[scope.DMWorker](state), + common.TaskInstanceConditionRunning[scope.DMWorker](state), + tasks.TaskStatus(state, r.Client), + ) + + return runner +} diff --git a/pkg/controllers/dmworker/controller.go b/pkg/controllers/dmworker/controller.go new file mode 100644 index 00000000000..1b13b083262 --- /dev/null +++ b/pkg/controllers/dmworker/controller.go @@ -0,0 +1,84 @@ +// 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 dmworker + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworker/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" + "github.com/pingcap/tidb-operator/v2/pkg/volumes" +) + +type Reconciler struct { + Logger logr.Logger + Client client.Client + VolumeModifierFactory volumes.ModifierFactory + Tracker tracker.Tracker +} + +func Setup(mgr manager.Manager, c client.Client, vm volumes.ModifierFactory, t tracker.Tracker) error { + r := &Reconciler{ + Logger: mgr.GetLogger().WithName("DMWorker"), + Client: c, + VolumeModifierFactory: vm, + Tracker: t, + } + return ctrl.NewControllerManagedBy(mgr).For(&v1alpha1.DMWorker{}). + Owns(&corev1.Pod{}). + Owns(&corev1.ConfigMap{}). + Owns(&corev1.PersistentVolumeClaim{}). + Watches(&v1alpha1.Cluster{}, r.ClusterEventHandler(), + builder.WithPredicates(predicate.GenerationChangedPredicate{})). + WithOptions(controller.Options{RateLimiter: k8s.RateLimiter}). + Complete(r) +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logr.FromContextOrDiscard(ctx) + reporter := task.NewTableTaskReporter(uuid.NewString()) + + startTime := time.Now() + logger.Info("start reconcile") + defer func() { + dur := time.Since(startTime) + logger.Info("end reconcile", "duration", dur) + summary := fmt.Sprintf("summary for %v\n%s", req.NamespacedName, reporter.Summary()) + logger.Info(summary) + }() + + rtx := &tasks.ReconcileContext{ + State: tasks.NewState(req.NamespacedName), + } + + runner := r.NewRunner(rtx, reporter) + + return runner.Run(ctx) +} diff --git a/pkg/controllers/dmworker/handler.go b/pkg/controllers/dmworker/handler.go new file mode 100644 index 00000000000..6aaaac3cabd --- /dev/null +++ b/pkg/controllers/dmworker/handler.go @@ -0,0 +1,66 @@ +// 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 dmworker + +import ( + "context" + "reflect" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" +) + +func (r *Reconciler) ClusterEventHandler() handler.TypedEventHandler[client.Object, reconcile.Request] { + return handler.TypedFuncs[client.Object, reconcile.Request]{ + UpdateFunc: func(ctx context.Context, event event.TypedUpdateEvent[client.Object], + queue workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + oldObj := event.ObjectOld.(*v1alpha1.Cluster) + newObj := event.ObjectNew.(*v1alpha1.Cluster) + + if newObj.Status.ID == oldObj.Status.ID && + reflect.DeepEqual(oldObj.Spec.SuspendAction, newObj.Spec.SuspendAction) && + oldObj.Spec.Paused == newObj.Spec.Paused { + return + } + + var dwl v1alpha1.DMWorkerList + if err := r.Client.List(ctx, &dwl, client.MatchingLabels{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyCluster: newObj.Name, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMWorker, + }, client.InNamespace(newObj.Namespace)); err != nil { + r.Logger.Error(err, "cannot list all dm-worker instances", "ns", newObj.Namespace, "cluster", newObj.Name) + return + } + + for i := range dwl.Items { + dw := &dwl.Items[i] + queue.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: dw.Name, + Namespace: dw.Namespace, + }, + }) + } + }, + } +} diff --git a/pkg/controllers/dmworker/tasks/cm.go b/pkg/controllers/dmworker/tasks/cm.go new file mode 100644 index 00000000000..06cac64915c --- /dev/null +++ b/pkg/controllers/dmworker/tasks/cm.go @@ -0,0 +1,78 @@ +// 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" + "strconv" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + dmworkercfg "github.com/pingcap/tidb-operator/v2/pkg/configs/dmworker" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/toml" +) + +func TaskConfigMap(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ConfigMap", func(ctx context.Context) task.Result { + cfg := dmworkercfg.Config{} + decoder, encoder := toml.Codec[dmworkercfg.Config]() + if err := decoder.Decode([]byte(state.DMWorker().Spec.Config), &cfg); err != nil { + return task.Fail().With("dm-worker config cannot be decoded: %v", err) + } + + dw := state.DMWorker() + dmMasterSvcName := fmt.Sprintf("%s-dm-master", dw.Spec.DMGroupRef.Name) + dmMasterAddr := coreutil.ServiceHost(state.Cluster(), dmMasterSvcName) + + ":" + strconv.Itoa(int(v1alpha1.DefaultDMPort)) + + if err := cfg.Overlay(state.Cluster(), dw, dmMasterAddr); err != nil { + return task.Fail().With("cannot generate dm-worker config: %v", err) + } + + data, err := encoder.Encode(&cfg) + if err != nil { + return task.Fail().With("dm-worker config cannot be encoded: %v", err) + } + + expected := newConfigMap(dw, data) + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't create/update the cm of dm-worker: %v", err) + } + return task.Complete().With("cm is synced") + }) +} + +func newConfigMap(dw *v1alpha1.DMWorker, data []byte) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.PodName[scope.DMWorker](dw), + Namespace: dw.Namespace, + Labels: coreutil.ConfigMapLabels[scope.DMWorker](dw), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dw, v1alpha1.SchemeGroupVersion.WithKind("DMWorker")), + }, + }, + Data: map[string]string{ + v1alpha1.FileNameConfig: string(data), + }, + } +} diff --git a/pkg/controllers/dmworker/tasks/ctx.go b/pkg/controllers/dmworker/tasks/ctx.go new file mode 100644 index 00000000000..126550cb738 --- /dev/null +++ b/pkg/controllers/dmworker/tasks/ctx.go @@ -0,0 +1,76 @@ +// 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" + "net/http" + + "github.com/pingcap/tidb-operator/v2/pkg/apicall" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +type ReconcileContext struct { + State +} + +func TaskContextHealthFromDMWorker(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("ContextHealthFromDMWorker", func(ctx context.Context) task.Result { + cluster := state.Cluster() + dw := state.DMWorker() + + addr := coreutil.InstanceAdvertiseAddress[scope.DMWorker](cluster, dw, coreutil.DMWorkerPort(dw)) + scheme := "http" + + var httpClient *http.Client + if coreutil.IsTLSClusterEnabled(cluster) { + tlsConfig, err := apicall.GetClientTLSConfig(ctx, c, cluster) + if err != nil { + return task.Fail().With("cannot get tls config from secret: %w", err) + } + httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + scheme = "https" + } else { + httpClient = http.DefaultClient + } + + url := fmt.Sprintf("%s://%s/status", scheme, addr) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return task.Complete().With("cannot build request for dm-worker status: %v", err) + } + + resp, err := httpClient.Do(req) + if err != nil { + return task.Complete().With("context without health info is completed, dm-worker can't be reached: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return task.Complete().With("dm-worker returned non-200 status: %d", resp.StatusCode) + } + + state.SetHealthy() + return task.Complete().With("dm-worker is healthy") + }) +} diff --git a/pkg/controllers/dmworker/tasks/pod.go b/pkg/controllers/dmworker/tasks/pod.go new file mode 100644 index 00000000000..fd61415f626 --- /dev/null +++ b/pkg/controllers/dmworker/tasks/pod.go @@ -0,0 +1,191 @@ +// 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" + "path/filepath" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/image" + "github.com/pingcap/tidb-operator/v2/pkg/overlay" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + maputil "github.com/pingcap/tidb-operator/v2/pkg/utils/map" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +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.DMWorker()) + pod := state.Pod() + if pod == nil { + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't create pod of dm-worker: %v", err) + } + state.SetPod(expected) + return task.Complete().With("pod is created") + } + + logger.Info("will update the pod in place") + if err := c.Apply(ctx, expected); err != nil { + return task.Fail().With("can't apply pod of dm-worker: %v", err) + } + + state.SetPod(expected) + return task.Complete().With("pod is synced") + }) +} + +func newPod(cluster *v1alpha1.Cluster, dw *v1alpha1.DMWorker) *corev1.Pod { + vols := []corev1.Volume{ + { + Name: v1alpha1.VolumeNameConfig, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: coreutil.PodName[scope.DMWorker](dw), + }, + }, + }, + }, + } + + mounts := []corev1.VolumeMount{ + { + Name: v1alpha1.VolumeNameConfig, + MountPath: v1alpha1.DirPathConfigDMWorker, + }, + } + + // RelayVolume mount + relayVolName := dmWorkerVolumeName(dw.Spec.RelayVolume.Name) + relayClaimName := coreutil.PersistentVolumeClaimName[scope.DMWorker](dw, dw.Spec.RelayVolume.Name) + vols = append(vols, corev1.Volume{ + Name: relayVolName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: relayClaimName, + }, + }, + }) + + relayMountPath := v1alpha1.VolumeMountDMWorkerRelayDefaultPath + for i := range dw.Spec.RelayVolume.Mounts { + mount := &dw.Spec.RelayVolume.Mounts[i] + if mount.Type == v1alpha1.VolumeMountTypeDMWorkerRelay && mount.MountPath != "" { + relayMountPath = mount.MountPath + break + } + } + mounts = append(mounts, corev1.VolumeMount{ + Name: relayVolName, + MountPath: relayMountPath, + }) + + // Additional volumes from dw.Spec.Volumes + for i := range dw.Spec.Volumes { + vol := &dw.Spec.Volumes[i] + name := dmWorkerVolumeName(vol.Name) + vols = append(vols, corev1.Volume{ + Name: name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: coreutil.PersistentVolumeClaimName[scope.DMWorker](dw, vol.Name), + }, + }, + }) + for j := range vol.Mounts { + mount := &vol.Mounts[j] + mounts = append(mounts, corev1.VolumeMount{ + Name: name, + MountPath: mount.MountPath, + SubPath: mount.SubPath, + }) + } + } + + // TLS volume + if coreutil.IsTLSClusterEnabled(cluster) { + vols = append(vols, *coreutil.ClusterTLSVolume[scope.DMWorker](dw)) + mounts = append(mounts, corev1.VolumeMount{ + Name: v1alpha1.VolumeNameClusterTLS, + MountPath: v1alpha1.DirPathClusterTLSDMWorker, + ReadOnly: true, + }) + } + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dw.Namespace, + Name: coreutil.PodName[scope.DMWorker](dw), + Labels: maputil.Merge( + coreutil.PodLabels[scope.DMWorker](dw), + map[string]string{ + v1alpha1.LabelKeyClusterID: cluster.Status.ID, + }, + k8s.LabelsK8sApp(cluster.Name, v1alpha1.LabelValComponentDMWorker), + ), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dw, v1alpha1.SchemeGroupVersion.WithKind("DMWorker")), + }, + }, + Spec: corev1.PodSpec{ + Hostname: coreutil.PodName[scope.DMWorker](dw), + Subdomain: dw.Spec.Subdomain, + NodeSelector: dw.Spec.Topology, + Containers: []corev1.Container{ + { + Name: v1alpha1.ContainerNameDMWorker, + Image: image.DM.Image(dw.Spec.Image, dw.Spec.Version), + ImagePullPolicy: corev1.PullIfNotPresent, + Command: []string{ + "/dm-worker", + "--config", + filepath.Join(v1alpha1.DirPathConfigDMWorker, v1alpha1.FileNameConfig), + }, + Ports: []corev1.ContainerPort{ + { + Name: v1alpha1.DMWorkerPortName, + ContainerPort: coreutil.DMWorkerPort(dw), + }, + }, + VolumeMounts: mounts, + Resources: k8s.GetResourceRequirements(dw.Spec.Resources), + }, + }, + Volumes: vols, + }, + } + + if dw.Spec.Overlay != nil { + overlay.OverlayPod(pod, dw.Spec.Overlay.Pod) + } + + return pod +} + +func dmWorkerVolumeName(volName string) string { + return metav1alpha1.VolNamePrefix + volName +} diff --git a/pkg/controllers/dmworker/tasks/pvc.go b/pkg/controllers/dmworker/tasks/pvc.go new file mode 100644 index 00000000000..80f28b13ace --- /dev/null +++ b/pkg/controllers/dmworker/tasks/pvc.go @@ -0,0 +1,80 @@ +// 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 ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + meta "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/features" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" +) + +func PVCNewer() common.PVCNewer[*v1alpha1.DMWorker] { + return common.PVCNewerFunc[*v1alpha1.DMWorker]( + func(cluster *v1alpha1.Cluster, dw *v1alpha1.DMWorker, fg features.Gates) []*corev1.PersistentVolumeClaim { + // RelayVolume PVC (the primary relay log storage for dm-worker) + relayVol := &dw.Spec.RelayVolume + relayPVC := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.PersistentVolumeClaimName[scope.DMWorker](dw, relayVol.Name), + Namespace: dw.Namespace, + Labels: coreutil.PersistentVolumeClaimLabels[scope.DMWorker](dw, relayVol.Name), + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dw, v1alpha1.SchemeGroupVersion.WithKind("DMWorker")), + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: relayVol.Storage, + }, + }, + StorageClassName: relayVol.StorageClassName, + }, + } + if fg.Enabled(meta.VolumeAttributesClass) { + relayPVC.Spec.VolumeAttributesClassName = relayVol.VolumeAttributesClassName + } + if cluster.Status.ID != "" { + relayPVC.Labels[v1alpha1.LabelKeyClusterID] = cluster.Status.ID + } + + pvcs := []*corev1.PersistentVolumeClaim{relayPVC} + + // Additional volumes + additionalPVCs := coreutil.PVCs[scope.DMWorker]( + cluster, + dw, + coreutil.EnableVAC(fg.Enabled(meta.VolumeAttributesClass)), + coreutil.PVCPatchFunc(func(_ *v1alpha1.Volume, pvc *corev1.PersistentVolumeClaim) { + if cluster.Status.ID != "" { + pvc.Labels[v1alpha1.LabelKeyClusterID] = cluster.Status.ID + } + }), + ) + pvcs = append(pvcs, additionalPVCs...) + + return pvcs + }, + ) +} diff --git a/pkg/controllers/dmworker/tasks/state.go b/pkg/controllers/dmworker/tasks/state.go new file mode 100644 index 00000000000..54fbf250eac --- /dev/null +++ b/pkg/controllers/dmworker/tasks/state.go @@ -0,0 +1,135 @@ +// 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 ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + stateutil "github.com/pingcap/tidb-operator/v2/pkg/state" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + dw *v1alpha1.DMWorker + pod *corev1.Pod + + isPodTerminating bool + + statusChanged bool + + healthy bool + + stateutil.IFeatureGates +} + +type State interface { + common.DMWorkerState + common.ClusterState + + common.PodState + common.PodStateUpdater + + common.InstanceState[*runtime.DMWorker] + + common.ContextClusterNewer[*v1alpha1.DMWorker] + common.ContextObjectNewer[*v1alpha1.DMWorker] + + common.StatusUpdater + common.StatusPersister[*v1alpha1.DMWorker] + + common.HealthyState + common.HealthyStateUpdater + + stateutil.IFeatureGates +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + s.IFeatureGates = stateutil.NewFeatureGates[scope.DMWorker](s) + return s +} + +func (s *state) Key() types.NamespacedName { + return s.key +} + +func (s *state) Object() *v1alpha1.DMWorker { + return s.dw +} + +func (s *state) SetObject(dw *v1alpha1.DMWorker) { + s.dw = dw +} + +func (s *state) DMWorker() *v1alpha1.DMWorker { + return s.dw +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) Pod() *corev1.Pod { + return s.pod +} + +func (s *state) IsPodTerminating() bool { + return s.isPodTerminating +} + +func (s *state) Instance() *runtime.DMWorker { + return runtime.FromDMWorker(s.dw) +} + +func (s *state) SetPod(pod *corev1.Pod) { + s.pod = pod + if pod != nil && !pod.GetDeletionTimestamp().IsZero() { + s.isPodTerminating = true + } +} + +func (s *state) DeletePod(pod *corev1.Pod) { + s.isPodTerminating = true + s.pod = pod +} + +func (s *state) SetCluster(cluster *v1alpha1.Cluster) { + s.cluster = cluster +} + +func (s *state) IsStatusChanged() bool { + return s.statusChanged +} + +func (s *state) SetStatusChanged() { + s.statusChanged = true +} + +func (s *state) IsHealthy() bool { + return s.healthy +} + +func (s *state) SetHealthy() { + s.healthy = true +} diff --git a/pkg/controllers/dmworker/tasks/status.go b/pkg/controllers/dmworker/tasks/status.go new file mode 100644 index 00000000000..43983375d84 --- /dev/null +++ b/pkg/controllers/dmworker/tasks/status.go @@ -0,0 +1,83 @@ +// 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" + "time" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/compare" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +const ( + defaultTaskWaitDuration = 5 * time.Second +) + +func TaskStatus(state *ReconcileContext, c client.Client) task.Task { + return task.NameTaskFunc("Status", func(ctx context.Context) task.Result { + needUpdate := state.IsStatusChanged() + dw := state.DMWorker() + pod := state.Pod() + ready := coreutil.IsReady[scope.DMWorker](dw) + + needUpdate = syncSuspendCond(dw) || needUpdate + + needUpdate = compare.SetIfChanged(&dw.Status.ObservedGeneration, dw.Generation) || needUpdate + needUpdate = compare.SetIfNotEmptyAndChanged( + &dw.Status.UpdateRevision, + dw.Labels[v1alpha1.LabelKeyInstanceRevisionHash], + ) || needUpdate + + if ready && pod != nil { + needUpdate = compare.SetIfNotEmptyAndChanged( + &dw.Status.CurrentRevision, + pod.Labels[v1alpha1.LabelKeyInstanceRevisionHash], + ) || needUpdate + } + + if needUpdate { + if err := c.Status().Update(ctx, dw); err != nil { + return task.Fail().With("cannot update status: %v", err) + } + } + + if !ready { + if state.IsPodTerminating() { + return task.Retry(defaultTaskWaitDuration).With("pod may be terminating, requeue to retry") + } + return task.Retry(defaultTaskWaitDuration).With("dm-worker is not ready, requeue to retry") + } + + return task.Complete().With("status is synced") + }) +} + +func syncSuspendCond(dw *v1alpha1.DMWorker) bool { + return meta.SetStatusCondition(&dw.Status.Conditions, metav1.Condition{ + Type: v1alpha1.CondSuspended, + Status: metav1.ConditionFalse, + ObservedGeneration: dw.Generation, + Reason: v1alpha1.ReasonUnsuspended, + Message: "instance is not suspended", + }) +} diff --git a/pkg/controllers/dmworkergroup/builder.go b/pkg/controllers/dmworkergroup/builder.go new file mode 100644 index 00000000000..8b4368dfdfe --- /dev/null +++ b/pkg/controllers/dmworkergroup/builder.go @@ -0,0 +1,70 @@ +// 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 dmworkergroup + +import ( + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworkergroup/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func (r *Reconciler) NewRunner(state *tasks.ReconcileContext, reporter task.TaskReporter) task.TaskRunner { + runner := task.NewTaskRunner(reporter, + // get dmworkergroup + common.TaskContextObject[scope.DMWorkerGroup](state, r.Client), + // if it's gone just return + task.IfBreak(common.CondObjectHasBeenDeleted[scope.DMWorkerGroup](state)), + + // get cluster + common.TaskContextCluster[scope.DMWorkerGroup](state, r.Client), + // if it's paused just return + task.IfBreak(common.CondClusterIsPaused(state)), + task.IfBreak(common.CondFeatureGatesIsNotSynced[scope.DMWorkerGroup](state)), + + // get all dm workers + common.TaskContextSlice[scope.DMWorkerGroup](state, r.Client), + + task.IfBreak(common.CondObjectIsDeleting[scope.DMWorkerGroup](state), + tasks.TaskFinalizerDel(state, r.Client), + common.TaskGroupConditionReady[scope.DMWorkerGroup](state), + common.TaskGroupConditionSynced[scope.DMWorkerGroup](state), + common.TaskStatusRevisionAndReplicas[scope.DMWorkerGroup](state), + common.TaskStatusPersister[scope.DMWorkerGroup](state, r.Client), + ), + common.TaskFinalizerAdd[scope.DMWorkerGroup](state, r.Client), + + common.TaskRevision[runtime.DMWorkerGroupTuple](state, r.Client), + + task.IfBreak( + common.CondClusterIsSuspending(state), + common.TaskGroupConditionSuspended[scope.DMWorkerGroup](state), + common.TaskGroupConditionReady[scope.DMWorkerGroup](state), + common.TaskGroupConditionSynced[scope.DMWorkerGroup](state), + common.TaskStatusPersister[scope.DMWorkerGroup](state, r.Client), + ), + tasks.TaskService(state, r.Client), + tasks.TaskUpdater(state, r.Client, r.AllocateFactory), + common.TaskGroupStatusSelector[scope.DMWorkerGroup](state), + common.TaskGroupConditionSuspended[scope.DMWorkerGroup](state), + common.TaskGroupConditionReady[scope.DMWorkerGroup](state), + common.TaskGroupConditionSynced[scope.DMWorkerGroup](state), + common.TaskStatusRevisionAndReplicas[scope.DMWorkerGroup](state), + common.TaskStatusPersister[scope.DMWorkerGroup](state, r.Client), + ) + + return runner +} diff --git a/pkg/controllers/dmworkergroup/controller.go b/pkg/controllers/dmworkergroup/controller.go new file mode 100644 index 00000000000..87a07e62e90 --- /dev/null +++ b/pkg/controllers/dmworkergroup/controller.go @@ -0,0 +1,113 @@ +// 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 dmworkergroup + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + "github.com/google/uuid" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/dmworkergroup/tasks" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" +) + +type Reconciler struct { + Logger logr.Logger + Client client.Client + AllocateFactory tracker.AllocateFactory +} + +func Setup(mgr manager.Manager, c client.Client, af tracker.AllocateFactory) error { + r := &Reconciler{ + Logger: mgr.GetLogger().WithName("DMWorkerGroup"), + Client: c, + AllocateFactory: af, + } + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.DMWorkerGroup{}). + Owns(&v1alpha1.DMWorker{}). + Watches(&v1alpha1.Cluster{}, r.ClusterEventHandler(), builder.WithPredicates(predicate.GenerationChangedPredicate{})). + WithOptions(controller.Options{RateLimiter: k8s.RateLimiter}). + Complete(r) +} + +func (r *Reconciler) ClusterEventHandler() handler.TypedEventHandler[client.Object, reconcile.Request] { + return handler.TypedFuncs[client.Object, reconcile.Request]{ + UpdateFunc: func(ctx context.Context, event event.TypedUpdateEvent[client.Object], + queue workqueue.TypedRateLimitingInterface[reconcile.Request], + ) { + cluster := event.ObjectNew.(*v1alpha1.Cluster) + + var list v1alpha1.DMWorkerGroupList + if err := r.Client.List(ctx, &list, client.InNamespace(cluster.Namespace), + client.MatchingFields{"spec.cluster.name": cluster.Name}); err != nil { + if !errors.IsNotFound(err) { + r.Logger.Error(err, "cannot list all dm-worker groups", "ns", cluster.Namespace, "cluster", cluster.Name) + } + return + } + + for i := range list.Items { + dwg := &list.Items[i] + queue.Add(reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: dwg.Name, + Namespace: dwg.Namespace, + }, + }) + } + }, + } +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + logger := logr.FromContextOrDiscard(ctx) + reporter := task.NewTableTaskReporter(uuid.NewString()) + + startTime := time.Now() + logger.Info("start reconcile") + defer func() { + dur := time.Since(startTime) + logger.Info("end reconcile", "duration", dur) + summary := fmt.Sprintf("summary for %v\n%s", req.NamespacedName, reporter.Summary()) + logger.Info(summary) + }() + + rtx := &tasks.ReconcileContext{ + State: tasks.NewState(req.NamespacedName), + } + + runner := r.NewRunner(rtx, reporter) + + return runner.Run(ctx) +} diff --git a/pkg/controllers/dmworkergroup/tasks/ctx.go b/pkg/controllers/dmworkergroup/tasks/ctx.go new file mode 100644 index 00000000000..eb9c98a8d91 --- /dev/null +++ b/pkg/controllers/dmworkergroup/tasks/ctx.go @@ -0,0 +1,19 @@ +// 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 + +type ReconcileContext struct { + State +} diff --git a/pkg/controllers/dmworkergroup/tasks/finalizer.go b/pkg/controllers/dmworkergroup/tasks/finalizer.go new file mode 100644 index 00000000000..b8be28d2510 --- /dev/null +++ b/pkg/controllers/dmworkergroup/tasks/finalizer.go @@ -0,0 +1,71 @@ +// 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" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + utilerr "k8s.io/apimachinery/pkg/util/errors" + + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/utils/k8s" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func TaskFinalizerDel(state State, c client.Client) task.Task { + return task.NameTaskFunc("FinalizerDel", func(ctx context.Context) task.Result { + var errList []error + for _, dw := range state.DMWorkerSlice() { + if dw.GetDeletionTimestamp().IsZero() { + if err := c.Delete(ctx, dw); err != nil { + if errors.IsNotFound(err) { + continue + } + errList = append(errList, err) + continue + } + } + + if err := k8s.RemoveFinalizer(ctx, c, dw); err != nil { + errList = append(errList, err) + } + } + + if len(errList) != 0 { + return task.Fail().With("failed to delete all dm-worker instances: %v", utilerr.NewAggregate(errList)) + } + + if len(state.DMWorkerSlice()) != 0 { + return task.Wait().With("wait for all dm-worker instances being removed, %v still exists", len(state.DMWorkerSlice())) + } + + wait, err := k8s.DeleteGroupSubresource(ctx, c, runtime.FromDMWorkerGroup(state.DMWorkerGroup()), &corev1.ServiceList{}) + if err != nil { + return task.Fail().With("cannot delete subresources: %w", err) + } + if wait { + return task.Retry(task.DefaultRequeueAfter).With("wait all subresources deleted") + } + + if err := k8s.RemoveFinalizer(ctx, c, state.DMWorkerGroup()); err != nil { + return task.Fail().With("failed to ensure finalizer has been removed: %w", err) + } + + return task.Complete().With("finalizer has been removed") + }) +} diff --git a/pkg/controllers/dmworkergroup/tasks/state.go b/pkg/controllers/dmworkergroup/tasks/state.go new file mode 100644 index 00000000000..61abce11d12 --- /dev/null +++ b/pkg/controllers/dmworkergroup/tasks/state.go @@ -0,0 +1,167 @@ +// 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 ( + "k8s.io/apimachinery/pkg/types" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/controllers/common" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + stateutil "github.com/pingcap/tidb-operator/v2/pkg/state" +) + +type state struct { + key types.NamespacedName + + cluster *v1alpha1.Cluster + dwg *v1alpha1.DMWorkerGroup + dws []*v1alpha1.DMWorker + + updateRevision string + currentRevision string + collisionCount int32 + + statusChanged bool + + stateutil.IFeatureGates +} + +type State interface { + common.DMWorkerSliceStateInitializer + common.RevisionStateInitializer[*runtime.DMWorkerGroup] + + common.DMWorkerGroupState + common.ClusterState + common.DMWorkerSliceState + common.RevisionState + + common.GroupState[*runtime.DMWorkerGroup] + + common.ContextClusterNewer[*v1alpha1.DMWorkerGroup] + common.ContextObjectNewer[*v1alpha1.DMWorkerGroup] + common.ContextSliceNewer[*v1alpha1.DMWorkerGroup, *v1alpha1.DMWorker] + + common.InstanceSliceState[*runtime.DMWorker] + common.SliceState[*v1alpha1.DMWorker] + + common.StatusUpdater + common.StatusPersister[*v1alpha1.DMWorkerGroup] + + stateutil.IFeatureGates +} + +func NewState(key types.NamespacedName) State { + s := &state{ + key: key, + } + s.IFeatureGates = stateutil.NewFeatureGates[scope.DMWorkerGroup](s) + return s +} + +func (s *state) Key() types.NamespacedName { + return s.key +} + +func (s *state) Object() *v1alpha1.DMWorkerGroup { + return s.dwg +} + +func (s *state) SetObject(dwg *v1alpha1.DMWorkerGroup) { + s.dwg = dwg +} + +func (s *state) DMWorkerGroup() *v1alpha1.DMWorkerGroup { + return s.dwg +} + +func (s *state) Group() *runtime.DMWorkerGroup { + return runtime.FromDMWorkerGroup(s.dwg) +} + +func (s *state) Cluster() *v1alpha1.Cluster { + return s.cluster +} + +func (s *state) DMWorkerSlice() []*v1alpha1.DMWorker { + return s.dws +} + +func (s *state) Slice() []*runtime.DMWorker { + return runtime.FromDMWorkerSlice(s.dws) +} + +func (s *state) InstanceSlice() []*v1alpha1.DMWorker { + return s.dws +} + +func (s *state) SetInstanceSlice(dws []*v1alpha1.DMWorker) { + s.dws = dws +} + +func (s *state) SetCluster(cluster *v1alpha1.Cluster) { + s.cluster = cluster +} + +func (s *state) IsStatusChanged() bool { + return s.statusChanged +} + +func (s *state) SetStatusChanged() { + s.statusChanged = true +} + +func (s *state) DMWorkerSliceInitializer() common.DMWorkerSliceInitializer { + return common.NewResourceSlice(func(dws []*v1alpha1.DMWorker) { s.dws = dws }). + WithNamespace(common.Namespace(s.key.Namespace)). + WithLabels(s.Labels()). + Initializer() +} + +func (s *state) RevisionInitializer() common.RevisionInitializer[*runtime.DMWorkerGroup] { + return common.NewRevision[*runtime.DMWorkerGroup]( + common.RevisionSetterFunc(func(update, current string, collisionCount int32) { + s.updateRevision = update + s.currentRevision = current + s.collisionCount = collisionCount + })). + WithCurrentRevision(common.Lazy[string](func() string { + return s.dwg.Status.CurrentRevision + })). + WithCollisionCount(common.Lazy[*int32](func() *int32 { + return s.dwg.Status.CollisionCount + })). + WithParent(common.Lazy[*runtime.DMWorkerGroup](func() *runtime.DMWorkerGroup { + return runtime.FromDMWorkerGroup(s.dwg) + })). + WithLabels(s.Labels()). + Initializer() +} + +func (s *state) Revision() (update, current string, collisionCount int32) { + return s.updateRevision, s.currentRevision, s.collisionCount +} + +func (s *state) Labels() common.LabelsOption { + return common.Lazy[map[string]string](func() map[string]string { + return map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMWorker, + v1alpha1.LabelKeyCluster: s.cluster.Name, + v1alpha1.LabelKeyGroup: s.dwg.Name, + } + }) +} diff --git a/pkg/controllers/dmworkergroup/tasks/svc.go b/pkg/controllers/dmworkergroup/tasks/svc.go new file mode 100644 index 00000000000..b768f927451 --- /dev/null +++ b/pkg/controllers/dmworkergroup/tasks/svc.go @@ -0,0 +1,82 @@ +// 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" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" +) + +func TaskService(state State, c client.Client) task.Task { + return task.NameTaskFunc("Service", func(ctx context.Context) task.Result { + dwg := state.DMWorkerGroup() + + headless := newHeadlessService(dwg) + if err := c.Apply(ctx, headless); err != nil { + return task.Fail().With(fmt.Sprintf("can't create headless service of dm-worker: %v", err)) + } + + return task.Complete().With("headless service of dm-worker has been applied") + }) +} + +func newHeadlessService(dwg *v1alpha1.DMWorkerGroup) *corev1.Service { + ipFamilyPolicy := corev1.IPFamilyPolicyPreferDualStack + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: coreutil.HeadlessServiceName[scope.DMWorkerGroup](dwg), + Namespace: dwg.Namespace, + Labels: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMWorker, + v1alpha1.LabelKeyCluster: dwg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dwg.Name, + }, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dwg, v1alpha1.SchemeGroupVersion.WithKind("DMWorkerGroup")), + }, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: corev1.ClusterIPNone, + IPFamilyPolicy: &ipFamilyPolicy, + Selector: map[string]string{ + v1alpha1.LabelKeyManagedBy: v1alpha1.LabelValManagedByOperator, + v1alpha1.LabelKeyComponent: v1alpha1.LabelValComponentDMWorker, + v1alpha1.LabelKeyCluster: dwg.Spec.Cluster.Name, + v1alpha1.LabelKeyGroup: dwg.Name, + }, + Ports: []corev1.ServicePort{ + { + Name: v1alpha1.DMWorkerPortName, + Port: coreutil.DMWorkerGroupPort(dwg), + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromString(v1alpha1.DMWorkerPortName), + }, + }, + PublishNotReadyAddresses: true, + }, + } +} diff --git a/pkg/controllers/dmworkergroup/tasks/updater.go b/pkg/controllers/dmworkergroup/tasks/updater.go new file mode 100644 index 00000000000..cff8c78eb50 --- /dev/null +++ b/pkg/controllers/dmworkergroup/tasks/updater.go @@ -0,0 +1,139 @@ +// 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" + "time" + + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/action" + coreutil "github.com/pingcap/tidb-operator/v2/pkg/apiutil/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/client" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "github.com/pingcap/tidb-operator/v2/pkg/runtime/scope" + "github.com/pingcap/tidb-operator/v2/pkg/updater" + "github.com/pingcap/tidb-operator/v2/pkg/updater/policy" + "github.com/pingcap/tidb-operator/v2/pkg/utils/task/v3" + "github.com/pingcap/tidb-operator/v2/pkg/utils/tracker" +) + +const ( + defaultUpdateWaitTime = time.Second * 30 +) + +func TaskUpdater(state *ReconcileContext, c client.Client, af tracker.AllocateFactory) task.Task { + return task.NameTaskFunc("Updater", func(ctx context.Context) task.Result { + logger := logr.FromContextOrDiscard(ctx) + dwg := state.DMWorkerGroup() + + checker := action.NewUpgradeChecker[scope.DMWorkerGroup](c, state.Cluster(), logger) + + if needVersionUpgrade(dwg) && !checker.CanUpgrade(ctx, dwg) { + return task.Retry(defaultUpdateWaitTime).With("wait until preconditions of upgrading is met") + } + + retryAfter := coreutil.RetryIfInstancesReadyButNotAvailable[scope.DMWorker](state.InstanceSlice(), coreutil.MinReadySeconds[scope.DMWorkerGroup](dwg)) + if retryAfter != 0 { + return task.Retry(retryAfter).With("wait until no instances is ready but not available") + } + + var topos []v1alpha1.ScheduleTopology + for _, p := range dwg.Spec.SchedulePolicies { + switch p.Type { + case v1alpha1.SchedulePolicyTypeEvenlySpread: + topos = p.EvenlySpread.Topologies + default: + } + } + + updateRevision, _, _ := state.Revision() + + dws := state.Slice() + topoPolicy, err := policy.NewTopologyPolicy(topos, updateRevision, dws...) + if err != nil { + return task.Fail().With("invalid topo policy, it should be validated: %w", err) + } + + var instances []string + for _, in := range dws { + instances = append(instances, in.Name) + } + + allocator := af.New(dwg.Namespace, dwg.Name, instances...) + + wait, err := updater.New[runtime.DMWorkerTuple](). + WithInstances(dws...). + WithDesired(int(state.Group().Replicas())). + WithClient(c). + WithMaxSurge(0). + WithMaxUnavailable(1). + WithRevision(updateRevision). + WithNewFactory(DMWorkerNewer(dwg, updateRevision)). + WithAddHooks( + updater.AllocateName[*runtime.DMWorker](allocator), + topoPolicy, + ). + WithDelHooks(topoPolicy). + WithUpdateHooks(topoPolicy). + WithScaleInPreferPolicy(topoPolicy.PolicyScaleIn()). + WithMinReadySeconds(coreutil.MinReadySeconds[scope.DMWorkerGroup](dwg)). + Build(). + Do(ctx) + if err != nil { + return task.Fail().With("cannot update instances: %w", err) + } + if wait { + return task.Wait().With("wait for all instances ready") + } + return task.Complete().With("all instances are synced") + }) +} + +func needVersionUpgrade(dwg *v1alpha1.DMWorkerGroup) bool { + return dwg.Spec.Template.Spec.Version != dwg.Status.Version && dwg.Status.Version != "" +} + +func DMWorkerNewer(dwg *v1alpha1.DMWorkerGroup, rev string) updater.NewFactory[*runtime.DMWorker] { + return updater.NewFunc[*runtime.DMWorker](func() *runtime.DMWorker { + spec := dwg.Spec.Template.Spec.DeepCopy() + + dw := &v1alpha1.DMWorker{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dwg.Namespace, + // Name will be allocated by updater.AllocateName + Labels: coreutil.InstanceLabels[scope.DMWorkerGroup](dwg, rev), + Annotations: coreutil.InstanceAnnotations[scope.DMWorkerGroup](dwg), + Finalizers: []string{metav1alpha1.Finalizer}, + OwnerReferences: []metav1.OwnerReference{ + *metav1.NewControllerRef(dwg, v1alpha1.SchemeGroupVersion.WithKind("DMWorkerGroup")), + }, + }, + Spec: v1alpha1.DMWorkerSpec{ + Cluster: dwg.Spec.Cluster, + DMGroupRef: dwg.Spec.DMGroupRef, + Features: dwg.Spec.Features, + Subdomain: coreutil.HeadlessServiceName[scope.DMWorkerGroup](dwg), + DMWorkerTemplateSpec: *spec, + }, + } + + return runtime.FromDMWorker(dw) + }) +} diff --git a/pkg/image/image.go b/pkg/image/image.go index cd1dc470062..c9f9a508490 100644 --- a/pkg/image/image.go +++ b/pkg/image/image.go @@ -38,6 +38,7 @@ const ( // Scheduler also use pd image Scheduler Untagged = "pingcap/pd" TiProxy Untagged = "pingcap/tiproxy" + DM Untagged = "pingcap/dm" Helper Tagged = "busybox:1.37.0" ) diff --git a/pkg/runtime/group.go b/pkg/runtime/group.go index b7bd25121d6..677a51e1488 100644 --- a/pkg/runtime/group.go +++ b/pkg/runtime/group.go @@ -58,7 +58,7 @@ type GroupT[T GroupSet] interface { } type GroupSet interface { - PDGroup | TiDBGroup | TiKVGroup | TiFlashGroup | TiCDCGroup | TiProxyGroup | TSOGroup | SchedulingGroup | SchedulerGroup + PDGroup | TiDBGroup | TiKVGroup | TiFlashGroup | TiCDCGroup | TiProxyGroup | TSOGroup | SchedulingGroup | SchedulerGroup | DMGroup | DMWorkerGroup } type GroupTuple[PT client.Object, PU Group] interface { diff --git a/pkg/runtime/instance.go b/pkg/runtime/instance.go index 7b311c7c87f..397650d9281 100644 --- a/pkg/runtime/instance.go +++ b/pkg/runtime/instance.go @@ -71,7 +71,7 @@ type InstanceT[T InstanceSet] interface { } type InstanceSet interface { - PD | TiDB | TiKV | TiFlash | TiCDC | TiProxy | TSO | Scheduling | Scheduler + PD | TiDB | TiKV | TiFlash | TiCDC | TiProxy | TSO | Scheduling | Scheduler | DM | DMWorker } type InstanceTuple[PT client.Object, PU Instance] interface { diff --git a/pkg/runtime/scope/zz_generated.scope.dm.go b/pkg/runtime/scope/zz_generated.scope.dm.go new file mode 100644 index 00000000000..da3542814f7 --- /dev/null +++ b/pkg/runtime/scope/zz_generated.scope.dm.go @@ -0,0 +1,92 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// 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 runtime-gen. DO NOT EDIT. + +package scope + +import ( + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ( + DM struct{} +) + +func (DM) From(f *v1alpha1.DM) *runtime.DM { + return runtime.FromDM(f) +} + +func (DM) To(t *runtime.DM) *v1alpha1.DM { + return runtime.ToDM(t) +} + +func (DM) Component() string { + return v1alpha1.LabelValComponentDMMaster +} + +func (DM) GVK() schema.GroupVersionKind { + return v1alpha1.SchemeGroupVersion.WithKind("DM") +} + +func (DM) NewList() *v1alpha1.DMList { + return &v1alpha1.DMList{} +} + +func (DM) GetItems(l *v1alpha1.DMList) []*v1alpha1.DM { + items := make([]*v1alpha1.DM, 0, len(l.Items)) + for i := range l.Items { + items = append(items, &l.Items[i]) + } + return items +} + +type DMGroup struct{} + +func (DMGroup) From(f *v1alpha1.DMGroup) *runtime.DMGroup { + return runtime.FromDMGroup(f) +} + +func (DMGroup) To(t *runtime.DMGroup) *v1alpha1.DMGroup { + return runtime.ToDMGroup(t) +} + +func (DMGroup) Component() string { + return v1alpha1.LabelValComponentDMMaster +} + +func (DMGroup) GVK() schema.GroupVersionKind { + return v1alpha1.SchemeGroupVersion.WithKind("DMGroup") +} + +func (DMGroup) NewList() *v1alpha1.DMGroupList { + return &v1alpha1.DMGroupList{} +} + +func (DMGroup) GetItems(l *v1alpha1.DMGroupList) []*v1alpha1.DMGroup { + items := make([]*v1alpha1.DMGroup, 0, len(l.Items)) + for i := range l.Items { + items = append(items, &l.Items[i]) + } + return items +} + +func (DMGroup) Instance() DM { + return DM{} +} diff --git a/pkg/runtime/scope/zz_generated.scope.dmworker.go b/pkg/runtime/scope/zz_generated.scope.dmworker.go new file mode 100644 index 00000000000..110562f4c69 --- /dev/null +++ b/pkg/runtime/scope/zz_generated.scope.dmworker.go @@ -0,0 +1,92 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// 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 runtime-gen. DO NOT EDIT. + +package scope + +import ( + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + "github.com/pingcap/tidb-operator/v2/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +type ( + DMWorker struct{} +) + +func (DMWorker) From(f *v1alpha1.DMWorker) *runtime.DMWorker { + return runtime.FromDMWorker(f) +} + +func (DMWorker) To(t *runtime.DMWorker) *v1alpha1.DMWorker { + return runtime.ToDMWorker(t) +} + +func (DMWorker) Component() string { + return v1alpha1.LabelValComponentDMWorker +} + +func (DMWorker) GVK() schema.GroupVersionKind { + return v1alpha1.SchemeGroupVersion.WithKind("DMWorker") +} + +func (DMWorker) NewList() *v1alpha1.DMWorkerList { + return &v1alpha1.DMWorkerList{} +} + +func (DMWorker) GetItems(l *v1alpha1.DMWorkerList) []*v1alpha1.DMWorker { + items := make([]*v1alpha1.DMWorker, 0, len(l.Items)) + for i := range l.Items { + items = append(items, &l.Items[i]) + } + return items +} + +type DMWorkerGroup struct{} + +func (DMWorkerGroup) From(f *v1alpha1.DMWorkerGroup) *runtime.DMWorkerGroup { + return runtime.FromDMWorkerGroup(f) +} + +func (DMWorkerGroup) To(t *runtime.DMWorkerGroup) *v1alpha1.DMWorkerGroup { + return runtime.ToDMWorkerGroup(t) +} + +func (DMWorkerGroup) Component() string { + return v1alpha1.LabelValComponentDMWorker +} + +func (DMWorkerGroup) GVK() schema.GroupVersionKind { + return v1alpha1.SchemeGroupVersion.WithKind("DMWorkerGroup") +} + +func (DMWorkerGroup) NewList() *v1alpha1.DMWorkerGroupList { + return &v1alpha1.DMWorkerGroupList{} +} + +func (DMWorkerGroup) GetItems(l *v1alpha1.DMWorkerGroupList) []*v1alpha1.DMWorkerGroup { + items := make([]*v1alpha1.DMWorkerGroup, 0, len(l.Items)) + for i := range l.Items { + items = append(items, &l.Items[i]) + } + return items +} + +func (DMWorkerGroup) Instance() DMWorker { + return DMWorker{} +} diff --git a/pkg/runtime/zz_generated.runtime.dm.go b/pkg/runtime/zz_generated.runtime.dm.go new file mode 100644 index 00000000000..0e9f50a8f1f --- /dev/null +++ b/pkg/runtime/zz_generated.runtime.dm.go @@ -0,0 +1,463 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// 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 runtime-gen. DO NOT EDIT. + +package runtime + +import ( + "time" + "unsafe" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +type ( + DM v1alpha1.DM +) + +type DMTuple struct{} + +var _ InstanceTuple[*v1alpha1.DM, *DM] = DMTuple{} + +func (DMTuple) From(t *v1alpha1.DM) *DM { + return FromDM(t) +} + +func (DMTuple) FromSlice(t []*v1alpha1.DM) []*DM { + return FromDMSlice(t) +} + +func (DMTuple) To(t *DM) *v1alpha1.DM { + return ToDM(t) +} + +func (DMTuple) ToSlice(t []*DM) []*v1alpha1.DM { + return ToDMSlice(t) +} + +func FromDM(in *v1alpha1.DM) *DM { + return (*DM)(in) +} + +func ToDM(in *DM) *v1alpha1.DM { + return (*v1alpha1.DM)(in) +} + +func FromDMSlice(ins []*v1alpha1.DM) []*DM { + return *(*[]*DM)(unsafe.Pointer(&ins)) +} + +func ToDMSlice(ins []*DM) []*v1alpha1.DM { + return *(*[]*v1alpha1.DM)(unsafe.Pointer(&ins)) +} + +var _ Instance = &DM{} + +func (in *DM) DeepCopyObject() runtime.Object { + return (*v1alpha1.DM)(in).DeepCopyObject() +} + +func (in *DM) To() *v1alpha1.DM { + return ToDM(in) +} + +func (in *DM) GetTopology() v1alpha1.Topology { + return in.Spec.Topology +} + +func (in *DM) SetTopology(t v1alpha1.Topology) { + in.Spec.Topology = t +} + +func (in *DM) GetUpdateRevision() string { + if in.Labels == nil { + return "" + } + return in.Labels[v1alpha1.LabelKeyInstanceRevisionHash] +} + +func (in *DM) CurrentRevision() string { + return in.Status.CurrentRevision +} + +func (in *DM) SetCurrentRevision(rev string) { + in.Status.CurrentRevision = rev +} + +func (in *DM) IsReady() bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondReady) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + + return cond.Status == metav1.ConditionTrue +} + +func (in *DM) IsNotRunning() bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondRunning) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + return cond.Status == metav1.ConditionFalse +} + +func (in *DM) IsAvailable(minReadySeconds int64, now time.Time) bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondReady) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + if cond.Status != metav1.ConditionTrue { + return false + } + if minReadySeconds == 0 { + return true + } + minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second + if !cond.LastTransitionTime.IsZero() && cond.LastTransitionTime.Add(minReadySecondsDuration).Before(now) { + return true + } + + return false +} + +func (in *DM) IsUpToDate() bool { + return in.Status.ObservedGeneration == in.GetGeneration() && in.GetUpdateRevision() == in.Status.CurrentRevision +} + +func (in *DM) Conditions() []metav1.Condition { + return in.Status.Conditions +} + +func (in *DM) SetConditions(conds []metav1.Condition) { + in.Status.Conditions = conds +} + +func (in *DM) ObservedGeneration() int64 { + return in.Status.ObservedGeneration +} + +func (in *DM) SetObservedGeneration(gen int64) { + in.Status.ObservedGeneration = gen +} + +func (in *DM) SetCluster(cluster string) { + in.Spec.Cluster.Name = cluster +} + +func (in *DM) Cluster() string { + return in.Spec.Cluster.Name +} + +func (*DM) Component() string { + return v1alpha1.LabelValComponentDMMaster +} + +func (in *DM) Volumes() []v1alpha1.Volume { + return in.Spec.Volumes +} + +func (in *DM) PodOverlay() *v1alpha1.PodOverlay { + if in.Spec.Overlay == nil { + return nil + } + return in.Spec.Overlay.Pod +} + +func (in *DM) PVCOverlay() []v1alpha1.NamedPersistentVolumeClaimOverlay { + if in.Spec.Overlay == nil { + return nil + } + return in.Spec.Overlay.PersistentVolumeClaims +} + +func (in *DM) Features() []metav1alpha1.Feature { + return in.Spec.Features +} + +func (in *DM) SetVersion(version string) { + in.Spec.Version = version +} + +func (in *DM) Version() string { + return in.Spec.Version +} + +func (in *DM) SetImage(image string) { + in.Spec.Image = ptr.To(image) +} + +func (in *DM) Subdomain() string { + return in.Spec.Subdomain +} + +func (in *DM) ServerLabels() map[string]string { + return nil +} + +func (in *DM) ClusterCertKeyPairSecretName() string { + sec := in.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CertKeyPair != nil { + return sec.TLS.Cluster.CertKeyPair.Name + } + prefix, _ := NamePrefixAndSuffix(in.GetName()) + return prefix + "-" + in.Component() + "-cluster-secret" +} + +func (in *DM) ClusterCASecretName() string { + sec := in.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CA != nil { + return sec.TLS.Cluster.CA.Name + } + prefix, _ := NamePrefixAndSuffix(in.GetName()) + return prefix + "-" + in.Component() + "-cluster-secret" +} + +func (in *DM) IsOffline() bool { + return false +} + +func (in *DM) IsStore() bool { + return false +} + +type ( + DMGroup v1alpha1.DMGroup +) + +type DMGroupTuple struct{} + +var _ GroupTuple[*v1alpha1.DMGroup, *DMGroup] = DMGroupTuple{} + +func (DMGroupTuple) From(t *v1alpha1.DMGroup) *DMGroup { + return FromDMGroup(t) +} + +func (DMGroupTuple) FromSlice(t []*v1alpha1.DMGroup) []*DMGroup { + return FromDMGroupSlice(t) +} + +func (DMGroupTuple) To(t *DMGroup) *v1alpha1.DMGroup { + return ToDMGroup(t) +} + +func (DMGroupTuple) ToSlice(t []*DMGroup) []*v1alpha1.DMGroup { + return ToDMGroupSlice(t) +} + +func FromDMGroup(g *v1alpha1.DMGroup) *DMGroup { + return (*DMGroup)(g) +} + +func ToDMGroup(g *DMGroup) *v1alpha1.DMGroup { + return (*v1alpha1.DMGroup)(g) +} + +func FromDMGroupSlice(gs []*v1alpha1.DMGroup) []*DMGroup { + return *(*[]*DMGroup)(unsafe.Pointer(&gs)) +} + +func ToDMGroupSlice(gs []*DMGroup) []*v1alpha1.DMGroup { + return *(*[]*v1alpha1.DMGroup)(unsafe.Pointer(&gs)) +} + +var _ Group = &DMGroup{} + +func (g *DMGroup) DeepCopyObject() runtime.Object { + return (*v1alpha1.DMGroup)(g) +} + +func (g *DMGroup) To() *v1alpha1.DMGroup { + return ToDMGroup(g) +} + +func (g *DMGroup) SetReplicas(replicas int32) { + g.Spec.Replicas = &replicas +} + +func (g *DMGroup) Replicas() int32 { + if g.Spec.Replicas == nil { + return 1 + } + return *g.Spec.Replicas +} + +func (g *DMGroup) SetVersion(version string) { + g.Spec.Template.Spec.Version = version +} + +func (g *DMGroup) Version() string { + return g.Spec.Template.Spec.Version +} + +func (g *DMGroup) SetImage(image string) { + g.Spec.Template.Spec.Image = ptr.To(image) +} + +func (g *DMGroup) SetCluster(cluster string) { + g.Spec.Cluster.Name = cluster +} + +func (g *DMGroup) Cluster() string { + return g.Spec.Cluster.Name +} + +func (*DMGroup) Component() string { + return v1alpha1.LabelValComponentDMMaster +} + +func (g *DMGroup) Conditions() []metav1.Condition { + return g.Status.Conditions +} + +func (g *DMGroup) SetConditions(conds []metav1.Condition) { + g.Status.Conditions = conds +} + +func (g *DMGroup) ObservedGeneration() int64 { + return g.Status.ObservedGeneration +} + +func (g *DMGroup) SetObservedGeneration(gen int64) { + g.Status.ObservedGeneration = gen +} + +func (g *DMGroup) SetStatusVersion(version string) { + g.Status.Version = version +} + +func (g *DMGroup) StatusVersion() string { + return g.Status.Version +} + +func (g *DMGroup) SetStatusReplicas(replicas, ready, update, current int32) { + g.Status.Replicas = replicas + g.Status.ReadyReplicas = ready + g.Status.UpdatedReplicas = update + g.Status.CurrentReplicas = current +} + +func (g *DMGroup) StatusReplicas() (replicas, ready, update, current int32) { + return g.Status.Replicas, + g.Status.ReadyReplicas, + g.Status.UpdatedReplicas, + g.Status.CurrentReplicas +} + +func (g *DMGroup) SetStatusRevision(update, current string, collisionCount *int32) { + g.Status.UpdateRevision = update + g.Status.CurrentRevision = current + g.Status.CollisionCount = collisionCount +} + +func (g *DMGroup) StatusRevision() (update, current string, collisionCount *int32) { + return g.Status.UpdateRevision, + g.Status.CurrentRevision, + g.Status.CollisionCount +} + +func (g *DMGroup) SetStatusSelector(l string) { + g.Status.Selector = l +} + +func (g *DMGroup) StatusSelector() string { + return g.Status.Selector +} + +func (g *DMGroup) TemplateLabels() map[string]string { + return g.Spec.Template.Labels +} + +func (g *DMGroup) TemplateAnnotations() map[string]string { + return g.Spec.Template.Annotations +} + +func (g *DMGroup) SetTemplateLabels(ls map[string]string) { + g.Spec.Template.Labels = ls +} + +func (g *DMGroup) SetTemplateAnnotations(anno map[string]string) { + g.Spec.Template.Annotations = anno +} + +func (g *DMGroup) Features() []metav1alpha1.Feature { + return g.Spec.Features +} + +func (g *DMGroup) SetTemplateClusterTLS(ca, certKeyPair string) { + if g.Spec.Template.Spec.Security == nil { + g.Spec.Template.Spec.Security = &v1alpha1.Security{} + } + sec := g.Spec.Template.Spec.Security + if sec.TLS == nil { + sec.TLS = &v1alpha1.ComponentTLSConfig{} + } + sec.TLS.Cluster = &v1alpha1.InternalTLS{} + if ca != "" { + sec.TLS.Cluster.CA = &v1alpha1.CAReference{ + Name: ca, + } + } + if certKeyPair != "" { + sec.TLS.Cluster.CertKeyPair = &v1alpha1.CertKeyPairReference{ + Name: certKeyPair, + } + } +} + +func (g *DMGroup) ClusterCertKeyPairSecretName() string { + defaultName := g.Name + "-" + g.Component() + "-cluster-secret" + sec := g.Spec.Template.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CertKeyPair != nil { + return sec.TLS.Cluster.CertKeyPair.Name + } + return defaultName +} + +func (g *DMGroup) ClusterCASecretName() string { + defaultName := g.Name + "-" + g.Component() + "-cluster-secret" + sec := g.Spec.Template.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CA != nil { + return sec.TLS.Cluster.CA.Name + } + return defaultName +} + +func (g *DMGroup) MinReadySeconds() int64 { + if g.Spec.MinReadySeconds == nil { + return v1alpha1.DefaultDMMinReadySeconds + } + return *g.Spec.MinReadySeconds +} + +func (g *DMGroup) SchedulePolicies() []v1alpha1.SchedulePolicy { + return g.Spec.SchedulePolicies +} diff --git a/pkg/runtime/zz_generated.runtime.dmworker.go b/pkg/runtime/zz_generated.runtime.dmworker.go new file mode 100644 index 00000000000..b4d49f797be --- /dev/null +++ b/pkg/runtime/zz_generated.runtime.dmworker.go @@ -0,0 +1,463 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// 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 runtime-gen. DO NOT EDIT. + +package runtime + +import ( + "time" + "unsafe" + + "github.com/pingcap/tidb-operator/api/v2/core/v1alpha1" + metav1alpha1 "github.com/pingcap/tidb-operator/api/v2/meta/v1alpha1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" +) + +type ( + DMWorker v1alpha1.DMWorker +) + +type DMWorkerTuple struct{} + +var _ InstanceTuple[*v1alpha1.DMWorker, *DMWorker] = DMWorkerTuple{} + +func (DMWorkerTuple) From(t *v1alpha1.DMWorker) *DMWorker { + return FromDMWorker(t) +} + +func (DMWorkerTuple) FromSlice(t []*v1alpha1.DMWorker) []*DMWorker { + return FromDMWorkerSlice(t) +} + +func (DMWorkerTuple) To(t *DMWorker) *v1alpha1.DMWorker { + return ToDMWorker(t) +} + +func (DMWorkerTuple) ToSlice(t []*DMWorker) []*v1alpha1.DMWorker { + return ToDMWorkerSlice(t) +} + +func FromDMWorker(in *v1alpha1.DMWorker) *DMWorker { + return (*DMWorker)(in) +} + +func ToDMWorker(in *DMWorker) *v1alpha1.DMWorker { + return (*v1alpha1.DMWorker)(in) +} + +func FromDMWorkerSlice(ins []*v1alpha1.DMWorker) []*DMWorker { + return *(*[]*DMWorker)(unsafe.Pointer(&ins)) +} + +func ToDMWorkerSlice(ins []*DMWorker) []*v1alpha1.DMWorker { + return *(*[]*v1alpha1.DMWorker)(unsafe.Pointer(&ins)) +} + +var _ Instance = &DMWorker{} + +func (in *DMWorker) DeepCopyObject() runtime.Object { + return (*v1alpha1.DMWorker)(in).DeepCopyObject() +} + +func (in *DMWorker) To() *v1alpha1.DMWorker { + return ToDMWorker(in) +} + +func (in *DMWorker) GetTopology() v1alpha1.Topology { + return in.Spec.Topology +} + +func (in *DMWorker) SetTopology(t v1alpha1.Topology) { + in.Spec.Topology = t +} + +func (in *DMWorker) GetUpdateRevision() string { + if in.Labels == nil { + return "" + } + return in.Labels[v1alpha1.LabelKeyInstanceRevisionHash] +} + +func (in *DMWorker) CurrentRevision() string { + return in.Status.CurrentRevision +} + +func (in *DMWorker) SetCurrentRevision(rev string) { + in.Status.CurrentRevision = rev +} + +func (in *DMWorker) IsReady() bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondReady) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + + return cond.Status == metav1.ConditionTrue +} + +func (in *DMWorker) IsNotRunning() bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondRunning) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + return cond.Status == metav1.ConditionFalse +} + +func (in *DMWorker) IsAvailable(minReadySeconds int64, now time.Time) bool { + cond := meta.FindStatusCondition(in.Status.Conditions, v1alpha1.CondReady) + if cond == nil { + return false + } + if cond.ObservedGeneration != in.GetGeneration() { + return false + } + if cond.Status != metav1.ConditionTrue { + return false + } + if minReadySeconds == 0 { + return true + } + minReadySecondsDuration := time.Duration(minReadySeconds) * time.Second + if !cond.LastTransitionTime.IsZero() && cond.LastTransitionTime.Add(minReadySecondsDuration).Before(now) { + return true + } + + return false +} + +func (in *DMWorker) IsUpToDate() bool { + return in.Status.ObservedGeneration == in.GetGeneration() && in.GetUpdateRevision() == in.Status.CurrentRevision +} + +func (in *DMWorker) Conditions() []metav1.Condition { + return in.Status.Conditions +} + +func (in *DMWorker) SetConditions(conds []metav1.Condition) { + in.Status.Conditions = conds +} + +func (in *DMWorker) ObservedGeneration() int64 { + return in.Status.ObservedGeneration +} + +func (in *DMWorker) SetObservedGeneration(gen int64) { + in.Status.ObservedGeneration = gen +} + +func (in *DMWorker) SetCluster(cluster string) { + in.Spec.Cluster.Name = cluster +} + +func (in *DMWorker) Cluster() string { + return in.Spec.Cluster.Name +} + +func (*DMWorker) Component() string { + return v1alpha1.LabelValComponentDMWorker +} + +func (in *DMWorker) Volumes() []v1alpha1.Volume { + return in.Spec.Volumes +} + +func (in *DMWorker) PodOverlay() *v1alpha1.PodOverlay { + if in.Spec.Overlay == nil { + return nil + } + return in.Spec.Overlay.Pod +} + +func (in *DMWorker) PVCOverlay() []v1alpha1.NamedPersistentVolumeClaimOverlay { + if in.Spec.Overlay == nil { + return nil + } + return in.Spec.Overlay.PersistentVolumeClaims +} + +func (in *DMWorker) Features() []metav1alpha1.Feature { + return in.Spec.Features +} + +func (in *DMWorker) SetVersion(version string) { + in.Spec.Version = version +} + +func (in *DMWorker) Version() string { + return in.Spec.Version +} + +func (in *DMWorker) SetImage(image string) { + in.Spec.Image = ptr.To(image) +} + +func (in *DMWorker) Subdomain() string { + return in.Spec.Subdomain +} + +func (in *DMWorker) ServerLabels() map[string]string { + return nil +} + +func (in *DMWorker) ClusterCertKeyPairSecretName() string { + sec := in.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CertKeyPair != nil { + return sec.TLS.Cluster.CertKeyPair.Name + } + prefix, _ := NamePrefixAndSuffix(in.GetName()) + return prefix + "-" + in.Component() + "-cluster-secret" +} + +func (in *DMWorker) ClusterCASecretName() string { + sec := in.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CA != nil { + return sec.TLS.Cluster.CA.Name + } + prefix, _ := NamePrefixAndSuffix(in.GetName()) + return prefix + "-" + in.Component() + "-cluster-secret" +} + +func (in *DMWorker) IsOffline() bool { + return false +} + +func (in *DMWorker) IsStore() bool { + return false +} + +type ( + DMWorkerGroup v1alpha1.DMWorkerGroup +) + +type DMWorkerGroupTuple struct{} + +var _ GroupTuple[*v1alpha1.DMWorkerGroup, *DMWorkerGroup] = DMWorkerGroupTuple{} + +func (DMWorkerGroupTuple) From(t *v1alpha1.DMWorkerGroup) *DMWorkerGroup { + return FromDMWorkerGroup(t) +} + +func (DMWorkerGroupTuple) FromSlice(t []*v1alpha1.DMWorkerGroup) []*DMWorkerGroup { + return FromDMWorkerGroupSlice(t) +} + +func (DMWorkerGroupTuple) To(t *DMWorkerGroup) *v1alpha1.DMWorkerGroup { + return ToDMWorkerGroup(t) +} + +func (DMWorkerGroupTuple) ToSlice(t []*DMWorkerGroup) []*v1alpha1.DMWorkerGroup { + return ToDMWorkerGroupSlice(t) +} + +func FromDMWorkerGroup(g *v1alpha1.DMWorkerGroup) *DMWorkerGroup { + return (*DMWorkerGroup)(g) +} + +func ToDMWorkerGroup(g *DMWorkerGroup) *v1alpha1.DMWorkerGroup { + return (*v1alpha1.DMWorkerGroup)(g) +} + +func FromDMWorkerGroupSlice(gs []*v1alpha1.DMWorkerGroup) []*DMWorkerGroup { + return *(*[]*DMWorkerGroup)(unsafe.Pointer(&gs)) +} + +func ToDMWorkerGroupSlice(gs []*DMWorkerGroup) []*v1alpha1.DMWorkerGroup { + return *(*[]*v1alpha1.DMWorkerGroup)(unsafe.Pointer(&gs)) +} + +var _ Group = &DMWorkerGroup{} + +func (g *DMWorkerGroup) DeepCopyObject() runtime.Object { + return (*v1alpha1.DMWorkerGroup)(g) +} + +func (g *DMWorkerGroup) To() *v1alpha1.DMWorkerGroup { + return ToDMWorkerGroup(g) +} + +func (g *DMWorkerGroup) SetReplicas(replicas int32) { + g.Spec.Replicas = &replicas +} + +func (g *DMWorkerGroup) Replicas() int32 { + if g.Spec.Replicas == nil { + return 1 + } + return *g.Spec.Replicas +} + +func (g *DMWorkerGroup) SetVersion(version string) { + g.Spec.Template.Spec.Version = version +} + +func (g *DMWorkerGroup) Version() string { + return g.Spec.Template.Spec.Version +} + +func (g *DMWorkerGroup) SetImage(image string) { + g.Spec.Template.Spec.Image = ptr.To(image) +} + +func (g *DMWorkerGroup) SetCluster(cluster string) { + g.Spec.Cluster.Name = cluster +} + +func (g *DMWorkerGroup) Cluster() string { + return g.Spec.Cluster.Name +} + +func (*DMWorkerGroup) Component() string { + return v1alpha1.LabelValComponentDMWorker +} + +func (g *DMWorkerGroup) Conditions() []metav1.Condition { + return g.Status.Conditions +} + +func (g *DMWorkerGroup) SetConditions(conds []metav1.Condition) { + g.Status.Conditions = conds +} + +func (g *DMWorkerGroup) ObservedGeneration() int64 { + return g.Status.ObservedGeneration +} + +func (g *DMWorkerGroup) SetObservedGeneration(gen int64) { + g.Status.ObservedGeneration = gen +} + +func (g *DMWorkerGroup) SetStatusVersion(version string) { + g.Status.Version = version +} + +func (g *DMWorkerGroup) StatusVersion() string { + return g.Status.Version +} + +func (g *DMWorkerGroup) SetStatusReplicas(replicas, ready, update, current int32) { + g.Status.Replicas = replicas + g.Status.ReadyReplicas = ready + g.Status.UpdatedReplicas = update + g.Status.CurrentReplicas = current +} + +func (g *DMWorkerGroup) StatusReplicas() (replicas, ready, update, current int32) { + return g.Status.Replicas, + g.Status.ReadyReplicas, + g.Status.UpdatedReplicas, + g.Status.CurrentReplicas +} + +func (g *DMWorkerGroup) SetStatusRevision(update, current string, collisionCount *int32) { + g.Status.UpdateRevision = update + g.Status.CurrentRevision = current + g.Status.CollisionCount = collisionCount +} + +func (g *DMWorkerGroup) StatusRevision() (update, current string, collisionCount *int32) { + return g.Status.UpdateRevision, + g.Status.CurrentRevision, + g.Status.CollisionCount +} + +func (g *DMWorkerGroup) SetStatusSelector(l string) { + g.Status.Selector = l +} + +func (g *DMWorkerGroup) StatusSelector() string { + return g.Status.Selector +} + +func (g *DMWorkerGroup) TemplateLabels() map[string]string { + return g.Spec.Template.Labels +} + +func (g *DMWorkerGroup) TemplateAnnotations() map[string]string { + return g.Spec.Template.Annotations +} + +func (g *DMWorkerGroup) SetTemplateLabels(ls map[string]string) { + g.Spec.Template.Labels = ls +} + +func (g *DMWorkerGroup) SetTemplateAnnotations(anno map[string]string) { + g.Spec.Template.Annotations = anno +} + +func (g *DMWorkerGroup) Features() []metav1alpha1.Feature { + return g.Spec.Features +} + +func (g *DMWorkerGroup) SetTemplateClusterTLS(ca, certKeyPair string) { + if g.Spec.Template.Spec.Security == nil { + g.Spec.Template.Spec.Security = &v1alpha1.Security{} + } + sec := g.Spec.Template.Spec.Security + if sec.TLS == nil { + sec.TLS = &v1alpha1.ComponentTLSConfig{} + } + sec.TLS.Cluster = &v1alpha1.InternalTLS{} + if ca != "" { + sec.TLS.Cluster.CA = &v1alpha1.CAReference{ + Name: ca, + } + } + if certKeyPair != "" { + sec.TLS.Cluster.CertKeyPair = &v1alpha1.CertKeyPairReference{ + Name: certKeyPair, + } + } +} + +func (g *DMWorkerGroup) ClusterCertKeyPairSecretName() string { + defaultName := g.Name + "-" + g.Component() + "-cluster-secret" + sec := g.Spec.Template.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CertKeyPair != nil { + return sec.TLS.Cluster.CertKeyPair.Name + } + return defaultName +} + +func (g *DMWorkerGroup) ClusterCASecretName() string { + defaultName := g.Name + "-" + g.Component() + "-cluster-secret" + sec := g.Spec.Template.Spec.Security + if sec != nil && sec.TLS != nil && sec.TLS.Cluster != nil && sec.TLS.Cluster.CA != nil { + return sec.TLS.Cluster.CA.Name + } + return defaultName +} + +func (g *DMWorkerGroup) MinReadySeconds() int64 { + if g.Spec.MinReadySeconds == nil { + return v1alpha1.DefaultDMWorkerMinReadySeconds + } + return *g.Spec.MinReadySeconds +} + +func (g *DMWorkerGroup) SchedulePolicies() []v1alpha1.SchedulePolicy { + return g.Spec.SchedulePolicies +}