From f0333f11790ec493fdd53e3a84d141cac15a6689 Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Wed, 22 Sep 2021 10:51:33 +0800 Subject: [PATCH 1/6] *: Add mysqluser api and related code generated by the kuberbuilder. #175 --- Dockerfile | 1 + PROJECT | 9 + api/v1alpha1/mysqluser_types.go | 144 ++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 187 ++++++++++++++++++ .../bases/mysql.radondb.com_mysqlusers.yaml | 156 +++++++++++++++ config/crd/kustomization.yaml | 1 + config/rbac/role.yaml | 26 +++ controllers/suite_test.go | 3 + 8 files changed, 527 insertions(+) create mode 100644 api/v1alpha1/mysqluser_types.go create mode 100644 config/crd/bases/mysql.radondb.com_mysqlusers.yaml diff --git a/Dockerfile b/Dockerfile index 19d1fbe1..97831e30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ COPY controllers/ controllers/ COPY backup/ backup/ COPY internal/ internal/ COPY utils/ utils/ +COPY mysqluser/ mysqluser/ # Build RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager cmd/manager/main.go diff --git a/PROJECT b/PROJECT index 3fb29742..285fd4ed 100644 --- a/PROJECT +++ b/PROJECT @@ -27,4 +27,13 @@ resources: kind: Backup path: github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: radondb.com + group: mysql + kind: MysqlUser + path: github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/v1alpha1/mysqluser_types.go b/api/v1alpha1/mysqluser_types.go new file mode 100644 index 00000000..53a34cf0 --- /dev/null +++ b/api/v1alpha1/mysqluser_types.go @@ -0,0 +1,144 @@ +/* +Copyright 2021 RadonDB. + +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 ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// UserSpec defines the desired state of User. +type UserSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + // Username is the name of user to be operated. + // This field should be immutable. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern="^[A-Za-z0-9_]{2,26}$" + User string `json:"user,omitempty"` + + // Hosts is the grants hosts. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + Hosts []string `json:"hosts,omitempty"` + + // UserOwner Contains parameters about the cluster bound by user. + // +kubebuilder:validation:Required + UserOwner UserOwner `json:"userOwner,omitempty"` + + // SecretSelector Contains parameters about the secret object bound by user. + // +kubebuilder:validation:Required + SecretSelector SecretSelector `json:"secretSelector,omitempty"` + + // Permissions is the list of roles that user has in the specified database. + // +optional + Permissions []UserPermission `json:"permissions,omitempty"` +} + +type UserOwner struct { + // ClusterName is the name of cluster. + ClusterName string `json:"clusterName,omitempty"` + + // NameSpace is the nameSpace of cluster. + NameSpace string `json:"nameSpace,omitempty"` +} + +type SecretSelector struct { + // SecretName is the name of secret object. + SecretName string `json:"secretName,omitempty"` + + // SecretKey is the key of secret object. + SecretKey string `json:"secretKey,omitempty"` +} + +// UserPermission defines a UserPermission permission. +type UserPermission struct { + // Database is the grants database. + // +kubebuilder:validation:Pattern="^([*]|[A-Za-z0-9_]{2,26})$" + Database string `json:"database,omitempty"` + + // Tables is the grants tables inside the database. + // +kubebuilder:validation:MinItems=1 + Tables []string `json:"tables,omitempty"` + + // Privileges is the normal privileges(comma delimited, such as "SELECT,CREATE"). + // Optional parameters can refer to: https://dev.mysql.com/doc/refman/5.7/en/privileges-provided.html. + // +kubebuilder:validation:MinItems=1 + Privileges []string `json:"privileges,omitempty"` +} + +// UserStatus defines the observed state of MysqlUser. +type UserStatus struct { + // Conditions represents the MysqlUser resource conditions list. + // +optional + Conditions []MySQLUserCondition `json:"conditions,omitempty"` + + // AllowedHosts contains the list of hosts that the user is allowed to connect from. + AllowedHosts []string `json:"allowedHosts,omitempty"` +} + +// MysqlUserConditionType defines the condition types of a MysqlUser resource. +type MysqlUserConditionType string + +const ( + // MySQLUserReady means the MySQL user is ready when database exists. + MySQLUserReady MysqlUserConditionType = "Ready" +) + +// MySQLUserCondition defines the condition struct for a MysqlUser resource. +type MySQLUserCondition struct { + // Type of MysqlUser condition. + Type MysqlUserConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status corev1.ConditionStatus `json:"status"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + // The reason for the condition's last transition. + Reason string `json:"reason"` + // A human readable message indicating details about the transition. + Message string `json:"message"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:subresource:finalizers +// MysqlUser is the Schema for the users API. +type MysqlUser struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UserSpec `json:"spec,omitempty"` + Status UserStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true +// MysqlUserList contains a list of MysqlUser. +type MysqlUserList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MysqlUser `json:"items"` +} + +func init() { + SchemeBuilder.Register(&MysqlUser{}, &MysqlUserList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 220c4523..e5af7d93 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -287,6 +287,23 @@ func (in *MetricsOpts) DeepCopy() *MetricsOpts { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MySQLUserCondition) DeepCopyInto(out *MySQLUserCondition) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MySQLUserCondition. +func (in *MySQLUserCondition) DeepCopy() *MySQLUserCondition { + if in == nil { + return nil + } + out := new(MySQLUserCondition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in MysqlConf) DeepCopyInto(out *MysqlConf) { { @@ -331,6 +348,65 @@ func (in *MysqlOpts) DeepCopy() *MysqlOpts { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MysqlUser) DeepCopyInto(out *MysqlUser) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MysqlUser. +func (in *MysqlUser) DeepCopy() *MysqlUser { + if in == nil { + return nil + } + out := new(MysqlUser) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MysqlUser) 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 *MysqlUserList) DeepCopyInto(out *MysqlUserList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]MysqlUser, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MysqlUserList. +func (in *MysqlUserList) DeepCopy() *MysqlUserList { + if in == nil { + return nil + } + out := new(MysqlUserList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *MysqlUserList) 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 *NodeCondition) DeepCopyInto(out *NodeCondition) { *out = *in @@ -436,6 +512,117 @@ func (in *PodSpec) DeepCopy() *PodSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretSelector) DeepCopyInto(out *SecretSelector) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretSelector. +func (in *SecretSelector) DeepCopy() *SecretSelector { + if in == nil { + return nil + } + out := new(SecretSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserOwner) DeepCopyInto(out *UserOwner) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserOwner. +func (in *UserOwner) DeepCopy() *UserOwner { + if in == nil { + return nil + } + out := new(UserOwner) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserPermission) DeepCopyInto(out *UserPermission) { + *out = *in + if in.Tables != nil { + in, out := &in.Tables, &out.Tables + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Privileges != nil { + in, out := &in.Privileges, &out.Privileges + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserPermission. +func (in *UserPermission) DeepCopy() *UserPermission { + if in == nil { + return nil + } + out := new(UserPermission) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserSpec) DeepCopyInto(out *UserSpec) { + *out = *in + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.UserOwner = in.UserOwner + out.SecretSelector = in.SecretSelector + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = make([]UserPermission, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserSpec. +func (in *UserSpec) DeepCopy() *UserSpec { + if in == nil { + return nil + } + out := new(UserSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *UserStatus) DeepCopyInto(out *UserStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]MySQLUserCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.AllowedHosts != nil { + in, out := &in.AllowedHosts, &out.AllowedHosts + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UserStatus. +func (in *UserStatus) DeepCopy() *UserStatus { + if in == nil { + return nil + } + out := new(UserStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *XenonOpts) DeepCopyInto(out *XenonOpts) { *out = *in diff --git a/config/crd/bases/mysql.radondb.com_mysqlusers.yaml b/config/crd/bases/mysql.radondb.com_mysqlusers.yaml new file mode 100644 index 00000000..840853f2 --- /dev/null +++ b/config/crd/bases/mysql.radondb.com_mysqlusers.yaml @@ -0,0 +1,156 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: mysqlusers.mysql.radondb.com +spec: + group: mysql.radondb.com + names: + kind: MysqlUser + listKind: MysqlUserList + plural: mysqlusers + singular: mysqluser + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MysqlUser is the Schema for the users API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: UserSpec defines the desired state of User. + properties: + hosts: + description: Hosts is the grants hosts. + items: + type: string + minItems: 1 + type: array + permissions: + description: Permissions is the list of roles that user has in the + specified database. + items: + description: UserPermission defines a UserPermission permission. + properties: + database: + description: Database is the grants database. + pattern: ^([*]|[A-Za-z0-9_]{2,26})$ + type: string + privileges: + description: 'Privileges is the normal privileges(comma delimited, + such as "SELECT,CREATE"). Optional parameters can refer to: + https://dev.mysql.com/doc/refman/5.7/en/privileges-provided.html.' + items: + type: string + minItems: 1 + type: array + tables: + description: Tables is the grants tables inside the database. + items: + type: string + minItems: 1 + type: array + type: object + type: array + secretSelector: + description: SecretSelector Contains parameters about the secret object + bound by user. + properties: + secretKey: + description: SecretKey is the key of secret object. + type: string + secretName: + description: SecretName is the name of secret object. + type: string + type: object + user: + description: Username is the name of user to be operated. This field + should be immutable. + pattern: ^[A-Za-z0-9_]{2,26}$ + type: string + userOwner: + description: UserOwner Contains parameters about the cluster bound + by user. + properties: + clusterName: + description: ClusterName is the name of cluster. + type: string + nameSpace: + description: NameSpace is the nameSpace of cluster. + type: string + type: object + type: object + status: + description: UserStatus defines the observed state of MysqlUser. + properties: + allowedHosts: + description: AllowedHosts contains the list of hosts that the user + is allowed to connect from. + items: + type: string + type: array + conditions: + description: Conditions represents the MysqlUser resource conditions + list. + items: + description: MySQLUserCondition defines the condition struct for + a MysqlUser resource. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of MysqlUser condition. + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index a704f9b0..59c61f37 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -4,6 +4,7 @@ resources: - bases/mysql.radondb.com_clusters.yaml - bases/mysql.radondb.com_backups.yaml +- bases/mysql.radondb.com_mysqlusers.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dd05c4dc..d5f5340f 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -122,6 +122,32 @@ rules: - get - patch - update +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers/finalizers + verbs: + - update +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers/status + verbs: + - get + - patch + - update - apiGroups: - policy resources: diff --git a/controllers/suite_test.go b/controllers/suite_test.go index ea5334e3..10da7288 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -67,6 +67,9 @@ var _ = BeforeSuite(func() { err = mysqlv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = mysqlv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) From 5a28a54ff486edddba14511916201ae32de0e240 Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Wed, 22 Sep 2021 11:29:52 +0800 Subject: [PATCH 2/6] *: Add sample yaml for user management crd. --- config/samples/mysql_v1alpha1_mysqluser.yaml | 23 ++++++++++++++++++++ config/samples/mysqluser_secret.yaml | 12 ++++++++++ 2 files changed, 35 insertions(+) create mode 100644 config/samples/mysql_v1alpha1_mysqluser.yaml create mode 100644 config/samples/mysqluser_secret.yaml diff --git a/config/samples/mysql_v1alpha1_mysqluser.yaml b/config/samples/mysql_v1alpha1_mysqluser.yaml new file mode 100644 index 00000000..d8c4c304 --- /dev/null +++ b/config/samples/mysql_v1alpha1_mysqluser.yaml @@ -0,0 +1,23 @@ +apiVersion: mysql.radondb.com/v1alpha1 +kind: MysqlUser +metadata: + name: sample-user-cr +spec: + ## User to operate. + user: sample_user + hosts: + - "%" + permissions: + - database: "*" + tables: + - "*" + privileges: + - SELECT + ## Specify the cluster where the user is located. + userOwner: + clusterName: sample + nameSpace: default + ## Specify the secret object for user. + secretSelector: + secretName: sample-user-password + secretKey: pwdForSample diff --git a/config/samples/mysqluser_secret.yaml b/config/samples/mysqluser_secret.yaml new file mode 100644 index 00000000..fb9b7707 --- /dev/null +++ b/config/samples/mysqluser_secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + # Corresponding to the user's secretName + name: sample-user-password +data: + # The key corresponding to the user's secretKey + # RadonDB@123 + pwdForSample: UmFkb25EQkAxMjM= + # A secret may store many passwords like this, so different keys are needed to distinguish + # pwdForSample2: + # pwdForSample3: From c9cc3691c48ab8950016cce144829352bf3cc9e4 Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Wed, 22 Sep 2021 11:32:33 +0800 Subject: [PATCH 3/6] *: Support installing user crd when installing operator. --- .../crds/mysql.radondb.com_mysqlusers.yaml | 156 ++++++++++++++++++ .../templates/cluster_rbac.yaml | 26 +++ 2 files changed, 182 insertions(+) create mode 100644 charts/mysql-operator/crds/mysql.radondb.com_mysqlusers.yaml diff --git a/charts/mysql-operator/crds/mysql.radondb.com_mysqlusers.yaml b/charts/mysql-operator/crds/mysql.radondb.com_mysqlusers.yaml new file mode 100644 index 00000000..840853f2 --- /dev/null +++ b/charts/mysql-operator/crds/mysql.radondb.com_mysqlusers.yaml @@ -0,0 +1,156 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: mysqlusers.mysql.radondb.com +spec: + group: mysql.radondb.com + names: + kind: MysqlUser + listKind: MysqlUserList + plural: mysqlusers + singular: mysqluser + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: MysqlUser is the Schema for the users API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: UserSpec defines the desired state of User. + properties: + hosts: + description: Hosts is the grants hosts. + items: + type: string + minItems: 1 + type: array + permissions: + description: Permissions is the list of roles that user has in the + specified database. + items: + description: UserPermission defines a UserPermission permission. + properties: + database: + description: Database is the grants database. + pattern: ^([*]|[A-Za-z0-9_]{2,26})$ + type: string + privileges: + description: 'Privileges is the normal privileges(comma delimited, + such as "SELECT,CREATE"). Optional parameters can refer to: + https://dev.mysql.com/doc/refman/5.7/en/privileges-provided.html.' + items: + type: string + minItems: 1 + type: array + tables: + description: Tables is the grants tables inside the database. + items: + type: string + minItems: 1 + type: array + type: object + type: array + secretSelector: + description: SecretSelector Contains parameters about the secret object + bound by user. + properties: + secretKey: + description: SecretKey is the key of secret object. + type: string + secretName: + description: SecretName is the name of secret object. + type: string + type: object + user: + description: Username is the name of user to be operated. This field + should be immutable. + pattern: ^[A-Za-z0-9_]{2,26}$ + type: string + userOwner: + description: UserOwner Contains parameters about the cluster bound + by user. + properties: + clusterName: + description: ClusterName is the name of cluster. + type: string + nameSpace: + description: NameSpace is the nameSpace of cluster. + type: string + type: object + type: object + status: + description: UserStatus defines the observed state of MysqlUser. + properties: + allowedHosts: + description: AllowedHosts contains the list of hosts that the user + is allowed to connect from. + items: + type: string + type: array + conditions: + description: Conditions represents the MysqlUser resource conditions + list. + items: + description: MySQLUserCondition defines the condition struct for + a MysqlUser resource. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: The last time this condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of MysqlUser condition. + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/mysql-operator/templates/cluster_rbac.yaml b/charts/mysql-operator/templates/cluster_rbac.yaml index 0dedf81f..a991b237 100644 --- a/charts/mysql-operator/templates/cluster_rbac.yaml +++ b/charts/mysql-operator/templates/cluster_rbac.yaml @@ -99,6 +99,32 @@ rules: - get - patch - update +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers/finalizers + verbs: + - update +- apiGroups: + - mysql.radondb.com + resources: + - mysqlusers/status + verbs: + - get + - patch + - update - apiGroups: - policy resources: From fd644d788a077b5f197c188929fde96619767487 Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Wed, 22 Sep 2021 11:37:04 +0800 Subject: [PATCH 4/6] *: Support root account remote login for user management. #189 --- config/samples/mysql_v1alpha1_cluster.yaml | 2 +- sidecar/config.go | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/config/samples/mysql_v1alpha1_cluster.yaml b/config/samples/mysql_v1alpha1_cluster.yaml index 03d6c5d9..3ee42c01 100644 --- a/config/samples/mysql_v1alpha1_cluster.yaml +++ b/config/samples/mysql_v1alpha1_cluster.yaml @@ -14,7 +14,7 @@ spec: # restoreFrom: mysqlOpts: - rootPassword: "" + rootPassword: "RadonDB@123" rootHost: localhost user: radondb_usr password: RadonDB@123 diff --git a/sidecar/config.go b/sidecar/config.go index 8a08c9f3..ab07dda7 100644 --- a/sidecar/config.go +++ b/sidecar/config.go @@ -376,6 +376,8 @@ func (cfg *Config) buildInitSql() []byte { CREATE DATABASE IF NOT EXISTS %s; DROP user IF EXISTS 'root'@'127.0.0.1'; GRANT ALL ON *.* TO 'root'@'127.0.0.1' IDENTIFIED BY '%s' with grant option; +DROP user IF EXISTS 'root'@'%%'; +GRANT ALL ON *.* TO 'root'@'%%' IDENTIFIED BY '%s' with grant option; DROP user IF EXISTS '%s'@'%%'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '%s'@'%%' IDENTIFIED BY '%s'; DROP user IF EXISTS '%s'@'%%'; @@ -385,7 +387,7 @@ GRANT SUPER, PROCESS, RELOAD, CREATE, SELECT ON *.* TO '%s'@'%%' IDENTIFIED BY ' DROP user IF EXISTS '%s'@'%%'; GRANT ALL ON %s.* TO '%s'@'%%' IDENTIFIED BY '%s'; FLUSH PRIVILEGES; -`, cfg.Database, cfg.RootPassword, cfg.ReplicationUser, cfg.ReplicationUser, cfg.ReplicationPassword, +`, cfg.Database, cfg.RootPassword, cfg.RootPassword, cfg.ReplicationUser, cfg.ReplicationUser, cfg.ReplicationPassword, cfg.MetricsUser, cfg.MetricsUser, cfg.MetricsPassword, cfg.OperatorUser, cfg.OperatorUser, cfg.OperatorPassword, cfg.User, cfg.Database, cfg.User, cfg.Password) From 8b55b5ea55a76c2ceb42ed5099be08c7214f3635 Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Wed, 22 Sep 2021 11:26:50 +0800 Subject: [PATCH 5/6] *: add user management related operations. --- go.mod | 1 + internal/query.go | 29 ++++++++- internal/sql_runner.go | 138 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 166 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ed8b78d2..48fd4307 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/blang/semver v3.5.1+incompatible github.com/go-ini/ini v1.62.0 github.com/go-sql-driver/mysql v1.6.0 + github.com/go-test/deep v1.0.7 // indirect github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 github.com/imdario/mergo v0.3.12 github.com/onsi/ginkgo v1.16.4 diff --git a/internal/query.go b/internal/query.go index 5c514864..f8b4f96a 100644 --- a/internal/query.go +++ b/internal/query.go @@ -17,8 +17,8 @@ limitations under the License. package internal import ( - "strings" "errors" + "strings" ) // Query contains a escaped query string with variables marked with a question mark (?) and a slice @@ -53,3 +53,30 @@ func NewQuery(q string, args ...interface{}) Query { args: args, } } + +// ConcatenateQueries concatenates the provided queries into a single query. +func ConcatenateQueries(queries ...Query) Query { + args := []interface{}{} + query := "" + + for _, pq := range queries { + if query != "" { + if !strings.HasSuffix(query, "\n") { + query += "\n" + } + } + + query += pq.escapedQuery + args = append(args, pq.args...) + } + + return NewQuery(query, args...) +} + +// BuildAtomicQuery concatenates the provided queries into a single query wrapped in a BEGIN COMMIT block. +func BuildAtomicQuery(queries ...Query) Query { + queries = append([]Query{NewQuery("BEGIN")}, queries...) + queries = append(queries, NewQuery("COMMIT")) + + return ConcatenateQueries(queries...) +} diff --git a/internal/sql_runner.go b/internal/sql_runner.go index c3c8cb7b..6a1ed21a 100644 --- a/internal/sql_runner.go +++ b/internal/sql_runner.go @@ -19,6 +19,7 @@ package internal import ( "context" "database/sql" + "errors" "fmt" "strconv" "strings" @@ -84,7 +85,6 @@ func NewConfigFromClusterKey(c client.Client, clusterKey client.ObjectKey, userN Port: utils.MysqlPort, }, nil - case utils.RootUser: password, ok := secret.Data["root-password"] if !ok { @@ -323,3 +323,139 @@ func columnValue(scanArgs []interface{}, slaveCols []string, colName string) str return string(*scanArgs[columnIndex].(*sql.RawBytes)) } + +// CreateUserIfNotExists creates a user if it doesn't already exist and it gives it the specified permissions. +func (s sqlRunner) CreateUserIfNotExists( + user, pass string, allowedHosts []string, permissions []apiv1alpha1.UserPermission, +) error { + + // Throw error if there are no allowed hosts. + if len(allowedHosts) == 0 { + return errors.New("no allowedHosts specified") + } + + queries := []Query{ + getCreateUserQuery(user, pass, allowedHosts), + // todo: getAlterUserQuery + } + + if len(permissions) > 0 { + queries = append(queries, permissionsToQuery(permissions, user, allowedHosts)) + } + + query := BuildAtomicQuery(queries...) + + if err := s.QueryExec(query); err != nil { + return fmt.Errorf("failed to configure user (user/pass/access), err: %s", err) + } + + return nil +} + +func getCreateUserQuery(user, pwd string, allowedHosts []string) Query { + idsTmpl, idsArgs := getUsersIdentification(user, &pwd, allowedHosts) + + return NewQuery(fmt.Sprintf("CREATE USER IF NOT EXISTS%s", idsTmpl), idsArgs...) +} + +func getUsersIdentification(user string, pwd *string, allowedHosts []string) (ids string, args []interface{}) { + for i, host := range allowedHosts { + // Add comma if more than one allowed hosts are used. + if i > 0 { + ids += "," + } + + if pwd != nil { + ids += " ?@? IDENTIFIED BY ?" + args = append(args, user, host, *pwd) + } else { + ids += " ?@?" + args = append(args, user, host) + } + } + + return ids, args +} + +// DropUser removes a MySQL user if it exists, along with its privileges. +func (s sqlRunner) DropUser(user, host string) error { + query := NewQuery("DROP USER IF EXISTS ?@?;", user, host) + + if err := s.QueryExec(query); err != nil { + return fmt.Errorf("failed to delete user, err: %s", err) + } + + return nil +} + +func permissionsToQuery(permissions []apiv1alpha1.UserPermission, user string, allowedHosts []string) Query { + permQueries := []Query{} + + for _, perm := range permissions { + // If you wish to grant permissions on all tables, you should explicitly use "*". + for _, table := range perm.Tables { + args := []interface{}{} + + escPerms := []string{} + for _, perm := range perm.Privileges { + escPerms = append(escPerms, Escape(perm)) + } + + schemaTable := fmt.Sprintf("%s.%s", escapeID(perm.Database), escapeID(table)) + + // Build GRANT query. + idsTmpl, idsArgs := getUsersIdentification(user, nil, allowedHosts) + + query := "GRANT " + strings.Join(escPerms, ", ") + " ON " + schemaTable + " TO" + idsTmpl + args = append(args, idsArgs...) + + permQueries = append(permQueries, NewQuery(query, args...)) + } + } + + return ConcatenateQueries(permQueries...) +} + +func escapeID(id string) string { + if id == "*" { + return id + } + + // don't allow using ` in id name + id = strings.ReplaceAll(id, "`", "") + + return fmt.Sprintf("`%s`", id) +} + +// Escape escapes a string. +func Escape(sql string) string { + dest := make([]byte, 0, 2*len(sql)) + var escape byte + for i := 0; i < len(sql); i++ { + escape = 0 + switch sql[i] { + case 0: /* Must be escaped for 'mysql' */ + escape = '0' + case '\n': /* Must be escaped for logs */ + escape = 'n' + case '\r': + escape = 'r' + case '\\': + escape = '\\' + case '\'': + escape = '\'' + case '"': /* Better safe than sorry */ + escape = '"' + case '\032': /* This gives problems on Win32 */ + escape = 'Z' + } + + if escape != 0 { + dest = append(dest, '\\', escape) + } else { + dest = append(dest, sql[i]) + } + } + + return string(dest) +} From 582192a0b3561bcf6feee3a9a909de2020e3dbcb Mon Sep 17 00:00:00 2001 From: runkecheng <1131648942@qq.com> Date: Thu, 23 Sep 2021 11:25:19 +0800 Subject: [PATCH 6/6] *: Support user management through crd. #175 --- cluster/cluster.go | 11 +- cmd/manager/main.go | 9 + controllers/mysqluser_controller.go | 279 ++++++++++++++++++++++++++++ internal/sql_runner.go | 20 +- mysqluser/mysqluser.go | 74 ++++++++ mysqluser/status.go | 84 +++++++++ utils/common.go | 22 +++ 7 files changed, 488 insertions(+), 11 deletions(-) create mode 100644 controllers/mysqluser_controller.go create mode 100644 mysqluser/mysqluser.go create mode 100644 mysqluser/status.go diff --git a/cluster/cluster.go b/cluster/cluster.go index f42c7b9b..d6340261 100644 --- a/cluster/cluster.go +++ b/cluster/cluster.go @@ -334,7 +334,16 @@ func sizeToBytes(s string) (uint64, error) { return 0, fmt.Errorf("'%s' format error, must be a positive integer with a unit of measurement like K, M or G", s) } -// GetClusterKey returns the MysqlUser's Cluster key. +// IsClusterKind for the given kind checks if CRD kind is for Cluster CRD. +func IsClusterKind(kind string) bool { + switch kind { + case "Cluster", "cluster", "clusters": + return true + } + return false +} + +// GetClusterKey returns the MysqlUser's MySQLCluster key. func (c *Cluster) GetClusterKey() client.ObjectKey { return client.ObjectKey{ Name: c.Name, diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 4bee7e5c..3e93cd3b 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -105,6 +105,15 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Backup") os.Exit(1) } + if err = (&controllers.MysqlUserReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("controller.mysqluser"), + SQLRunnerFactory: internal.NewSQLRunner, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MysqlUser") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/controllers/mysqluser_controller.go b/controllers/mysqluser_controller.go new file mode 100644 index 00000000..d035e87a --- /dev/null +++ b/controllers/mysqluser_controller.go @@ -0,0 +1,279 @@ +/* +Copyright 2021 RadonDB. + +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 controllers + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/go-test/deep" + "github.com/presslabs/controller-util/meta" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" + mysqlcluster "github.com/radondb/radondb-mysql-kubernetes/cluster" + "github.com/radondb/radondb-mysql-kubernetes/internal" + mysqluser "github.com/radondb/radondb-mysql-kubernetes/mysqluser" + "github.com/radondb/radondb-mysql-kubernetes/utils" +) + +// MysqlUserReconciler reconciles a MysqlUser object. +type MysqlUserReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder + + // MySQL query runner. + internal.SQLRunnerFactory +} + +var ( + userLog = log.Log.WithName("controller").WithName("mysqluser") + userFinalizer = "mysqluser-finalizer" +) + +//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// Modify the Reconcile function to compare the state specified by +// the MysqlUser object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the MysqlUser. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile +func (r *MysqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // your logic here. + user := mysqluser.New(&apiv1alpha1.MysqlUser{}) + + err := r.Get(ctx, req.NamespacedName, user.Unwrap()) + if err != nil { + if errors.IsNotFound(err) { + // Object not found, return. Created objects are automatically garbage collected. + // For additional cleanup logic use finalizers. + userLog.Info("mysql user not found, maybe deleted") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + oldStatus := user.Status.DeepCopy() + + // If mysql user has been deleted then delete it from mysql cluster. + if !user.ObjectMeta.DeletionTimestamp.IsZero() { + return ctrl.Result{}, r.removeUser(ctx, user) + } + + // Write the desired status into mysql cluster. + ruErr := r.reconcileUserInCluster(ctx, user) + if err := r.updateStatusAndErr(ctx, user, oldStatus, ruErr); err != nil { + return ctrl.Result{}, err + } + + // Enqueue the resource again after to keep the resource up to date in mysql + // in case is changed directly into mysql. + return ctrl.Result{ + Requeue: true, + RequeueAfter: 2 * time.Minute, + }, nil +} + +// removeUser deletes the corresponding user in mysql before mysql user cr is deleted. +func (r *MysqlUserReconciler) removeUser(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error { + // The resource has been deleted. + if meta.HasFinalizer(&mysqlUser.ObjectMeta, userFinalizer) { + // Drop the user if the finalizer is still present. + if err := r.dropUserFromDB(ctx, mysqlUser); err != nil { + return err + } + + meta.RemoveFinalizer(&mysqlUser.ObjectMeta, userFinalizer) + + // Update resource so it will remove the finalizer. + if err := r.Update(ctx, mysqlUser.Unwrap()); err != nil { + return err + } + } + return nil +} + +// reconcileUserInCluster reconcileUserInCluster creates or updates users in mysql. +// Proceed as follows: +// 1. Create users and authorize according to the Spec. +// 2. Remove the host that does not exist in the spec from MySQL. +// 3. Make sure mysqluser has finalizer set. +// 4. Update status and condition. +func (r *MysqlUserReconciler) reconcileUserInCluster(ctx context.Context, mysqlUser *mysqluser.MysqlUser) (err error) { + // Catch the error and set the failed status. + defer setFailedStatus(&err, mysqlUser) + + // Reconcile the mysqlUser into mysql. + if err = r.reconcileUserInDB(ctx, mysqlUser); err != nil { + return + } + + // Add finalizer if is not added on the resource. + if !meta.HasFinalizer(&mysqlUser.ObjectMeta, userFinalizer) { + meta.AddFinalizer(&mysqlUser.ObjectMeta, userFinalizer) + if err = r.Update(ctx, mysqlUser.Unwrap()); err != nil { + return + } + } + + // Update status for allowedHosts if needed, mark that status need to be updated. + if !reflect.DeepEqual(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts) { + mysqlUser.Status.AllowedHosts = mysqlUser.Spec.Hosts + } + + // Update the status according to the result. + mysqlUser.UpdateStatusCondition( + apiv1alpha1.MySQLUserReady, corev1.ConditionTrue, + mysqluser.ProvisionSucceededReason, "The user provisioning has succeeded.", + ) + + return +} + +// reconcileUserInDB creates and authorizes(If needed) users based on +// spec.Hosts, and then deletes users that do not exist in spec.Hosts. +func (r *MysqlUserReconciler) reconcileUserInDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error { + sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey( + r.Client, mysqlUser.GetClusterKey(), utils.RootUser, utils.LeaderHost)) + if err != nil { + return err + } + defer closeConn() + + secret := &corev1.Secret{} + secretKey := client.ObjectKey{Name: mysqlUser.Spec.SecretSelector.SecretName, Namespace: mysqlUser.Namespace} + + if err := r.Get(ctx, secretKey, secret); err != nil { + return err + } + + password := string(secret.Data[mysqlUser.Spec.SecretSelector.SecretKey]) + if password == "" { + return fmt.Errorf("the MySQL user's password must not be empty") + } + + // Create/Update user in database. + userLog.Info("creating mysql user", "key", mysqlUser.GetKey(), "username", mysqlUser.Spec.User, "cluster", mysqlUser.GetClusterKey()) + if err := internal.CreateUserIfNotExists(sqlRunner, mysqlUser.Spec.User, password, mysqlUser.Spec.Hosts, + mysqlUser.Spec.Permissions); err != nil { + return err + } + + // Remove allowed hosts for user. + toRemove := stringDiffIn(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts) + for _, host := range toRemove { + if err := internal.DropUser(sqlRunner, mysqlUser.Spec.User, host); err != nil { + return err + } + } + + return nil +} + +func stringDiffIn(actual, desired []string) []string { + diff := []string{} + for _, aStr := range actual { + // If is not in the desired list remove it. + if _, exists := stringIn(aStr, desired); !exists { + diff = append(diff, aStr) + } + } + + return diff +} + +func stringIn(str string, strs []string) (int, bool) { + for i, s := range strs { + if s == str { + return i, true + } + } + return 0, false +} + +func (r *MysqlUserReconciler) dropUserFromDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error { + sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey( + r.Client, mysqlUser.GetClusterKey(), utils.RootUser, utils.LeaderHost)) + if errors.IsNotFound(err) { + // If the mysql cluster does not exists then we can safely assume that + // the user is deleted so exist successfully. + statusErr, ok := err.(*errors.StatusError) + if ok && mysqlcluster.IsClusterKind(statusErr.Status().Details.Kind) { + // It seems the cluster is not to be found, so we assume it has been deleted. + return nil + } + } + + if err != nil { + return err + } + defer closeConn() + + for _, host := range mysqlUser.Status.AllowedHosts { + userLog.Info("removing user from mysql cluster", "key", mysqlUser.GetKey(), "username", mysqlUser.Spec.User, "cluster", mysqlUser.GetClusterKey()) + if err := internal.DropUser(sqlRunner, mysqlUser.Spec.User, host); err != nil { + return err + } + } + return nil +} + +// updateStatusAndErr update the status and catch create/update error. +func (r *MysqlUserReconciler) updateStatusAndErr(ctx context.Context, mysqlUser *mysqluser.MysqlUser, oldStatus *apiv1alpha1.UserStatus, cuErr error) error { + if !reflect.DeepEqual(oldStatus, &mysqlUser.Status) { + userLog.Info("update mysql user status", "key", mysqlUser.GetKey(), "diff", deep.Equal(oldStatus, &mysqlUser.Status)) + if err := r.Status().Update(ctx, mysqlUser.Unwrap()); err != nil { + if cuErr != nil { + return fmt.Errorf("failed to update status: %s, previous error was: %s", err, cuErr) + } + return err + } + } + + return cuErr +} + +func setFailedStatus(err *error, mysqlUser *mysqluser.MysqlUser) { + if *err != nil { + mysqlUser.UpdateStatusCondition( + apiv1alpha1.MySQLUserReady, corev1.ConditionFalse, + mysqluser.ProvisionFailedReason, fmt.Sprintf("The user provisioning has failed: %s", *err), + ) + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *MysqlUserReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&apiv1alpha1.MysqlUser{}). + Complete(r) +} diff --git a/internal/sql_runner.go b/internal/sql_runner.go index 6a1ed21a..a836acd9 100644 --- a/internal/sql_runner.go +++ b/internal/sql_runner.go @@ -325,27 +325,27 @@ func columnValue(scanArgs []interface{}, slaveCols []string, colName string) str } // CreateUserIfNotExists creates a user if it doesn't already exist and it gives it the specified permissions. -func (s sqlRunner) CreateUserIfNotExists( - user, pass string, allowedHosts []string, permissions []apiv1alpha1.UserPermission, +func CreateUserIfNotExists( + sqlRunner SQLRunner, user, pass string, hosts []string, permissions []apiv1alpha1.UserPermission, ) error { // Throw error if there are no allowed hosts. - if len(allowedHosts) == 0 { + if len(hosts) == 0 { return errors.New("no allowedHosts specified") } queries := []Query{ - getCreateUserQuery(user, pass, allowedHosts), - // todo: getAlterUserQuery + getCreateUserQuery(user, pass, hosts), + // todo: getAlterUserQuery. } if len(permissions) > 0 { - queries = append(queries, permissionsToQuery(permissions, user, allowedHosts)) + queries = append(queries, permissionsToQuery(permissions, user, hosts)) } query := BuildAtomicQuery(queries...) - if err := s.QueryExec(query); err != nil { + if err := sqlRunner.QueryExec(query); err != nil { return fmt.Errorf("failed to configure user (user/pass/access), err: %s", err) } @@ -378,10 +378,10 @@ func getUsersIdentification(user string, pwd *string, allowedHosts []string) (id } // DropUser removes a MySQL user if it exists, along with its privileges. -func (s sqlRunner) DropUser(user, host string) error { +func DropUser(sqlRunner SQLRunner, user, host string) error { query := NewQuery("DROP USER IF EXISTS ?@?;", user, host) - if err := s.QueryExec(query); err != nil { + if err := sqlRunner.QueryExec(query); err != nil { return fmt.Errorf("failed to delete user, err: %s", err) } @@ -421,7 +421,7 @@ func escapeID(id string) string { return id } - // don't allow using ` in id name + // don't allow using ` in id name. id = strings.ReplaceAll(id, "`", "") return fmt.Sprintf("`%s`", id) diff --git a/mysqluser/mysqluser.go b/mysqluser/mysqluser.go new file mode 100644 index 00000000..1fb833af --- /dev/null +++ b/mysqluser/mysqluser.go @@ -0,0 +1,74 @@ +/* +Copyright 2021 RadonDB. + +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 mysqluser + +import ( + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + apiv1alhpa1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" +) + +const ( + // ProvisionFailedReason is the condition reason when MysqlUser provisioning + // has failed. + ProvisionFailedReason = "ProvisionFailed" + // ProvisionInProgressReason is the reason when MysqlUser provisioning has + // started. + ProvisionInProgressReason = "ProvisionInProgress" + + // ProvisionSucceededReason the reason used when provision was successful. + ProvisionSucceededReason = "ProvisionSucceeded" +) + +// MysqlUser is a type wrapper over MysqlUser that contains the Business logic. +type MysqlUser struct { + *apiv1alhpa1.MysqlUser +} + +// New returns a wraper object over MysqlUser. +func New(mysqlUser *apiv1alhpa1.MysqlUser) *MysqlUser { + return &MysqlUser{ + MysqlUser: mysqlUser, + } +} + +// Unwrap returns the api MysqlUser object. +func (u *MysqlUser) Unwrap() *apiv1alhpa1.MysqlUser { + return u.MysqlUser +} + +// GetClusterKey returns the MysqlUser's MySQLCluster key. +func (u *MysqlUser) GetClusterKey() client.ObjectKey { + ns := u.Spec.UserOwner.NameSpace + if ns == "" { + ns = u.Namespace + } + + return client.ObjectKey{ + Name: u.Spec.UserOwner.ClusterName, + Namespace: ns, + } +} + +// GetKey return the user key. Usually used for logging or for runtime.Client.Get as key. +func (u *MysqlUser) GetKey() client.ObjectKey { + return types.NamespacedName{ + Namespace: u.Namespace, + Name: u.Name, + } +} diff --git a/mysqluser/status.go b/mysqluser/status.go new file mode 100644 index 00000000..c08dc851 --- /dev/null +++ b/mysqluser/status.go @@ -0,0 +1,84 @@ +/* +Copyright 2021 RadonDB. + +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 mysqluser + +import ( + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + apiv1alhpa1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1" +) + +// UpdateStatusCondition sets the condition to a status. +// for example Ready condition to True, or False. +func (u *MysqlUser) UpdateStatusCondition( + condType apiv1alhpa1.MysqlUserConditionType, + status corev1.ConditionStatus, reason, message string, +) ( + cond *apiv1alhpa1.MySQLUserCondition, changed bool, +) { + t := metav1.NewTime(time.Now()) + + existingCondition, exists := u.ConditionExists(condType) + if !exists { + newCondition := apiv1alhpa1.MySQLUserCondition{ + Type: condType, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: t, + LastUpdateTime: t, + } + u.Status.Conditions = append(u.Status.Conditions, newCondition) + + return &newCondition, true + } + + if status != existingCondition.Status { + existingCondition.LastTransitionTime = t + changed = true + } + + if message != existingCondition.Message || reason != existingCondition.Reason { + existingCondition.LastUpdateTime = t + changed = true + } + + existingCondition.Status = status + existingCondition.Message = message + existingCondition.Reason = reason + + return existingCondition, changed +} + +// ConditionExists returns a condition and whether it exists. +func (u *MysqlUser) ConditionExists( + ct apiv1alhpa1.MysqlUserConditionType, +) ( + *apiv1alhpa1.MySQLUserCondition, bool, +) { + for i := range u.Status.Conditions { + cond := &u.Status.Conditions[i] + if cond.Type == ct { + return cond, true + } + } + + return nil, false +} diff --git a/utils/common.go b/utils/common.go index 4f48ede6..c4b3923c 100644 --- a/utils/common.go +++ b/utils/common.go @@ -98,3 +98,25 @@ func BuildBackupName() string { return fmt.Sprintf("backup_%v%v%v%v%v%v", cur_time.Year(), int(cur_time.Month()), cur_time.Day(), cur_time.Hour(), cur_time.Minute(), cur_time.Second()) } + +func StringDiffIn(actual, desired []string) []string { + diff := []string{} + for _, aStr := range actual { + // if is not in the desired list remove it. + if _, exists := stringIn(aStr, desired); !exists { + diff = append(diff, aStr) + } + } + + return diff +} + +func stringIn(str string, strs []string) (int, bool) { + for i, s := range strs { + if s == str { + return i, true + } + } + + return 0, false +}