From ffc61a8a595d50d0234ce5081e7df0a570a00d4a Mon Sep 17 00:00:00 2001 From: bfabricio Date: Fri, 14 Mar 2025 09:59:31 -0300 Subject: [PATCH 1/2] feature: add gcp provider to new package --- pkg/providers/crossplane_gcp.go | 273 ++++++++++++++++- pkg/providers/crossplane_gcp_test.go | 426 +++++++++++++++++++++++++++ pkg/providers/providers.go | 2 +- 3 files changed, 691 insertions(+), 10 deletions(-) create mode 100644 pkg/providers/crossplane_gcp_test.go diff --git a/pkg/providers/crossplane_gcp.go b/pkg/providers/crossplane_gcp.go index 418198e..6b1407c 100644 --- a/pkg/providers/crossplane_gcp.go +++ b/pkg/providers/crossplane_gcp.go @@ -2,25 +2,280 @@ package providers import ( "context" + "fmt" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + persistanceinfobloxcomv1alpha1 "github.com/infobloxopen/db-controller/api/persistance.infoblox.com/v1alpha1" + basefun "github.com/infobloxopen/db-controller/pkg/basefunctions" "github.com/spf13/viper" + crossplanegcp "github.com/upbound/provider-gcp/apis/alloydb/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "strings" ) -type GCPProvider struct { +const ( + // https://cloud.google.com/alloydb/docs/reference/rest/v1beta/DatabaseVersion + alloyVersion14 = "POSTGRES_14" + alloyVersion15 = "POSTGRES_15" + networkRecordNameSuffix = "-psc-network" +) + +type gcpProvider struct { + k8sClient client.Client + config *viper.Viper + serviceNS string +} + +func newGCPProvider(k8sClient client.Client, config *viper.Viper, serviceNS string) Provider { + return &gcpProvider{ + k8sClient: k8sClient, + config: config, + serviceNS: serviceNS, + } +} + +func (p *gcpProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bool, error) { + clusterKey := client.ObjectKey{Name: spec.ResourceName} + cluster := &crossplanegcp.Cluster{} + err := ensureResource(ctx, p.k8sClient, clusterKey, cluster, func() *crossplanegcp.Cluster { + newCluster := p.dbCluster(spec) + if err := ManageMasterPassword(ctx, &newCluster.Spec.ForProvider.InitialUser.PasswordSecretRef, p.k8sClient); err != nil { + log.FromContext(ctx).Error(err, "Failed to manage master password", "resource", spec.ResourceName) + } + return newCluster + }) + if err != nil { + return false, err + } + + instanceKey := client.ObjectKey{Name: spec.ResourceName} + instance := &crossplanegcp.Instance{} + err = ensureResource(ctx, p.k8sClient, instanceKey, instance, func() *crossplanegcp.Instance { + return p.dbInstance(spec) + }) + if err != nil { + return false, err + } + + instanceReady, err := isReady(instance.Status.Conditions) + if err != nil { + log.FromContext(ctx).Error(err, "Failed to check instance readiness", "resource", spec.ResourceName) + return false, fmt.Errorf("failed to check instance readiness: %w", err) + } + if !instanceReady { + log.FromContext(ctx).Info("Waiting for DB Instance to be ready", "name", spec.ResourceName) + return false, nil + } + + networkRecordKey := client.ObjectKey{Name: spec.ResourceName + networkRecordNameSuffix} + networkRecord := &persistanceinfobloxcomv1alpha1.XNetworkRecord{} + err = ensureResource(ctx, p.k8sClient, networkRecordKey, networkRecord, func() *persistanceinfobloxcomv1alpha1.XNetworkRecord { + return p.dbNetworkRecord(spec, instance) + }) + if err != nil { + return false, err + } + + err = p.createSecretWithConnInfo(ctx, spec, instance) + if err != nil { + return false, err + } + + if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { + return false, fmt.Errorf("can not create Cloud DB cluster %s it is being deleted", spec.ResourceName) + } + + err = p.updateDBClusterGCP(ctx, spec, cluster) + if err != nil { + return false, err + } + + return true, nil +} + +func (p *gcpProvider) DeleteDatabase(ctx context.Context, spec DatabaseSpec) (bool, error) { + if basefun.GetDefaultReclaimPolicy(p.config) != "delete" { + return false, nil + } + + deletionPolicy := client.PropagationPolicy(metav1.DeletePropagationBackground) + // Delete DBCluster if it exists + dbCluster := &crossplanegcp.Cluster{} + clusterKey := client.ObjectKey{Name: spec.ResourceName} + if err := p.k8sClient.Get(ctx, clusterKey, dbCluster); err == nil { + if err := p.k8sClient.Delete(ctx, dbCluster, deletionPolicy); err != nil { + return false, err + } + } + + // Delete DBInstance if it exists + instanceKey := client.ObjectKey{Name: spec.ResourceName} + dbInstance := &crossplanegcp.Instance{} + if err := p.k8sClient.Get(ctx, instanceKey, dbInstance); err == nil { + if err := p.k8sClient.Delete(ctx, dbInstance, deletionPolicy); err != nil { + return false, err + } + } + + return true, nil +} + +func (p *gcpProvider) GetDatabase(ctx context.Context, name string) (*DatabaseSpec, error) { + panic("implement me") } -func newGCPProvider(k8sClient client.Client, config *viper.Viper) Provider { - return &GCPProvider{} +func (p *gcpProvider) dbCluster(params DatabaseSpec) *crossplanegcp.Cluster { + var dbVersion string + if strings.HasPrefix(params.DBVersion, "14") { + dbVersion = alloyVersion14 + } else { + dbVersion = alloyVersion15 + } + + return &crossplanegcp.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: params.ResourceName, + Labels: params.Labels, + }, + Spec: crossplanegcp.ClusterSpec{ + ForProvider: crossplanegcp.ClusterParameters{ + AutomatedBackupPolicy: &crossplanegcp.AutomatedBackupPolicyParameters{ + Enabled: ptr.To(true), + }, + DatabaseVersion: &dbVersion, + DeletionPolicy: ptr.To(string(params.DeletionPolicy)), + DisplayName: ptr.To(params.ResourceName), + InitialUser: &crossplanegcp.InitialUserParameters{ + PasswordSecretRef: xpv1.SecretKeySelector{ + SecretReference: xpv1.SecretReference{ + Name: params.ResourceName + MasterPasswordSuffix, + Namespace: p.serviceNS, + }, + Key: MasterPasswordSecretKey, + }, + User: ¶ms.MasterUsername, + }, + + Location: ptr.To(basefun.GetRegion(p.config)), + PscConfig: &crossplanegcp.PscConfigParameters{ + PscEnabled: ptr.To(true), + }, + }, + ResourceSpec: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: &xpv1.SecretReference{ + Name: params.ResourceName, + Namespace: p.serviceNS, + }, + ProviderConfigReference: &xpv1.Reference{ + Name: basefun.GetProviderConfig(p.config), + }, + DeletionPolicy: params.DeletionPolicy, + }, + }, + } } -func (p *GCPProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bool, error) { - return false, nil +func (p *gcpProvider) dbInstance(params DatabaseSpec) *crossplanegcp.Instance { + multiAZ := basefun.GetMultiAZEnabled(p.config) + return &crossplanegcp.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: params.ResourceName, + Labels: params.Labels, + }, + Spec: crossplanegcp.InstanceSpec{ + ForProvider: crossplanegcp.InstanceParameters{ + AvailabilityType: ptr.To((map[bool]string{true: "ZONAL", false: "REGIONAL"})[multiAZ]), + ClientConnectionConfig: &crossplanegcp.ClientConnectionConfigParameters{ + SSLConfig: &crossplanegcp.SSLConfigParameters{ + SSLMode: ptr.To("ENCRYPTED_ONLY"), + }, + }, + ClusterRef: &xpv1.Reference{ + Name: params.ResourceName, + }, + + DisplayName: ptr.To(params.ResourceName), + GceZone: ptr.To(basefun.GetRegion(p.config)), + InstanceType: ptr.To("PRIMARY"), + NetworkConfig: &crossplanegcp.InstanceNetworkConfigParameters{ + EnablePublicIP: ptr.To(false), + }, + PscInstanceConfig: &crossplanegcp.PscInstanceConfigParameters{ + AllowedConsumerProjects: []*string{ptr.To(basefun.GetProject(p.config))}, + }, + DatabaseFlags: map[string]*string{"alloydb.iam_authentication": ptr.To("on")}, + }, + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{ + Name: basefun.GetProviderConfig(p.config), + }, + DeletionPolicy: params.DeletionPolicy, + }, + }, + } } -func (p *GCPProvider) DeleteDatabase(ctx context.Context, spec DatabaseSpec) (bool, error) { - return false, nil +func (p *gcpProvider) dbNetworkRecord(spec DatabaseSpec, dbInstance *crossplanegcp.Instance) *persistanceinfobloxcomv1alpha1.XNetworkRecord { + return &persistanceinfobloxcomv1alpha1.XNetworkRecord{ + ObjectMeta: metav1.ObjectMeta{ + Name: spec.ResourceName + "-psc-network", + Namespace: p.serviceNS, + }, + Spec: persistanceinfobloxcomv1alpha1.XNetworkRecordSpec{ + Parameters: persistanceinfobloxcomv1alpha1.XNetworkRecordParameters{ + PSCDNSName: *dbInstance.Status.AtProvider.PscInstanceConfig.PscDNSName, + ServiceAttachmentLink: *dbInstance.Status.AtProvider.PscInstanceConfig.ServiceAttachmentLink, + Region: basefun.GetRegion(p.config), + Subnetwork: basefun.GetSubNetwork(p.config), + Network: basefun.GetNetwork(p.config), + }, + }, + } } -func (p *GCPProvider) GetDatabase(ctx context.Context, name string) (*DatabaseSpec, error) { - return nil, nil +func (p *gcpProvider) updateDBClusterGCP(ctx context.Context, params DatabaseSpec, dbCluster *crossplanegcp.Cluster) error { + logr := log.FromContext(ctx) + // Create a patch snapshot from current DBCluster + patchDBCluster := client.MergeFrom(dbCluster.DeepCopy()) + // Update DBCluster + if params.BackupRetentionDays != 0 { + dbCluster.Spec.ForProvider.AutomatedBackupPolicy = &crossplanegcp.AutomatedBackupPolicyParameters{ + Enabled: ptr.To(true), + QuantityBasedRetention: &crossplanegcp.QuantityBasedRetentionParameters{ + Count: basefun.GetNumBackupsToRetain(p.config), + }, + } + } + dbCluster.Spec.DeletionPolicy = params.DeletionPolicy + + logr.Info("updating crossplane DBCluster resource", "DBCluster", dbCluster.Name) + err := p.k8sClient.Patch(ctx, dbCluster, patchDBCluster) + if err != nil { + return err + } + + return nil +} + +func (p *gcpProvider) createSecretWithConnInfo(ctx context.Context, params DatabaseSpec, instance *crossplanegcp.Instance) error { + var secret = &corev1.Secret{} + err := p.k8sClient.Get(ctx, client.ObjectKey{ + Name: params.ResourceName, + Namespace: p.serviceNS, + }, secret) + if err != nil { + return err + } + + pass := string(secret.Data["attribute.initial_user.0.password"]) + + secret.Data["username"] = []byte(params.MasterUsername) + secret.Data["password"] = []byte(pass) + secret.Data["endpoint"] = []byte(*instance.Status.AtProvider.PscInstanceConfig.PscDNSName) + secret.Data["port"] = []byte("5432") + + return p.k8sClient.Update(ctx, secret) } diff --git a/pkg/providers/crossplane_gcp_test.go b/pkg/providers/crossplane_gcp_test.go new file mode 100644 index 0000000..bd37bfa --- /dev/null +++ b/pkg/providers/crossplane_gcp_test.go @@ -0,0 +1,426 @@ +package providers + +import ( + "context" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + "github.com/spf13/viper" + crossplanegcp "github.com/upbound/provider-gcp/apis/alloydb/v1beta2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +func TestAlloyDBCluster(t *testing.T) { + tests := []struct { + name string + params DatabaseSpec + expectedResult *crossplanegcp.Cluster + }{ + { + name: "Creates AlloyDB cluster with Postgres 14", + params: DatabaseSpec{ + ResourceName: "test-alloydb-cluster", + Labels: map[string]string{"env": "test", "app": "myapp"}, + DBVersion: "14.1", + MasterUsername: "admin", + DeletionPolicy: xpv1.DeletionDelete, + }, + expectedResult: &crossplanegcp.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-alloydb-cluster", + Labels: map[string]string{"env": "test", "app": "myapp"}, + }, + Spec: crossplanegcp.ClusterSpec{ + ForProvider: crossplanegcp.ClusterParameters{ + AutomatedBackupPolicy: &crossplanegcp.AutomatedBackupPolicyParameters{ + Enabled: ptr.To(true), + }, + DatabaseVersion: ptr.To("POSTGRES_14"), + DeletionPolicy: ptr.To("Delete"), + DisplayName: ptr.To("test-alloydb-cluster"), + InitialUser: &crossplanegcp.InitialUserParameters{ + PasswordSecretRef: xpv1.SecretKeySelector{ + SecretReference: xpv1.SecretReference{ + Name: "test-alloydb-cluster-master-password", + Namespace: "default", + }, + Key: "password", + }, + User: ptr.To("admin"), + }, + Location: ptr.To("us-central1"), + PscConfig: &crossplanegcp.PscConfigParameters{ + PscEnabled: ptr.To(true), + }, + }, + ResourceSpec: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: &xpv1.SecretReference{ + Name: "test-alloydb-cluster", + Namespace: "default", + }, + ProviderConfigReference: &xpv1.Reference{ + Name: "gcp-provider", + }, + DeletionPolicy: xpv1.DeletionDelete, + }, + }, + }, + }, + { + name: "Creates AlloyDB cluster with Postgres 15", + params: DatabaseSpec{ + ResourceName: "prod-alloydb-cluster", + Labels: map[string]string{"env": "prod", "tier": "db"}, + DBVersion: "15.0", + MasterUsername: "postgres", + DeletionPolicy: xpv1.DeletionOrphan, + }, + expectedResult: &crossplanegcp.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "prod-alloydb-cluster", + Labels: map[string]string{"env": "prod", "tier": "db"}, + }, + Spec: crossplanegcp.ClusterSpec{ + ForProvider: crossplanegcp.ClusterParameters{ + AutomatedBackupPolicy: &crossplanegcp.AutomatedBackupPolicyParameters{ + Enabled: ptr.To(true), + }, + DatabaseVersion: ptr.To("POSTGRES_15"), + DeletionPolicy: ptr.To("Orphan"), + DisplayName: ptr.To("prod-alloydb-cluster"), + InitialUser: &crossplanegcp.InitialUserParameters{ + PasswordSecretRef: xpv1.SecretKeySelector{ + SecretReference: xpv1.SecretReference{ + Name: "prod-alloydb-cluster-master-password", + Namespace: "default", + }, + Key: MasterPasswordSecretKey, + }, + User: ptr.To("postgres"), + }, + Location: ptr.To("us-central1"), + PscConfig: &crossplanegcp.PscConfigParameters{ + PscEnabled: ptr.To(true), + }, + }, + ResourceSpec: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: &xpv1.SecretReference{ + Name: "prod-alloydb-cluster", + Namespace: "default", + }, + ProviderConfigReference: &xpv1.Reference{ + Name: "gcp-provider", + }, + DeletionPolicy: xpv1.DeletionOrphan, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + RegisterTestingT(t) + mockConfig := viper.New() + mockConfig.Set("providerConfig", "gcp-provider") + mockConfig.Set("region", "us-central1") + provider := &gcpProvider{config: mockConfig} + + result := provider.dbCluster(tc.params) + + // Assert expectations using GoMega + Expect(result.ObjectMeta.Name).To(Equal(tc.expectedResult.ObjectMeta.Name)) + Expect(result.ObjectMeta.Labels).To(Equal(tc.expectedResult.ObjectMeta.Labels)) + + // Check ForProvider fields + Expect(result.Spec.ForProvider.DatabaseVersion).To(Equal(tc.expectedResult.Spec.ForProvider.DatabaseVersion)) + + // Verify database version selection logic + expectedDbVersion := alloyVersion15 + if strings.HasPrefix(tc.params.DBVersion, "14") { + expectedDbVersion = alloyVersion14 + } + Expect(*result.Spec.ForProvider.DatabaseVersion).To(Equal(expectedDbVersion)) + + Expect(result.Spec.ForProvider.DeletionPolicy).To(Equal(tc.expectedResult.Spec.ForProvider.DeletionPolicy)) + Expect(result.Spec.ForProvider.DisplayName).To(Equal(tc.expectedResult.Spec.ForProvider.DisplayName)) + + // Check AutomatedBackupPolicy + Expect(result.Spec.ForProvider.AutomatedBackupPolicy.Enabled).To(Equal(tc.expectedResult.Spec.ForProvider.AutomatedBackupPolicy.Enabled)) + + // Check InitialUser + Expect(result.Spec.ForProvider.InitialUser.User).To(Equal(tc.expectedResult.Spec.ForProvider.InitialUser.User)) + Expect(result.Spec.ForProvider.InitialUser.PasswordSecretRef.SecretReference.Name).To(Equal(tc.params.ResourceName + MasterPasswordSuffix)) + Expect(result.Spec.ForProvider.InitialUser.PasswordSecretRef.SecretReference.Namespace).To(Equal(provider.serviceNS)) + Expect(result.Spec.ForProvider.InitialUser.PasswordSecretRef.Key).To(Equal(MasterPasswordSecretKey)) + + // Check Location + Expect(result.Spec.ForProvider.Location).To(Equal(tc.expectedResult.Spec.ForProvider.Location)) + + // Check PscConfig + Expect(result.Spec.ForProvider.PscConfig.PscEnabled).To(Equal(tc.expectedResult.Spec.ForProvider.PscConfig.PscEnabled)) + + // Check ResourceSpec + Expect(result.Spec.DeletionPolicy).To(Equal(tc.expectedResult.Spec.DeletionPolicy)) + Expect(result.Spec.ProviderConfigReference.Name).To(Equal(tc.expectedResult.Spec.ProviderConfigReference.Name)) + Expect(result.Spec.WriteConnectionSecretToReference.Name).To(Equal(tc.params.ResourceName)) + Expect(result.Spec.WriteConnectionSecretToReference.Namespace).To(Equal(provider.serviceNS)) + }) + } +} + +func TestAlloyDBInstance(t *testing.T) { + tests := []struct { + name string + params DatabaseSpec + expectedResult *crossplanegcp.Instance + }{ + { + name: "Creates AlloyDB instance with REGIONAL availability type", + params: DatabaseSpec{ + ResourceName: "test-alloydb-instance", + Labels: map[string]string{"env": "test", "app": "myapp"}, + DeletionPolicy: xpv1.DeletionDelete, + }, + expectedResult: &crossplanegcp.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-alloydb-instance", + Labels: map[string]string{"env": "test", "app": "myapp"}, + }, + Spec: crossplanegcp.InstanceSpec{ + ForProvider: crossplanegcp.InstanceParameters{ + AvailabilityType: ptr.To("REGIONAL"), + ClientConnectionConfig: &crossplanegcp.ClientConnectionConfigParameters{ + SSLConfig: &crossplanegcp.SSLConfigParameters{ + SSLMode: ptr.To("ENCRYPTED_ONLY"), + }, + }, + ClusterRef: &xpv1.Reference{ + Name: "test-alloydb-instance", + }, + DisplayName: ptr.To("test-alloydb-instance"), + GceZone: ptr.To("us-central1"), + InstanceType: ptr.To("PRIMARY"), + NetworkConfig: &crossplanegcp.InstanceNetworkConfigParameters{ + EnablePublicIP: ptr.To(false), + }, + PscInstanceConfig: &crossplanegcp.PscInstanceConfigParameters{ + AllowedConsumerProjects: []*string{ptr.To("test-project")}, + }, + DatabaseFlags: map[string]*string{"alloydb.iam_authentication": ptr.To("on")}, + }, + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{ + Name: "gcp-provider", + }, + DeletionPolicy: xpv1.DeletionDelete, + }, + }, + }, + }, + { + name: "Creates AlloyDB instance with ZONAL availability type", + params: DatabaseSpec{ + ResourceName: "prod-alloydb-instance", + Labels: map[string]string{"env": "prod", "tier": "db"}, + DeletionPolicy: xpv1.DeletionOrphan, + }, + expectedResult: &crossplanegcp.Instance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "prod-alloydb-instance", + Labels: map[string]string{"env": "prod", "tier": "db"}, + }, + Spec: crossplanegcp.InstanceSpec{ + ForProvider: crossplanegcp.InstanceParameters{ + AvailabilityType: ptr.To("ZONAL"), + ClientConnectionConfig: &crossplanegcp.ClientConnectionConfigParameters{ + SSLConfig: &crossplanegcp.SSLConfigParameters{ + SSLMode: ptr.To("ENCRYPTED_ONLY"), + }, + }, + ClusterRef: &xpv1.Reference{ + Name: "prod-alloydb-instance", + }, + DisplayName: ptr.To("prod-alloydb-instance"), + GceZone: ptr.To("us-central1"), + InstanceType: ptr.To("PRIMARY"), + NetworkConfig: &crossplanegcp.InstanceNetworkConfigParameters{ + EnablePublicIP: ptr.To(false), + }, + PscInstanceConfig: &crossplanegcp.PscInstanceConfigParameters{ + AllowedConsumerProjects: []*string{ptr.To("test-project")}, + }, + DatabaseFlags: map[string]*string{"alloydb.iam_authentication": ptr.To("on")}, + }, + ResourceSpec: xpv1.ResourceSpec{ + ProviderConfigReference: &xpv1.Reference{ + Name: "gcp-provider", + }, + DeletionPolicy: xpv1.DeletionOrphan, + }, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + RegisterTestingT(t) + mockConfig := viper.New() + mockConfig.Set("providerConfig", "gcp-provider") + mockConfig.Set("region", "us-central1") + mockConfig.Set("project", "test-project") + if *tc.expectedResult.Spec.ForProvider.AvailabilityType == "ZONAL" { + mockConfig.Set("dbMultiAZEnabled", true) + } + + provider := &gcpProvider{config: mockConfig} + + result := provider.dbInstance(tc.params) + + Expect(result.ObjectMeta.Name).To(Equal(tc.expectedResult.ObjectMeta.Name)) + Expect(result.ObjectMeta.Labels).To(Equal(tc.expectedResult.ObjectMeta.Labels)) + + // Check ForProvider fields + Expect(result.Spec.ForProvider.AvailabilityType).To(Equal(tc.expectedResult.Spec.ForProvider.AvailabilityType)) + Expect(result.Spec.ForProvider.ClientConnectionConfig.SSLConfig.SSLMode).To(Equal(tc.expectedResult.Spec.ForProvider.ClientConnectionConfig.SSLConfig.SSLMode)) + Expect(result.Spec.ForProvider.ClusterRef.Name).To(Equal(tc.expectedResult.Spec.ForProvider.ClusterRef.Name)) + Expect(result.Spec.ForProvider.DisplayName).To(Equal(tc.expectedResult.Spec.ForProvider.DisplayName)) + Expect(result.Spec.ForProvider.GceZone).To(Equal(tc.expectedResult.Spec.ForProvider.GceZone)) + Expect(result.Spec.ForProvider.InstanceType).To(Equal(tc.expectedResult.Spec.ForProvider.InstanceType)) + Expect(result.Spec.ForProvider.NetworkConfig.EnablePublicIP).To(Equal(tc.expectedResult.Spec.ForProvider.NetworkConfig.EnablePublicIP)) + + // Check PSC instance config + Expect(len(result.Spec.ForProvider.PscInstanceConfig.AllowedConsumerProjects)).To(Equal(1)) + Expect(*result.Spec.ForProvider.PscInstanceConfig.AllowedConsumerProjects[0]).To(Equal(*tc.expectedResult.Spec.ForProvider.PscInstanceConfig.AllowedConsumerProjects[0])) + + // Check database flags + Expect(result.Spec.ForProvider.DatabaseFlags).To(HaveLen(1)) + Expect(*result.Spec.ForProvider.DatabaseFlags["alloydb.iam_authentication"]).To(Equal("on")) + + // Check ResourceSpec + Expect(result.Spec.DeletionPolicy).To(Equal(tc.expectedResult.Spec.DeletionPolicy)) + Expect(result.Spec.ProviderConfigReference.Name).To(Equal(tc.expectedResult.Spec.ProviderConfigReference.Name)) + }) + } +} + +func TestCreateSecretWithConnInfo(t *testing.T) { + RegisterTestingT(t) + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = crossplanegcp.AddToScheme(scheme) + + var ( + namespace = "test-namespace" + resourceName = "test-db" + password = "test-password" + username = "admin" + dnsName = "test-dns-name" + ) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: resourceName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "attribute.initial_user.0.password": []byte(password), + }, + } + + cl := fake.NewClientBuilder(). + WithScheme(scheme). + WithObjects(secret). + Build() + + provider := &gcpProvider{ + k8sClient: cl, + serviceNS: namespace, + } + + dns := dnsName + instance := &crossplanegcp.Instance{ + Status: crossplanegcp.InstanceStatus{ + AtProvider: crossplanegcp.InstanceObservation{ + PscInstanceConfig: &crossplanegcp.PscInstanceConfigObservation{ + PscDNSName: &dns, + }, + }, + }, + } + + err := provider.createSecretWithConnInfo(context.Background(), + DatabaseSpec{ + ResourceName: resourceName, + MasterUsername: username, + }, + instance) + + Expect(err).To(BeNil()) + updatedSecret := &corev1.Secret{} + err = cl.Get(context.Background(), + client.ObjectKey{Name: resourceName, Namespace: namespace}, + updatedSecret) + + Expect(updatedSecret.Data["username"]).To(Equal([]byte(username))) + Expect(updatedSecret.Data["password"]).To(Equal(secret.Data["attribute.initial_user.0.password"])) + Expect(updatedSecret.Data["endpoint"]).To(Equal([]byte(*instance.Status.AtProvider.PscInstanceConfig.PscDNSName))) + Expect(updatedSecret.Data["port"]).To(Equal([]byte("5432"))) +} + +func TestDbNetworkRecord(t *testing.T) { + RegisterTestingT(t) + var ( + namespace = "test-namespace" + resourceName = "test-db" + dnsName = "test-dns-name.example.com" + saLink = "projects/test-project/serviceAttachments/test-attachment" + region = "us-central1" + network = "projects/test-subnet/global/networks/projects/test-project/regions/us-central1/subnetworks/" + ) + + mockConfig := viper.New() + mockConfig.Set("project", "test-subnet") + mockConfig.Set("subnetwork", "test-subnet") + mockConfig.Set("region", "us-central1") + mockConfig.Set("network", "projects/test-project/regions/us-central1/subnetworks/") + + provider := &gcpProvider{ + serviceNS: namespace, + config: mockConfig, + } + spec := DatabaseSpec{ + ResourceName: resourceName, + } + dns := dnsName + link := saLink + instance := &crossplanegcp.Instance{ + Status: crossplanegcp.InstanceStatus{ + AtProvider: crossplanegcp.InstanceObservation{ + PscInstanceConfig: &crossplanegcp.PscInstanceConfigObservation{ + PscDNSName: &dns, + ServiceAttachmentLink: &link, + }, + }, + }, + } + + result := provider.dbNetworkRecord(spec, instance) + Expect(result).NotTo(BeNil()) + Expect(result.Name).To(Equal(resourceName + "-psc-network")) + Expect(result.Namespace).To(Equal(namespace)) + + params := result.Spec.Parameters + Expect(params.PSCDNSName).To(Equal(dnsName)) + Expect(params.ServiceAttachmentLink).To(Equal(saLink)) + Expect(params.Region).To(Equal(region)) + Expect(params.Network).To(Equal(network)) +} diff --git a/pkg/providers/providers.go b/pkg/providers/providers.go index 5c64e19..b1846e6 100644 --- a/pkg/providers/providers.go +++ b/pkg/providers/providers.go @@ -60,7 +60,7 @@ func NewProvider(config *viper.Viper, k8sClient client.Client, serviceNS string) case "aws": return newAWSProvider(k8sClient, config, serviceNS) case "gcp": - return newGCPProvider(k8sClient, config) + return newGCPProvider(k8sClient, config, serviceNS) case "cloudnative-pg": return nil default: From 18f9d1724ccd1790a2bc42aca7149163b73e2b44 Mon Sep 17 00:00:00 2001 From: bfabricio Date: Thu, 20 Mar 2025 11:39:47 -0300 Subject: [PATCH 2/2] test: add unit tests --- pkg/providers/crossplane_gcp.go | 34 +++++----- pkg/providers/crossplane_gcp_test.go | 94 ++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/pkg/providers/crossplane_gcp.go b/pkg/providers/crossplane_gcp.go index 6b1407c..e16653f 100644 --- a/pkg/providers/crossplane_gcp.go +++ b/pkg/providers/crossplane_gcp.go @@ -40,12 +40,12 @@ func newGCPProvider(k8sClient client.Client, config *viper.Viper, serviceNS stri func (p *gcpProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bool, error) { clusterKey := client.ObjectKey{Name: spec.ResourceName} cluster := &crossplanegcp.Cluster{} - err := ensureResource(ctx, p.k8sClient, clusterKey, cluster, func() *crossplanegcp.Cluster { - newCluster := p.dbCluster(spec) - if err := ManageMasterPassword(ctx, &newCluster.Spec.ForProvider.InitialUser.PasswordSecretRef, p.k8sClient); err != nil { + err := ensureResource(ctx, p.k8sClient, clusterKey, cluster, func() (*crossplanegcp.Cluster, error) { + cluster = p.dbCluster(spec) + if err := ManageMasterPassword(ctx, &cluster.Spec.ForProvider.InitialUser.PasswordSecretRef, p.k8sClient); err != nil { log.FromContext(ctx).Error(err, "Failed to manage master password", "resource", spec.ResourceName) } - return newCluster + return cluster, nil }) if err != nil { return false, err @@ -53,13 +53,22 @@ func (p *gcpProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bo instanceKey := client.ObjectKey{Name: spec.ResourceName} instance := &crossplanegcp.Instance{} - err = ensureResource(ctx, p.k8sClient, instanceKey, instance, func() *crossplanegcp.Instance { - return p.dbInstance(spec) + err = ensureResource(ctx, p.k8sClient, instanceKey, instance, func() (*crossplanegcp.Instance, error) { + return p.dbInstance(spec), nil }) if err != nil { return false, err } + if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { + return false, fmt.Errorf("can not create Cloud DB cluster %s it is being deleted", spec.ResourceName) + } + + err = p.updateDBClusterGCP(ctx, spec, cluster) + if err != nil { + return false, err + } + instanceReady, err := isReady(instance.Status.Conditions) if err != nil { log.FromContext(ctx).Error(err, "Failed to check instance readiness", "resource", spec.ResourceName) @@ -72,8 +81,8 @@ func (p *gcpProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bo networkRecordKey := client.ObjectKey{Name: spec.ResourceName + networkRecordNameSuffix} networkRecord := &persistanceinfobloxcomv1alpha1.XNetworkRecord{} - err = ensureResource(ctx, p.k8sClient, networkRecordKey, networkRecord, func() *persistanceinfobloxcomv1alpha1.XNetworkRecord { - return p.dbNetworkRecord(spec, instance) + err = ensureResource(ctx, p.k8sClient, networkRecordKey, networkRecord, func() (*persistanceinfobloxcomv1alpha1.XNetworkRecord, error) { + return p.dbNetworkRecord(spec, instance), nil }) if err != nil { return false, err @@ -84,15 +93,6 @@ func (p *gcpProvider) CreateDatabase(ctx context.Context, spec DatabaseSpec) (bo return false, err } - if !cluster.ObjectMeta.DeletionTimestamp.IsZero() { - return false, fmt.Errorf("can not create Cloud DB cluster %s it is being deleted", spec.ResourceName) - } - - err = p.updateDBClusterGCP(ctx, spec, cluster) - if err != nil { - return false, err - } - return true, nil } diff --git a/pkg/providers/crossplane_gcp_test.go b/pkg/providers/crossplane_gcp_test.go index bd37bfa..3de9862 100644 --- a/pkg/providers/crossplane_gcp_test.go +++ b/pkg/providers/crossplane_gcp_test.go @@ -3,11 +3,13 @@ package providers import ( "context" xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + persistanceinfobloxcomv1alpha1 "github.com/infobloxopen/db-controller/api/persistance.infoblox.com/v1alpha1" "github.com/spf13/viper" crossplanegcp "github.com/upbound/provider-gcp/apis/alloydb/v1beta2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -424,3 +426,95 @@ func TestDbNetworkRecord(t *testing.T) { Expect(params.Region).To(Equal(region)) Expect(params.Network).To(Equal(network)) } + +func TestCreateDatabase(t *testing.T) { + RegisterTestingT(t) + ctx := context.Background() + scheme := runtime.NewScheme() + _ = corev1.AddToScheme(scheme) + _ = crossplanegcp.AddToScheme(scheme) + _ = persistanceinfobloxcomv1alpha1.AddToScheme(scheme) + + mockClient := fake.NewClientBuilder().WithScheme(scheme).Build() + + mockConfig := viper.New() + mockConfig.Set("providerConfig", "gcp-provider") + mockConfig.Set("region", "us-central1") + mockConfig.Set("project", "test-project") + mockConfig.Set("serviceNamespace", "default") + + provider := &gcpProvider{ + k8sClient: mockClient, + config: mockConfig, + serviceNS: "default", + } + + spec := DatabaseSpec{ + ResourceName: "test-db", + DBVersion: "14.9", + MasterUsername: "admin", + + DeletionPolicy: xpv1.DeletionDelete, + BackupRetentionDays: 14, + Labels: map[string]string{ + "managed-by": "crossplane", + "service": "user-management", + }, + } + + result, err := provider.CreateDatabase(ctx, spec) + Expect(err).To(BeNil()) + Expect(result).To(BeFalse()) + + instance := &crossplanegcp.Instance{} + err = mockClient.Get(ctx, types.NamespacedName{Name: "test-db"}, instance) + Expect(err).To(BeNil()) + + cluster := &crossplanegcp.Cluster{} + err = mockClient.Get(ctx, types.NamespacedName{Name: "test-db"}, cluster) + Expect(err).To(BeNil()) + + // We need the instance ready to create the secret and network record + updatedInstance := instance.DeepCopy() + updatedInstance.Status.Conditions = []xpv1.Condition{ + { + Type: xpv1.TypeReady, + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }, + } + // mock the provider readiness + updatedInstance.Status.AtProvider = crossplanegcp.InstanceObservation{ + PscInstanceConfig: &crossplanegcp.PscInstanceConfigObservation{ + PscDNSName: ptr.To("test-psc-dns-name"), + ServiceAttachmentLink: ptr.To("test-service-attachment-link"), + }, + } + + Expect(mockClient.Update(ctx, updatedInstance)).To(Succeed()) + + // Simulate the secret being created by crossplane + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: spec.ResourceName, Namespace: provider.serviceNS, + }, + Data: map[string][]byte{ + "attribute.initial_user.0.password": []byte("password"), + }, + } + err = mockClient.Create(ctx, secret) + Expect(err).To(BeNil()) + + result, err = provider.CreateDatabase(ctx, spec) + Expect(err).To(BeNil()) + Expect(result).To(BeTrue()) + + networkRecord := &persistanceinfobloxcomv1alpha1.XNetworkRecord{} + err = mockClient.Get(ctx, types.NamespacedName{Name: spec.ResourceName + networkRecordNameSuffix, Namespace: provider.serviceNS}, networkRecord) + Expect(err).To(BeNil()) + + secret = &corev1.Secret{} + secretKey := client.ObjectKey{Name: spec.ResourceName, Namespace: provider.serviceNS} + err = mockClient.Get(ctx, secretKey, secret) + Expect(err).To(BeNil()) +}