From 4a93e3b161099b0a4936d4bd174eb9d312cccafc Mon Sep 17 00:00:00 2001 From: roypaulin Date: Wed, 29 May 2024 23:38:47 +0200 Subject: [PATCH] Sync from server repo (d338d028bb4) --- vclusterops/cluster_op_engine_context.go | 1 - vclusterops/helpers.go | 24 +++ vclusterops/helpers_test.go | 29 ++++ .../https_disallow_multiple_namespaces_op.go | 157 ++++++++++++++++++ vclusterops/https_start_replication_op.go | 31 +--- vclusterops/nma_vertica_version_op.go | 140 +++++++++++----- vclusterops/replication.go | 21 ++- vclusterops/start_node.go | 2 +- vclusterops/util/util.go | 13 +- vclusterops/util/util_test.go | 15 +- 10 files changed, 353 insertions(+), 80 deletions(-) create mode 100644 vclusterops/https_disallow_multiple_namespaces_op.go diff --git a/vclusterops/cluster_op_engine_context.go b/vclusterops/cluster_op_engine_context.go index 1f23bf3..1bd4024 100644 --- a/vclusterops/cluster_op_engine_context.go +++ b/vclusterops/cluster_op_engine_context.go @@ -36,7 +36,6 @@ type opEngineExecContext struct { dbInfo string // store the db info that retrieved from communal storage restorePoints []RestorePoint // store list existing restore points that queried from an archive systemTableList systemTableListInfo // used for staging system tables - // hosts on which the wrong authentication occurred hostsWithWrongAuth []string } diff --git a/vclusterops/helpers.go b/vclusterops/helpers.go index 5f082fe..8878245 100644 --- a/vclusterops/helpers.go +++ b/vclusterops/helpers.go @@ -183,6 +183,30 @@ func getInitiatorHostInCluster(name, sandbox, scname string, vdb *VCoordinationD return initiatorHost, nil } +// getInitiatorHostForReplication returns an initiator that is the first up source host in the main cluster +// or a sandbox +func getInitiatorHostForReplication(name, sandbox string, hosts []string, vdb *VCoordinationDatabase) ([]string, error) { + // source hosts will be : + // 1. up hosts from the main subcluster if the sandbox is empty + // 2. up hosts from the sandbox if the sandbox is specified + var sourceHosts []string + for _, node := range vdb.HostNodeMap { + if node.State != util.NodeDownState && node.Sandbox == sandbox { + sourceHosts = append(sourceHosts, node.Address) + } + } + sourceHosts = util.SliceCommon(hosts, sourceHosts) + if len(sourceHosts) == 0 { + if sandbox == "" { + return nil, fmt.Errorf("[%s] cannot find any up hosts from source database", name) + } + return nil, fmt.Errorf("[%s] cannot find any up hosts in the sandbox %s", name, sandbox) + } + + initiatorHost := []string{getInitiator(sourceHosts)} + return initiatorHost, nil +} + // getVDBFromRunningDB will retrieve db configurations from a non-sandboxed host by calling https endpoints of a running db func (vcc VClusterCommands) getVDBFromRunningDB(vdb *VCoordinationDatabase, options *DatabaseOptions) error { return vcc.getVDBFromRunningDBImpl(vdb, options, false, util.MainClusterSandbox) diff --git a/vclusterops/helpers_test.go b/vclusterops/helpers_test.go index df8d450..91d3b66 100644 --- a/vclusterops/helpers_test.go +++ b/vclusterops/helpers_test.go @@ -110,6 +110,35 @@ func TestForgetInitiatorHost(t *testing.T) { assert.Equal(t, initiatorHost, "") } +func TestForGetSourceHostForReplication(t *testing.T) { + mockHostNodeMap := map[string]*VCoordinationNode{ + "192.168.1.101": {Address: "192.168.1.101", State: "UP", Sandbox: "sand"}, + "192.168.1.102": {Address: "192.168.1.102", State: "UP", Sandbox: "sand"}, + "192.168.1.103": {Address: "192.168.1.103", State: "UP", Sandbox: ""}, + "192.168.1.104": {Address: "192.168.1.104", State: "UP", Sandbox: ""}, + } + + // successfully find source hosts from sandbox sand + vdb := VCoordinationDatabase{HostNodeMap: mockHostNodeMap} + hosts := []string{"192.168.1.102"} + sourceHosts, err := getInitiatorHostForReplication("", "sand", hosts, &vdb) + assert.NoError(t, err) + assert.Equal(t, sourceHosts, hosts) + + // successfully find source hosts from main cluster + vdb = VCoordinationDatabase{HostNodeMap: mockHostNodeMap} + hosts = []string{"192.168.1.103"} + sourceHosts, err = getInitiatorHostForReplication("", "", hosts, &vdb) + assert.NoError(t, err) + assert.Equal(t, sourceHosts, hosts) + + // unable to find any up hosts from main cluster + vdb = VCoordinationDatabase{HostNodeMap: mockHostNodeMap} + hosts = []string{} + _, err = getInitiatorHostForReplication("", "", hosts, &vdb) + assert.ErrorContains(t, err, "cannot find any up hosts from source database") +} + func TestForgetCatalogPath(t *testing.T) { nodeName := "v_vertdb_node0001" fullPath := fmt.Sprintf("/data/vertdb/%s_catalog/Catalog", nodeName) diff --git a/vclusterops/https_disallow_multiple_namespaces_op.go b/vclusterops/https_disallow_multiple_namespaces_op.go new file mode 100644 index 0000000..ebc9302 --- /dev/null +++ b/vclusterops/https_disallow_multiple_namespaces_op.go @@ -0,0 +1,157 @@ +/* + (c) Copyright [2023-2024] Open Text. + Licensed under the Apache License, Version 2.0 (the "License"); + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package vclusterops + +import ( + "errors" + "fmt" + + "github.com/vertica/vcluster/vclusterops/util" +) + +type httpsDisallowMultipleNamespacesOp struct { + opBase + opHTTPSBase + sandbox string + vdb *VCoordinationDatabase +} + +func makeHTTPSDisallowMultipleNamespacesOp(hosts []string, useHTTPPassword bool, + userName string, httpsPassword *string, sandbox string, vdb *VCoordinationDatabase) (httpsDisallowMultipleNamespacesOp, error) { + op := httpsDisallowMultipleNamespacesOp{} + op.name = "HTTPSGetNamespaceOp" + op.description = "Get information about all namespaces" + op.hosts = hosts + op.useHTTPPassword = useHTTPPassword + op.sandbox = sandbox + op.vdb = vdb + + if useHTTPPassword { + err := util.ValidateUsernameAndPassword(op.name, useHTTPPassword, userName) + if err != nil { + return op, err + } + + op.userName = userName + op.httpsPassword = httpsPassword + } + + return op, nil +} + +func (op *httpsDisallowMultipleNamespacesOp) setupClusterHTTPRequest(hosts []string) error { + for _, host := range hosts { + httpRequest := hostHTTPRequest{} + httpRequest.Method = GetMethod + httpRequest.buildHTTPSEndpoint("namespaces") + if op.useHTTPPassword { + httpRequest.Password = op.httpsPassword + httpRequest.Username = op.userName + } + op.clusterHTTPRequest.RequestCollection[host] = httpRequest + } + + return nil +} + +func (op *httpsDisallowMultipleNamespacesOp) prepare(execContext *opEngineExecContext) error { + sourceHost, err := getInitiatorHostForReplication(op.name, op.sandbox, op.hosts, op.vdb) + if err != nil { + return err + } + // hosts to be used to make the request should be an up host from source database or sandbox + op.hosts = sourceHost + execContext.dispatcher.setup(op.hosts) + + return op.setupClusterHTTPRequest(op.hosts) +} + +func (op *httpsDisallowMultipleNamespacesOp) execute(execContext *opEngineExecContext) error { + if err := op.runExecute(execContext); err != nil { + return err + } + + return op.processResult(execContext) +} + +type namespace struct { + NamespaceID int `json:"namespace_id"` + NamespaceName string `json:"namespace_name"` + IsDefault bool `json:"is_default"` + DefaultShardCount int `json:"default_shard_count"` +} + +type namespaceListResponse struct { + NamespaceList []namespace `json:"namespace_list"` +} + +func (op *httpsDisallowMultipleNamespacesOp) processResult(_ *opEngineExecContext) error { + var allErrs error + + for host, result := range op.clusterHTTPRequest.ResultCollection { + op.logResponse(host, result) + + if result.isUnauthorizedRequest() { + // skip checking response from other nodes because we will get the same error there + return result.err + } + if !result.isPassing() { + allErrs = errors.Join(allErrs, result.err) + // try processing other hosts' responses when the current host has some server errors + continue + } + + // decode the json-format response + // The successful response object will be a dictionary: + /* + { + "namespace_list": [ + { + "namespace_id": NAMESPACE_ID, + "namespace_name": "default_namespace", + "is_default": true, + "default_shard_count": 4 + }, + { + "namespace_id": NAMESPACE_ID, + "namespace_name": "test_ns", + "is_default": false, + "default_shard_count": 6 + } + ] + } + */ + namespaceResponse := namespaceListResponse{} + err := op.parseAndCheckResponse(host, result.content, &namespaceResponse) + if err != nil { + allErrs = errors.Join(allErrs, err) + continue + } + // check if the response contains multiple namespaces + if len(namespaceResponse.NamespaceList) > 1 { + err := fmt.Errorf("[%s] replication is not supported in multiple namespace database", op.name) + allErrs = errors.Join(allErrs, err) + return allErrs + } + return nil + } + + return allErrs +} + +func (op *httpsDisallowMultipleNamespacesOp) finalize(_ *opEngineExecContext) error { + return nil +} diff --git a/vclusterops/https_start_replication_op.go b/vclusterops/https_start_replication_op.go index a2c20eb..e22bcb8 100644 --- a/vclusterops/https_start_replication_op.go +++ b/vclusterops/https_start_replication_op.go @@ -34,12 +34,13 @@ type httpsStartReplicationOp struct { targetUserName string targetPassword *string tlsConfig string + vdb *VCoordinationDatabase } func makeHTTPSStartReplicationOp(dbName string, sourceHosts []string, sourceUseHTTPPassword bool, sourceUserName string, sourceHTTPPassword *string, targetUseHTTPPassword bool, targetDB, targetUserName, targetHosts string, - targetHTTPSPassword *string, tlsConfig, sandbox string) (httpsStartReplicationOp, error) { + targetHTTPSPassword *string, tlsConfig, sandbox string, vdb *VCoordinationDatabase) (httpsStartReplicationOp, error) { op := httpsStartReplicationOp{} op.name = "HTTPSStartReplicationOp" op.description = "Start database replication" @@ -50,6 +51,7 @@ func makeHTTPSStartReplicationOp(dbName string, sourceHosts []string, op.targetHosts = targetHosts op.tlsConfig = tlsConfig op.sandbox = sandbox + op.vdb = vdb if sourceUseHTTPPassword { err := util.ValidateUsernameAndPassword(op.name, sourceUseHTTPPassword, sourceUserName) @@ -117,29 +119,14 @@ func (op *httpsStartReplicationOp) setupClusterHTTPRequest(hosts []string) error } func (op *httpsStartReplicationOp) prepare(execContext *opEngineExecContext) error { - if len(execContext.nodesInfo) == 0 { - return fmt.Errorf(`[%s] cannot find any hosts in OpEngineExecContext`, op.name) - } - // source hosts will be : - // 1. up hosts from the main subcluster if the sandbox is empty - // 2. up hosts from the sandbox if the sandbox is specified - var sourceHosts []string - for _, node := range execContext.nodesInfo { - if node.State != util.NodeDownState && node.Sandbox == op.sandbox { - sourceHosts = append(sourceHosts, node.Address) - } - } - sourceHosts = util.SliceCommon(op.hosts, sourceHosts) - if len(sourceHosts) == 0 { - if op.sandbox == "" { - return fmt.Errorf("[%s] cannot find any up hosts from source database %s", op.name, op.sourceDB) - } - return fmt.Errorf("[%s] cannot find any up hosts in the sandbox %s", op.name, op.sandbox) + sourceHost, err := getInitiatorHostForReplication(op.name, op.sandbox, op.hosts, op.vdb) + if err != nil { + return err } + // use first up host to execute https post request + op.hosts = sourceHost - op.hosts = []string{sourceHosts[0]} - - err := op.setupRequestBody(op.hosts) + err = op.setupRequestBody(op.hosts) if err != nil { return err } diff --git a/vclusterops/nma_vertica_version_op.go b/vclusterops/nma_vertica_version_op.go index 539bd6a..b19bb09 100644 --- a/vclusterops/nma_vertica_version_op.go +++ b/vclusterops/nma_vertica_version_op.go @@ -102,6 +102,15 @@ func makeNMAVerticaVersionOpWithVDB(sameVersion bool, vdb *VCoordinationDatabase return op } +// makeNMAVerticaVersionOpBeforeStartNode is used in start_node, VCluster will check Vertica +// version for the nodes which are in the same cluster(main cluster or sandbox) as the target hosts +func makeNMAVerticaVersionOpBeforeStartNode(vdb *VCoordinationDatabase, hosts []string) nmaVerticaVersionOp { + op := makeNMACheckVerticaVersionOp(nil /*hosts*/, true /*sameVersion*/, vdb.IsEon) + op.targetNodeIPs = hosts + op.vdb = vdb + return op +} + func (op *nmaVerticaVersionOp) setupClusterHTTPRequest(hosts []string) error { for _, host := range hosts { httpRequest := hostHTTPRequest{} @@ -160,52 +169,19 @@ func (op *nmaVerticaVersionOp) prepare(execContext *opEngineExecContext) error { } else if len(op.hosts) == 0 { if op.vdb != nil { // db is up - op.HasIncomingSCNames = true - for host, vnode := range op.vdb.HostNodeMap { - op.hosts = append(op.hosts, host) - sc := vnode.Subcluster - // Update subcluster of new nodes that will be assigned to default subcluster. - // When we created vdb in add_node without specifying subcluster, we did not know the default subcluster name - // so new nodes is using "" as their subclusters. Below line will correct node nodes' subclusters. - if op.vdb.IsEon && sc == "" && execContext.defaultSCName != "" { - op.vdb.HostNodeMap[host].Subcluster = execContext.defaultSCName - sc = execContext.defaultSCName - } - - // initialize the SCToHostVersionMap with empty versions - if op.SCToHostVersionMap[sc] == nil { - op.SCToHostVersionMap[sc] = makeHostVersionMap() - } - op.SCToHostVersionMap[sc][host] = "" + err := op.buildHostVersionMapWithVDB(execContext) + if err != nil { + return err } } else { // start db - op.HasIncomingSCNames = true - if execContext.nmaVDatabase.CommunalStorageLocation != "" { - op.IsEon = true - } - hostNodeMap, err := op.prepareHostNodeMap(execContext) + err := op.buildHostVersionMapWhenDBDown(execContext) if err != nil { return err } - for host, vnode := range hostNodeMap { - op.hosts = append(op.hosts, host) - // initialize the SCToHostVersionMap with empty versions - sc := vnode.Subcluster.Name - if op.SCToHostVersionMap[sc] == nil { - op.SCToHostVersionMap[sc] = makeHostVersionMap() - } - op.SCToHostVersionMap[sc][host] = "" - } } } else { - // When creating a db, the subclusters of all nodes will be the same so set it to a fixed value. - sc := DefaultSC - // initialize the SCToHostVersionMap with empty versions - op.SCToHostVersionMap[sc] = makeHostVersionMap() - for _, host := range op.hosts { - op.SCToHostVersionMap[sc][host] = "" - } + op.buildHostVersionMapDefault() } execContext.dispatcher.setup(op.hosts) @@ -387,3 +363,91 @@ func (op *nmaVerticaVersionOp) prepareHostNodeMap(execContext *opEngineExecConte } return hostNodeMap, nil } + +// prepareHostNodeMapWithVDB is a helper to make a host-node map for nodes in the main cluster +// or in a sandbox +func (op *nmaVerticaVersionOp) prepareHostNodeMapWithVDB() (vHostNodeMap, error) { + if len(op.targetNodeIPs) == 0 { + return op.vdb.HostNodeMap, nil + } + hostNodeMap := makeVHostNodeMap() + // we pass in the first host because we expect all of the + // target hosts to belong to the same cluster + sbName, err := op.getSandboxName(op.targetNodeIPs[0]) + if err != nil { + return hostNodeMap, err + } + for host, vnode := range op.vdb.HostNodeMap { + if vnode.Sandbox == sbName { + hostNodeMap[host] = vnode + } + } + return hostNodeMap, nil +} + +func (op *nmaVerticaVersionOp) buildHostVersionMapDefault() { + // When creating a db, the subclusters of all nodes will be the same so set it to a fixed value. + sc := DefaultSC + // initialize the SCToHostVersionMap with empty versions + op.SCToHostVersionMap[sc] = makeHostVersionMap() + for _, host := range op.hosts { + op.SCToHostVersionMap[sc][host] = "" + } +} + +// buildHostVersionMapWhenDBDown sets an hostVersionMap for start_db +func (op *nmaVerticaVersionOp) buildHostVersionMapWhenDBDown(execContext *opEngineExecContext) error { + op.HasIncomingSCNames = true + if execContext.nmaVDatabase.CommunalStorageLocation != "" { + op.IsEon = true + } + hostNodeMap, err := op.prepareHostNodeMap(execContext) + if err != nil { + return err + } + for host, vnode := range hostNodeMap { + op.hosts = append(op.hosts, host) + // initialize the SCToHostVersionMap with empty versions + sc := vnode.Subcluster.Name + if op.SCToHostVersionMap[sc] == nil { + op.SCToHostVersionMap[sc] = makeHostVersionMap() + } + op.SCToHostVersionMap[sc][host] = "" + } + return nil +} + +// buildHostVersionMapWithVDB sets an hostVersionMap from a vdb +func (op *nmaVerticaVersionOp) buildHostVersionMapWithVDB(execContext *opEngineExecContext) error { + op.HasIncomingSCNames = true + hostNodeMap, err := op.prepareHostNodeMapWithVDB() + if err != nil { + return err + } + for host, vnode := range hostNodeMap { + op.hosts = append(op.hosts, host) + sc := vnode.Subcluster + // Update subcluster of new nodes that will be assigned to default subcluster. + // When we created vdb in add_node without specifying subcluster, we did not know the default subcluster name + // so new nodes is using "" as their subclusters. Below line will correct node nodes' subclusters. + if op.vdb.IsEon && sc == "" && execContext.defaultSCName != "" { + op.vdb.HostNodeMap[host].Subcluster = execContext.defaultSCName + sc = execContext.defaultSCName + } + + // initialize the SCToHostVersionMap with empty versions + if op.SCToHostVersionMap[sc] == nil { + op.SCToHostVersionMap[sc] = makeHostVersionMap() + } + op.SCToHostVersionMap[sc][host] = "" + } + return nil +} + +func (op *nmaVerticaVersionOp) getSandboxName(host string) (string, error) { + vnode, ok := op.vdb.HostNodeMap[host] + if !ok { + return "", fmt.Errorf("[%s] host %s does not exist in the database", op.name, host) + } + return vnode.Sandbox, nil +} diff --git a/vclusterops/replication.go b/vclusterops/replication.go index a12168e..89c6c5c 100644 --- a/vclusterops/replication.go +++ b/vclusterops/replication.go @@ -157,8 +157,15 @@ func (vcc VClusterCommands) VReplicateDatabase(options *VReplicationDatabaseOpti return err } + // retrieve information from the database to accurately determine the state of each node in both the main cluster and andbox + vdb := makeVCoordinationDatabase() + err = vcc.getVDBFromRunningDBIncludeSandbox(&vdb, &options.DatabaseOptions, AnySandbox) + if err != nil { + return err + } + // produce database replication instructions - instructions, err := vcc.produceDBReplicationInstructions(options) + instructions, err := vcc.produceDBReplicationInstructions(options, &vdb) if err != nil { return fmt.Errorf("fail to produce instructions, %w", err) } @@ -183,11 +190,11 @@ func (vcc VClusterCommands) VReplicateDatabase(options *VReplicationDatabaseOpti // The generated instructions will later perform the following operations necessary // for a successful replication. -// - Check nodes state // - Check NMA connectivity // - Check Vertica versions // - Replicate database -func (vcc VClusterCommands) produceDBReplicationInstructions(options *VReplicationDatabaseOptions) ([]clusterOp, error) { +func (vcc VClusterCommands) produceDBReplicationInstructions(options *VReplicationDatabaseOptions, + vdb *VCoordinationDatabase) ([]clusterOp, error) { var instructions []clusterOp // need username for https operations in source database @@ -210,8 +217,8 @@ func (vcc VClusterCommands) produceDBReplicationInstructions(options *VReplicati vcc.Log.Info("Current target username", "username", options.TargetUserName) } - httpsGetUpNodesOp, err := makeHTTPSCheckNodeStateOp(options.Hosts, - options.usePassword, options.UserName, options.Password) + httpsDisallowMultipleNamespacesOp, err := makeHTTPSDisallowMultipleNamespacesOp(options.Hosts, + options.usePassword, options.UserName, options.Password, options.SandboxName, vdb) if err != nil { return instructions, err } @@ -224,15 +231,15 @@ func (vcc VClusterCommands) produceDBReplicationInstructions(options *VReplicati initiatorTargetHost := getInitiator(options.TargetHosts) httpsStartReplicationOp, err := makeHTTPSStartReplicationOp(options.DBName, options.Hosts, options.usePassword, options.UserName, options.Password, targetUsePassword, options.TargetDB, options.TargetUserName, initiatorTargetHost, - options.TargetPassword, options.SourceTLSConfig, options.SandboxName) + options.TargetPassword, options.SourceTLSConfig, options.SandboxName, vdb) if err != nil { return instructions, err } instructions = append(instructions, - &httpsGetUpNodesOp, &nmaHealthOp, &nmaVerticaVersionOp, + &httpsDisallowMultipleNamespacesOp, &httpsStartReplicationOp, ) return instructions, nil diff --git a/vclusterops/start_node.go b/vclusterops/start_node.go index b7dde2e..26da453 100644 --- a/vclusterops/start_node.go +++ b/vclusterops/start_node.go @@ -316,7 +316,7 @@ func (vcc VClusterCommands) produceStartNodesInstructions(startNodeInfo *VStartN } // require to have the same vertica version - nmaVerticaVersionOp := makeNMAVerticaVersionOpWithVDB(true /*hosts need to have the same Vertica version*/, vdb) + nmaVerticaVersionOp := makeNMAVerticaVersionOpBeforeStartNode(vdb, startNodeInfo.HostsToStart) instructions = append(instructions, &nmaVerticaVersionOp) // The second parameter (sourceConfHost) in produceTransferConfigOps is set to a nil value in the upload and download step diff --git a/vclusterops/util/util.go b/vclusterops/util/util.go index 6d3be4d..d187521 100644 --- a/vclusterops/util/util.go +++ b/vclusterops/util/util.go @@ -472,8 +472,11 @@ func IsOptionSet(f *flag.FlagSet, optionName string) bool { // ValidateName will validate the name of an obj, the obj can be database, subcluster, etc. // when a name is provided, make sure no special chars are in it -func ValidateName(name, obj string) error { - escapeChars := `=<>'^\".@*?#&/-:;{}()[] \~!%+|,` + "`$" +func ValidateName(name, obj string, allowDash bool) error { + escapeChars := `=<>'^\".@*?#&/:;{}()[] \~!%+|,` + "`$" + if !allowDash { + escapeChars += "-" + } for _, c := range name { if strings.Contains(escapeChars, string(c)) { return fmt.Errorf("invalid character in %s name: %c", obj, c) @@ -483,15 +486,15 @@ func ValidateName(name, obj string) error { } func ValidateDBName(dbName string) error { - return ValidateName(dbName, "database") + return ValidateName(dbName, "database", false) } func ValidateScName(dbName string) error { - return ValidateName(dbName, "subcluster") + return ValidateName(dbName, "subcluster", true) } func ValidateSandboxName(dbName string) error { - return ValidateName(dbName, "sandbox") + return ValidateName(dbName, "sandbox", true) } // suppress help message for hidden options diff --git a/vclusterops/util/util_test.go b/vclusterops/util/util_test.go index 5eb692d..be46120 100644 --- a/vclusterops/util/util_test.go +++ b/vclusterops/util/util_test.go @@ -259,24 +259,27 @@ func TestNewErrorFormatVerb(t *testing.T) { func TestValidateName(t *testing.T) { // positive cases obj := "database" - err := ValidateName("test_db", obj) + err := ValidateName("test_db", obj, false) assert.Nil(t, err) - err = ValidateName("db1", obj) + err = ValidateName("db1", obj, false) assert.Nil(t, err) // negative cases - err = ValidateName("test$db", obj) + err = ValidateName("test$db", obj, false) assert.ErrorContains(t, err, "invalid character in "+obj+" name: $") - err = ValidateName("[db1]", obj) + err = ValidateName("[db1]", obj, false) assert.ErrorContains(t, err, "invalid character in "+obj+" name: [") - err = ValidateName("!!??!!db1", obj) + err = ValidateName("!!??!!db1", obj, false) assert.ErrorContains(t, err, "invalid character in "+obj+" name: !") - err = ValidateName("test-db", obj) + err = ValidateName("test-db", obj, false) assert.ErrorContains(t, err, "invalid character in "+obj+" name: -") + + err = ValidateName("test-db", obj, true) + assert.Nil(t, err) } func TestSetEonFlagHelpMsg(t *testing.T) {