Skip to content

Commit

Permalink
e2e and unit tests work for mysql
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Cadetg committed Apr 3, 2024
1 parent 94e75b5 commit 0640ac1
Show file tree
Hide file tree
Showing 16 changed files with 278 additions and 46 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ RUN go mod download
# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
COPY internal/ internal/

# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ test: manifests generate fmt vet envtest ## Run tests.
# Utilize Kind or modify the e2e tests to load the image locally, enabling compatibility with other vendors.
.PHONY: test-e2e # Run the e2e tests against a Kind k8s instance that is spun up.
test-e2e:
kind export kubeconfig --name=kind
go test ./test/e2e/ -v -ginkgo.v

.PHONY: lint
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ It allows for provisiong and deprovisioning of shared MySQL/MariaDB, PostgreSQL,
WIP - Work in Progress
There is still a lot of work to be done on this project. The current status is that the controller is able to provision and deprovision MySQL databases. But there is still a lot of work to be done to make it production ready.

- [ ] Setup e2e tests
- [x] Setup e2e tests
- [x] Provision MySQL databases
- [x] Deprovision MySQL databases
- [ ] Provision PostgreSQL databases
Expand All @@ -20,6 +20,24 @@ There is still a lot of work to be done on this project. The current status is t
- [ ] Deprovision MongoDB databases
- [ ] Plan to migrate from old `dbaaas-operator` to `dbaas-controller`

## Testing

To run the unit tests, you can use the following command:

```bash
make test
```

To run the end-to-end tests, you can use the following command:

```bash
make test-e2e
```

Note that the end-to-end tests require a kind cluster to be running. Make sure to have kind installed and running before running the tests.

DANGER ZONE: Do not run the end-to-end tests on a production cluster. The tests will create and delete resources in the cluster!!!

## Installation

See [lagoon-charts](https://github.com/uselagoon/lagoon-charts)
Expand Down
7 changes: 4 additions & 3 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,17 @@ func main() {
}

if err = (&controller.DatabaseRequestReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
MySQLClient: &mysql.MySQLImpl{},
}).SetupWithManager(mgr, maxConcurrentReconciles); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DatabaseRequest")
os.Exit(1)
}
if err = (&controller.DatabaseMySQLProviderReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
MySQLClient: &mysql.MySQLMock{}, // change to mysql.MySQLImpl{} to use the real implementation
MySQLClient: &mysql.MySQLImpl{},
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "DatabaseMySQLProvider")
os.Exit(1)
Expand Down
6 changes: 6 additions & 0 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: example.com/dbaas-controller
newTag: v0.0.1
11 changes: 10 additions & 1 deletion config/samples/crd_v1alpha1_databasemysqlprovider.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,13 @@ metadata:
app.kubernetes.io/created-by: dbaas-controller
name: databasemysqlprovider-sample
spec:
# TODO(user): Add fields here
scope: development
mysqlConnections:
- name: primary-test-mysql-connection
hostname: mysql-service.mysql
passwordSecretRef:
name: mysql-secret
namespace: mysql
port: 3306
username: root
enabled: true
3 changes: 2 additions & 1 deletion config/samples/crd_v1alpha1_databaserequest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ metadata:
app.kubernetes.io/created-by: dbaas-controller
name: databaserequest-sample
spec:
# TODO(user): Add fields here
scope: development
type: mysql
14 changes: 9 additions & 5 deletions internal/controller/databasemysqlprovider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,15 @@ func (r *DatabaseMySQLProviderReconciler) Reconcile(ctx context.Context, req ctr
return ctrl.Result{}, nil
}

// Check if we need to reconcile based on Generation and ObservedGeneration
if instance.Status.ObservedGeneration >= instance.Generation {
logger.Info("No updates to reconcile")
r.Recorder.Event(instance, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile")
return ctrl.Result{}, nil
// Check if we need to reconcile based on Generation and ObservedGeneration but only if
// the status condition is not false. This makes sure that in case of an error the controller
// will try to reconcile again.
if instance.Status.Conditions != nil && meta.IsStatusConditionTrue(instance.Status.Conditions, "Ready") {
if instance.Status.ObservedGeneration >= instance.Generation {
logger.Info("No updates to reconcile")
r.Recorder.Event(instance, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile")
return ctrl.Result{}, nil
}
}

if controllerutil.AddFinalizer(instance, databaseMySQLProviderFinalizer) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ var _ = Describe("DatabaseMySQLProvider Controller", func() {
})
It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
fakeRecorder := record.NewFakeRecorder(1)
fakeRecorder := record.NewFakeRecorder(100)
controllerReconciler := &DatabaseMySQLProviderReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Expand Down
56 changes: 28 additions & 28 deletions internal/controller/databaserequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,17 +128,25 @@ func (r *DatabaseRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
}
}

if databaseRequest.Status.ObservedGeneration >= databaseRequest.Generation {
logger.Info("No updates to reconcile")
r.Recorder.Event(databaseRequest, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile")
return ctrl.Result{}, nil
// Check if we need to reconcile based on Generation and ObservedGeneration but only if
// the status condition is not false. This makes sure that in case of an error the controller
// will try to reconcile again.
if databaseRequest.Status.Conditions != nil && meta.IsStatusConditionTrue(databaseRequest.Status.Conditions, "Ready") {
if databaseRequest.Status.ObservedGeneration >= databaseRequest.Generation {
logger.Info("No updates to reconcile")
r.Recorder.Event(databaseRequest, v1.EventTypeNormal, "ReconcileSkipped", "No updates to reconcile")
return ctrl.Result{}, nil
}
}

if databaseRequest.Spec.DatabaseConnectionReference == nil {
if err := r.createDatabase(ctx, databaseRequest); err != nil {
return r.handleError(ctx, databaseRequest, "create-database", err)
}
return ctrl.Result{}, nil
if databaseRequest.Spec.DatabaseConnectionReference == nil {
return r.handleError(
ctx, databaseRequest, "missing-connection-reference", errors.New("missing database connection reference"))
}
}

if databaseRequest.Status.ObservedDatabaseConnectionReference != databaseRequest.Spec.DatabaseConnectionReference {
Expand Down Expand Up @@ -192,6 +200,15 @@ func (r *DatabaseRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ
Message: "The database request has been changed",
})
r.Recorder.Event(databaseRequest, "Normal", "DatabaseRequestUpdated", "The database request has been updated")
} else {
// set the status condition to true if the database request has been created
meta.SetStatusCondition(&databaseRequest.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "DatabaseRequestCreated",
Message: "The database request has been created",
})
r.Recorder.Event(databaseRequest, "Normal", "DatabaseRequestUnchanged", "The database request has been created")
}

promDatabaseRequestReconcileStatus.With(promLabels(databaseRequest, "")).Set(1)
Expand Down Expand Up @@ -253,7 +270,8 @@ func (r *DatabaseRequestReconciler) handleService(
Name: serviceName,
Namespace: databaseRequest.Namespace,
Labels: map[string]string{
"service.lagoon.sh/dbaas": "true", // The label could be used to find services in case the hostname changed.
"service.lagoon.sh/dbaas": "true", // The label could be used to find services in case the hostname changed.
"app.kubernetes.io/instance": databaseRequest.Name,
},
},
Spec: v1.ServiceSpec{
Expand Down Expand Up @@ -299,7 +317,8 @@ func (r *DatabaseRequestReconciler) handleSecret(
Name: databaseRequest.Name,
Namespace: databaseRequest.Namespace,
Labels: map[string]string{
"secret.lagoon.sh/dbaas": "true",
"secret.lagoon.sh/dbaas": "true",
"app.kubernetes.io/instance": databaseRequest.Name,
},
},
Data: dbInfo.getSecretData(databaseRequest.Spec.Type, serviceName),
Expand Down Expand Up @@ -396,9 +415,8 @@ func (r *DatabaseRequestReconciler) createDatabase(
if err := r.mysqlCreation(ctx, databaseRequest); err != nil {
return fmt.Errorf("mysql db creation failed: %w", err)
}
// update the status
if err := r.Status().Update(ctx, databaseRequest); err != nil {
return fmt.Errorf("mysql db creation failed to update status: %w", err)
if databaseRequest.Spec.DatabaseConnectionReference == nil {
return fmt.Errorf("mysql db creation failed due to missing database connection reference")
}
case "mariadb":
// handle mariadb creation
Expand All @@ -414,24 +432,6 @@ func (r *DatabaseRequestReconciler) createDatabase(
logger.Error(ErrInvalidDatabaseType, "Unsupported database type", "type", databaseRequest.Spec.Type)
return fmt.Errorf("failed to create database: %w", ErrInvalidDatabaseType)
}
// set the status condition to true if the database request has been created
meta.SetStatusCondition(&databaseRequest.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "DatabaseRequestCreated",
Message: "The database request has been created",
})
r.Recorder.Event(databaseRequest, "Normal", "DatabaseRequestUnchanged", "The database request has been created")

promDatabaseRequestReconcileStatus.With(promLabels(databaseRequest, "")).Set(1)
databaseRequest.Status.ObservedGeneration = databaseRequest.Generation

// update the status
if err := r.Status().Update(ctx, databaseRequest); err != nil {
promDatabaseRequestReconcileErrorCounter.With(
promLabels(databaseRequest, "update-status")).Inc()
return err
}

return nil
}
Expand Down
26 changes: 23 additions & 3 deletions internal/controller/databaserequest_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import (
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -143,7 +145,7 @@ var _ = Describe("DatabaseRequest Controller", func() {

It("should successfully reconcile the resource", func() {
By("Reconciling the created resource")
fakeRecoder := record.NewFakeRecorder(1)
fakeRecoder := record.NewFakeRecorder(100)
controllerReconciler := &DatabaseRequestReconciler{
Client: k8sClient,
Scheme: k8sClient.Scheme(),
Expand All @@ -158,8 +160,26 @@ var _ = Describe("DatabaseRequest Controller", func() {
},
})
Expect(err).NotTo(HaveOccurred())
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
// Example: If you expect a certain status condition after reconciliation, verify it here.

By("Checking state of secret and service")
secret := &v1.Secret{}
err = k8sClient.Get(ctx, types.NamespacedName{
Name: dbRequestResource,
Namespace: "default",
}, secret)
Expect(err).NotTo(HaveOccurred())

// use label selector to get the service
serviceList := &v1.ServiceList{}
err = k8sClient.List(ctx, serviceList, &client.ListOptions{
Namespace: "default",
LabelSelector: labels.SelectorFromSet(
map[string]string{
"app.kubernetes.io/instance": dbRequestResource,
},
),
})
Expect(err).NotTo(HaveOccurred())
})
})
})
3 changes: 2 additions & 1 deletion internal/database/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ func (mi *MySQLImpl) CreateDatabase(ctx context.Context, dsn, name, namespace st

// Create the user and grant permissions
// Use prepared statements to avoid SQL injection vulnerabilities.
_, err = db.ExecContext(ctx, fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY ?", database.username), database.password)
_, err = db.ExecContext(
ctx, fmt.Sprintf("CREATE USER IF NOT EXISTS '%s'@'%%' IDENTIFIED BY '%s'", database.username, database.password))
if err != nil {
return fmt.Errorf("create database error creating user `%s`: %w", database.username, err)
}
Expand Down
8 changes: 8 additions & 0 deletions kind-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: dbaas-controller-cluster
nodes:
- role: control-plane
- role: worker
- role: worker
image: kindest/node:v1.29.2
Loading

0 comments on commit 0640ac1

Please sign in to comment.