Skip to content

Commit

Permalink
*: Support mysql user management through crd. #175
Browse files Browse the repository at this point in the history
  • Loading branch information
runkecheng committed Sep 22, 2021
1 parent 924f5ca commit d5a6407
Show file tree
Hide file tree
Showing 6 changed files with 453 additions and 0 deletions.
9 changes: 9 additions & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
269 changes: 269 additions & 0 deletions controllers/mysqluser_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/*
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/[email protected]/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 {
// setFailedStatus(&err, user)
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
}

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
}

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
}

func (r *MysqlUserReconciler) reconcileUserInDB(ctx context.Context, mysqlUser *mysqluser.MysqlUser) error {
sqlRunner, closeConn, err := r.SQLRunnerFactory(internal.NewConfigFromClusterKey(r.Client, mysqlUser.GetClusterKey(), utils.RootUser, string(utils.LeaderHost)))
if err != nil {
return err
}
defer closeConn()

secret := &corev1.Secret{}
secretKey := client.ObjectKey{Name: mysqlUser.Spec.SecretBinder.SecretName, Namespace: mysqlUser.Namespace}

if err := r.Get(ctx, secretKey, secret); err != nil {
return err
}

password := string(secret.Data[mysqlUser.Spec.SecretBinder.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 := sqlRunner.CreateUserIfNotExists(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 := sqlRunner.DropUser(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, string(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.IsMysqlClusterKind(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 := sqlRunner.DropUser(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)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions mysqluser/mysqluser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
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.ClusterBinder.NameSpace
if ns == "" {
ns = u.Namespace
}

return client.ObjectKey{
Name: u.Spec.ClusterBinder.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,
}
}
Loading

0 comments on commit d5a6407

Please sign in to comment.