Skip to content

Commit 582192a

Browse files
committed
*: Support user management through crd. #175
1 parent 8b55b5e commit 582192a

File tree

7 files changed

+488
-11
lines changed

7 files changed

+488
-11
lines changed

cluster/cluster.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,16 @@ func sizeToBytes(s string) (uint64, error) {
334334
return 0, fmt.Errorf("'%s' format error, must be a positive integer with a unit of measurement like K, M or G", s)
335335
}
336336

337-
// GetClusterKey returns the MysqlUser's Cluster key.
337+
// IsClusterKind for the given kind checks if CRD kind is for Cluster CRD.
338+
func IsClusterKind(kind string) bool {
339+
switch kind {
340+
case "Cluster", "cluster", "clusters":
341+
return true
342+
}
343+
return false
344+
}
345+
346+
// GetClusterKey returns the MysqlUser's MySQLCluster key.
338347
func (c *Cluster) GetClusterKey() client.ObjectKey {
339348
return client.ObjectKey{
340349
Name: c.Name,

cmd/manager/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,15 @@ func main() {
105105
setupLog.Error(err, "unable to create controller", "controller", "Backup")
106106
os.Exit(1)
107107
}
108+
if err = (&controllers.MysqlUserReconciler{
109+
Client: mgr.GetClient(),
110+
Scheme: mgr.GetScheme(),
111+
Recorder: mgr.GetEventRecorderFor("controller.mysqluser"),
112+
SQLRunnerFactory: internal.NewSQLRunner,
113+
}).SetupWithManager(mgr); err != nil {
114+
setupLog.Error(err, "unable to create controller", "controller", "MysqlUser")
115+
os.Exit(1)
116+
}
108117
//+kubebuilder:scaffold:builder
109118

110119
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
Copyright 2021 RadonDB.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"reflect"
23+
"time"
24+
25+
"github.com/go-test/deep"
26+
"github.com/presslabs/controller-util/meta"
27+
corev1 "k8s.io/api/core/v1"
28+
"k8s.io/apimachinery/pkg/api/errors"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
"k8s.io/client-go/tools/record"
31+
ctrl "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/log"
34+
35+
apiv1alpha1 "github.com/radondb/radondb-mysql-kubernetes/api/v1alpha1"
36+
mysqlcluster "github.com/radondb/radondb-mysql-kubernetes/cluster"
37+
"github.com/radondb/radondb-mysql-kubernetes/internal"
38+
mysqluser "github.com/radondb/radondb-mysql-kubernetes/mysqluser"
39+
"github.com/radondb/radondb-mysql-kubernetes/utils"
40+
)
41+
42+
// MysqlUserReconciler reconciles a MysqlUser object.
43+
type MysqlUserReconciler struct {
44+
client.Client
45+
Scheme *runtime.Scheme
46+
Recorder record.EventRecorder
47+
48+
// MySQL query runner.
49+
internal.SQLRunnerFactory
50+
}
51+
52+
var (
53+
userLog = log.Log.WithName("controller").WithName("mysqluser")
54+
userFinalizer = "mysqluser-finalizer"
55+
)
56+
57+
//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers,verbs=get;list;watch;create;update;patch;delete
58+
//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers/status,verbs=get;update;patch
59+
//+kubebuilder:rbac:groups=mysql.radondb.com,resources=mysqlusers/finalizers,verbs=update
60+
61+
// Reconcile is part of the main kubernetes reconciliation loop which aims to
62+
// move the current state of the cluster closer to the desired state.
63+
// Modify the Reconcile function to compare the state specified by
64+
// the MysqlUser object against the actual cluster state, and then
65+
// perform operations to make the cluster state reflect the state specified by
66+
// the MysqlUser.
67+
//
68+
// For more details, check Reconcile and its Result here:
69+
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile
70+
func (r *MysqlUserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
71+
// your logic here.
72+
user := mysqluser.New(&apiv1alpha1.MysqlUser{})
73+
74+
err := r.Get(ctx, req.NamespacedName, user.Unwrap())
75+
if err != nil {
76+
if errors.IsNotFound(err) {
77+
// Object not found, return. Created objects are automatically garbage collected.
78+
// For additional cleanup logic use finalizers.
79+
userLog.Info("mysql user not found, maybe deleted")
80+
return ctrl.Result{}, nil
81+
}
82+
return ctrl.Result{}, err
83+
}
84+
85+
oldStatus := user.Status.DeepCopy()
86+
87+
// If mysql user has been deleted then delete it from mysql cluster.
88+
if !user.ObjectMeta.DeletionTimestamp.IsZero() {
89+
return ctrl.Result{}, r.removeUser(ctx, user)
90+
}
91+
92+
// Write the desired status into mysql cluster.
93+
ruErr := r.reconcileUserInCluster(ctx, user)
94+
if err := r.updateStatusAndErr(ctx, user, oldStatus, ruErr); err != nil {
95+
return ctrl.Result{}, err
96+
}
97+
98+
// Enqueue the resource again after to keep the resource up to date in mysql
99+
// in case is changed directly into mysql.
100+
return ctrl.Result{
101+
Requeue: true,
102+
RequeueAfter: 2 * time.Minute,
103+
}, nil
104+
}
105+
106+
// removeUser deletes the corresponding user in mysql before mysql user cr is deleted.
107+
func (r *MysqlUserReconciler) removeUser(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error {
108+
// The resource has been deleted.
109+
if meta.HasFinalizer(&mysqlUser.ObjectMeta, userFinalizer) {
110+
// Drop the user if the finalizer is still present.
111+
if err := r.dropUserFromDB(ctx, mysqlUser); err != nil {
112+
return err
113+
}
114+
115+
meta.RemoveFinalizer(&mysqlUser.ObjectMeta, userFinalizer)
116+
117+
// Update resource so it will remove the finalizer.
118+
if err := r.Update(ctx, mysqlUser.Unwrap()); err != nil {
119+
return err
120+
}
121+
}
122+
return nil
123+
}
124+
125+
// reconcileUserInCluster reconcileUserInCluster creates or updates users in mysql.
126+
// Proceed as follows:
127+
// 1. Create users and authorize according to the Spec.
128+
// 2. Remove the host that does not exist in the spec from MySQL.
129+
// 3. Make sure mysqluser has finalizer set.
130+
// 4. Update status and condition.
131+
func (r *MysqlUserReconciler) reconcileUserInCluster(ctx context.Context, mysqlUser *mysqluser.MysqlUser) (err error) {
132+
// Catch the error and set the failed status.
133+
defer setFailedStatus(&err, mysqlUser)
134+
135+
// Reconcile the mysqlUser into mysql.
136+
if err = r.reconcileUserInDB(ctx, mysqlUser); err != nil {
137+
return
138+
}
139+
140+
// Add finalizer if is not added on the resource.
141+
if !meta.HasFinalizer(&mysqlUser.ObjectMeta, userFinalizer) {
142+
meta.AddFinalizer(&mysqlUser.ObjectMeta, userFinalizer)
143+
if err = r.Update(ctx, mysqlUser.Unwrap()); err != nil {
144+
return
145+
}
146+
}
147+
148+
// Update status for allowedHosts if needed, mark that status need to be updated.
149+
if !reflect.DeepEqual(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts) {
150+
mysqlUser.Status.AllowedHosts = mysqlUser.Spec.Hosts
151+
}
152+
153+
// Update the status according to the result.
154+
mysqlUser.UpdateStatusCondition(
155+
apiv1alpha1.MySQLUserReady, corev1.ConditionTrue,
156+
mysqluser.ProvisionSucceededReason, "The user provisioning has succeeded.",
157+
)
158+
159+
return
160+
}
161+
162+
// reconcileUserInDB creates and authorizes(If needed) users based on
163+
// spec.Hosts, and then deletes users that do not exist in spec.Hosts.
164+
func (r *MysqlUserReconciler) reconcileUserInDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error {
165+
sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey(
166+
r.Client, mysqlUser.GetClusterKey(), utils.RootUser, utils.LeaderHost))
167+
if err != nil {
168+
return err
169+
}
170+
defer closeConn()
171+
172+
secret := &corev1.Secret{}
173+
secretKey := client.ObjectKey{Name: mysqlUser.Spec.SecretSelector.SecretName, Namespace: mysqlUser.Namespace}
174+
175+
if err := r.Get(ctx, secretKey, secret); err != nil {
176+
return err
177+
}
178+
179+
password := string(secret.Data[mysqlUser.Spec.SecretSelector.SecretKey])
180+
if password == "" {
181+
return fmt.Errorf("the MySQL user's password must not be empty")
182+
}
183+
184+
// Create/Update user in database.
185+
userLog.Info("creating mysql user", "key", mysqlUser.GetKey(), "username", mysqlUser.Spec.User, "cluster", mysqlUser.GetClusterKey())
186+
if err := internal.CreateUserIfNotExists(sqlRunner, mysqlUser.Spec.User, password, mysqlUser.Spec.Hosts,
187+
mysqlUser.Spec.Permissions); err != nil {
188+
return err
189+
}
190+
191+
// Remove allowed hosts for user.
192+
toRemove := stringDiffIn(mysqlUser.Status.AllowedHosts, mysqlUser.Spec.Hosts)
193+
for _, host := range toRemove {
194+
if err := internal.DropUser(sqlRunner, mysqlUser.Spec.User, host); err != nil {
195+
return err
196+
}
197+
}
198+
199+
return nil
200+
}
201+
202+
func stringDiffIn(actual, desired []string) []string {
203+
diff := []string{}
204+
for _, aStr := range actual {
205+
// If is not in the desired list remove it.
206+
if _, exists := stringIn(aStr, desired); !exists {
207+
diff = append(diff, aStr)
208+
}
209+
}
210+
211+
return diff
212+
}
213+
214+
func stringIn(str string, strs []string) (int, bool) {
215+
for i, s := range strs {
216+
if s == str {
217+
return i, true
218+
}
219+
}
220+
return 0, false
221+
}
222+
223+
func (r *MysqlUserReconciler) dropUserFromDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error {
224+
sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey(
225+
r.Client, mysqlUser.GetClusterKey(), utils.RootUser, utils.LeaderHost))
226+
if errors.IsNotFound(err) {
227+
// If the mysql cluster does not exists then we can safely assume that
228+
// the user is deleted so exist successfully.
229+
statusErr, ok := err.(*errors.StatusError)
230+
if ok && mysqlcluster.IsClusterKind(statusErr.Status().Details.Kind) {
231+
// It seems the cluster is not to be found, so we assume it has been deleted.
232+
return nil
233+
}
234+
}
235+
236+
if err != nil {
237+
return err
238+
}
239+
defer closeConn()
240+
241+
for _, host := range mysqlUser.Status.AllowedHosts {
242+
userLog.Info("removing user from mysql cluster", "key", mysqlUser.GetKey(), "username", mysqlUser.Spec.User, "cluster", mysqlUser.GetClusterKey())
243+
if err := internal.DropUser(sqlRunner, mysqlUser.Spec.User, host); err != nil {
244+
return err
245+
}
246+
}
247+
return nil
248+
}
249+
250+
// updateStatusAndErr update the status and catch create/update error.
251+
func (r *MysqlUserReconciler) updateStatusAndErr(ctx context.Context, mysqlUser *mysqluser.MysqlUser, oldStatus *apiv1alpha1.UserStatus, cuErr error) error {
252+
if !reflect.DeepEqual(oldStatus, &mysqlUser.Status) {
253+
userLog.Info("update mysql user status", "key", mysqlUser.GetKey(), "diff", deep.Equal(oldStatus, &mysqlUser.Status))
254+
if err := r.Status().Update(ctx, mysqlUser.Unwrap()); err != nil {
255+
if cuErr != nil {
256+
return fmt.Errorf("failed to update status: %s, previous error was: %s", err, cuErr)
257+
}
258+
return err
259+
}
260+
}
261+
262+
return cuErr
263+
}
264+
265+
func setFailedStatus(err *error, mysqlUser *mysqluser.MysqlUser) {
266+
if *err != nil {
267+
mysqlUser.UpdateStatusCondition(
268+
apiv1alpha1.MySQLUserReady, corev1.ConditionFalse,
269+
mysqluser.ProvisionFailedReason, fmt.Sprintf("The user provisioning has failed: %s", *err),
270+
)
271+
}
272+
}
273+
274+
// SetupWithManager sets up the controller with the Manager.
275+
func (r *MysqlUserReconciler) SetupWithManager(mgr ctrl.Manager) error {
276+
return ctrl.NewControllerManagedBy(mgr).
277+
For(&apiv1alpha1.MysqlUser{}).
278+
Complete(r)
279+
}

internal/sql_runner.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -325,27 +325,27 @@ func columnValue(scanArgs []interface{}, slaveCols []string, colName string) str
325325
}
326326

327327
// CreateUserIfNotExists creates a user if it doesn't already exist and it gives it the specified permissions.
328-
func (s sqlRunner) CreateUserIfNotExists(
329-
user, pass string, allowedHosts []string, permissions []apiv1alpha1.UserPermission,
328+
func CreateUserIfNotExists(
329+
sqlRunner SQLRunner, user, pass string, hosts []string, permissions []apiv1alpha1.UserPermission,
330330
) error {
331331

332332
// Throw error if there are no allowed hosts.
333-
if len(allowedHosts) == 0 {
333+
if len(hosts) == 0 {
334334
return errors.New("no allowedHosts specified")
335335
}
336336

337337
queries := []Query{
338-
getCreateUserQuery(user, pass, allowedHosts),
339-
// todo: getAlterUserQuery
338+
getCreateUserQuery(user, pass, hosts),
339+
// todo: getAlterUserQuery.
340340
}
341341

342342
if len(permissions) > 0 {
343-
queries = append(queries, permissionsToQuery(permissions, user, allowedHosts))
343+
queries = append(queries, permissionsToQuery(permissions, user, hosts))
344344
}
345345

346346
query := BuildAtomicQuery(queries...)
347347

348-
if err := s.QueryExec(query); err != nil {
348+
if err := sqlRunner.QueryExec(query); err != nil {
349349
return fmt.Errorf("failed to configure user (user/pass/access), err: %s", err)
350350
}
351351

@@ -378,10 +378,10 @@ func getUsersIdentification(user string, pwd *string, allowedHosts []string) (id
378378
}
379379

380380
// DropUser removes a MySQL user if it exists, along with its privileges.
381-
func (s sqlRunner) DropUser(user, host string) error {
381+
func DropUser(sqlRunner SQLRunner, user, host string) error {
382382
query := NewQuery("DROP USER IF EXISTS ?@?;", user, host)
383383

384-
if err := s.QueryExec(query); err != nil {
384+
if err := sqlRunner.QueryExec(query); err != nil {
385385
return fmt.Errorf("failed to delete user, err: %s", err)
386386
}
387387

@@ -421,7 +421,7 @@ func escapeID(id string) string {
421421
return id
422422
}
423423

424-
// don't allow using ` in id name
424+
// don't allow using ` in id name.
425425
id = strings.ReplaceAll(id, "`", "")
426426

427427
return fmt.Sprintf("`%s`", id)

0 commit comments

Comments
 (0)