diff --git a/.gitignore b/.gitignore index 4cb7d403..2ef2395e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ testbin/* !vendor/**/zz_generated.* -# editor and IDE paraphernalia +# editor and IDE paraphernalia .idea *.swp *.swo @@ -27,7 +27,7 @@ testbin/* sandbox -config/samples/ndb.yaml +config/samples/* .DS_Store test/__debug_bin diff --git a/README.md b/README.md index 958e4e2c..140f0b44 100644 --- a/README.md +++ b/README.md @@ -156,6 +156,8 @@ spec: size: 10 timezone: "UTC" type: postgres + # isHighAvailability is an optional parameter. In case nothing is specified, it is set to false + isHighAvailability: false # You can specify any (or none) of these types of profiles: compute, software, network, dbParam # If not specified, the corresponding Out-of-Box (OOB) profile will be used wherever applicable @@ -214,6 +216,8 @@ spec: # Cluster id of the cluster where the Database has to be provisioned # Can be fetched from the GET /clusters endpoint clusterId: "Nutanix Cluster Id" + # isHighAvailability is an optional parameter. In case nothing is specified, it is set to false + isHighAvailability: false # You can specify any (or none) of these types of profiles: compute, software, network, dbParam # If not specified, the corresponding Out-of-Box (OOB) profile will be used wherever applicable # Name is case-sensitive. ID is the UUID of the profile. Profile should be in the "READY" state diff --git a/api/v1alpha1/database_types.go b/api/v1alpha1/database_types.go index 06247563..81e03a4a 100644 --- a/api/v1alpha1/database_types.go +++ b/api/v1alpha1/database_types.go @@ -107,6 +107,8 @@ type Instance struct { // +optional // Additional database engine specific arguments AdditionalArguments map[string]string `json:"additionalArguments"` + // +optional + IsHighAvailability bool `json:"isHighAvailability"` } type Clone struct { @@ -133,6 +135,8 @@ type Clone struct { // +optional // Additional database engine specific arguments AdditionalArguments map[string]string `json:"additionalArguments"` + // +optional + IsHighAvailability bool `json:"isHighAvailability"` } // Time Machine details diff --git a/api/v1alpha1/webhook_suite_test.go b/api/v1alpha1/webhook_suite_test.go index 2173c2db..89219420 100644 --- a/api/v1alpha1/webhook_suite_test.go +++ b/api/v1alpha1/webhook_suite_test.go @@ -60,6 +60,7 @@ const ( CREDENTIAL_SECRET = "database-secret" TIMEZONE = "UTC" SIZE = 10 + HA = false ) func TestAPIs(t *testing.T) { @@ -615,6 +616,7 @@ func createDefaultDatabase(metadataName string) *Database { Type: common.DATABASE_TYPE_POSTGRES, Profiles: &(Profiles{}), AdditionalArguments: map[string]string{}, + IsHighAvailability: HA, }, }, } @@ -639,6 +641,7 @@ func createDefaultClone(metadataName string) *Database { SnapshotId: DEFAULT_UUID, Profiles: &(Profiles{}), AdditionalArguments: map[string]string{}, + IsHighAvailability: HA, }, }, } diff --git a/config/crd/bases/ndb.nutanix.com_databases.yaml b/config/crd/bases/ndb.nutanix.com_databases.yaml index c9c2438d..8110df33 100644 --- a/config/crd/bases/ndb.nutanix.com_databases.yaml +++ b/config/crd/bases/ndb.nutanix.com_databases.yaml @@ -65,6 +65,8 @@ spec: description: description: Description of the clone instance type: string + isHighAvailability: + type: boolean name: description: Name of the clone instance type: string @@ -151,6 +153,8 @@ spec: description: description: Description of the database instance type: string + isHighAvailability: + type: boolean name: description: Name of the database instance type: string diff --git a/controller_adapters/database.go b/controller_adapters/database.go index c2bc4506..3757ae23 100644 --- a/controller_adapters/database.go +++ b/controller_adapters/database.go @@ -145,6 +145,10 @@ func (d *Database) GetInstanceSize() int { return d.Spec.Instance.Size } +func (d *Database) GetInstanceIsHighAvailability() bool { + return d.Spec.Instance.IsHighAvailability +} + // Returns basic details about the Time Machine if provided in the // underlying database, else returns defaults like: // TM Name: _TM diff --git a/controller_adapters/database_test.go b/controller_adapters/database_test.go index 60a91bde..a0de35a0 100644 --- a/controller_adapters/database_test.go +++ b/controller_adapters/database_test.go @@ -231,6 +231,30 @@ func TestDatabase_GetInstanceSize(t *testing.T) { }) } +// Tests the GetInstanceIsHighAvailability() function retrieves Size correctly: +func TestDatabase_GetInstanceIsHighAvailability(t *testing.T) { + + name := "Contains IsHighAvailability" + database := Database{ + Database: v1alpha1.Database{ + Spec: v1alpha1.DatabaseSpec{ + Instance: &v1alpha1.Instance{ + IsHighAvailability: true, + }, + }, + }, + } + wantIsHighAvailability := true + + t.Run(name, func(t *testing.T) { + + gotIsHighAvailability := database.GetInstanceIsHighAvailability() + if gotIsHighAvailability != wantIsHighAvailability { + t.Errorf("Database.GetInstanceIsHighAvailability() gotIsHighAvailability= %v, want %v", gotIsHighAvailability, wantIsHighAvailability) + } + }) +} + // Tests the GetClusterId() function retrieves ClusterId correctly: func TestDatabase_GetClusterId(t *testing.T) { diff --git a/ndb_api/clone_helpers.go b/ndb_api/clone_helpers.go index aa0f787e..c2b99a7b 100644 --- a/ndb_api/clone_helpers.go +++ b/ndb_api/clone_helpers.go @@ -16,6 +16,7 @@ package ndb_api import ( "context" "fmt" + "strconv" "github.com/nutanix-cloud-native/ndb-operator/common" "github.com/nutanix-cloud-native/ndb-operator/ndb_client" @@ -79,7 +80,7 @@ func GenerateCloningRequest(ctx context.Context, ndb_client *ndb_client.NDBClien NetworkProfileId: profilesMap[common.PROFILE_TYPE_NETWORK].Id, NewDbServerTimeZone: "", NxClusterId: database.GetClusterId(), - Properties: make([]string, 0), + Properties: make([]map[string]string, 0), }, }, // Added by request appenders as per the engine @@ -90,8 +91,12 @@ func GenerateCloningRequest(ctx context.Context, ndb_client *ndb_client.NDBClien NetworkProfileId: profilesMap[common.PROFILE_TYPE_NETWORK].Id, DatabaseParameterProfileId: profilesMap[common.PROFILE_TYPE_DATABASE_PARAMETER].Id, } + + // boolean for high availability + isHighAvailability := false + // Appending request body based on database type - appender, err := GetRequestAppender(databaseType) + appender, err := GetRequestAppender(databaseType, isHighAvailability) if err != nil { log.Error(err, "Error while getting a request appender") return @@ -204,6 +209,97 @@ func (a *PostgresRequestAppender) appendCloningRequest(req *DatabaseCloneRequest return req, nil } +func setCloneNodesParameters(req *DatabaseCloneRequest, database DatabaseInterface) { + // Extract values of ComputeProfileId and NetworkProfileId + computeProfileId := req.Nodes[0].ComputeProfileId + networkProfileId := req.Nodes[0].NetworkProfileId + serverTimeZone := req.Nodes[0].NewDbServerTimeZone + + // Clear the original req.Nodes array + req.Nodes = []Node{} + + // Create node object for HA Proxy + for i := 0; i < 2; i++ { + // Hard coding the HA Proxy properties + props := make([]map[string]string, 1) + props[0] = map[string]string{ + "name": "node_type", + "value": "haproxy", + } + req.Nodes = append(req.Nodes, Node{ + Properties: props, + VmName: database.GetName() + "_haproxy" + strconv.Itoa(i), + NxClusterId: database.GetClusterId(), + }) + } + + // Create node object for Database Instances + for i := 0; i < 3; i++ { + // Hard coding the DB properties + props := make([]map[string]string, 4) + props[0] = map[string]string{ + "name": "role", + "value": "Secondary", + } + // 1st node will be the primary node + if i == 0 { + props[0]["value"] = "Primary" + } + props[1] = map[string]string{ + "name": "failover_mode", + "value": "Automatic", + } + props[2] = map[string]string{ + "name": "node_type", + "value": "database", + } + props[3] = map[string]string{ + "name": "remote_archive_destination", + "value": "", + } + req.Nodes = append(req.Nodes, Node{ + ComputeProfileId: computeProfileId, + NetworkProfileId: networkProfileId, + NewDbServerTimeZone: serverTimeZone, + Properties: props, + VmName: database.GetName() + "-" + strconv.Itoa(i), + NxClusterId: database.GetClusterId(), + }) + } +} + +func (a *PostgresHARequestAppender) appendCloningRequest(req *DatabaseCloneRequest, database DatabaseInterface, reqData map[string]interface{}) (*DatabaseCloneRequest, error) { + req.SSHPublicKey = reqData[common.NDB_PARAM_SSH_PUBLIC_KEY].(string) + dbPassword := reqData[common.NDB_PARAM_PASSWORD].(string) + + // Set the number of nodes to 5, 3 Postgres nodes + 2 HA Proxy nodes + req.NodeCount = 5 + setCloneNodesParameters(req, database) + + // Default action arguments + actionArguments := map[string]string{ + /* Non-Configurable from additionalArguments*/ + "vm_name": database.GetName(), + "dbserver_description": "DB Server VM for " + database.GetName(), + "db_password": dbPassword, + } + + // Appending/overwriting database actionArguments to actionArguments + if err := setConfiguredActionArguments(database, actionArguments); err != nil { + return nil, err + } + + // Converting action arguments map to list and appending to req.ActionArguments + req.ActionArguments = append(req.ActionArguments, convertMapToActionArguments(actionArguments)...) + + // Appending LCMConfig Details if specified + if err := appendLCMConfigDetailsToRequest(req, database.GetAdditionalArguments()); err != nil { + return nil, err + } + + return req, nil +} + func (a *MySqlRequestAppender) appendCloningRequest(req *DatabaseCloneRequest, database DatabaseInterface, reqData map[string]interface{}) (*DatabaseCloneRequest, error) { req.SSHPublicKey = reqData[common.NDB_PARAM_SSH_PUBLIC_KEY].(string) dbPassword := reqData[common.NDB_PARAM_PASSWORD].(string) diff --git a/ndb_api/common_helpers.go b/ndb_api/common_helpers.go index 58514ead..b0cbc2af 100644 --- a/ndb_api/common_helpers.go +++ b/ndb_api/common_helpers.go @@ -68,12 +68,16 @@ func GetDatabasePortByType(dbType string) int32 { } // Get specific implementation of the DBProvisionRequestAppender interface based on the provided databaseType -func GetRequestAppender(databaseType string) (requestAppender RequestAppender, err error) { +func GetRequestAppender(databaseType string, isHighAvailability bool) (requestAppender RequestAppender, err error) { switch databaseType { case common.DATABASE_TYPE_MYSQL: requestAppender = &MySqlRequestAppender{} case common.DATABASE_TYPE_POSTGRES: - requestAppender = &PostgresRequestAppender{} + if isHighAvailability { + requestAppender = &PostgresHARequestAppender{} + } else { + requestAppender = &PostgresRequestAppender{} + } case common.DATABASE_TYPE_MONGODB: requestAppender = &MongoDbRequestAppender{} case common.DATABASE_TYPE_MSSQL: diff --git a/ndb_api/common_types.go b/ndb_api/common_types.go index 26c57a91..1dae7ed0 100644 --- a/ndb_api/common_types.go +++ b/ndb_api/common_types.go @@ -88,12 +88,12 @@ type ActionArgument struct { } type Node struct { - VmName string `json:"vmName"` - ComputeProfileId string `json:"computeProfileId,omitempty"` - NetworkProfileId string `json:"networkProfileId,omitempty"` - NewDbServerTimeZone string `json:"newDbServerTimeZone,omitempty"` - NxClusterId string `json:"nxClusterId,omitempty"` - Properties []string `json:"properties"` + VmName string `json:"vmName"` + ComputeProfileId string `json:"computeProfileId,omitempty"` + NetworkProfileId string `json:"networkProfileId,omitempty"` + NewDbServerTimeZone string `json:"newDbServerTimeZone,omitempty"` + NxClusterId string `json:"nxClusterId,omitempty"` + Properties []map[string]string `json:"properties"` } type Property struct { diff --git a/ndb_api/db_helpers.go b/ndb_api/db_helpers.go index 0af5412a..74775787 100644 --- a/ndb_api/db_helpers.go +++ b/ndb_api/db_helpers.go @@ -92,7 +92,7 @@ func GenerateProvisioningRequest(ctx context.Context, ndb_client *ndb_client.NDB }, Nodes: []Node{ { - Properties: make([]string, 0), + Properties: make([]map[string]string, 0), VmName: database.GetName() + "_VM", }, }, @@ -109,7 +109,7 @@ func GenerateProvisioningRequest(ctx context.Context, ndb_client *ndb_client.NDB } // Appending request body based on database type - appender, err := GetRequestAppender(database.GetInstanceType()) + appender, err := GetRequestAppender(database.GetInstanceType(), database.GetInstanceIsHighAvailability()) if err != nil { log.Error(err, "Error while appending provisioning request") return @@ -304,6 +304,105 @@ func (a *PostgresRequestAppender) appendProvisioningRequest(req *DatabaseProvisi return req, nil } +func setNodesParameters(req *DatabaseProvisionRequest, database DatabaseInterface) { + // Clear the original req.Nodes array + req.Nodes = []Node{} + + // Create node object for HA Proxy + for i := 0; i < 2; i++ { + // Hard coding the HA Proxy properties + props := make([]map[string]string, 1) + props[0] = map[string]string{ + "name": "node_type", + "value": "haproxy", + } + req.Nodes = append(req.Nodes, Node{ + Properties: props, + VmName: database.GetName() + "_haproxy" + strconv.Itoa(i+1), + NxClusterId: database.GetClusterId(), + }) + } + + // Create node object for Database Instances + for i := 0; i < 3; i++ { + // Hard coding the DB properties + props := make([]map[string]string, 4) + props[0] = map[string]string{ + "name": "role", + "value": "Secondary", + } + // 1st node will be the primary node + if i == 0 { + props[0]["value"] = "Primary" + } + props[1] = map[string]string{ + "name": "failover_mode", + "value": "Automatic", + } + props[2] = map[string]string{ + "name": "node_type", + "value": "database", + } + props[3] = map[string]string{ + "name": "remote_archive_destination", + "value": "", + } + req.Nodes = append(req.Nodes, Node{ + Properties: props, + VmName: database.GetName() + "-" + strconv.Itoa(i+1), + NetworkProfileId: req.NetworkProfileId, + ComputeProfileId: req.ComputeProfileId, + NxClusterId: database.GetClusterId(), + }) + } +} + +func (a *PostgresHARequestAppender) appendProvisioningRequest(req *DatabaseProvisionRequest, database DatabaseInterface, reqData map[string]interface{}) (*DatabaseProvisionRequest, error) { + dbPassword := reqData[common.NDB_PARAM_PASSWORD].(string) + databaseNames := database.GetInstanceDatabaseNames() + req.SSHPublicKey = reqData[common.NDB_PARAM_SSH_PUBLIC_KEY].(string) + + // Set the number of nodes to 5, 3 Postgres nodes + 2 HA Proxy nodes + req.NodeCount = 5 + setNodesParameters(req, database) + + // Set clustered to true + req.Clustered = true + + // Default action arguments + actionArguments := map[string]string{ + /* Non-Configurable from additionalArguments*/ + "proxy_read_port": "5001", + "listener_port": "5432", + "proxy_write_port": "5000", + "enable_synchronous_mode": "true", + "auto_tune_staging_drive": "true", + "backup_policy": "primary_only", + "db_password": dbPassword, + "database_names": databaseNames, + "provision_virtual_ip": "true", + "deploy_haproxy": "true", + "failover_mode": "Automatic", + "node_type": "database", + "allocate_pg_hugepage": "false", + "cluster_database": "false", + "archive_wal_expire_days": "-1", + "enable_peer_auth": "false", + "cluster_name": "psqlcluster", + "patroni_cluster_name": "patroni", + } + + // Appending/overwriting database actionArguments to actionArguments + if err := setConfiguredActionArguments(database, actionArguments); err != nil { + return nil, err + } + + // Converting action arguments map to list and appending to req.ActionArguments + req.ActionArguments = append(req.ActionArguments, convertMapToActionArguments(actionArguments)...) + + return req, nil +} + func (a *MySqlRequestAppender) appendProvisioningRequest(req *DatabaseProvisionRequest, database DatabaseInterface, reqData map[string]interface{}) (*DatabaseProvisionRequest, error) { dbPassword := reqData[common.NDB_PARAM_PASSWORD].(string) databaseNames := database.GetInstanceDatabaseNames() diff --git a/ndb_api/db_helpers_test.go b/ndb_api/db_helpers_test.go index f39af280..a36522a0 100644 --- a/ndb_api/db_helpers_test.go +++ b/ndb_api/db_helpers_test.go @@ -85,28 +85,38 @@ func TestGetRequestAppenderByType(t *testing.T) { // test data map tests := []struct { - databaseType string - expected interface{} + databaseType string + isHighAvailability bool + expected interface{} }{ {databaseType: common.DATABASE_TYPE_POSTGRES, - expected: &PostgresRequestAppender{}, + isHighAvailability: false, + expected: &PostgresRequestAppender{}, + }, + {databaseType: common.DATABASE_TYPE_POSTGRES, + isHighAvailability: true, + expected: &PostgresHARequestAppender{}, }, {databaseType: common.DATABASE_TYPE_MYSQL, - expected: &MySqlRequestAppender{}, + isHighAvailability: false, + expected: &MySqlRequestAppender{}, }, {databaseType: common.DATABASE_TYPE_MSSQL, - expected: &MSSQLRequestAppender{}, + isHighAvailability: false, + expected: &MSSQLRequestAppender{}, }, {databaseType: common.DATABASE_TYPE_MONGODB, - expected: &MongoDbRequestAppender{}, + isHighAvailability: false, + expected: &MongoDbRequestAppender{}, }, {databaseType: "test", - expected: nil, + isHighAvailability: false, + expected: nil, }, } for _, tc := range tests { - got, _ := GetRequestAppender(tc.databaseType) + got, _ := GetRequestAppender(tc.databaseType, tc.isHighAvailability) if !reflect.DeepEqual(tc.expected, got) { t.Fatalf("expected: %v, got: %v", tc.expected, got) } @@ -166,7 +176,7 @@ func TestPostgresProvisionRequestAppender_withoutAdditionalArguments_positiveWor } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -248,7 +258,7 @@ func TestPostgresProvisionRequestAppender_withAdditionalArguments_positiveWorkfl } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -294,7 +304,292 @@ func TestPostgresProvisionRequestAppender_withAdditionalArguments_negativeWorkfl }) mockDatabase.On("IsClone").Return(false) // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, false) + + // Call function being tested + resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) + + // Checks if error was returned + if err == nil { + t.Errorf("Should have errored. Expected: Setting configured action arguments failed! invalid-key is not an allowed additional argument, Got: %v", err) + } + // Checks if resultRequestIsNil + if resultRequest != nil { + t.Errorf("Should have errored. Expected: resultRequest to be nil, Got: %v", resultRequest) + } + + // Verify that the mock method was called with the expected arguments + mockDatabase.AssertCalled(t, "GetInstanceDatabaseNames") +} + +// Tests PostgresHAProvisionRequestAppender(), without additional arguments, positive workflow +func TestPostgresHAProvisionRequestAppender_withoutAdditionalArguments_positiveWorkflow(t *testing.T) { + + baseRequest := &DatabaseProvisionRequest{} + // Create a mock implementation of DatabaseInterface + mockDatabase := &MockDatabaseInterface{} + + reqData := map[string]interface{}{ + common.NDB_PARAM_SSH_PUBLIC_KEY: TEST_SSHKEY, + common.NDB_PARAM_PASSWORD: TEST_PASSWORD, + } + + // Mock required Mock Database Interface methods + mockDatabase.On("GetInstanceDatabaseNames").Return(TEST_DB_NAMES) + mockDatabase.On("GetName").Return("TestPostgresHADB") + mockDatabase.On("GetInstanceType").Return(common.DATABASE_TYPE_POSTGRES) + mockDatabase.On("GetAdditionalArguments").Return(map[string]string{}) + mockDatabase.On("GetClusterId").Return(TEST_CLUSTER_ID) + mockDatabase.On("IsClone").Return(false) + expectedActionArgs := []ActionArgument{ + { + Name: "proxy_read_port", + Value: "5001", + }, + { + Name: "listener_port", + Value: "5432", + }, + { + Name: "proxy_write_port", + Value: "5000", + }, + { + Name: "enable_synchronous_mode", + Value: "true", + }, + { + Name: "auto_tune_staging_drive", + Value: "true", + }, + { + Name: "backup_policy", + Value: "primary_only", + }, + { + Name: "db_password", + Value: TEST_PASSWORD, + }, + { + Name: "database_names", + Value: TEST_DB_NAMES, + }, + { + Name: "provision_virtual_ip", + Value: "true", + }, + { + Name: "deploy_haproxy", + Value: "true", + }, + { + Name: "failover_mode", + Value: "Automatic", + }, + { + Name: "node_type", + Value: "database", + }, + { + Name: "allocate_pg_hugepage", + Value: "false", + }, + { + Name: "cluster_database", + Value: "false", + }, + { + Name: "archive_wal_expire_days", + Value: "-1", + }, + { + Name: "enable_peer_auth", + Value: "false", + }, + { + Name: "cluster_name", + Value: "psqlcluster", + }, + { + Name: "patroni_cluster_name", + Value: "patroni", + }, + } + + // Get specific implementation of RequestAppender + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, true) + + // Call function being tested + resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) + // Assert expected results + if resultRequest.SSHPublicKey != reqData[common.NDB_PARAM_SSH_PUBLIC_KEY] { + t.Errorf("Unexpected SSHPublicKey value. Expected: %s, Got: %s", reqData[common.NDB_PARAM_SSH_PUBLIC_KEY], resultRequest.SSHPublicKey) + } + + // Checks if expected and retrieved action arguments are equal + sortWantAndGotActionArgsByName(expectedActionArgs, resultRequest.ActionArguments) + + // Checks if no error was returned + if err != nil { + t.Errorf("Unexpected error. Expected: %v, Got: %v", nil, err) + } + + // Checks requestAppender.appendProvisioningRequest return type has no error and resultRequest.ActionArguments correctly configured + if !reflect.DeepEqual(expectedActionArgs, resultRequest.ActionArguments) { + t.Errorf("Unexpected ActionArguments. Expected: %v, Got: %v", expectedActionArgs, resultRequest.ActionArguments) + } + + // Verify that the mock method was called with the expected arguments + mockDatabase.AssertCalled(t, "GetInstanceDatabaseNames") +} + +// Test PostgresHAProvisionRequestAppender(), with additional arguments, positive workflow +func TestPostgresHAProvisionRequestAppender_withAdditionalArguments_positiveWorkflow(t *testing.T) { + + baseRequest := &DatabaseProvisionRequest{} + // Create a mock implementation of DatabaseInterface + mockDatabase := &MockDatabaseInterface{} + + reqData := map[string]interface{}{ + common.NDB_PARAM_SSH_PUBLIC_KEY: TEST_SSHKEY, + common.NDB_PARAM_PASSWORD: TEST_PASSWORD, + } + + // Mock required Mock Database Interface methods + mockDatabase.On("GetInstanceDatabaseNames").Return(TEST_DB_NAMES) + mockDatabase.On("GetName").Return("TestPostgresHADB") + mockDatabase.On("GetInstanceType").Return(common.DATABASE_TYPE_POSTGRES) + mockDatabase.On("GetAdditionalArguments").Return(map[string]string{ + "listener_port": "0000", + }) + mockDatabase.On("GetClusterId").Return(TEST_CLUSTER_ID) + mockDatabase.On("IsClone").Return(false) + + expectedActionArgs := []ActionArgument{ + { + Name: "listener_port", + Value: "0000", + }, + { + Name: "proxy_read_port", + Value: "5001", + }, + { + Name: "proxy_write_port", + Value: "5000", + }, + { + Name: "enable_synchronous_mode", + Value: "true", + }, + { + Name: "auto_tune_staging_drive", + Value: "true", + }, + { + Name: "backup_policy", + Value: "primary_only", + }, + { + Name: "db_password", + Value: TEST_PASSWORD, + }, + { + Name: "database_names", + Value: TEST_DB_NAMES, + }, + { + Name: "provision_virtual_ip", + Value: "true", + }, + { + Name: "deploy_haproxy", + Value: "true", + }, + { + Name: "failover_mode", + Value: "Automatic", + }, + { + Name: "node_type", + Value: "database", + }, + { + Name: "allocate_pg_hugepage", + Value: "false", + }, + { + Name: "cluster_database", + Value: "false", + }, + { + Name: "archive_wal_expire_days", + Value: "-1", + }, + { + Name: "enable_peer_auth", + Value: "false", + }, + { + Name: "cluster_name", + Value: "psqlcluster", + }, + { + Name: "patroni_cluster_name", + Value: "patroni", + }, + } + + // Get specific implementation of RequestAppender + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, true) + + // Call function being tested + resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) + + // Assert expected results + if resultRequest.SSHPublicKey != reqData[common.NDB_PARAM_SSH_PUBLIC_KEY] { + t.Errorf("Unexpected SSHPublicKey value. Expected: %s, Got: %s", reqData[common.NDB_PARAM_SSH_PUBLIC_KEY], resultRequest.SSHPublicKey) + } + + // Sort expected and retrieved action arguments + sortWantAndGotActionArgsByName(expectedActionArgs, resultRequest.ActionArguments) + + // Checks if no error was returned + if err != nil { + t.Errorf("Unexpected error. Expected: %v, Got: %v", nil, err) + } + // Check if the lengths of expected and retrieved action arguments are equal + if !reflect.DeepEqual(expectedActionArgs, resultRequest.ActionArguments) { + t.Errorf("Unexpected ActionArguments. Expected: %v, Got: %v", expectedActionArgs, resultRequest.ActionArguments) + } + + // Verify that the mock method was called with the expected arguments + mockDatabase.AssertCalled(t, "GetInstanceDatabaseNames") +} + +// Test PostgresHAProvisionRequestAppender(), with additional arguments, negative workflow +func TestPostgresHAProvisionRequestAppender_withoutAdditionalArguments_negativeWorkflow(t *testing.T) { + + baseRequest := &DatabaseProvisionRequest{} + // Create a mock implementation of DatabaseInterface + mockDatabase := &MockDatabaseInterface{} + + reqData := map[string]interface{}{ + common.NDB_PARAM_SSH_PUBLIC_KEY: TEST_SSHKEY, + common.NDB_PARAM_PASSWORD: TEST_PASSWORD, + } + + // Mock required Mock Database Interface methods + mockDatabase.On("GetInstanceDatabaseNames").Return(TEST_DB_NAMES) + mockDatabase.On("GetName").Return("TestPostgresHADB") + mockDatabase.On("GetInstanceType").Return(common.DATABASE_TYPE_POSTGRES) + mockDatabase.On("GetAdditionalArguments").Return(map[string]string{ + "invalid-key": "invalid-value", + }) + mockDatabase.On("GetClusterId").Return(TEST_CLUSTER_ID) + mockDatabase.On("IsClone").Return(false) + // Get specific implementation of RequestAppender + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_POSTGRES, true) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -400,7 +695,7 @@ func TestMSSQLProvisionRequestAppender_withoutAdditionalArguments_positiveWorklo } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -531,7 +826,7 @@ func TestMSSQLProvisionRequestAppender_withAdditionalArguments_positiveWorkflow( } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -593,7 +888,7 @@ func TestMSSQLProvisionRequestAppender_withAdditionalArguments_negativeWorkflow( }) mockDatabase.On("IsClone").Return(false) // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MSSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -669,7 +964,7 @@ func TestMongoDbProvisionRequestAppender_withoutAdditionalArguments_positiveWork } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -756,7 +1051,7 @@ func TestMongoDbProvisionRequestAppender_withAdditionalArguments_positiveWorkflo } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -802,7 +1097,7 @@ func TestMongoDbProvisionRequestAppender_withAdditionalArguments_negativeWorkflo }) mockDatabase.On("IsClone").Return(false) // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MONGODB, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -857,7 +1152,7 @@ func TestMySqlProvisionRequestAppender_withoutAdditionalArguments_positiveWorkfl } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -922,7 +1217,7 @@ func TestMySqlProvisionRequestAppender_withAdditionalArguments_positiveWorkflow( } // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -968,7 +1263,7 @@ func TestMySqlProvisionRequestAppender_withAdditionalArguments_negativeWorkflow( }) mockDatabase.On("IsClone").Return(false) // Get specific implementation of RequestAppender - requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL) + requestAppender, _ := GetRequestAppender(common.DATABASE_TYPE_MYSQL, false) // Call function being tested resultRequest, err := requestAppender.appendProvisioningRequest(baseRequest, mockDatabase, reqData) @@ -1288,6 +1583,7 @@ func TestGenerateProvisioningRequest_AgainstDifferentReqData(t *testing.T) { mockDatabase.On("GetInstanceSize").Return(TEST_INSTANCE_SIZE) mockDatabase.On("GetInstanceDatabaseNames").Return(TEST_DB_NAMES) mockDatabase.On("GetAdditionalArguments").Return(map[string]string{}) + mockDatabase.On("GetInstanceIsHighAvailability").Return(false) mockDatabase.On("IsClone").Return(false) // Test diff --git a/ndb_api/interface_mock_test.go b/ndb_api/interface_mock_test.go index 4190debc..8ec1c253 100644 --- a/ndb_api/interface_mock_test.go +++ b/ndb_api/interface_mock_test.go @@ -136,3 +136,9 @@ func (m *MockDatabaseInterface) GetAdditionalArguments() map[string]string { // If the type assertion fails, return default return map[string]string{} } + +// GetInstanceIsHighAvailability is a mock implementation of the GetInstanceIsHighAvailability method in the Database interface +func (m *MockDatabaseInterface) GetInstanceIsHighAvailability() bool { + args := m.Called() + return args.Bool(0) +} diff --git a/ndb_api/interfaces.go b/ndb_api/interfaces.go index f0c3dedd..60a0aabd 100644 --- a/ndb_api/interfaces.go +++ b/ndb_api/interfaces.go @@ -49,6 +49,7 @@ type DatabaseInterface interface { GetCloneSourceDBId() string GetCloneSnapshotId() string GetAdditionalArguments() map[string]string + GetInstanceIsHighAvailability() bool } // Internal Interfaces @@ -72,3 +73,6 @@ type PostgresRequestAppender struct{} // Implements RequestAppender type MySqlRequestAppender struct{} + +// Implements RequestAppender +type PostgresHARequestAppender struct{}