Skip to content

Commit

Permalink
implement from seed to db reference in databasereqeust controller
Browse files Browse the repository at this point in the history
  • Loading branch information
Marco Cadetg committed May 30, 2024
1 parent 58f6f78 commit 9952e17
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 31 deletions.
161 changes: 146 additions & 15 deletions internal/controller/databaserequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (

"github.com/google/go-cmp/cmp"
"github.com/prometheus/client_golang/prometheus"
"github.com/uselagoon/dbaas-controller/api/v1alpha1"
crdv1alpha1 "github.com/uselagoon/dbaas-controller/api/v1alpha1"
"github.com/uselagoon/dbaas-controller/internal/database"
)
Expand Down Expand Up @@ -151,14 +152,16 @@ func (r *DatabaseRequestReconciler) Reconcile(ctx context.Context, req ctrl.Requ

var dbInfo *dbInfo
if databaseRequest.Spec.Seed != nil {
var err error
dbInfo, err = r.seedDatabase(ctx, databaseRequest)
seedInfo, err := r.handleSeed(ctx, databaseRequest)
if err != nil {
return r.handleError(ctx, databaseRequest, "seed-database", err)
}
if err := r.relDBTestConnection(ctx, dbInfo, databaseRequest.Spec.Type); err != nil {
return r.handleError(ctx, databaseRequest, "seed-database-connection", err)
}
return r.handleError(ctx, databaseRequest, "handle-seed", err)
}
// now let's update the database request with the connection reference
databaseRequest.Spec.DatabaseConnectionReference = seedInfo.databaseProviderRef
databaseRequest.Status.ObservedDatabaseConnectionReference = databaseRequest.Spec.DatabaseConnectionReference
databaseRequest.Spec.Seed = nil
dbInfo = seedInfo.dbInfo
logger.Info("Seed database setup complete")
} else {
if databaseRequest.Spec.DatabaseConnectionReference == nil {
if err := r.createDatabase(ctx, databaseRequest); err != nil {
Expand Down Expand Up @@ -275,6 +278,49 @@ func (r *DatabaseRequestReconciler) handleError(
return ctrl.Result{}, err
}

func (r *DatabaseRequestReconciler) handleSeed(ctx context.Context, databaseRequest *crdv1alpha1.DatabaseRequest) (*seedDatabaseInfo, error) {
logger := log.FromContext(ctx)
logger.Info("Get seed database info")
seedInfo, err := r.relationalDatabaseInfoFromSeed(
ctx,
databaseRequest.Spec.Seed,
databaseRequest.Spec.Type,
databaseRequest.Spec.Scope,
)
if err != nil {
return nil, err
}
logger.Info("Found seed connection", "connectionName", seedInfo.conn.name)

// if we got a provider connection and db info we set the database info.
if err = r.RelationalDatabaseClient.SetDatabaseInfo(
ctx,
seedInfo.conn.getDSN(),
databaseRequest.Name,
databaseRequest.Namespace,
databaseRequest.Spec.Type,
database.RelationalDatabaseInfo{
Username: seedInfo.dbInfo.userName,
Password: seedInfo.dbInfo.password,
Dbname: seedInfo.dbInfo.database,
},
); err != nil {
return nil, err
}
logger.Info("Set database info", "username", seedInfo.dbInfo.userName, "database", seedInfo.dbInfo.database)
// get rid of the seed secret
if err := r.Delete(ctx, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: databaseRequest.Spec.Seed.Name,
Namespace: databaseRequest.Spec.Seed.Namespace,
},
}); err != nil {
return nil, err
}
logger.Info("Deleted seed secret", "secret", databaseRequest.Spec.Seed.Name)
return seedInfo, nil
}

// handleService creates or updates the service for the database request
// returns true if the service has been updated
func (r *DatabaseRequestReconciler) handleService(
Expand Down Expand Up @@ -456,16 +502,16 @@ func (r *DatabaseRequestReconciler) createDatabase(
// seedDatabase returns the database information from the seed secret
func (r *DatabaseRequestReconciler) seedDatabase(
ctx context.Context,
databaseRequest *crdv1alpha1.DatabaseRequest,
seed *v1.SecretReference,
) (*dbInfo, error) {
seed := &v1.Secret{}
secret := &v1.Secret{}
if err := r.Get(ctx, types.NamespacedName{
Name: databaseRequest.Spec.Seed.Name,
Namespace: databaseRequest.Spec.Seed.Namespace,
}, seed); err != nil {
return nil, fmt.Errorf("failed to get seed secret %s: %w", databaseRequest.Spec.Seed.Name, err)
Name: seed.Name,
Namespace: seed.Namespace,
}, secret); err != nil {
return nil, fmt.Errorf("failed to get seed secret %s: %w", seed.Name, err)
}
return dbInfoFromSeed(seed)
return dbInfoFromSeed(secret)
}

// promLabels returns the prometheus labels for the database request
Expand Down Expand Up @@ -707,7 +753,7 @@ func (r *DatabaseRequestReconciler) relationalDatabaseOperation(
return fmt.Errorf("%s db operation %s failed due to missing dbInfo", databaseRequest.Spec.Type, operation)
}
// get the database information
info, err := r.RelationalDatabaseClient.GetDatabase(
info, err := r.RelationalDatabaseClient.GetDatabaseInfo(
ctx,
conn.getDSN(),
databaseRequest.Name,
Expand All @@ -729,6 +775,91 @@ func (r *DatabaseRequestReconciler) relationalDatabaseOperation(
}
}

// seedDatabaseInfo is a struct to hold the seed database information
type seedDatabaseInfo struct {
dbInfo *dbInfo
conn *reldbConn
databaseProviderRef *v1alpha1.DatabaseConnectionReference
}

// relationalDatabaseInfoFromSeed finds the relational database provider based on the seed secret
func (r *DatabaseRequestReconciler) relationalDatabaseInfoFromSeed(
ctx context.Context,
seed *v1.SecretReference,
dbType string,
scope string,
) (*seedDatabaseInfo, error) {
dbInfo, err := r.seedDatabase(ctx, seed)
if err != nil {
return nil, fmt.Errorf("%s db find connection from seed failed to get seed database: %w", dbType, err)
}

// test if the connection works with the seed
if err := r.relDBTestConnection(ctx, dbInfo, dbType); err != nil {
return nil, fmt.Errorf("%s db find connection from seed failed to test connection: %w", dbType, err)
}

dbProviders := &crdv1alpha1.RelationalDatabaseProviderList{}
if err := r.List(ctx, dbProviders); err != nil {
return nil, fmt.Errorf("%s db find connection from seed failed to list database providers: %w",
dbType, err,
)
}

var connection *crdv1alpha1.Connection
var databaseProviderRef *v1alpha1.DatabaseConnectionReference
for _, dbProvider := range dbProviders.Items {
if dbProvider.Spec.Scope == scope && dbProvider.Spec.Type == dbType {
for _, dbConnection := range dbProvider.Spec.Connections {
if dbConnection.Enabled && dbConnection.Hostname == dbInfo.hostName &&
dbConnection.Port == dbInfo.port {
log.FromContext(ctx).Info("Found provider", "provider", dbProvider.Name)
connection = &dbConnection
databaseProviderRef = &crdv1alpha1.DatabaseConnectionReference{
Name: connection.Name,
DatabaseObjectReference: v1.ObjectReference{
Kind: dbProvider.Kind,
Name: dbProvider.Name,
UID: dbProvider.UID,
ResourceVersion: dbProvider.ResourceVersion,
},
}
}
}
}
}

if connection == nil {
return nil, fmt.Errorf("%s db find connection from seed failed due to provider not found", dbType)
}

secret := &v1.Secret{}
if err := r.Get(ctx, types.NamespacedName{
Name: connection.PasswordSecretRef.Name,
Namespace: connection.PasswordSecretRef.Namespace,
}, secret); err != nil {
return nil, fmt.Errorf("%s db find connection from seed failed to get connection password from secret: %w",
dbType, err,
)
}

password := string(secret.Data["password"])
if password == "" {
return nil, fmt.Errorf("%s db find connection from seed failed due to empty password", dbType)
}

conn := &reldbConn{
dbType: dbType,
name: connection.Name,
hostname: connection.Hostname,
username: connection.Username,
password: password,
port: connection.Port,
}

return &seedDatabaseInfo{dbInfo: dbInfo, conn: conn, databaseProviderRef: databaseProviderRef}, nil
}

// findRelationalDatabaseProvider finds the relational database provider with the same scope and the lower load
// returns the provider, connection name and an error
func (r *DatabaseRequestReconciler) findRelationalDatabaseProvider(
Expand Down
72 changes: 68 additions & 4 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ type RelationalDatabaseInterface interface {
// This function is idempotent and can be called multiple times without side effects.
DropDatabase(ctx context.Context, dsn, name, namespace, dbType string) error

// GetDatabase returns the database name, username, and password for the given name and namespace.
GetDatabase(ctx context.Context, dsn, name, namespace, dbType string) (RelationalDatabaseInfo, error)
// GetDatabaseInfo returns the database name, username, and password for the given name and namespace.
GetDatabaseInfo(ctx context.Context, dsn, name, namespace, dbType string) (RelationalDatabaseInfo, error)

// SetDatabaseInfo sets the database name, username, and password for the given name and namespace.
SetDatabaseInfo(ctx context.Context, dsn, name, namespace, dbType string, info RelationalDatabaseInfo) error
}

// RelationalDatabaseImpl is the implementation of the RelationalDatabaseInterface
Expand Down Expand Up @@ -449,6 +452,31 @@ func (ri *RelationalDatabaseImpl) databaseInfoMySQL(
return info, nil
}

// insertUserinfoIntoMysql inserts the user into the users table
// This function is idempotent and can be called multiple times without side effects.
func (ri *RelationalDatabaseImpl) insertUserInfoIntoMysql(
ctx context.Context,
dsn, name, namespace string,
info RelationalDatabaseInfo,
) error {
db, err := ri.GetConnection(ctx, dsn, mysql)
if err != nil {
return fmt.Errorf("set database info failed to open %s database: %w", mysql, err)
}
_, err = db.ExecContext(ctx, "USE dbaas_controller")
if err != nil {
return fmt.Errorf("failed to select database: %w", err)
}
// Insert the user into the users table
_, err = db.ExecContext(
ctx, "INSERT IGNORE INTO users (name, namespace, username, password, dbname) VALUES (?, ?, ?, ?, ?)",
name, namespace, info.Username, info.Password, info.Dbname)
if err != nil {
return fmt.Errorf("failed to insert user into users table: %w", err)
}
return nil
}

// databaseInfoPostgreSQL returns the username, password, and database name for the given name and namespace.
// It also creates the user and database if they do not exist.
// This function is idempotent and can be called multiple times without side effects.
Expand Down Expand Up @@ -489,8 +517,29 @@ func (ri *RelationalDatabaseImpl) databaseInfoPostgreSQL(
return info, nil
}

// GetDatabase returns the database name, username, and password for the given name and namespace.
func (ri *RelationalDatabaseImpl) GetDatabase(
// insertUserInfoIntoPostgreSQL inserts the user into the users table
// This function is idempotent and can be called multiple times without side effects.
func (ri *RelationalDatabaseImpl) insertUserInfoIntoPostgreSQL(
ctx context.Context,
dsn, name, namespace string,
info RelationalDatabaseInfo,
) error {
db, err := ri.GetConnection(ctx, dsn, postgres)
if err != nil {
return fmt.Errorf("set database info failed to open %s database: %w", postgres, err)
}
// Insert the user into the users table
_, err = db.ExecContext(
ctx, "INSERT INTO dbaas_controller.users (name, namespace, username, password, dbname) VALUES ($1, $2, $3, $4, $5) ON CONFLICT DO NOTHING",
name, namespace, info.Username, info.Password, info.Dbname)
if err != nil {
return fmt.Errorf("failed to insert user into users table: %w", err)
}
return nil
}

// GetDatabaseInfo returns the database name, username, and password for the given name and namespace.
func (ri *RelationalDatabaseImpl) GetDatabaseInfo(
ctx context.Context,
dsn, name, namespace, dbType string,
) (RelationalDatabaseInfo, error) {
Expand All @@ -502,3 +551,18 @@ func (ri *RelationalDatabaseImpl) GetDatabase(
}
return RelationalDatabaseInfo{}, fmt.Errorf("get database failed to get %s database: unsupported dbType", dbType)
}

// SetDatabaseInfo sets the database name, username, and password for the given name and namespace.
func (ri *RelationalDatabaseImpl) SetDatabaseInfo(
ctx context.Context,
dsn, name, namespace, dbType string,
info RelationalDatabaseInfo,
) error {
log.FromContext(ctx).Info("Setting database", "dbType", dbType, "name", name, "namespace", namespace)
if dbType == "mysql" {
return ri.insertUserInfoIntoMysql(ctx, dsn, name, namespace, info)
} else if dbType == "postgres" {
return ri.insertUserInfoIntoPostgreSQL(ctx, dsn, name, namespace, info)
}
return nil
}
7 changes: 6 additions & 1 deletion internal/database/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ func (mi *RelationalDatabaseMock) DropDatabase(ctx context.Context, dsn, name, n
return nil
}

func (mi *RelationalDatabaseMock) GetDatabase(
func (mi *RelationalDatabaseMock) GetDatabaseInfo(
ctx context.Context, dsn, name, namespace, kind string) (RelationalDatabaseInfo, error) {
return RelationalDatabaseInfo{Username: "user", Password: "pass", Dbname: "db"}, nil
}

func (mi *RelationalDatabaseMock) SetDatabaseInfo(
ctx context.Context, dsn, name, namespace, kind string, info RelationalDatabaseInfo) error {
return nil
}

func (mi *RelationalDatabaseMock) Load(ctx context.Context, dsn string, kind string) (int, error) {
return 10, nil
}
Expand Down
19 changes: 8 additions & 11 deletions test/e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ var _ = Describe("controller", Ordered, func() {
secretNames := utils.GetNonEmptyLines(string(secretOutput))
ExpectWithOffset(1, secretNames).Should(HaveLen(2))

if name == "seed" {
By("checking that the seed secret is deleted")
cmd = exec.Command("kubectl", "get", "secret", "seed-mysql-secret")
_, err := utils.Run(cmd)
// expect error to occurred
ExpectWithOffset(1, err).To(HaveOccurred())
}

By("deleting the DatabaseRequest resource the database is getting deprovisioned")
cmd = exec.Command(
"kubectl",
Expand Down Expand Up @@ -296,17 +304,6 @@ var _ = Describe("controller", Ordered, func() {
ExpectWithOffset(1, err).NotTo(HaveOccurred())
secretNames = utils.GetNonEmptyLines(string(secretOutput))
ExpectWithOffset(1, secretNames).Should(HaveLen(1))
if name == "seed" {
By("deleting the seed secret")
cmd = exec.Command(
"kubectl",
"delete",
"secret",
"seed-mysql-secret",
)
_, err = utils.Run(cmd)
ExpectWithOffset(1, err).NotTo(HaveOccurred())
}
}

// uncomment to debug ...
Expand Down

0 comments on commit 9952e17

Please sign in to comment.