From 98bee83ba00064304eb0687d77488510a54251d7 Mon Sep 17 00:00:00 2001 From: Boris Budini Date: Wed, 21 Feb 2024 18:55:15 +0100 Subject: [PATCH] Add database --- PROJECT | 14 +- api/v1alpha1/database_types.go | 67 +++++ api/v1alpha1/database_webhook.go | 138 +++++++++++ .../databases.digitalocean.com_databases.yaml | 88 +++++++ config/crd/kustomization.yaml | 3 + .../crd/patches/cainjection_in_databases.yaml | 7 + config/crd/patches/webhook_in_databases.yaml | 16 ++ config/rbac/database_editor_role.yaml | 24 ++ config/rbac/database_viewer_role.yaml | 20 ++ config/rbac/role.yaml | 26 ++ .../samples/databases_v1alpha1_database.yaml | 22 ++ config/webhook/manifests.yaml | 20 ++ controllers/database_controller.go | 234 ++++++++++++++++++ main.go | 12 + 14 files changed, 690 insertions(+), 1 deletion(-) create mode 100644 api/v1alpha1/database_types.go create mode 100644 api/v1alpha1/database_webhook.go create mode 100644 config/crd/bases/databases.digitalocean.com_databases.yaml create mode 100644 config/crd/patches/cainjection_in_databases.yaml create mode 100644 config/crd/patches/webhook_in_databases.yaml create mode 100644 config/rbac/database_editor_role.yaml create mode 100644 config/rbac/database_viewer_role.yaml create mode 100644 config/samples/databases_v1alpha1_database.yaml create mode 100644 controllers/database_controller.go diff --git a/PROJECT b/PROJECT index a6a8263e..f0e23430 100644 --- a/PROJECT +++ b/PROJECT @@ -52,4 +52,16 @@ resources: webhooks: validation: true webhookVersion: v1 -version: "3" +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: digitalocean.com + group: databases + kind: Database + path: github.com/digitalocean/do-operator/api/v1alpha1 + version: v1alpha1 + webhooks: + conversion: true + webhookVersion: v1 +version: "3" \ No newline at end of file diff --git a/api/v1alpha1/database_types.go b/api/v1alpha1/database_types.go new file mode 100644 index 00000000..d6daf63d --- /dev/null +++ b/api/v1alpha1/database_types.go @@ -0,0 +1,67 @@ +/* +Copyright 2022 DigitalOcean. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// DatabaseSpec defines the desired state of Database +type DatabaseSpec struct { + // Cluster is a reference to the DatabaseCluster or DatabaseClusterReference + // that represents the database cluster in which the database will be created. + Cluster corev1.TypedLocalObjectReference `json:"databaseCluster"` + // name is the name for the database. + Name string `json:"name"` +} + +// DatabaseStatus defines the observed state of Database +type DatabaseStatus struct { + // ClusterUUID is the UUID of the cluster this database is in. We keep this in + // the status so that we can manage the user even if the referenced Cluster + // CR is deleted. + ClusterUUID string `json:"clusterUUID,omitempty"` + Name string `json:"name,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +//+kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name` + +// Database is the Schema for the databases API +type Database struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DatabaseSpec `json:"spec,omitempty"` + Status DatabaseStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// DatabaseList contains a list of Database +type DatabaseList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Database `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Database{}, &DatabaseList{}) +} diff --git a/api/v1alpha1/database_webhook.go b/api/v1alpha1/database_webhook.go new file mode 100644 index 00000000..6f9837d4 --- /dev/null +++ b/api/v1alpha1/database_webhook.go @@ -0,0 +1,138 @@ +/* +Copyright 2022 DigitalOcean. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "context" + "fmt" + "net/http" + "strings" + + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" + + "github.com/digitalocean/godo" + "github.com/google/go-cmp/cmp" + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var databaselog = logf.Log.WithName("database-resource") + +func (r *Database) SetupWebhookWithManager(mgr ctrl.Manager, godoClient *godo.Client) error { + initGlobalGodoClient(godoClient) + initGlobalK8sClient(mgr.GetClient()) + + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/validate-databases-digitalocean-com-v1alpha1-database,mutating=false,failurePolicy=fail,sideEffects=None,groups=databases.digitalocean.com,resources=databases,verbs=create;update,versions=v1alpha1,name=vdatabase.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &Database{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *Database) ValidateCreate() (warnings admission.Warnings, err error) { + databaselog.Info("validate create", "name", r.Name) + ctx := context.TODO() + + clusterPath := field.NewPath("spec").Child("cluster") + + clusterAPIGroup := pointer.StringDeref(r.Spec.Cluster.APIGroup, "") + if clusterAPIGroup != GroupVersion.Group { + return warnings, field.Invalid(clusterPath.Child("apiGroup"), clusterAPIGroup, "apiGroup must be "+GroupVersion.Group) + } + + var ( + clusterNN = types.NamespacedName{ + Namespace: r.Namespace, + Name: r.Spec.Cluster.Name, + } + clusterKind = r.Spec.Cluster.Kind + clusterUUID string + ) + + switch strings.ToLower(clusterKind) { + case strings.ToLower(DatabaseClusterKind): + var cluster DatabaseCluster + if err := webhookClient.Get(ctx, clusterNN, &cluster); err != nil { + if kerrors.IsNotFound(err) { + return warnings, field.NotFound(clusterPath, clusterNN) + } + return warnings, fmt.Errorf("failed to fetch DatabaseCluster %s: %s", clusterNN, err) + } + clusterUUID = cluster.Status.UUID + case strings.ToLower(DatabaseClusterReferenceKind): + var clusterRef DatabaseClusterReference + if err := webhookClient.Get(ctx, clusterNN, &clusterRef); err != nil { + if kerrors.IsNotFound(err) { + return warnings, field.NotFound(clusterPath, clusterNN) + } + return warnings, fmt.Errorf("failed to fetch DatabaseClusterReference %s: %s", clusterNN, err) + } + clusterUUID = clusterRef.Spec.UUID + default: + return warnings, field.TypeInvalid( + clusterPath.Child("kind"), + clusterKind, + "kind must be DatabaseCluster or DatabaseClusterReference", + ) + } + + _, resp, err := godoClient.Databases.GetDB(ctx, clusterUUID, r.Spec.Name) + if err != nil && resp.StatusCode != http.StatusNotFound { + return warnings, fmt.Errorf("failed to look up database: %v", err) + } + if err == nil { + return warnings, field.Duplicate(field.NewPath("spec").Child("name"), r.Spec.Name) + } + + return warnings, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *Database) ValidateUpdate(old runtime.Object) (warnings admission.Warnings, err error) { + databaselog.Info("validate update", "name", r.Name) + + oldDatabase, ok := old.(*Database) + if !ok { + return warnings, fmt.Errorf("old is unexpected type %T", old) + } + namePath := field.NewPath("spec").Child("name") + if r.Spec.Name != oldDatabase.Spec.Name { + return warnings, field.Forbidden(namePath, "name is immutable") + } + clusterPath := field.NewPath("spec").Child("cluster") + if !cmp.Equal(r.Spec.Cluster, oldDatabase.Spec.Cluster) { + return warnings, field.Forbidden(clusterPath, "cluster is immutable") + } + + return warnings, nil +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *Database) ValidateDelete() (warnings admission.Warnings, err error) { + databaselog.Info("validate delete", "name", r.Name) + return warnings, nil +} diff --git a/config/crd/bases/databases.digitalocean.com_databases.yaml b/config/crd/bases/databases.digitalocean.com_databases.yaml new file mode 100644 index 00000000..32403557 --- /dev/null +++ b/config/crd/bases/databases.digitalocean.com_databases.yaml @@ -0,0 +1,88 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: databases.databases.digitalocean.com +spec: + group: databases.digitalocean.com + names: + kind: Database + listKind: DatabaseList + plural: databases + singular: database + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .spec.name + name: Name + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: Database is the Schema for the databases 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: DatabaseSpec defines the desired state of Database + properties: + databaseCluster: + description: Cluster is a reference to the DatabaseCluster or DatabaseClusterReference + that represents the database cluster in which the database will + be created. + properties: + apiGroup: + description: APIGroup is the group for the resource being referenced. + If APIGroup is not specified, the specified Kind must be in + the core API group. For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + name: + description: name is the name for the database. + type: string + required: + - databaseCluster + - name + type: object + status: + description: DatabaseStatus defines the observed state of Database + properties: + clusterUUID: + description: ClusterUUID is the UUID of the cluster this database + is in. We keep this in the status so that we can manage the user + even if the referenced Cluster CR is deleted. + type: string + name: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5c3c892d..43195f7c 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/databases.digitalocean.com_databaseclusterreferences.yaml - bases/databases.digitalocean.com_databaseusers.yaml - bases/databases.digitalocean.com_databaseuserreferences.yaml +- bases/databases.digitalocean.com_databases.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -15,6 +16,7 @@ patchesStrategicMerge: - patches/webhook_in_databaseclusterreferences.yaml - patches/webhook_in_databaseusers.yaml - patches/webhook_in_databaseuserreferences.yaml +- patches/webhook_in_databases.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -23,6 +25,7 @@ patchesStrategicMerge: - patches/cainjection_in_databaseclusterreferences.yaml - patches/cainjection_in_databaseusers.yaml - patches/cainjection_in_databaseuserreferences.yaml +- patches/cainjection_in_databases.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_databases.yaml b/config/crd/patches/cainjection_in_databases.yaml new file mode 100644 index 00000000..477e9b0b --- /dev/null +++ b/config/crd/patches/cainjection_in_databases.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: databases.databases.digitalocean.com \ No newline at end of file diff --git a/config/crd/patches/webhook_in_databases.yaml b/config/crd/patches/webhook_in_databases.yaml new file mode 100644 index 00000000..0e99461b --- /dev/null +++ b/config/crd/patches/webhook_in_databases.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: databases.databases.digitalocean.com +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 \ No newline at end of file diff --git a/config/rbac/database_editor_role.yaml b/config/rbac/database_editor_role.yaml new file mode 100644 index 00000000..3d22570a --- /dev/null +++ b/config/rbac/database_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end s to edit databases. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: database-editor-role +rules: +- apiGroups: + - databases.digitalocean.com + resources: + - databases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - databases.digitalocean.com + resources: + - databases/status + verbs: + - get \ No newline at end of file diff --git a/config/rbac/database_viewer_role.yaml b/config/rbac/database_viewer_role.yaml new file mode 100644 index 00000000..cc9c3003 --- /dev/null +++ b/config/rbac/database_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end s to view databases. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: database-viewer-role +rules: +- apiGroups: + - databases.digitalocean.com + resources: + - databases + verbs: + - get + - list + - watch +- apiGroups: + - databases.digitalocean.com + resources: + - databases/status + verbs: + - get \ No newline at end of file diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index d364931d..2714181a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -72,6 +72,32 @@ rules: - get - patch - update +- apiGroups: + - databases.digitalocean.com + resources: + - databases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - databases.digitalocean.com + resources: + - databases/finalizers + verbs: + - update +- apiGroups: + - databases.digitalocean.com + resources: + - databases/status + verbs: + - get + - patch + - update - apiGroups: - databases.digitalocean.com resources: diff --git a/config/samples/databases_v1alpha1_database.yaml b/config/samples/databases_v1alpha1_database.yaml new file mode 100644 index 00000000..393be11e --- /dev/null +++ b/config/samples/databases_v1alpha1_database.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: databases.digitalocean.com/v1alpha1 +kind: Database +metadata: + name: databasecluster-database +spec: + databaseCluster: + apiGroup: databases.digitalocean.com + kind: DatabaseCluster + name: sample-mysql-database + name: sample_user_1 +--- +apiVersion: databases.digitalocean.com/v1alpha1 +kind: Database +metadata: + name: databaseclusterreference-database +spec: + databaseCluster: + apiGroup: databases.digitalocean.com + kind: DatabaseClusterReference + name: sample-db-reference + name: sample_user_2 \ No newline at end of file diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index fdeaca4c..358836b2 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -5,6 +5,26 @@ metadata: creationTimestamp: null name: validating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-databases-digitalocean-com-v1alpha1-database + failurePolicy: Fail + name: vdatabase.kb.io + rules: + - apiGroups: + - databases.digitalocean.com + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - databases + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/controllers/database_controller.go b/controllers/database_controller.go new file mode 100644 index 00000000..8b9f9bf8 --- /dev/null +++ b/controllers/database_controller.go @@ -0,0 +1,234 @@ +/* +Copyright 2022 DigitalOcean. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package controllers + +import ( + "context" + "fmt" + "net/http" + "time" + + kerrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + utilerror "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/digitalocean/do-operator/api/v1alpha1" + databasesv1alpha1 "github.com/digitalocean/do-operator/api/v1alpha1" + "github.com/digitalocean/godo" + "github.com/google/go-cmp/cmp" +) + +// DatabaseReconciler reconciles a Database object +type DatabaseReconciler struct { + client.Client + Scheme *runtime.Scheme + GodoClient *godo.Client +} + +//+kubebuilder:rbac:groups=databases.digitalocean.com,resources=databases,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=databases.digitalocean.com,resources=databases/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=databases.digitalocean.com,resources=databases/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=create;patch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, retErr error) { + ll := log.FromContext(ctx) + ll.Info("reconciling Database", "name", req.Name) + + var database v1alpha1.Database + err := r.Get(ctx, req.NamespacedName, &database) + if err != nil { + if kerrors.IsNotFound(err) { + return result, nil + } + return result, fmt.Errorf("failed to get Database %s: %s", req.NamespacedName, err) + } + + originalDatabase := database.DeepCopy() + inDeletion := !database.DeletionTimestamp.IsZero() + + defer func() { + var ( + updated = false + errs []error + ) + + if !cmp.Equal(database.Finalizers, originalDatabase.Finalizers) { + ll.Info("updating Database finalizers") + if err := r.Patch(ctx, database.DeepCopy(), client.MergeFrom(originalDatabase)); err != nil { + errs = append(errs, fmt.Errorf("failed to update Database: %s", err)) + } else { + updated = true + } + } + + if diff := cmp.Diff(database.Status, originalDatabase.Status); diff != "" { + ll.WithValues("diff", diff).Info("status diff detected") + + if err := r.Status().Patch(ctx, &database, client.MergeFrom(originalDatabase)); err != nil { + errs = append(errs, fmt.Errorf("failed to update Database status: %s", err)) + } else { + updated = true + } + } + + if len(errs) == 0 { + if updated { + ll.Info("Database update succeeded") + } else { + ll.Info("no Database update necessary") + } + } + + retErr = utilerror.NewAggregate(append([]error{retErr}, errs...)) + }() + + if inDeletion { + ll.Info("deleting Database") + if database.Status.ClusterUUID == "" { + // Database was never actually created; nothing to do. + controllerutil.RemoveFinalizer(&database, finalizerName) + return ctrl.Result{}, nil + } + return r.reconcileDeletedDB(ctx, database.Status.ClusterUUID, &database) + } + + var ( + clusterUUID = database.Status.ClusterUUID + clusterStatus string + clusterNN = types.NamespacedName{ + Namespace: database.Namespace, + Name: database.Spec.Cluster.Name, + } + ) + + // If we haven't noted the cluster's UUID yet, look it up. Subsequent + // reconciles won't have to do this. + if clusterUUID == "" { + switch database.Spec.Cluster.Kind { + case v1alpha1.DatabaseClusterKind: + var cluster v1alpha1.DatabaseCluster + if err := r.Get(ctx, clusterNN, &cluster); err != nil { + return result, fmt.Errorf("failed to get DatabaseCluster %s: %s", clusterNN.Name, err) + } + clusterUUID = cluster.Status.UUID + clusterStatus = cluster.Status.Status + case v1alpha1.DatabaseClusterReferenceKind: + var clusterRef v1alpha1.DatabaseClusterReference + if err := r.Get(ctx, clusterNN, &clusterRef); err != nil { + return result, fmt.Errorf("failed to get DatabaseClusterReference %s: %s", clusterNN.Name, err) + } + clusterUUID = clusterRef.Spec.UUID + clusterStatus = clusterRef.Status.Status + default: + // Validating webhook should ensure we never get here. + return result, fmt.Errorf("unexpected Kind for Cluster: %s", database.Spec.Cluster.Kind) + } + + // Database creation will fail if the cluster is still being created. Schedule a + // quick retry in those cases so we don't exponentially back off. + if clusterStatus == "" || clusterStatus == "creating" { + ll.Info("cluster is still creating; waiting to create database") + return ctrl.Result{RequeueAfter: 30 * time.Second}, nil + } + + database.Status.ClusterUUID = clusterUUID + } + + database.Status.ClusterUUID = clusterUUID + ll.Info("reconciling Database") + return r.reconcileDB(ctx, clusterUUID, &database) +} + +func (r *DatabaseReconciler) reconcileDB(ctx context.Context, clusterUUID string, database *v1alpha1.Database) (ctrl.Result, error) { + ll := log.FromContext(ctx) + ll = ll.WithValues( + "cluster_uuid", clusterUUID, + "name", database.Spec.Name, + ) + + // The validating webhook checks that the database doesn't already exist, so we + // assume that if we find it to exist now we created it. If the database was + // created between validation passing and getting here, we could assume + // ownership of an existing DB database. That's not ideal, but since databases don't + // have an ID other than the databasename we don't have a way to distinguish for + // sure. + + dbName, resp, err := r.GodoClient.Databases.GetDB(ctx, clusterUUID, database.Spec.Name) + if err != nil && resp.StatusCode != http.StatusNotFound { + return ctrl.Result{}, fmt.Errorf("checking for existing DB : %v", err) + } + + if resp.StatusCode == http.StatusNotFound { + createReq := &godo.DatabaseCreateDBRequest{ + Name: database.Spec.Name, + } + dbName, _, err = r.GodoClient.Databases.CreateDB(ctx, clusterUUID, createReq) + if err != nil { + ll.Error(err, "unable to create database") + return ctrl.Result{}, fmt.Errorf("creating DB database: %v", err) + } + } + + controllerutil.AddFinalizer(database, finalizerName) + + err = r.ensureOwnedObjects(ctx, database, dbName) + if err != nil { + ll.Error(err, "unable to ensure database-related objects") + return ctrl.Result{}, fmt.Errorf("ensuring database-related objects: %v", err) + } + + return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil +} + +func (r *DatabaseReconciler) ensureOwnedObjects(ctx context.Context, database *v1alpha1.Database, dbName *godo.DatabaseDB) error { + // For some database engines the password is not returned when fetching a + // database, only on initial creation. Avoid creating or updating the database + // credentials secret if the password is empty, so we don't clear the + // password after creation. + + return nil +} + +func (r *DatabaseReconciler) reconcileDeletedDB(ctx context.Context, clusterUUID string, database *v1alpha1.Database) (ctrl.Result, error) { + ll := log.FromContext(ctx) + ll = ll.WithValues( + "cluster_uuid", clusterUUID, + "name", database.Spec.Name, + ) + + _, err := r.GodoClient.Databases.DeleteDB(ctx, clusterUUID, database.Spec.Name) + if err != nil { + ll.Error(err, "unable to delete database") + return ctrl.Result{}, fmt.Errorf("deleting database: %v", err) + } + controllerutil.RemoveFinalizer(database, finalizerName) + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DatabaseReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&databasesv1alpha1.Database{}). + Complete(r) +} diff --git a/main.go b/main.go index 2068af04..f6eae656 100644 --- a/main.go +++ b/main.go @@ -166,6 +166,18 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "DatabaseCluster") os.Exit(1) } + if err = (&controllers.DatabaseReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + GodoClient: godoClient, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Database") + os.Exit(1) + } + if err = (&databasesv1alpha1.Database{}).SetupWebhookWithManager(mgr, godoClient); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "Database") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {