From e89fb4a69a73683a43e112c893b51ade2ea2efa0 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Wed, 21 Feb 2024 22:45:23 -0800 Subject: [PATCH 1/2] Add URIs to connection information Some applications like to have connection strings which isn't really possible to build with the current implementation, so take in the cluster connection string, and replace the user information with the user --- controllers/credentials_secret.go | 26 ++++++++++++++++--- controllers/databaseuser_controller.go | 14 +++++++--- .../databaseuserreference_controller.go | 14 +++++++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/controllers/credentials_secret.go b/controllers/credentials_secret.go index 4c663272..684d152d 100644 --- a/controllers/credentials_secret.go +++ b/controllers/credentials_secret.go @@ -1,9 +1,11 @@ package controllers import ( + "fmt" "github.com/digitalocean/godo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net/url" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -32,8 +34,8 @@ func credentialsSecretForDefaultDBUser(owner client.Object, db *godo.Database) * return secret } -func credentialsSecretForDBUser(owner client.Object, user *godo.DatabaseUser) *corev1.Secret { - return &corev1.Secret{ +func credentialsSecretForDBUser(db *godo.Database, owner client.Object, user *godo.DatabaseUser) (*corev1.Secret, error) { + secret := &corev1.Secret{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", @@ -45,7 +47,25 @@ func credentialsSecretForDBUser(owner client.Object, user *godo.DatabaseUser) *c StringData: map[string]string{ "username": user.Name, "password": user.Password, - // TODO(awg): Construct uri and private_uri from DB info. }, } + if db.Connection != nil { + dbUri, err := url.Parse(db.Connection.URI) + if err != nil { + return nil, fmt.Errorf("unable to parse connection uri: %s", err) + } + dbUri.User = url.UserPassword(user.Name, user.Password) + secret.StringData["uri"] = dbUri.String() + } + + if db.PrivateConnection != nil { + dbPrivateUri, err := url.Parse(db.PrivateConnection.URI) + if err != nil { + return nil, fmt.Errorf("unable to parse private connection uri: %s", err) + } + dbPrivateUri.User = url.UserPassword(user.Name, user.Password) + secret.StringData["private_uri"] = dbPrivateUri.String() + } + + return secret, nil } diff --git a/controllers/databaseuser_controller.go b/controllers/databaseuser_controller.go index dae3f34a..2c4adbf4 100644 --- a/controllers/databaseuser_controller.go +++ b/controllers/databaseuser_controller.go @@ -167,6 +167,11 @@ func (r *DatabaseUserReconciler) reconcileDBUser(ctx context.Context, clusterUUI "user_name", user.Spec.Username, ) + db, resp, err := r.GodoClient.Databases.Get(ctx, clusterUUID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("checking for existing DB Cluster: %v", err) + } + // The validating webhook checks that the user doesn't already exist, so we // assume that if we find it to exist now we created it. If the user was // created between validation passing and getting here, we could assume @@ -193,7 +198,7 @@ func (r *DatabaseUserReconciler) reconcileDBUser(ctx context.Context, clusterUUI controllerutil.AddFinalizer(user, finalizerName) user.Status.Role = dbUser.Role - err = r.ensureOwnedObjects(ctx, user, dbUser) + err = r.ensureOwnedObjects(ctx, db, user, dbUser) if err != nil { ll.Error(err, "unable to ensure user-related objects") return ctrl.Result{}, fmt.Errorf("ensuring user-related objects: %v", err) @@ -202,7 +207,7 @@ func (r *DatabaseUserReconciler) reconcileDBUser(ctx context.Context, clusterUUI return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil } -func (r *DatabaseUserReconciler) ensureOwnedObjects(ctx context.Context, user *v1alpha1.DatabaseUser, dbUser *godo.DatabaseUser) error { +func (r *DatabaseUserReconciler) ensureOwnedObjects(ctx context.Context, db *godo.Database, user *v1alpha1.DatabaseUser, dbUser *godo.DatabaseUser) error { // For some database engines the password is not returned when fetching a // user, only on initial creation. Avoid creating or updating the user // credentials secret if the password is empty, so we don't clear the @@ -211,7 +216,10 @@ func (r *DatabaseUserReconciler) ensureOwnedObjects(ctx context.Context, user *v return nil } - obj := credentialsSecretForDBUser(user, dbUser) + obj, err := credentialsSecretForDBUser(db, user, dbUser) + if err != nil { + return fmt.Errorf("creating secrett: %s", err) + } controllerutil.SetControllerReference(user, obj, r.Scheme) if err := r.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner("do-operator")); err != nil { return fmt.Errorf("applying object %s: %s", client.ObjectKeyFromObject(obj), err) diff --git a/controllers/databaseuserreference_controller.go b/controllers/databaseuserreference_controller.go index 03224c7e..da05d679 100644 --- a/controllers/databaseuserreference_controller.go +++ b/controllers/databaseuserreference_controller.go @@ -135,6 +135,11 @@ func (r *DatabaseUserReferenceReconciler) reconcileDBUserReference(ctx context.C "user_name", userRef.Spec.Username, ) + db, _, err := r.GodoClient.Databases.Get(ctx, clusterUUID) + if err != nil { + return ctrl.Result{}, fmt.Errorf("looking up DB cluster: %v", err) + } + // The validating webhook checks that the user exists, so normally this // should work. However, the user could have been deleted in which case // we'll fail and back off in case it gets re-created. @@ -145,7 +150,7 @@ func (r *DatabaseUserReferenceReconciler) reconcileDBUserReference(ctx context.C userRef.Status.Role = dbUser.Role - err = r.ensureOwnedObjects(ctx, userRef, dbUser) + err = r.ensureOwnedObjects(ctx, db, userRef, dbUser) if err != nil { ll.Error(err, "unable to ensure user-related objects") return ctrl.Result{}, fmt.Errorf("ensuring user-related objects: %v", err) @@ -154,8 +159,11 @@ func (r *DatabaseUserReferenceReconciler) reconcileDBUserReference(ctx context.C return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil } -func (r *DatabaseUserReferenceReconciler) ensureOwnedObjects(ctx context.Context, userRef *v1alpha1.DatabaseUserReference, dbUser *godo.DatabaseUser) error { - obj := credentialsSecretForDBUser(userRef, dbUser) +func (r *DatabaseUserReferenceReconciler) ensureOwnedObjects(ctx context.Context, db *godo.Database, userRef *v1alpha1.DatabaseUserReference, dbUser *godo.DatabaseUser) error { + obj, err := credentialsSecretForDBUser(db, userRef, dbUser) + if err != nil { + return fmt.Errorf("creating secrett: %s", err) + } controllerutil.SetControllerReference(userRef, obj, r.Scheme) if err := r.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner("do-operator")); err != nil { return fmt.Errorf("applying object %s: %s", client.ObjectKeyFromObject(obj), err) From e6ffa678bdc021768d3b54e52482eaa71bdd58d5 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Thu, 22 Feb 2024 09:28:26 -0800 Subject: [PATCH 2/2] \o/ tests --- controllers/credentials_secret.go | 3 ++- controllers/databaseuser_controller_test.go | 6 ++++++ controllers/databaseuserreference_controller_test.go | 6 ++++++ fakegodo/databases.go | 4 ++-- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/controllers/credentials_secret.go b/controllers/credentials_secret.go index 684d152d..9250de76 100644 --- a/controllers/credentials_secret.go +++ b/controllers/credentials_secret.go @@ -2,10 +2,11 @@ package controllers import ( "fmt" + "net/url" + "github.com/digitalocean/godo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "net/url" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/controllers/databaseuser_controller_test.go b/controllers/databaseuser_controller_test.go index 6e98a2c9..2db599ac 100644 --- a/controllers/databaseuser_controller_test.go +++ b/controllers/databaseuser_controller_test.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/digitalocean/do-operator/api/v1alpha1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -89,6 +91,8 @@ var _ = Describe("DatabaseUser controller", func() { Expect(secret.OwnerReferences).To(ContainElement(dbUserOwnerReference)) Expect(string(secret.Data["username"])).To(Equal(userName)) Expect(secret.Data["password"]).NotTo(BeEmpty()) + Expect(string(secret.Data["uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@host:12345/database?sslmode=require", secret.Data["username"], secret.Data["password"]))) + Expect(string(secret.Data["private_uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@private-host:12345/private-database?sslmode=require", secret.Data["username"], secret.Data["password"]))) }) By("deleting the DatabaseUser object", func() { @@ -184,6 +188,8 @@ var _ = Describe("DatabaseUser controller", func() { Expect(secret.OwnerReferences).To(ContainElement(dbUserOwnerReference)) Expect(string(secret.Data["username"])).To(Equal(userName)) Expect(secret.Data["password"]).NotTo(BeEmpty()) + Expect(string(secret.Data["uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@host:12345/database?sslmode=require", secret.Data["username"], secret.Data["password"]))) + Expect(string(secret.Data["private_uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@private-host:12345/private-database?sslmode=require", secret.Data["username"], secret.Data["password"]))) }) By("deleting the DatabaseUser object", func() { diff --git a/controllers/databaseuserreference_controller_test.go b/controllers/databaseuserreference_controller_test.go index 32dd9506..ecea69a6 100644 --- a/controllers/databaseuserreference_controller_test.go +++ b/controllers/databaseuserreference_controller_test.go @@ -1,6 +1,8 @@ package controllers import ( + "fmt" + "github.com/digitalocean/do-operator/api/v1alpha1" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -83,6 +85,8 @@ var _ = Describe("DatabaseUserReference controller", func() { Expect(secret.OwnerReferences).To(ContainElement(dbUserRefOwnerReference)) Expect(string(secret.Data["username"])).To(Equal(userName)) Expect(secret.Data["password"]).NotTo(BeEmpty()) + Expect(string(secret.Data["uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@host:12345/database?sslmode=require", secret.Data["username"], secret.Data["password"]))) + Expect(string(secret.Data["private_uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@private-host:12345/private-database?sslmode=require", secret.Data["username"], secret.Data["password"]))) }) By("deleting the DatabaseUserReference object", func() { @@ -172,6 +176,8 @@ var _ = Describe("DatabaseUserReference controller", func() { Expect(secret.OwnerReferences).To(ContainElement(dbUserRefOwnerReference)) Expect(string(secret.Data["username"])).To(Equal(userName)) Expect(secret.Data["password"]).NotTo(BeEmpty()) + Expect(string(secret.Data["uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@host:12345/database?sslmode=require", secret.Data["username"], secret.Data["password"]))) + Expect(string(secret.Data["private_uri"])).To(Equal(fmt.Sprintf("postgresql://%s:%s@private-host:12345/private-database?sslmode=require", secret.Data["username"], secret.Data["password"]))) }) By("deleting the DatabaseUserReference object", func() { diff --git a/fakegodo/databases.go b/fakegodo/databases.go index c1e22fa3..d3f314d8 100644 --- a/fakegodo/databases.go +++ b/fakegodo/databases.go @@ -90,7 +90,7 @@ func (f *FakeDatabasesService) Create(_ context.Context, req *godo.DatabaseCreat RegionSlug: req.Region, CreatedAt: time.Now(), Connection: &godo.DatabaseConnection{ - URI: "uri", + URI: "postgresql://user:password@host:12345/database?sslmode=require", Database: "database", Host: "host", Port: 12345, @@ -99,7 +99,7 @@ func (f *FakeDatabasesService) Create(_ context.Context, req *godo.DatabaseCreat SSL: true, }, PrivateConnection: &godo.DatabaseConnection{ - URI: "private-uri", + URI: "postgresql://private-user:private-password@private-host:12345/private-database?sslmode=require", Database: "private-database", Host: "private_host", Port: 12345,