diff --git a/README.md b/README.md index 958e4e2c..21cdbb1e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The NDB operator brings automated and simplified database administration, provis 4. A clone of the source code ([this](https://github.com/nutanix-cloud-native/ndb-operator) repository). 5. Cert-manager (only when running in non OpenShift clusters). Follow the instructions [here](https://cert-manager.io/docs/installation/). -With the pre-requisites completed, the NDB Operator can be deployed in one of the following ways: +With the pre-requisites completed, the NDB Operator can be deployed in one of the following ways: ### Outside Kubernetes Runs the controller outside the Kubernetes cluster as a process, but installs the CRDs, services and RBAC entities within the Kubernetes cluster. Generally used while development (without running webhooks): @@ -28,7 +28,7 @@ Runs the controller outside the Kubernetes cluster as a process, but installs th make install run ``` -### Within Kubernetes +### Within Kubernetes Runs the controller pod, installs the CRDs, services and RBAC entities within the Kubernetes cluster. Used to run the operator from the container image defined in the Makefile. Make sure that the cert-manager is installed if not using OpenShift. ```sh @@ -110,12 +110,12 @@ metadata: app.kubernetes.io/created-by: ndb-operator name: ndb spec: - # Name of the secret that holds the credentials for NDB: username, password and ca_certificate created earlier - credentialSecret: ndb-secret-name - # NDB Server's API URL - server: https://[NDB IP]:8443/era/v0.9 - # Set to true to skip SSL certificate validation, should be false if ca_certificate is provided in the credential secret. - skipCertificateVerification: true + # Name of the secret that holds the credentials for NDB: username, password and ca_certificate created earlier + credentialSecret: ndb-secret-name + # NDB Server's API URL + server: https://[NDB IP]:8443/era/v0.9 + # Set to true to skip SSL certificate validation, should be false if ca_certificate is provided in the credential secret. + skipCertificateVerification: true ``` Create the NDBServer resource using: @@ -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 @@ -257,7 +261,7 @@ kubectl apply -f ### Additional Arguments for Databases Below are the various optional addtionalArguments you can specify along with examples of their corresponding values. Arguments that have defaults will be indicated. -Provisioning Additional Arguments: +Provisioning Additional Arguments: ```yaml # PostGres additionalArguments: @@ -287,7 +291,7 @@ additionalArguments: vm_win_license_key: # NO Default. ``` -Cloning Additional Arguments: +Cloning Additional Arguments: ```yaml MSSQL: windows_domain_profile_id @@ -360,7 +364,7 @@ Run your controller locally (this will run in the foreground, so switch to a new make run ``` -**NOTES:** +**NOTES:** 1. You can also run this in one step by running: `make install run` 2. Run `make --help` for more information on all potential `make` targets @@ -439,4 +443,4 @@ This code is developed in the open with input from the community through issues ## License Copyright 2022-2023 Nutanix, Inc. -The project is released under version 2.0 of the [Apache license](http://www.apache.org/licenses/LICENSE-2.0). +The project is released under version 2.0 of the [Apache license](http://www.apache.org/licenses/LICENSE-2.0). \ No newline at end of file diff --git a/api/v1alpha1/database_types.go b/api/v1alpha1/database_types.go index 06247563..f8edc5d1 100644 --- a/api/v1alpha1/database_types.go +++ b/api/v1alpha1/database_types.go @@ -107,6 +107,7 @@ type Instance struct { // +optional // Additional database engine specific arguments AdditionalArguments map[string]string `json:"additionalArguments"` + IsHighAvailability bool `json:"isHighAvailability"` } type Clone struct { @@ -133,6 +134,7 @@ type Clone struct { // +optional // Additional database engine specific arguments AdditionalArguments map[string]string `json:"additionalArguments"` + 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..1c280771 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,11 @@ 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 +208,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{}