diff --git a/controllers/credentials_secret.go b/controllers/credentials_secret.go index 4c663272..9250de76 100644 --- a/controllers/credentials_secret.go +++ b/controllers/credentials_secret.go @@ -1,6 +1,9 @@ package controllers import ( + "fmt" + "net/url" + "github.com/digitalocean/godo" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,8 +35,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 +48,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/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.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) 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,