From 84b4a9b3f3cfb3f2f873663b05548718e234ce05 Mon Sep 17 00:00:00 2001 From: bjeevan-ib Date: Sun, 17 Sep 2023 10:43:28 -0700 Subject: [PATCH] support migration from use-existing-source to local db when sourceData is not provided in dbclaim --- api/v1/databaseclaim_types.go | 6 + api/v1/zz_generated.deepcopy.go | 5 + ...nce.atlas.infoblox.com_databaseclaims.yaml | 150 ++++++++++++++++++ controllers/databaseclaim_controller.go | 100 +++++++++--- controllers/databaseclaim_controller_test.go | 64 +++++++- ...nce.atlas.infoblox.com_databaseclaims.yaml | 150 ++++++++++++++++++ 6 files changed, 451 insertions(+), 24 deletions(-) diff --git a/api/v1/databaseclaim_types.go b/api/v1/databaseclaim_types.go index cb73c5a4..6e6c235f 100644 --- a/api/v1/databaseclaim_types.go +++ b/api/v1/databaseclaim_types.go @@ -252,6 +252,12 @@ type Status struct { // DbState of the DB. inprogress, "", ready DbState DbState `json:"DbState,omitempty"` + + // SourceDataFrom specifies an existing database or backup to use when initially provisioning the database. + // if the dbclaim has already provisioned a database, this field is ignored + // This field used when claim is use-existing-db and attempting to migrate to newdb + // +optional + SourceDataFrom *SourceDataFrom `json:"sourceDataFrom,omitempty"` } // DbState keeps track of state of the DB. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index f9d48e30..1c6b8c84 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -398,6 +398,11 @@ func (in *Status) DeepCopyInto(out *Status) { in, out := &in.UserUpdatedAt, &out.UserUpdatedAt *out = (*in).DeepCopy() } + if in.SourceDataFrom != nil { + in, out := &in.SourceDataFrom, &out.SourceDataFrom + *out = new(SourceDataFrom) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status. diff --git a/config/crd/bases/persistance.atlas.infoblox.com_databaseclaims.yaml b/config/crd/bases/persistance.atlas.infoblox.com_databaseclaims.yaml index d83b7a46..44ffcac7 100644 --- a/config/crd/bases/persistance.atlas.infoblox.com_databaseclaims.yaml +++ b/config/crd/bases/persistance.atlas.infoblox.com_databaseclaims.yaml @@ -280,6 +280,81 @@ spec: description: The optional Shape values are arbitrary and help drive instance selection type: string + sourceDataFrom: + description: SourceDataFrom specifies an existing database or + backup to use when initially provisioning the database. if the + dbclaim has already provisioned a database, this field is ignored + This field used when claim is use-existing-db and attempting + to migrate to newdb + properties: + database: + description: Database defines the connection information to + an existing db + properties: + dsn: + description: 'DSN is the connection string used to reach + the postgres database must have protocol specifier at + beginning (example: mysql:// postgres:// )' + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the postgresdb (should be master/root) + TODO: document/validate the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + required: + - dsn + type: object + s3: + description: S3 defines the location of a DB backup in an + S3 bucket + properties: + bucket: + type: string + prefix: + description: Prefix is the path prefix of the S3 bucket + within which the backup to restore is located. + type: string + region: + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the s3 bucket via AWS client TODO: document/validate + the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + sourceEngine: + description: SourceEngine is the engine used to create + the backup. + type: string + sourceEngineVersion: + description: 'SourceEngineVersion is the version of the + engine used to create the backup. Example: "5.7.30"' + type: string + required: + - bucket + - region + - sourceEngine + - sourceEngineVersion + type: object + type: + description: Type specifies the type of source + type: string + required: + - type + type: object type: description: Specifies the type of database to provision. Only postgres is supported. @@ -342,6 +417,81 @@ spec: description: The optional Shape values are arbitrary and help drive instance selection type: string + sourceDataFrom: + description: SourceDataFrom specifies an existing database or + backup to use when initially provisioning the database. if the + dbclaim has already provisioned a database, this field is ignored + This field used when claim is use-existing-db and attempting + to migrate to newdb + properties: + database: + description: Database defines the connection information to + an existing db + properties: + dsn: + description: 'DSN is the connection string used to reach + the postgres database must have protocol specifier at + beginning (example: mysql:// postgres:// )' + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the postgresdb (should be master/root) + TODO: document/validate the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + required: + - dsn + type: object + s3: + description: S3 defines the location of a DB backup in an + S3 bucket + properties: + bucket: + type: string + prefix: + description: Prefix is the path prefix of the S3 bucket + within which the backup to restore is located. + type: string + region: + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the s3 bucket via AWS client TODO: document/validate + the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + sourceEngine: + description: SourceEngine is the engine used to create + the backup. + type: string + sourceEngineVersion: + description: 'SourceEngineVersion is the version of the + engine used to create the backup. Example: "5.7.30"' + type: string + required: + - bucket + - region + - sourceEngine + - sourceEngineVersion + type: object + type: + description: Type specifies the type of source + type: string + required: + - type + type: object type: description: Specifies the type of database to provision. Only postgres is supported. diff --git a/controllers/databaseclaim_controller.go b/controllers/databaseclaim_controller.go index 85328f5b..d6318569 100644 --- a/controllers/databaseclaim_controller.go +++ b/controllers/databaseclaim_controller.go @@ -165,7 +165,7 @@ func (r *DatabaseClaimReconciler) getMode(dbClaim *persistancev1.DatabaseClaim) if dbClaim.Spec.SourceDataFrom != nil { if dbClaim.Spec.SourceDataFrom.Type == "database" { if dbClaim.Status.ActiveDB.DbState == persistancev1.UsingExistingDB { - if dbClaim.Status.MigrationState == "" { + if dbClaim.Status.MigrationState == "" || dbClaim.Status.MigrationState == pgctl.S_Initial.String() { logr.Info("selected mode for", "dbclaim", dbClaim.Spec, "selected mode", "M_MigrateExistingToNewDB") return M_MigrateExistingToNewDB } else if dbClaim.Status.MigrationState != pgctl.S_Completed.String() { @@ -177,7 +177,28 @@ func (r *DatabaseClaimReconciler) getMode(dbClaim *persistancev1.DatabaseClaim) return M_NotSupported } } + // use existing is false // source data is not present + if dbClaim.Spec.SourceDataFrom == nil { + if dbClaim.Status.ActiveDB.DbState == persistancev1.UsingExistingDB { + //make sure status contains all the requires sourceDataFrom info + if dbClaim.Status.ActiveDB.SourceDataFrom != nil { + dbClaim.Spec.SourceDataFrom = dbClaim.Status.ActiveDB.SourceDataFrom.DeepCopy() + if dbClaim.Status.MigrationState == "" || dbClaim.Status.MigrationState == pgctl.S_Initial.String() { + logr.Info("selected mode for", "dbclaim", dbClaim.Spec, "selected mode", "M_MigrateExistingToNewDB") + return M_MigrateExistingToNewDB + } else if dbClaim.Status.MigrationState != pgctl.S_Completed.String() { + logr.Info("selected mode for", "dbclaim", dbClaim.Spec, "selected mode", "M_MigrationInProgress") + return M_MigrationInProgress + } + } else { + logr.Info("something is wrong. use existing is false // source data is not present. sourceDataFrom is not present in status") + return M_NotSupported + } + } + } + // use existing is false; source data is not present ; active status is using-existing-db or ready + // activeDB does not have sourceDataFrom info if dbClaim.Status.ActiveDB.DbState == persistancev1.Ready { activeHostParams := hostparams.GetActiveHostParams(dbClaim) if r.Input.HostParams.IsUpgradeRequested(activeHostParams) { @@ -185,7 +206,7 @@ func (r *DatabaseClaimReconciler) getMode(dbClaim *persistancev1.DatabaseClaim) dbClaim.Status.NewDB.DbState = persistancev1.InProgress dbClaim.Status.MigrationState = "" } - if dbClaim.Status.MigrationState == "" { + if dbClaim.Status.MigrationState == "" || dbClaim.Status.MigrationState == pgctl.S_Initial.String() { logr.Info("selected mode for", "dbclaim", dbClaim.Spec, "selected mode", "M_InitiateDBUpgrade") return M_InitiateDBUpgrade } else if dbClaim.Status.MigrationState != pgctl.S_Completed.String() { @@ -295,8 +316,13 @@ func (r *DatabaseClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reques var dbClaim persistancev1.DatabaseClaim if err := r.Get(ctx, req.NamespacedName, &dbClaim); err != nil { - logr.Error(err, "unable to fetch DatabaseClaim") - return ctrl.Result{}, client.IgnoreNotFound(err) + if client.IgnoreNotFound(err) != nil { + logr.Error(err, "unable to fetch DatabaseClaim") + return ctrl.Result{}, err + } else { + logr.Info("DatabaseClaim not found. Ignoring since object might have been deleted") + return ctrl.Result{}, nil + } } logr.Info("object information", "uid", dbClaim.ObjectMeta.UID) @@ -305,6 +331,7 @@ func (r *DatabaseClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reques logr.Info("ignoring this claim as this controller does not own this class", "claimClass", *dbClaim.Spec.Class, "controllerClas", r.Class) return ctrl.Result{}, nil } + if dbClaim.Status.ActiveDB.ConnectionInfo == nil { dbClaim.Status.ActiveDB.ConnectionInfo = new(persistancev1.DatabaseClaimConnectionInfo) } @@ -315,7 +342,6 @@ func (r *DatabaseClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reques if err := r.setReqInfo(&dbClaim); err != nil { return r.manageError(ctx, &dbClaim, err) } - // name of our custom finalizer dbFinalizerName := "databaseclaims.persistance.atlas.infoblox.com/finalizer" @@ -349,7 +375,6 @@ func (r *DatabaseClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reques // Stop reconciliation as the item is being deleted return ctrl.Result{}, nil } - // The object is not being deleted, so if it does not have our finalizer, // then lets add the finalizer and update the object. This is equivalent // registering our finalizer. @@ -364,7 +389,6 @@ func (r *DatabaseClaimReconciler) Reconcile(ctx context.Context, req ctrl.Reques if err := r.createMetricsDeployment(ctx, dbClaim); err != nil { return ctrl.Result{}, err } - return r.updateStatus(ctx, &dbClaim) } @@ -382,9 +406,14 @@ func (r *DatabaseClaimReconciler) createMetricsDeployment(ctx context.Context, d func (r *DatabaseClaimReconciler) updateStatus(ctx context.Context, dbClaim *persistancev1.DatabaseClaim) (ctrl.Result, error) { logr := r.Log.WithValues("databaseclaim", dbClaim.Namespace+"/"+dbClaim.Name) + if dbClaim.Status.ActiveDB.ConnectionInfo == nil { + dbClaim.Status.ActiveDB.ConnectionInfo = new(persistancev1.DatabaseClaimConnectionInfo) + } + if dbClaim.Status.NewDB.ConnectionInfo == nil { + dbClaim.Status.NewDB.ConnectionInfo = new(persistancev1.DatabaseClaimConnectionInfo) + } r.Mode = r.getMode(dbClaim) - if r.Mode == M_UseExistingDB { logr.Info("existing db reconcile started") err := r.reconcileUseExistingDB(ctx, dbClaim) @@ -392,6 +421,7 @@ func (r *DatabaseClaimReconciler) updateStatus(ctx context.Context, dbClaim *per return r.manageError(ctx, dbClaim, err) } dbClaim.Status.ActiveDB.DbState = persistancev1.UsingExistingDB + dbClaim.Status.ActiveDB.SourceDataFrom = dbClaim.Spec.SourceDataFrom.DeepCopy() logr.Info("existing db reconcile complete") return r.manageSuccess(ctx, dbClaim) } @@ -482,10 +512,9 @@ func (r *DatabaseClaimReconciler) reconcileUseExistingDB(ctx context.Context, db if err != nil { return err } - if err := r.Status().Update(ctx, dbClaim); err != nil { - logr.Error(err, "could not update db claim") + if err = r.updateClientStatus(ctx, dbClaim); err != nil { return err - } // create connection info secret + } if r.Input.TempSecret != "" { logr.Info("password reset. updating secret") activeDBConnInfo := dbClaim.Status.ActiveDB.ConnectionInfo.DeepCopy() @@ -564,6 +593,13 @@ func (r *DatabaseClaimReconciler) reconcileNewDB(ctx context.Context, func (r *DatabaseClaimReconciler) reconcileMigrateToNewDB(ctx context.Context, dbClaim *persistancev1.DatabaseClaim) (ctrl.Result, error) { + if dbClaim.Status.MigrationState == "" { + dbClaim.Status.MigrationState = pgctl.S_Initial.String() + if err := r.updateClientStatus(ctx, dbClaim); err != nil { + r.Log.Error(err, "could not update db claim") + return r.manageError(ctx, dbClaim, err) + } + } result, err := r.reconcileNewDB(ctx, dbClaim) if err != nil { return r.manageError(ctx, dbClaim, err) @@ -681,28 +717,27 @@ loop: logr.Info("wait called") s = next dbClaim.Status.MigrationState = s.String() - if err := r.Status().Update(ctx, dbClaim); err != nil { - logr.Error(err, "could not update db claim status") + if err = r.updateClientStatus(ctx, dbClaim); err != nil { + logr.Error(err, "could not update db claim") return r.manageError(ctx, dbClaim, err) } return ctrl.Result{RequeueAfter: 60 * time.Second, Requeue: true}, nil case pgctl.S_RerouteTargetSecret: - if err := r.rerouteTargetSecret(ctx, sourceAppDsn, targetAppConn, dbClaim); err != nil { + if err = r.rerouteTargetSecret(ctx, sourceAppDsn, targetAppConn, dbClaim); err != nil { return r.manageError(ctx, dbClaim, err) } s = next dbClaim.Status.MigrationState = s.String() - if err := r.Status().Update(ctx, dbClaim); err != nil { - logr.Error(err, "could not update db claim status") + if err = r.updateClientStatus(ctx, dbClaim); err != nil { + logr.Error(err, "could not update db claim") return r.manageError(ctx, dbClaim, err) } - default: s = next dbClaim.Status.MigrationState = s.String() - if err := r.Status().Update(ctx, dbClaim); err != nil { - logr.Error(err, "could not update db claim status") + if err = r.updateClientStatus(ctx, dbClaim); err != nil { + logr.Error(err, "could not update db claim") return r.manageError(ctx, dbClaim, err) } } @@ -714,7 +749,7 @@ loop: dbClaim.Status.ActiveDB.DbState = persistancev1.Ready dbClaim.Status.NewDB = persistancev1.Status{ConnectionInfo: &persistancev1.DatabaseClaimConnectionInfo{}} - if err := r.Status().Update(ctx, dbClaim); err != nil { + if err = r.updateClientStatus(ctx, dbClaim); err != nil { logr.Error(err, "could not update db claim") return r.manageError(ctx, dbClaim, err) } @@ -734,6 +769,14 @@ func (r *DatabaseClaimReconciler) getClientForExistingDB(ctx context.Context, lo secretKey := "password" gs := &corev1.Secret{} + if connInfo == nil { + return nil, fmt.Errorf("invalid connection info") + } + + if connInfo.Host == "" { + return nil, fmt.Errorf("invalid host name") + } + if connInfo.Port == "" { return nil, fmt.Errorf("cannot get master port") } @@ -750,7 +793,7 @@ func (r *DatabaseClaimReconciler) getClientForExistingDB(ctx context.Context, lo ns := dbClaim.Spec.SourceDataFrom.Database.SecretRef.Namespace if ns == "" { - ns = "default" + ns = dbClaim.Namespace } err := r.Client.Get(ctx, client.ObjectKey{ Namespace: ns, @@ -2239,6 +2282,19 @@ func (r *DatabaseClaimReconciler) manageSuccess(ctx context.Context, dbClaim *pe } } +func (r *DatabaseClaimReconciler) updateClientStatus(ctx context.Context, dbClaim *persistancev1.DatabaseClaim) error { + + err := r.Client.Status().Update(ctx, dbClaim) + if err != nil { + // Ignore conflicts, resource might just be outdated. + if errors.IsConflict(err) { + return nil + } + return err + } + return nil +} + func GetDBName(dbClaim *persistancev1.DatabaseClaim) string { if dbClaim.Spec.DBNameOverride != "" { return dbClaim.Spec.DBNameOverride @@ -2291,7 +2347,7 @@ func (r *DatabaseClaimReconciler) getSrcAdminPasswdFromSecret(ctx context.Contex ns := dbClaim.Spec.SourceDataFrom.Database.SecretRef.Namespace if ns == "" { - ns = "default" + ns = dbClaim.Namespace } err := r.Client.Get(ctx, client.ObjectKey{ Namespace: ns, diff --git a/controllers/databaseclaim_controller_test.go b/controllers/databaseclaim_controller_test.go index 559b5853..80f0f58f 100644 --- a/controllers/databaseclaim_controller_test.go +++ b/controllers/databaseclaim_controller_test.go @@ -1328,6 +1328,66 @@ func TestDatabaseClaimReconciler_getMode(t *testing.T) { }, M_NotSupported, }, + { + "useExistingFalse-WithNoSource-WithStatusUsingExistingDB", + fields{ + Log: zap.New(zap.UseFlagOptions(&opts)), + Input: &input{ + SharedDBHost: false, + }, + }, + + args{ + dbClaim: &persistancev1.DatabaseClaim{ + ObjectMeta: v1.ObjectMeta{Name: "identity-dbclaim-name", + Namespace: "unitest"}, + + Spec: persistancev1.DatabaseClaimSpec{ + UseExistingSource: &flse, + SourceDataFrom: nil, + }, + Status: persistancev1.DatabaseClaimStatus{ + ActiveDB: persistancev1.Status{DbState: "using-existing-db", + SourceDataFrom: &persistancev1.SourceDataFrom{ + Type: persistancev1.SourceDataType("database"), + Database: &persistancev1.Database{DSN: "postgres://r@h:5432/pub?sslmode=require", + SecretRef: &persistancev1.SecretRef{Namespace: "unitest", Name: "test"}, + }, + }, + }, + }, + }, + }, + M_MigrateExistingToNewDB, + }, + { + "useExistingFalse-WithNoSource-WithStatusUsingExistingDB-noSourceDataFromInStatus", + fields{ + Log: zap.New(zap.UseFlagOptions(&opts)), + Input: &input{ + SharedDBHost: false, + }, + }, + + args{ + dbClaim: &persistancev1.DatabaseClaim{ + ObjectMeta: v1.ObjectMeta{Name: "identity-dbclaim-name", + Namespace: "unitest"}, + + Spec: persistancev1.DatabaseClaimSpec{ + UseExistingSource: &flse, + SourceDataFrom: nil, + }, + Status: persistancev1.DatabaseClaimStatus{ + ActiveDB: persistancev1.Status{DbState: "using-existing-db", + SourceDataFrom: nil, + }, + }, + }, + }, + M_NotSupported, + }, + { "useExisting_DatabaseType", fields{ @@ -1614,7 +1674,7 @@ func TestDatabaseClaimReconciler_getMode(t *testing.T) { MinStorageGB: 20, EngineVersion: "12.11", }, - SharedDBHost: false, + SharedDBHost: true, }, }, @@ -1628,7 +1688,7 @@ func TestDatabaseClaimReconciler_getMode(t *testing.T) { }, Status: persistancev1.DatabaseClaimStatus{ ActiveDB: persistancev1.Status{ - DbState: persistancev1.UsingExistingDB, + DbState: persistancev1.Ready, Type: "aurora-postgres", DBVersion: "13.11", Shape: "db.t4g.medium", diff --git a/helm/db-controller-crds/crd/persistance.atlas.infoblox.com_databaseclaims.yaml b/helm/db-controller-crds/crd/persistance.atlas.infoblox.com_databaseclaims.yaml index d83b7a46..44ffcac7 100644 --- a/helm/db-controller-crds/crd/persistance.atlas.infoblox.com_databaseclaims.yaml +++ b/helm/db-controller-crds/crd/persistance.atlas.infoblox.com_databaseclaims.yaml @@ -280,6 +280,81 @@ spec: description: The optional Shape values are arbitrary and help drive instance selection type: string + sourceDataFrom: + description: SourceDataFrom specifies an existing database or + backup to use when initially provisioning the database. if the + dbclaim has already provisioned a database, this field is ignored + This field used when claim is use-existing-db and attempting + to migrate to newdb + properties: + database: + description: Database defines the connection information to + an existing db + properties: + dsn: + description: 'DSN is the connection string used to reach + the postgres database must have protocol specifier at + beginning (example: mysql:// postgres:// )' + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the postgresdb (should be master/root) + TODO: document/validate the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + required: + - dsn + type: object + s3: + description: S3 defines the location of a DB backup in an + S3 bucket + properties: + bucket: + type: string + prefix: + description: Prefix is the path prefix of the S3 bucket + within which the backup to restore is located. + type: string + region: + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the s3 bucket via AWS client TODO: document/validate + the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + sourceEngine: + description: SourceEngine is the engine used to create + the backup. + type: string + sourceEngineVersion: + description: 'SourceEngineVersion is the version of the + engine used to create the backup. Example: "5.7.30"' + type: string + required: + - bucket + - region + - sourceEngine + - sourceEngineVersion + type: object + type: + description: Type specifies the type of source + type: string + required: + - type + type: object type: description: Specifies the type of database to provision. Only postgres is supported. @@ -342,6 +417,81 @@ spec: description: The optional Shape values are arbitrary and help drive instance selection type: string + sourceDataFrom: + description: SourceDataFrom specifies an existing database or + backup to use when initially provisioning the database. if the + dbclaim has already provisioned a database, this field is ignored + This field used when claim is use-existing-db and attempting + to migrate to newdb + properties: + database: + description: Database defines the connection information to + an existing db + properties: + dsn: + description: 'DSN is the connection string used to reach + the postgres database must have protocol specifier at + beginning (example: mysql:// postgres:// )' + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the postgresdb (should be master/root) + TODO: document/validate the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + required: + - dsn + type: object + s3: + description: S3 defines the location of a DB backup in an + S3 bucket + properties: + bucket: + type: string + prefix: + description: Prefix is the path prefix of the S3 bucket + within which the backup to restore is located. + type: string + region: + type: string + secretRef: + description: 'SecretRef specifies a secret to use for + connecting to the s3 bucket via AWS client TODO: document/validate + the secret format required' + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + sourceEngine: + description: SourceEngine is the engine used to create + the backup. + type: string + sourceEngineVersion: + description: 'SourceEngineVersion is the version of the + engine used to create the backup. Example: "5.7.30"' + type: string + required: + - bucket + - region + - sourceEngine + - sourceEngineVersion + type: object + type: + description: Type specifies the type of source + type: string + required: + - type + type: object type: description: Specifies the type of database to provision. Only postgres is supported.