From 4471b1ed81c9a6eb95fe9c08f6e41ed021e8f0db Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 24 Oct 2023 14:51:57 -0400 Subject: [PATCH] add controller to create and delete individual usernames in mariadb this is a first draft of a "create /drop account" controller that is separate from the main "create/drop database" controller, for the purpose of producing rotating username/passwords. The background for the change is based on discussions surrounding https://issues.redhat.com/browse/OSPRH-92 where internal control plane services such as Galera , Rabbit, Redis etc. would provide interfaces to add /remove arbitrary usernames, where a "password rotation" would involve adding a new username/password and having services switch there, retiring the old account once all finalizers have been removed. --- ...mariadb.openstack.org_mariadbaccounts.yaml | 60 ++++ api/v1beta1/mariadbaccount_types.go | 22 +- api/v1beta1/zz_generated.deepcopy.go | 9 +- ...mariadb.openstack.org_mariadbaccounts.yaml | 60 ++++ config/rbac/role.yaml | 26 ++ .../mariadb_v1beta1_mariadbaccount.yaml | 17 +- controllers/galera_controller.go | 44 +++ controllers/mariadbaccount_controller.go | 289 +++++++++++++++++- controllers/mariadbdatabase_controller.go | 40 +-- main.go | 9 + pkg/mariadb/account.go | 131 ++++++++ templates/account.sh | 4 + templates/delete_account.sh | 3 + 13 files changed, 656 insertions(+), 58 deletions(-) create mode 100644 api/bases/mariadb.openstack.org_mariadbaccounts.yaml create mode 100644 config/crd/bases/mariadb.openstack.org_mariadbaccounts.yaml create mode 100644 pkg/mariadb/account.go create mode 100755 templates/account.sh create mode 100755 templates/delete_account.sh diff --git a/api/bases/mariadb.openstack.org_mariadbaccounts.yaml b/api/bases/mariadb.openstack.org_mariadbaccounts.yaml new file mode 100644 index 00000000..e4c5f089 --- /dev/null +++ b/api/bases/mariadb.openstack.org_mariadbaccounts.yaml @@ -0,0 +1,60 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: mariadbaccounts.mariadb.openstack.org +spec: + group: mariadb.openstack.org + names: + kind: MariaDBAccount + listKind: MariaDBAccountList + plural: mariadbaccounts + singular: mariadbaccount + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: MariaDBAccount is the Schema for the mariadbaccounts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MariaDBAccountSpec defines the desired state of MariaDBAccount + properties: + secret: + description: Name of secret which contains DatabasePassword + type: string + userName: + description: UserName for new account + type: string + type: object + status: + description: MariaDBAccountStatus defines the observed state of MariaDBAccount + properties: + completed: + type: boolean + hash: + additionalProperties: + type: string + description: Map of hashes to track e.g. job status + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/v1beta1/mariadbaccount_types.go b/api/v1beta1/mariadbaccount_types.go index 944e2d66..94c3d577 100644 --- a/api/v1beta1/mariadbaccount_types.go +++ b/api/v1beta1/mariadbaccount_types.go @@ -20,22 +20,28 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. +const ( + // AccountCreateHash hash + AccountCreateHash = "accountcreate" + + // AccountDeleteHash hash + AccountDeleteHash = "accountdelete" +) // MariaDBAccountSpec defines the desired state of MariaDBAccount type MariaDBAccountSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file + // UserName for new account + UserName string `json:"userName,omitempty"` - // Foo is an example field of MariaDBAccount. Edit mariadbaccount_types.go to remove/update - Foo string `json:"foo,omitempty"` + // Name of secret which contains DatabasePassword + Secret string `json:"secret,omitempty"` } // MariaDBAccountStatus defines the observed state of MariaDBAccount type MariaDBAccountStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file + Completed bool `json:"completed,omitempty"` + // Map of hashes to track e.g. job status + Hash map[string]string `json:"hash,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 8d0a43bd..8de03cbc 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -232,7 +232,7 @@ func (in *MariaDBAccount) DeepCopyInto(out *MariaDBAccount) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) out.Spec = in.Spec - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MariaDBAccount. @@ -303,6 +303,13 @@ func (in *MariaDBAccountSpec) DeepCopy() *MariaDBAccountSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MariaDBAccountStatus) DeepCopyInto(out *MariaDBAccountStatus) { *out = *in + if in.Hash != nil { + in, out := &in.Hash, &out.Hash + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MariaDBAccountStatus. diff --git a/config/crd/bases/mariadb.openstack.org_mariadbaccounts.yaml b/config/crd/bases/mariadb.openstack.org_mariadbaccounts.yaml new file mode 100644 index 00000000..e4c5f089 --- /dev/null +++ b/config/crd/bases/mariadb.openstack.org_mariadbaccounts.yaml @@ -0,0 +1,60 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.1 + creationTimestamp: null + name: mariadbaccounts.mariadb.openstack.org +spec: + group: mariadb.openstack.org + names: + kind: MariaDBAccount + listKind: MariaDBAccountList + plural: mariadbaccounts + singular: mariadbaccount + scope: Namespaced + versions: + - name: v1beta1 + schema: + openAPIV3Schema: + description: MariaDBAccount is the Schema for the mariadbaccounts API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: MariaDBAccountSpec defines the desired state of MariaDBAccount + properties: + secret: + description: Name of secret which contains DatabasePassword + type: string + userName: + description: UserName for new account + type: string + type: object + status: + description: MariaDBAccountStatus defines the observed state of MariaDBAccount + properties: + completed: + type: boolean + hash: + additionalProperties: + type: string + description: Map of hashes to track e.g. job status + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 0db83485..5649a5f7 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -151,6 +151,32 @@ rules: - list - patch - update +- apiGroups: + - mariadb.openstack.org + resources: + - mariadbaccounts + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - mariadb.openstack.org + resources: + - mariadbaccounts/finalizers + verbs: + - update +- apiGroups: + - mariadb.openstack.org + resources: + - mariadbaccounts/status + verbs: + - get + - patch + - update - apiGroups: - mariadb.openstack.org resources: diff --git a/config/samples/mariadb_v1beta1_mariadbaccount.yaml b/config/samples/mariadb_v1beta1_mariadbaccount.yaml index e8ed3f6c..ef7f68b5 100644 --- a/config/samples/mariadb_v1beta1_mariadbaccount.yaml +++ b/config/samples/mariadb_v1beta1_mariadbaccount.yaml @@ -7,6 +7,19 @@ metadata: app.kubernetes.io/part-of: mariadb-operator app.kubernetes.io/managed-by: kustomize app.kubernetes.io/created-by: mariadb-operator - name: mariadbaccount-sample + mariaDBDatabaseName: neutron + name: neutron1 spec: - # TODO(user): Add fields here + userName: neutron1 + secret: neutrondb-secret + +--- + +apiVersion: v1 +data: + # neutron123 + DatabasePassword: bmV1dHJvbjEyMw== +kind: Secret +metadata: + name: neutrondb-secret +type: Opaque diff --git a/controllers/galera_controller.go b/controllers/galera_controller.go index a51a60d6..a97d2312 100644 --- a/controllers/galera_controller.go +++ b/controllers/galera_controller.go @@ -28,6 +28,7 @@ import ( corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/kubectl/pkg/util/podutils" @@ -49,6 +50,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + databasev1beta1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" mariadb "github.com/openstack-k8s-operators/mariadb-operator/pkg/mariadb" ) @@ -665,3 +667,45 @@ func (r *GaleraReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&rbacv1.RoleBinding{}). Complete(r) } + +// GetDatabaseObject - returns either a Galera or MariaDB object (and an associated client.Object interface). +// used by both MariaDBDatabaseReconciler and MariaDBAccountReconciler +// this will later return only Galera objects, so as a lookup it's part of the galera controller + +func GetDatabaseObject(clientObj client.Client, ctx context.Context, name string, namespace string) (client.Object, *databasev1beta1.Galera, *databasev1beta1.MariaDB, error) { + + dbGalera := &databasev1beta1.Galera{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + objectKey := client.ObjectKeyFromObject(dbGalera) + + err := clientObj.Get(ctx, objectKey, dbGalera) + if err != nil && !k8s_errors.IsNotFound(err) { + return nil, nil, nil, err + } + + if err != nil { + // Try to fetch MariaDB when Galera is not used + dbMariadb := &databasev1beta1.MariaDB{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + } + + objectKey = client.ObjectKeyFromObject(dbMariadb) + + err = clientObj.Get(ctx, objectKey, dbMariadb) + if err != nil { + return nil, nil, nil, err + } + + return dbMariadb, nil, dbMariadb, nil + } + + return dbGalera, dbGalera, nil, nil +} diff --git a/controllers/mariadbaccount_controller.go b/controllers/mariadbaccount_controller.go index f62cdc1a..69a6edc4 100644 --- a/controllers/mariadbaccount_controller.go +++ b/controllers/mariadbaccount_controller.go @@ -18,19 +18,36 @@ package controllers import ( "context" + "fmt" + "time" + "github.com/go-logr/logr" + helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + job "github.com/openstack-k8s-operators/lib-common/modules/common/job" + databasev1beta1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" + mariadb "github.com/openstack-k8s-operators/mariadb-operator/pkg/mariadb" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - mariadbv1beta1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) // MariaDBAccountReconciler reconciles a MariaDBAccount object type MariaDBAccountReconciler struct { client.Client - Scheme *runtime.Scheme + Kclient kubernetes.Interface + Log logr.Logger + Scheme *runtime.Scheme +} + +// SetupWithManager - +func (r *MariaDBAccountReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&databasev1beta1.MariaDBAccount{}). + Complete(r) } //+kubebuilder:rbac:groups=mariadb.openstack.org,resources=mariadbaccounts,verbs=get;list;watch;create;update;patch;delete @@ -46,17 +63,265 @@ type MariaDBAccountReconciler struct { // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile -func (r *MariaDBAccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) +func (r *MariaDBAccountReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, _err error) { + _ = r.Log.WithValues("MariaDBAccount", req.NamespacedName) + + var err error + + instance := &databasev1beta1.MariaDBAccount{} + err = r.Client.Get(ctx, req.NamespacedName, instance) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + helper, err := helper.NewHelper( + instance, + r.Client, + r.Kclient, + r.Scheme, + r.Log, + ) + if err != nil { + return ctrl.Result{}, err + } + + // Always patch the instance status when exiting this function so we can persist any changes. + defer func() { + err := helper.PatchInstance(ctx, instance) + if err != nil { + _err = err + return + } + }() + + isDelete := !instance.DeletionTimestamp.IsZero() + + // locate the MariaDBDatabase object that this account is associated with + mariadbDatabase, err := r.getMariaDBDatabaseObject(ctx, instance) + + // if not found because the label is missing, don't requeue, will be + // reconciled again when label is fixed + if err != nil && k8s_errors.IsNotFound(err) && instance.ObjectMeta.Labels["mariaDBDatabaseName"] == "" { + r.Log.Info(fmt.Sprintf("MariaDBAccount '%s' does not have a 'mariaDBDatabaseName' label; returning", instance.Name)) + return ctrl.Result{}, err + } + + // not found, or found but not in completed status. requeue for a create, + // exit operation for a delete + if (err != nil && k8s_errors.IsNotFound(err)) || (err == nil && !mariadbDatabase.Status.Completed) { + if !isDelete { + // for the create case, need to wait for the MariaDBDatabase to exists before we can continue; + // requeue + if err != nil { + r.Log.Info(fmt.Sprintf( + "MariaDBAccount '%s' didn't find MariaDBDatabase '%s'; requeueing", + instance.Name, instance.ObjectMeta.Labels["mariaDBDatabaseName"])) + } else { + r.Log.Info(fmt.Sprintf( + "MariaDBAccount '%s' MariaDBDatabase '%s' not yet complete; requeueing", + instance.Name, instance.ObjectMeta.Labels["mariaDBDatabaseName"])) + } + return ctrl.Result{RequeueAfter: time.Duration(10) * time.Second}, nil + } else { + // for the delete case, the database doesn't exist. so + // that means we don't, either. remove finalizer from + // our own instance and return + if err != nil { + r.Log.Info(fmt.Sprintf( + "MariaDBAccount '%s' Didn't find MariaDBDatabase '%s'; no account delete needed", + instance.Name, instance.ObjectMeta.Labels["mariaDBDatabaseName"])) + + } else { + r.Log.Info(fmt.Sprintf( + "MariaDBAccount '%s' MariaDBDatabase '%s' not yet complete; no account delete needed", + instance.Name, instance.ObjectMeta.Labels["mariaDBDatabaseName"])) + } + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + + return ctrl.Result{}, nil + } + } else if err != nil { + // unhandled error; exit + return ctrl.Result{}, nil + } + + if !isDelete { + // MariaDBdatabase exists and we are a create case. ensure finalizers set up + if controllerutil.AddFinalizer(mariadbDatabase, fmt.Sprintf("%s-%s", helper.GetFinalizer(), instance.Name)) { + err := r.Update(ctx, mariadbDatabase) + if err != nil { + return ctrl.Result{}, err + } + } + + if controllerutil.AddFinalizer(instance, helper.GetFinalizer()) { + // we need to persist this right away + return ctrl.Result{}, nil + } + } + + // now proceed to do actual work. acquire the galera/mariadb instance + // referenced by the MariaDBDatabase which will lead us to the hostname + // and container image to target + dbGalera, dbMariadb, err := r.getDatabaseObject(ctx, mariadbDatabase, instance) + if err != nil { + return ctrl.Result{}, err + } + + var dbInstance, dbAdminSecret, dbContainerImage, serviceAccountName string + + // ensure Galera/MariaDB instance itself is ready + // TODO: not sure to what extent galera / mariadb would not be ready + // at this point, considering MariaDBDatabase is dependent on this state + // as well and that's been assured as "ready" + if dbGalera != nil { + if !dbGalera.Status.Bootstrapped { + r.Log.Info("DB bootstrap not complete. Requeue...") + return ctrl.Result{RequeueAfter: time.Second * 10}, nil + } + + dbInstance = dbGalera.Name + dbAdminSecret = dbGalera.Spec.Secret + dbContainerImage = dbGalera.Spec.ContainerImage + serviceAccountName = dbGalera.RbacResourceName() + } else if dbMariadb != nil { + if dbMariadb.Status.DbInitHash == "" { + r.Log.Info("DB initialization not complete. Requeue...") + return ctrl.Result{RequeueAfter: time.Duration(10) * time.Second}, nil + } - // TODO(user): your logic here + dbInstance = dbMariadb.Name + dbAdminSecret = dbMariadb.Spec.Secret + dbContainerImage = dbMariadb.Spec.ContainerImage + serviceAccountName = dbMariadb.RbacResourceName() + } else { + r.Log.Error(err, "no mariadb or galera, should not be here") + return ctrl.Result{}, err + } + + if !isDelete { + // account create + + jobDef, err := mariadb.CreateDbAccountJob(instance, mariadbDatabase.Name, dbInstance, dbAdminSecret, dbContainerImage, serviceAccountName) + if err != nil { + return ctrl.Result{}, err + } + + accountCreateHash := instance.Status.Hash[databasev1beta1.AccountCreateHash] + accountCreateJob := job.NewJob( + jobDef, + databasev1beta1.AccountCreateHash, + false, + time.Duration(5)*time.Second, + accountCreateHash, + ) + ctrlResult, err := accountCreateJob.DoJob( + ctx, + helper, + ) + if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + if err != nil { + return ctrl.Result{}, err + } + if accountCreateJob.HasChanged() { + if instance.Status.Hash == nil { + instance.Status.Hash = make(map[string]string) + } + instance.Status.Hash[databasev1beta1.AccountCreateHash] = accountCreateJob.GetHash() + r.Log.Info(fmt.Sprintf("Job %s hash added - %s", jobDef.Name, instance.Status.Hash[databasev1beta1.AccountCreateHash])) + } + + // database creation finished + instance.Status.Completed = true + + } else { + // account delete + + jobDef, err := mariadb.DeleteDbAccountJob(instance, mariadbDatabase.Name, dbInstance, dbAdminSecret, dbContainerImage, serviceAccountName) + if err != nil { + return ctrl.Result{}, err + } + + accountDeleteHash := instance.Status.Hash[databasev1beta1.AccountDeleteHash] + accountDeleteJob := job.NewJob( + jobDef, + databasev1beta1.AccountDeleteHash, + false, + time.Duration(5)*time.Second, + accountDeleteHash, + ) + ctrlResult, err := accountDeleteJob.DoJob( + ctx, + helper, + ) + if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + if err != nil { + return ctrl.Result{}, err + } + if accountDeleteJob.HasChanged() { + if instance.Status.Hash == nil { + instance.Status.Hash = make(map[string]string) + } + // TODO: do we set a hash for deletes? + instance.Status.Hash[databasev1beta1.AccountDeleteHash] = accountDeleteJob.GetHash() + r.Log.Info(fmt.Sprintf("Job %s hash added - %s", jobDef.Name, instance.Status.Hash[databasev1beta1.AccountDeleteHash])) + } + + // remove finalizer from the MariaDBDatabase instance + if controllerutil.RemoveFinalizer(mariadbDatabase, fmt.Sprintf("%s-%s", helper.GetFinalizer(), instance.Name)) { + err = r.Update(ctx, mariadbDatabase) + } + + // remove finalizer from our own instance + controllerutil.RemoveFinalizer(instance, helper.GetFinalizer()) + + if err != nil { + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } -// SetupWithManager sets up the controller with the Manager. -func (r *MariaDBAccountReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&mariadbv1beta1.MariaDBAccount{}). - Complete(r) +// getDatabaseObject - returns either a Galera or MariaDB object (and an associated client.Object interface) +func (r *MariaDBAccountReconciler) getDatabaseObject(ctx context.Context, mariaDBDatabase *databasev1beta1.MariaDBDatabase, instance *databasev1beta1.MariaDBAccount) (*databasev1beta1.Galera, *databasev1beta1.MariaDB, error) { + dbName := mariaDBDatabase.ObjectMeta.Labels["dbName"] + _, dbGalera, dbMariaDB, err := GetDatabaseObject( + r.Client, ctx, + dbName, + instance.Namespace, + ) + + return dbGalera, dbMariaDB, err +} + +// getMariaDBDatabaseObject - returns either a Galera or MariaDB object (and an associated client.Object interface) +func (r *MariaDBAccountReconciler) getMariaDBDatabaseObject(ctx context.Context, instance *databasev1beta1.MariaDBAccount) (*databasev1beta1.MariaDBDatabase, error) { + // this is following from how the MariaDBDatabase CRD works. + // the related Galera / MariaDB object is given as a label, while + // the reference to the secret itself is given in the spec + // the convention appears to be: "things we are dependent on are named in labels, + // things we are setting up are named in the spec" + mariadbDatabaseName := instance.ObjectMeta.Labels["mariaDBDatabaseName"] + + mariaDBDatabase := &databasev1beta1.MariaDBDatabase{ + ObjectMeta: metav1.ObjectMeta{ + Name: mariadbDatabaseName, + Namespace: instance.Namespace, + }, + } + + objectKey := client.ObjectKeyFromObject(mariaDBDatabase) + + err := r.Client.Get(ctx, objectKey, mariaDBDatabase) + if err != nil { + return nil, err + } + + return mariaDBDatabase, err + } diff --git a/controllers/mariadbdatabase_controller.go b/controllers/mariadbdatabase_controller.go index 61bf0d52..f06afcac 100644 --- a/controllers/mariadbdatabase_controller.go +++ b/controllers/mariadbdatabase_controller.go @@ -23,7 +23,6 @@ import ( "github.com/go-logr/logr" k8s_errors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes" ctrl "sigs.k8s.io/controller-runtime" @@ -206,38 +205,9 @@ func (r *MariaDBDatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { // getDatabaseObject - returns either a Galera or MariaDB object (and an associated client.Object interface) func (r *MariaDBDatabaseReconciler) getDatabaseObject(ctx context.Context, instance *databasev1beta1.MariaDBDatabase) (client.Object, *databasev1beta1.Galera, *databasev1beta1.MariaDB, error) { - dbGalera := &databasev1beta1.Galera{ - ObjectMeta: metav1.ObjectMeta{ - Name: instance.ObjectMeta.Labels["dbName"], - Namespace: instance.Namespace, - }, - } - - objectKey := client.ObjectKeyFromObject(dbGalera) - - err := r.Client.Get(ctx, objectKey, dbGalera) - if err != nil && !k8s_errors.IsNotFound(err) { - return nil, nil, nil, err - } - - if err != nil { - // Try to fetch MariaDB when Galera is not used - dbMariadb := &databasev1beta1.MariaDB{ - ObjectMeta: metav1.ObjectMeta{ - Name: instance.ObjectMeta.Labels["dbName"], - Namespace: instance.Namespace, - }, - } - - objectKey = client.ObjectKeyFromObject(dbMariadb) - - err = r.Client.Get(ctx, objectKey, dbMariadb) - if err != nil { - return nil, nil, nil, err - } - - return dbMariadb, nil, dbMariadb, nil - } - - return dbGalera, dbGalera, nil, nil + return GetDatabaseObject( + r.Client, ctx, + instance.ObjectMeta.Labels["dbName"], + instance.Namespace, + ) } diff --git a/main.go b/main.go index 51395cf1..9a250d06 100644 --- a/main.go +++ b/main.go @@ -129,6 +129,15 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "MariaDBDatabase") os.Exit(1) } + if err = (&controllers.MariaDBAccountReconciler{ + Client: mgr.GetClient(), + Kclient: kclient, + Log: ctrl.Log.WithName("controllers").WithName("MariaDBAccount"), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "MariaDBAccount") + os.Exit(1) + } // Acquire environmental defaults and initialize operator defaults with them mariadbv1beta1.SetupDefaults() diff --git a/pkg/mariadb/account.go b/pkg/mariadb/account.go new file mode 100644 index 00000000..80fe952d --- /dev/null +++ b/pkg/mariadb/account.go @@ -0,0 +1,131 @@ +package mariadb + +import ( + "strings" + + util "github.com/openstack-k8s-operators/lib-common/modules/common/util" + databasev1beta1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type accountCreateOrDeleteOptions struct { + UserName string + DatabaseName string + DatabaseHostname string + DatabaseAdminUsername string +} + +func CreateDbAccountJob(account *databasev1beta1.MariaDBAccount, databaseName string, databaseHostName string, databaseSecret string, containerImage string, serviceAccountName string) (*batchv1.Job, error) { + + opts := accountCreateOrDeleteOptions{account.Spec.UserName, databaseName, databaseHostName, "root"} + dbCmd, err := util.ExecuteTemplateFile("account.sh", &opts) + if err != nil { + return nil, err + } + labels := map[string]string{ + "owner": "mariadb-operator", "cr": account.Spec.UserName, "app": "mariadbschema", + } + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + // provided db name is used as metadata name where underscore is a not allowed + // character. Lets replace all underscores with hypen. Underscores in the db name are + // possible. + Name: strings.Replace(account.Spec.UserName, "_", "-", -1) + "-account-create", + Namespace: account.Namespace, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: serviceAccountName, + Containers: []corev1.Container{ + { + Name: "mariadb-account-create", + Image: containerImage, + Command: []string{"/bin/sh", "-c", dbCmd}, + Env: []corev1.EnvVar{ + { + Name: "MYSQL_PWD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: databaseSecret, + }, + Key: "DbRootPassword", + }, + }, + }, + { + Name: "DatabasePassword", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: account.Spec.Secret, + }, + Key: "DatabasePassword", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + return job, nil +} + +func DeleteDbAccountJob(account *databasev1beta1.MariaDBAccount, databaseName string, databaseHostName string, databaseSecret string, containerImage string, serviceAccountName string) (*batchv1.Job, error) { + + opts := accountCreateOrDeleteOptions{account.Spec.UserName, databaseName, databaseHostName, "root"} + + delCmd, err := util.ExecuteTemplateFile("delete_account.sh", &opts) + if err != nil { + return nil, err + } + labels := map[string]string{ + "owner": "mariadb-operator", "cr": account.Spec.UserName, "app": "mariadbschema", + } + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: strings.Replace(account.Spec.UserName, "_", "", -1) + "-account-delete", + Namespace: account.Namespace, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + ServiceAccountName: serviceAccountName, + Containers: []corev1.Container{ + { + Name: "mariadb-account-delete", + Image: containerImage, + Command: []string{"/bin/sh", "-c", delCmd}, + Env: []corev1.EnvVar{ + { + Name: "MYSQL_PWD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: databaseSecret, + }, + Key: "DbRootPassword", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + return job, nil +} diff --git a/templates/account.sh b/templates/account.sh new file mode 100755 index 00000000..c928533e --- /dev/null +++ b/templates/account.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export DatabasePassword=${DatabasePassword:?"Please specify a DatabasePassword variable."} + +mysql -h {{.DatabaseHostname}} -u {{.DatabaseAdminUsername}} -P 3306 -e "GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.UserName}}'@'localhost' IDENTIFIED BY '$DatabasePassword';GRANT ALL PRIVILEGES ON {{.DatabaseName}}.* TO '{{.UserName}}'@'%' IDENTIFIED BY '$DatabasePassword';" diff --git a/templates/delete_account.sh b/templates/delete_account.sh new file mode 100755 index 00000000..f71b08dd --- /dev/null +++ b/templates/delete_account.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mysql -h {{.DatabaseHostname}} -u {{.DatabaseAdminUsername}} -P 3306 -e "DROP USER IF EXISTS '{{.UserName}}'@'localhost'; DROP USER IF EXISTS '{{.UserName}}'@'%';"