From 89bcc0f6f6941dbee125bea54bd3f962ddfbc168 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 29 Jan 2024 14:32:48 -0500 Subject: [PATCH] new API to allow services to generate MariaDBAccount --- api/test/helpers/crd.go | 28 ++ api/v1beta1/mariadbdatabase_funcs.go | 336 +++++++++++++++--- api/v1beta1/mariadbdatabase_types.go | 4 +- config/manager/kustomization.yaml | 4 +- ...ariadb-operator.clusterserviceversion.yaml | 4 + 5 files changed, 322 insertions(+), 54 deletions(-) diff --git a/api/test/helpers/crd.go b/api/test/helpers/crd.go index 6df6cd04..f7deb150 100644 --- a/api/test/helpers/crd.go +++ b/api/test/helpers/crd.go @@ -15,6 +15,7 @@ package helpers import ( "context" + "fmt" "time" "github.com/go-logr/logr" @@ -216,3 +217,30 @@ func (tc *TestHelper) SimulateMariaDBAccountCompleted(name types.NamespacedName) tc.Logger.Info("Simulated DB Account completed", "on", name) } + +// CreateMariaDBAccount creates and persists a MariaDBAccount resource and +// associated Secret in the Kubernetes cluster +func (tc *TestHelper) CreateMariaDBAccount(name types.NamespacedName) (*mariadbv1.MariaDBAccount, *corev1.Secret) { + secret := tc.CreateSecret( + types.NamespacedName{Namespace: name.Namespace, Name: fmt.Sprintf("%s-db-secret", name.Name)}, + map[string][]byte{ + "DatabasePassword": []byte(fmt.Sprintf("%s123", name.Name)), + }, + ) + + instance := &mariadbv1.MariaDBAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name.Name, + Namespace: name.Namespace, + }, + Spec: mariadbv1.MariaDBAccountSpec{ + UserName: fmt.Sprintf("%s_account", name.Name), + Secret: fmt.Sprintf("%s-db-secret", name.Name), + }, + } + gomega.Eventually(func(g gomega.Gomega) { + g.Expect(tc.K8sClient.Create(tc.Ctx, instance)).Should(gomega.Succeed()) + }, tc.Timeout, tc.Interval).Should(gomega.Succeed()) + + return instance, secret +} diff --git a/api/v1beta1/mariadbdatabase_funcs.go b/api/v1beta1/mariadbdatabase_funcs.go index 8aa94a1f..bff51f8e 100644 --- a/api/v1beta1/mariadbdatabase_funcs.go +++ b/api/v1beta1/mariadbdatabase_funcs.go @@ -18,11 +18,15 @@ package v1beta1 import ( "context" + "crypto/rand" + "encoding/hex" "fmt" - "strings" "time" + corev1 "k8s.io/api/core/v1" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/secret" "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/openstack-k8s-operators/lib-common/modules/common/util" @@ -30,10 +34,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) // NewDatabase returns an initialized DB. +// legacy; should use NewDatabaseForAccount func NewDatabase( databaseName string, databaseUser string, @@ -46,11 +52,13 @@ func NewDatabase( secret: secret, labels: labels, name: "", + accountName: "", namespace: "", } } // NewDatabaseWithNamespace returns an initialized DB. +// legacy; should use NewDatabaseForAccount func NewDatabaseWithNamespace( databaseName string, databaseUser string, @@ -65,10 +73,30 @@ func NewDatabaseWithNamespace( secret: secret, labels: labels, name: name, + accountName: "", namespace: namespace, } } +// NewDatabaseWithNamespace returns an initialized DB. +func NewDatabaseForAccount( + databaseInstanceName string, + databaseName string, + name string, + accountName string, + namespace string, +) *Database { + return &Database{ + databaseName: databaseName, + labels: map[string]string{ + "dbName": databaseInstanceName, + }, + name: name, + accountName: accountName, + namespace: namespace, + } +} + // setDatabaseHostname - set the service name of the DB as the databaseHostname // by looking up the Service via the name of the MariaDB CR which provides it. func (d *Database) setDatabaseHostname( @@ -157,6 +185,8 @@ func (d *Database) CreateOrPatchDBByName( db := d.database if db == nil { + // MariaDBDatabase not present; create one to be patched/created + db = &MariaDBDatabase{ ObjectMeta: metav1.ObjectMeta{ Name: d.name, @@ -170,24 +200,52 @@ func (d *Database) CreateOrPatchDBByName( } account := d.account + if account == nil { - // no account is present in this Database, so for forwards compatibility, - // make one. name it the same as the MariaDBDatabase so we can get it back - // again based on that name alone. + // MariaDBAccount not present + + accountName := d.accountName + if accountName == "" { + // no accountName at all. this indicates this Database came about + // using either NewDatabase or NewDatabaseWithNamespace; both + // legacy and both pass along a databaseUser and secret. + + //, so for forwards compatibility, + // make a name and a MariaDBAccount for it. name it the same as + // the MariaDBDatabase so we can get it back + // again based on that name alone (also for backwards compatibility). + accountName = d.name + d.accountName = accountName + } + account = &MariaDBAccount{ ObjectMeta: metav1.ObjectMeta{ - Name: d.name, + Name: accountName, Namespace: d.namespace, Labels: map[string]string{ "mariaDBDatabaseName": d.name, }, }, - Spec: MariaDBAccountSpec{ - UserName: d.databaseUser, - Secret: d.secret, - }, + } + + // databaseUser was given, this is from legacy mode. populate it + // into the account + if d.databaseUser != "" { + account.Spec.UserName = d.databaseUser + } + + // secret was given, this is also from legacy mode. populate it + // into the account. note here that this is osp-secret, which has + // many PW fields in it. By setting it here, as was the case when + // osp-secret was associated directly with MariaDBDatabase, the + // mariadb-controller is going to use the DatabasePassword value + // for the password, and **not** any of the controller-specific + // passwords. + if d.secret != "" { + account.Spec.Secret = d.secret } } + // set the database hostname on the db instance err := d.setDatabaseHostname(ctx, h, name) if err != nil { @@ -224,22 +282,12 @@ func (d *Database) CreateOrPatchDBByName( return ctrl.Result{RequeueAfter: time.Second * 5}, nil } - op_acc, err_acc := controllerutil.CreateOrPatch(ctx, h.GetClient(), account, func() error { - account.Labels = util.MergeStringMaps( - account.GetLabels(), - d.labels, - ) - - err := controllerutil.SetControllerReference(h.GetBeforeObject(), account, h.GetScheme()) - if err != nil { - return err - } - - // If the service object doesn't have our finalizer, add it. - controllerutil.AddFinalizer(account, h.GetFinalizer()) - - return nil - }) + op_acc, err_acc := CreateOrPatchAccount( + ctx, h, account, + map[string]string{ + "mariaDBDatabaseName": d.name, + }, + ) if err_acc != nil && !k8s_errors.IsNotFound(err_acc) { return ctrl.Result{}, util.WrapErrorForObject( @@ -368,39 +416,26 @@ func (d *Database) getDBWithName( d.database = db - account := &MariaDBAccount{} - username := d.databaseUser + accountName := d.accountName + + legacyAccount := false - if username == "" { - // no username, so this is a legacy lookup. locate MariaDBAccount + if accountName == "" { + // no account name, so this is a legacy lookup. locate MariaDBAccount // based on the same name as that of the MariaDBDatabase - err = h.GetClient().Get( - ctx, - types.NamespacedName{ - Name: d.name, - Namespace: namespace, - }, - account) - } else { - // username is given. locate MariaDBAccount based on that given - // username. this is also legacy and in practice should not occur - // for any current controller. - err = h.GetClient().Get( - ctx, - types.NamespacedName{ - Name: strings.Replace(username, "_", "-", -1), - Namespace: namespace, - }, - account) + accountName = d.name + legacyAccount = true } + account, err := GetAccount(ctx, h, accountName, d.namespace) + if err != nil { - if k8s_errors.IsNotFound(err) { + if legacyAccount && k8s_errors.IsNotFound(err) { // if account can't be found, log it, but don't quit, still // return the Database with MariaDBDatabase util.LogForObject( h, - fmt.Sprintf("Could not find account %s for Database named %s", username, namespace), + fmt.Sprintf("Could not find account %s for Database named %s", accountName, namespace), d.account, ) @@ -408,18 +443,22 @@ func (d *Database) getDBWithName( } return util.WrapErrorForObject( - fmt.Sprintf("account error %s %s ", username, namespace), + fmt.Sprintf("account error %s %s ", accountName, namespace), h.GetBeforeObject(), err, ) } else { d.account = account + d.databaseUser = account.Spec.UserName + d.secret = account.Spec.Secret } return nil } // GetDatabaseByName returns a *Database object with specified name and namespace +// deprecated; this needs to have the account name given as well for it to work +// completely func GetDatabaseByName( ctx context.Context, h *helper.Helper, @@ -436,6 +475,26 @@ func GetDatabaseByName( return db, nil } +func GetDatabaseByNameAndAccount( + ctx context.Context, + h *helper.Helper, + name string, + accountName string, + namespace string, +) (*Database, error) { + // create a Database by suppplying a resource name + db := &Database{ + name: name, + accountName: accountName, + namespace: namespace, + } + // then querying the MariaDBDatabase and store it in db by calling + if err := db.getDBWithName(ctx, h); err != nil { + return db, err + } + return db, nil +} + // DeleteFinalizer deletes a finalizer by its object func (d *Database) DeleteFinalizer( ctx context.Context, @@ -449,7 +508,6 @@ func (d *Database) DeleteFinalizer( } util.LogForObject(h, fmt.Sprintf("Removed finalizer %s from MariaDBAccount object", h.GetFinalizer()), d.account) } - if controllerutil.RemoveFinalizer(d.database, h.GetFinalizer()) { err := h.GetClient().Update(ctx, d.database) if err != nil && !k8s_errors.IsNotFound(err) { @@ -459,3 +517,179 @@ func (d *Database) DeleteFinalizer( } return nil } + +// DeleteUnusedMariaDBAccountFinalizers searches for all MariaDBAccounts +// associated with the given MariaDBDabase name and removes the finalizer for all +// of them except for the given named account. +func DeleteUnusedMariaDBAccountFinalizers( + ctx context.Context, + h *helper.Helper, + mariaDBDatabaseName string, + mariaDBAccountName string, + namespace string, +) error { + + accountList := &MariaDBAccountList{} + + opts := []client.ListOption{ + client.InNamespace(namespace), + client.MatchingLabels{"mariaDBDatabaseName": mariaDBDatabaseName}, + client.MatchingFields{"kind": "MariaDBAccount"}, + } + + err := h.GetClient().List(ctx, accountList, opts...) + + if err != nil { + return err + } + + for _, mariaDBAccount := range accountList.Items { + if mariaDBAccount.Name == mariaDBAccountName { + continue + } + + if controllerutil.RemoveFinalizer(&mariaDBAccount, h.GetFinalizer()) { + err := h.GetClient().Update(ctx, &mariaDBAccount) + if err != nil && !k8s_errors.IsNotFound(err) { + return err + } + util.LogForObject(h, fmt.Sprintf("Removed finalizer %s from MariaDBAccount object", h.GetFinalizer()), &mariaDBAccount) + } + + } + return nil + +} + +func CreateOrPatchAccount( + ctx context.Context, + h *helper.Helper, + account *MariaDBAccount, + labels map[string]string, +) (controllerutil.OperationResult, error) { + op_acc, err_acc := controllerutil.CreateOrPatch(ctx, h.GetClient(), account, func() error { + account.Labels = util.MergeStringMaps( + account.GetLabels(), + labels, + ) + + err := controllerutil.SetControllerReference(h.GetBeforeObject(), account, h.GetScheme()) + if err != nil { + return err + } + + // If the service object doesn't have our finalizer, add it. + controllerutil.AddFinalizer(account, h.GetFinalizer()) + + return nil + }) + + return op_acc, err_acc +} + +func GetAccount(ctx context.Context, + h *helper.Helper, + accountName string, namespace string, +) (*MariaDBAccount, error) { + databaseAccount := &MariaDBAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: accountName, + Namespace: namespace, + }, + } + objectKey := client.ObjectKeyFromObject(databaseAccount) + + err := h.GetClient().Get(ctx, objectKey, databaseAccount) + if err != nil { + return nil, err + } + return databaseAccount, err +} + +// EnsureMariaDBAccount ensures a MariaDBAccount has been created for a given +// operator calling the function, and returns the MariaDBAccount and its +// Secret for use in consumption into a configuration. +// The current version of the function creates the objects if they don't +// exist; a later version of this can be set to only ensure that the objects +// were already created by an external actor such as openstack-operator up +// front. +func EnsureMariaDBAccount(ctx context.Context, + helper *helper.Helper, + accountName string, username string, namespace string, +) (*MariaDBAccount, *corev1.Secret, error) { + + account, err := GetAccount(ctx, helper, accountName, namespace) + + if err != nil { + if !k8s_errors.IsNotFound(err) { + return nil, nil, err + } + + account = &MariaDBAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: accountName, + Namespace: namespace, + // note no labels yet; the account will not have a + // mariadbdatabase yet so the controller will not + // try to create a DB; it instead will respond again to the + // MariaDBAccount once this is filled in + }, + Spec: MariaDBAccountSpec{ + UserName: username, + Secret: accountName, + }, + } + + } else { + account.Spec.UserName = username + } + + dbSecret, _, err := secret.GetSecret(ctx, helper, account.Spec.Secret, namespace) + + if err != nil { + if !k8s_errors.IsNotFound(err) { + return nil, nil, err + } + + dbPassword, err := interimGenerateDBPassword() + if err != nil { + return nil, nil, err + } + + dbSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: account.Spec.Secret, + Namespace: namespace, + }, + StringData: map[string]string{ + "DatabasePassword": dbPassword, + }, + } + + } + + _, _, err_secret := secret.CreateOrPatchSecret( + ctx, + helper, + helper.GetBeforeObject(), + dbSecret, + ) + if err_secret != nil { + return nil, nil, err_secret + } + + _, err_acc := CreateOrPatchAccount(ctx, helper, account, map[string]string{}) + if err_acc != nil { + return nil, nil, err_acc + } + + return account, dbSecret, nil +} + +func interimGenerateDBPassword() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", err + } + return hex.EncodeToString(b), nil +} diff --git a/api/v1beta1/mariadbdatabase_types.go b/api/v1beta1/mariadbdatabase_types.go index 6afcdf3f..2838efaf 100644 --- a/api/v1beta1/mariadbdatabase_types.go +++ b/api/v1beta1/mariadbdatabase_types.go @@ -85,7 +85,8 @@ const ( DatabaseAdminPasswordKey = "AdminPassword" ) -// Database - +// Database - a facade on top of the combination of a MariaDBDatabase +// and MariaDBAccount pair type Database struct { database *MariaDBDatabase account *MariaDBAccount @@ -95,5 +96,6 @@ type Database struct { secret string labels map[string]string name string + accountName string namespace string } diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 147a8db8..00888a14 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -12,5 +12,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: quay.io/openstack-k8s-operators/mariadb-operator - newTag: latest + newName: quay.io/rhn_engineering_mbayer/mariadb-operator + newTag: v0.0.5 diff --git a/config/manifests/bases/mariadb-operator.clusterserviceversion.yaml b/config/manifests/bases/mariadb-operator.clusterserviceversion.yaml index 78fab284..bdc91868 100644 --- a/config/manifests/bases/mariadb-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/mariadb-operator.clusterserviceversion.yaml @@ -18,6 +18,10 @@ spec: displayName: Galera kind: Galera name: galeras.mariadb.openstack.org + specDescriptors: + - description: TLS settings for MySQL service and internal Galera replication + displayName: TLS + path: tls version: v1beta1 - description: MariaDBAccount is the Schema for the mariadbaccounts API displayName: Maria DBAccount