Skip to content

Commit

Permalink
chore: use shared MySQLClients (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
nakamasato authored Mar 30, 2023
1 parent 29095f9 commit 17903a6
Show file tree
Hide file tree
Showing 14 changed files with 414 additions and 216 deletions.
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ Run lint:
golangci-lint run ./...
```


# 2. Run mysql-operator

## 2.1. Local
Expand Down Expand Up @@ -238,6 +237,12 @@ docker rm -f $(docker ps | grep mysql | head -1 |awk '{print $1}')
make test
```
Run individual test
```
KUBEBUILDER_ASSETS="/Users/m.naka/Library/Application Support/io.kubebuilder.envtest/k8s/1.24.2-darwin-arm64" bin/ginkgo -skip-package=e2e --focus "Should have finalizer" --failFast ./...
```
## 4.3. e2e
### 4.3.1. e2e (kind + skaffold + ginkgo + gomega)
Expand Down
30 changes: 25 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ This is a go-based Kubernetes operator built with [operator-sdk](https://sdk.ope
- Go: 1.19
## Components

- `MySQL`: MySQL cluster (holds credentials to connect to MySQL)
- `MySQLUser`: MySQL user (`mysqlName` and `host`)
- `MySQLDB`: MySQL database (`mysqlName` and `dbName`)
![](diagram.drawio.svg)

1. Custom Resource
1. `MySQL`: MySQL cluster (holds credentials to connect to MySQL)
1. `MySQLUser`: MySQL user (`mysqlName` and `host`)
1. `MySQLDB`: MySQL database (`mysqlName` and `dbName`)
1. Reconciler
1. `MySQLReconciler` is responsible for updating `MySQLClients` based on `MySQL` resource
1. `MySQLUserReconciler` is responsible for managing `MySQLUser` using `MySQLClients`
1. `MySQLDBReconciler` is responsible for managing `MySQLDB` using `MySQLClients`

## Getting Started

Expand Down Expand Up @@ -88,8 +95,21 @@ This is a go-based Kubernetes operator built with [operator-sdk](https://sdk.ope
kubectl delete -k https://github.com/nakamasato/mysql-operator/config/samples-on-k8s
```
NOTICE: custom resources might get stuck if MySQL is deleted before (to be improved). → Remove finalizers to forcifully delete the stuck objects
`kubectl patch mysqluser <resource_name> -p '{"metadata":{"finalizers": []}}' --type=merge` or `kubectl patch mysql <resource_name> -p '{"metadata":{"finalizers": []}}' --type=merge` (Bug: https://github.com/nakamasato/mysql-operator/issues/162)
<details><summary>NOTICE</summary>
custom resources might get stuck if MySQL is deleted before (to be improved). → Remove finalizers to forcifully delete the stuck objects:
```
kubectl patch mysqluser <resource_name> -p '{"metadata":{"finalizers": []}}' --type=merge
```
```
kubectl patch mysql <resource_name> -p '{"metadata":{"finalizers": []}}' --type=merge
```
```
kubectl patch mysqldb <resource_name> -p '{"metadata":{"finalizers": []}}' --type=merge
```
</details>
1. (Optional) Delete MySQL
```
Expand Down
48 changes: 46 additions & 2 deletions controllers/mysql_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"context"
"database/sql"
"fmt"
"time"

Expand All @@ -29,14 +30,17 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"

mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1"
mysqlinternal "github.com/nakamasato/mysql-operator/internal/mysql"
)

const mysqlFinalizer = "mysql.nakamasato.com/finalizer"

// MySQLReconciler reconciles a MySQL object
type MySQLReconciler struct {
client.Client
Scheme *runtime.Scheme
Scheme *runtime.Scheme
MySQLClients mysqlinternal.MySQLClients
MySQLDriverName string
}

//+kubebuilder:rbac:groups=mysql.nakamasato.com,resources=mysqls,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -68,6 +72,12 @@ func (r *MySQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl
log.Error(err, "[FetchMySQL] Failed to get MySQL")
return ctrl.Result{}, err
}

// Update MySQLClients
if err := r.UpdateMySQLClients(ctx, mysql); err != nil {
return ctrl.Result{}, err
}

// Add a finalizer if not exists
if controllerutil.AddFinalizer(mysql, mysqlFinalizer) {
if err := r.Update(ctx, mysql); err != nil {
Expand Down Expand Up @@ -123,6 +133,28 @@ func (r *MySQLReconciler) SetupWithManager(mgr ctrl.Manager) error {
Complete(r)
}

func (r *MySQLReconciler) UpdateMySQLClients(ctx context.Context, mysql *mysqlv1alpha1.MySQL) error {
log := log.FromContext(ctx).WithName("MySQLReconciler")
if db, _ := r.MySQLClients.GetClient(mysql.Name); db != nil {
log.Info("MySQLClient already exists", "mysql.Name", mysql.Name)
return nil
}
// db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tpc(%s:%d)/", mysql.Spec.AdminUser, mysql.Spec.AdminPassword, mysql.Spec.Host, 3306))
db, err := sql.Open(r.MySQLDriverName, mysql.Spec.AdminUser+":"+mysql.Spec.AdminPassword+"@tcp("+mysql.Spec.Host+":3306)/")
if err != nil {
log.Error(err, "Failed to open MySQL database", "mysql.Name", mysql.Name)
return err
}
r.MySQLClients[mysql.Name] = db
err = db.PingContext(ctx)
if err != nil {
log.Error(err, "Ping failed", "mysql.Name", mysql.Name)
return err
}
log.Info("Successfully added MySQL client", "mysql.Name", mysql.Name)
return nil
}

func (r *MySQLReconciler) countReferencesByMySQLUser(ctx context.Context, mysql *mysqlv1alpha1.MySQL) (int, error) {
// 1. Get the referenced MySQLUser instances.
// 2. Return the number of referencing MySQLUser.
Expand All @@ -147,5 +179,17 @@ func (r *MySQLReconciler) countReferencesByMySQLDB(ctx context.Context, mysql *m

// finalizeMySQL return true if no user and no db is referencing the given MySQL
func (r *MySQLReconciler) finalizeMySQL(ctx context.Context, mysql *mysqlv1alpha1.MySQL) bool {
return mysql.Status.UserCount == 0 && mysql.Status.DBCount == 0
log := log.FromContext(ctx).WithName("MySQLReconciler")
if mysql.Status.UserCount > 0 || mysql.Status.DBCount > 0 {
log.Info("there's referencing user or database", "UserCount", mysql.Status.UserCount, "DBCount", mysql.Status.DBCount)
return false
}
if db, ok := r.MySQLClients[mysql.Name]; ok {
if err := db.Close(); err != nil {
return false
}
delete(r.MySQLClients, mysql.Name)
}
log.Info("Closed and removed MySQL client", "mysql.Name", mysql.Name)
return true
}
7 changes: 5 additions & 2 deletions controllers/mysql_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

mysqlv1alpha1 "github.com/nakamasato/mysql-operator/api/v1alpha1"
internalmysql "github.com/nakamasato/mysql-operator/internal/mysql"
)

var _ = Describe("MySQL controller", func() {
Expand Down Expand Up @@ -42,8 +43,10 @@ var _ = Describe("MySQL controller", func() {
}

err = (&MySQLReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
MySQLClients: internalmysql.MySQLClients{},
MySQLDriverName: "testdbdriver",
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())

Expand Down
82 changes: 26 additions & 56 deletions controllers/mysqldb_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package controllers

import (
"context"
"database/sql"
"fmt"
"time"

"k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -43,8 +45,8 @@ const (
// MySQLDBReconciler reconciles a MySQLDB object
type MySQLDBReconciler struct {
client.Client
Scheme *runtime.Scheme
MySQLClientFactory mysqlinternal.MySQLClientFactory
Scheme *runtime.Scheme
MySQLClients mysqlinternal.MySQLClients
}

//+kubebuilder:rbac:groups=mysql.nakamasato.com,resources=mysqldbs,verbs=get;list;watch;create;update;patch;delete
Expand Down Expand Up @@ -82,38 +84,12 @@ func (r *MySQLDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
return ctrl.Result{}, err
}

// 3. Connect to MySQL
cfg := mysqlinternal.MySQLConfig{
AdminUser: mysql.Spec.AdminUser,
AdminPassword: mysql.Spec.AdminPassword,
Host: mysql.Spec.Host,
}
mysqlClient, err := r.MySQLClientFactory(cfg)
// 3. Get MySQL client
mysqlClient, err := r.MySQLClients.GetClient(db.Spec.MysqlName)
if err != nil {
db.Status.Phase = mysqlDBPhaseNotReady
db.Status.Reason = mysqlDBReasonMySQLConnectionFailed
if serr := r.Status().Update(ctx, db); serr != nil {
log.Error(serr, "Failed to update db status", "db", db.Name)
}
log.Error(err, "[MySQLClient] Failed to create")
return ctrl.Result{}, err // requeue
}
log.Info("[MySQLClient] Ping")
ctxPing, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
err = mysqlClient.PingContext(ctxPing)
if err != nil {
db.Status.Phase = mysqlDBPhaseNotReady
db.Status.Reason = mysqlDBReasonMySQLConnectionFailed
log.Error(err, "[MySQLClient] Failed to connect to MySQL", "mysqlName", mysql.Name)
if serr := r.Status().Update(ctx, db); serr != nil {
log.Error(serr, "Failed to update db status", "db", db.Name)
return ctrl.Result{RequeueAfter: time.Second}, nil
}
return ctrl.Result{RequeueAfter: time.Second}, nil // requeue after 1 second
log.Error(err, "Failed to get MySQL client", "mysqlName", db.Spec.MysqlName)
return ctrl.Result{}, err
}
log.Info("[MySQLClient] Successfully connected")
defer mysqlClient.Close()

// 4. Delete if NotFound with finalizer
if controllerutil.AddFinalizer(db, mysqlDBFinalizer) {
Expand All @@ -124,7 +100,7 @@ func (r *MySQLDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
if !db.GetDeletionTimestamp().IsZero() {
if controllerutil.ContainsFinalizer(db, mysqlDBFinalizer) {
if err := r.finalizeMySQLDB(ctx, db, mysql); err != nil {
if err := r.finalizeMySQLDB(ctx, mysqlClient, db); err != nil {
return ctrl.Result{}, err
}
if controllerutil.RemoveFinalizer(db, mysqlDBFinalizer) {
Expand All @@ -138,7 +114,7 @@ func (r *MySQLDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}

// 5. Create if not exists
err = mysqlClient.Exec("CREATE DATABASE IF NOT EXISTS " + db.Spec.DBName + ";")
res, err := mysqlClient.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", db.Spec.DBName))
if err != nil {
log.Error(err, "[MySQL] Failed to create MySQL database.", "mysql", mysql.Name, "database", db.Spec.DBName)
db.Status.Phase = mysqlDBPhaseNotReady
Expand All @@ -149,34 +125,28 @@ func (r *MySQLDBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}
return ctrl.Result{}, err
}
db.Status.Phase = mysqlDBPhaseReady
db.Status.Reason = mysqlDBReasonCompleted
if serr := r.Status().Update(ctx, db); serr != nil {
log.Error(serr, "Failed to update MySQLDB status", "Name", db.Spec.DBName)
rows, err := res.RowsAffected()
if err != nil {
log.Error(err, "Failed to get res.RowsAffected")
return ctrl.Result{}, err
}
if rows > 0 {
db.Status.Phase = mysqlDBPhaseReady
db.Status.Reason = mysqlDBReasonCompleted
if serr := r.Status().Update(ctx, db); serr != nil {
log.Error(serr, "Failed to update MySQLDB status", "Name", db.Spec.DBName)
}
} else {
log.Info("database already exists", "database", db.Spec.DBName)
}

return ctrl.Result{}, nil
}

// finalizeMySQLDB drops MySQL database
func (r *MySQLDBReconciler) finalizeMySQLDB(ctx context.Context, db *mysqlv1alpha1.MySQLDB, mysql *mysqlv1alpha1.MySQL) error {
mysqlClient, err := r.MySQLClientFactory(
mysqlinternal.MySQLConfig{
AdminUser: mysql.Spec.AdminUser,
AdminPassword: mysql.Spec.AdminPassword,
Host: mysql.Spec.Host,
})
if err != nil {
return err
}
ctxPing, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
err = mysqlClient.PingContext(ctxPing)
if err != nil {
return err
}
defer mysqlClient.Close()
return mysqlClient.Exec("DROP DATABASE IF EXISTS " + db.Spec.DBName + ";")
func (r *MySQLDBReconciler) finalizeMySQLDB(ctx context.Context, mysqlClient *sql.DB, db *mysqlv1alpha1.MySQLDB) error {
_, err := mysqlClient.ExecContext(ctx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", db.Spec.DBName))
return err
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
Loading

0 comments on commit 17903a6

Please sign in to comment.