Skip to content

Commit 0e69ba7

Browse files
committed
honour PR comments
1 parent f3606e7 commit 0e69ba7

6 files changed

Lines changed: 133 additions & 40 deletions

File tree

api/v1alpha1/ha_validators.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,13 @@ func getHAValidator(dbType string) (HAParamsValidator, bool) {
4949
type MysqlHAParamsValidator struct{}
5050

5151
// Validate checks MySQL-specific HA constraints. Fields validated:
52-
// - haConfig.mysql — must be present (required)
53-
// - haConfig.mysql.innoDBClusterName — must be non-empty
54-
// - haConfig.nodes[*].nodeType — must be "database" or "mysqlrouter"
55-
// - haConfig.nodes — exactly one database node must have role "Master"
56-
// - haConfig.nodes — mysqlrouter nodes must not have a role set
52+
// - haConfig.mysql — must be present (required)
53+
// - haConfig.mysql.innoDBClusterName — must be non-empty
54+
// - haConfig.nodes[*].nodeType — must be "database" or "mysqlrouter"
55+
// - haConfig.nodes — exactly one database node must have role "Master"
56+
// - haConfig.nodes — mysqlrouter nodes must not have a role set
57+
// - haConfig.mysql.deployMySQLRouter — if true, at least one mysqlrouter node must be present;
58+
// if false, no mysqlrouter nodes must be present
5759
func (v *MysqlHAParamsValidator) Validate(haConfig *InstanceHAConfig, haPath *field.Path, errors *field.ErrorList) {
5860
myPath := haPath.Child("mysql")
5961

@@ -69,8 +71,10 @@ func (v *MysqlHAParamsValidator) Validate(haConfig *InstanceHAConfig, haPath *fi
6971
my.InnoDBClusterName, "innoDBClusterName must be specified"))
7072
}
7173

72-
// Validate node types and enforce exactly one Master database node.
74+
// Validate node types, enforce exactly one Master database node, and
75+
// count mysqlrouter nodes for the deployMySQLRouter consistency check below.
7376
masterCount := 0
77+
routerCount := 0
7478
for i, node := range haConfig.Nodes {
7579
nodePath := haPath.Child("nodes").Index(i)
7680
if node.NodeType != common.HA_NODE_TYPE_DATABASE && node.NodeType != common.HA_NODE_TYPE_MYSQLROUTER {
@@ -84,12 +88,25 @@ func (v *MysqlHAParamsValidator) Validate(haConfig *InstanceHAConfig, haPath *fi
8488
if node.NodeType == common.HA_NODE_TYPE_DATABASE && node.Role == common.HA_NODE_ROLE_MASTER {
8589
masterCount++
8690
}
91+
if node.NodeType == common.HA_NODE_TYPE_MYSQLROUTER {
92+
routerCount++
93+
}
8794
}
8895

8996
if len(haConfig.Nodes) > 0 && masterCount != 1 {
9097
*errors = append(*errors, field.Invalid(haPath.Child("nodes"), haConfig.Nodes,
9198
"exactly one database node must have role 'Master'"))
9299
}
100+
101+
// Enforce consistency between deployMySQLRouter and the nodes list.
102+
if my.DeployMySQLRouter && routerCount == 0 {
103+
*errors = append(*errors, field.Invalid(myPath.Child("deployMySQLRouter"), my.DeployMySQLRouter,
104+
"deployMySQLRouter is true but no mysqlrouter nodes are present in haConfig.nodes"))
105+
}
106+
if !my.DeployMySQLRouter && routerCount > 0 {
107+
*errors = append(*errors, field.Invalid(haPath.Child("nodes"), haConfig.Nodes,
108+
"mysqlrouter nodes are present but deployMySQLRouter is false"))
109+
}
93110
}
94111

95112
// PostgresHAParamsValidator validates Patroni and HAProxy specific fields for Postgres HA.

api/v1alpha1/ha_validators_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func TestMysqlHAParamsValidator_Validate(t *testing.T) {
9494

9595
t.Run("valid config with router enabled produces no errors", func(t *testing.T) {
9696
haConfig := &InstanceHAConfig{
97-
MySQL: validMySQLConfig(),
97+
MySQL: &MySQLHAConfig{InnoDBClusterName: "innodb-cluster", DeployMySQLRouter: true},
9898
Nodes: []InstanceHANode{mysqlRouterNode("router1"), mysqlRouterNode("router2"), masterNode("db1"), replicaNode("db2"), replicaNode("db3")},
9999
}
100100
errors := &field.ErrorList{}
@@ -112,6 +112,32 @@ func TestMysqlHAParamsValidator_Validate(t *testing.T) {
112112
assert.Empty(t, *errors)
113113
})
114114

115+
t.Run("deployMySQLRouter true but no router nodes returns invalid error", func(t *testing.T) {
116+
haConfig := &InstanceHAConfig{
117+
MySQL: &MySQLHAConfig{InnoDBClusterName: "innodb-cluster", DeployMySQLRouter: true},
118+
Nodes: []InstanceHANode{masterNode("db1"), replicaNode("db2"), replicaNode("db3")},
119+
}
120+
errors := &field.ErrorList{}
121+
validator.Validate(haConfig, haPath, errors)
122+
assert.Len(t, *errors, 1)
123+
assert.Equal(t, field.ErrorTypeInvalid, (*errors)[0].Type)
124+
assert.Equal(t, "haConfig.mysql.deployMySQLRouter", (*errors)[0].Field)
125+
assert.Contains(t, (*errors)[0].Detail, "no mysqlrouter nodes")
126+
})
127+
128+
t.Run("router nodes present but deployMySQLRouter false returns invalid error", func(t *testing.T) {
129+
haConfig := &InstanceHAConfig{
130+
MySQL: validMySQLConfig(), // DeployMySQLRouter defaults to false
131+
Nodes: []InstanceHANode{mysqlRouterNode("router1"), masterNode("db1"), replicaNode("db2")},
132+
}
133+
errors := &field.ErrorList{}
134+
validator.Validate(haConfig, haPath, errors)
135+
assert.Len(t, *errors, 1)
136+
assert.Equal(t, field.ErrorTypeInvalid, (*errors)[0].Type)
137+
assert.Equal(t, "haConfig.nodes", (*errors)[0].Field)
138+
assert.Contains(t, (*errors)[0].Detail, "deployMySQLRouter is false")
139+
})
140+
115141
t.Run("nil mysql returns required error and stops further node validation", func(t *testing.T) {
116142
haConfig := &InstanceHAConfig{
117143
MySQL: nil,
@@ -153,7 +179,7 @@ func TestMysqlHAParamsValidator_Validate(t *testing.T) {
153179

154180
t.Run("mysqlrouter node with role set returns error", func(t *testing.T) {
155181
haConfig := &InstanceHAConfig{
156-
MySQL: validMySQLConfig(),
182+
MySQL: &MySQLHAConfig{InnoDBClusterName: "innodb-cluster", DeployMySQLRouter: true},
157183
Nodes: []InstanceHANode{
158184
{VmName: "router1", NodeType: common.HA_NODE_TYPE_MYSQLROUTER, Role: "Master", ClusterName: "cluster-a"},
159185
masterNode("db1"),

api/v1alpha1/webhook_helpers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -322,18 +322,18 @@ func initializeObjects(spec *DatabaseSpec) {
322322
}
323323
}
324324
if pg := spec.Instance.HAConfig.Postgres; pg != nil {
325-
if pg.WritePort == 0 {
325+
if pg.WritePort <= 0 {
326326
pg.WritePort = common.HA_PROXY_DEFAULT_WRITE_PORT
327327
}
328-
if pg.ReadPort == 0 {
328+
if pg.ReadPort <= 0 {
329329
pg.ReadPort = common.HA_PROXY_DEFAULT_READ_PORT
330330
}
331331
}
332332
if my := spec.Instance.HAConfig.MySQL; my != nil {
333-
if my.RouterRWPort == 0 {
333+
if my.RouterRWPort <= 0 {
334334
my.RouterRWPort = common.HA_MYSQL_DEFAULT_RW_PORT
335335
}
336-
if my.RouterROPort == 0 {
336+
if my.RouterROPort <= 0 {
337337
my.RouterROPort = common.HA_MYSQL_DEFAULT_RO_PORT
338338
}
339339
if my.MySQLClusterUsername == "" {

controllers/database_reconciler_helpers.go

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"reflect"
23+
"strconv"
2324
"strings"
2425

2526
ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1"
@@ -122,13 +123,24 @@ func (r *DatabaseReconciler) handleDelete(ctx context.Context, req ctrl.Request,
122123
log.Info("Database CR is being deleted")
123124
instanceManager := getInstanceManager(*database)
124125
if controllerutil.ContainsFinalizer(database, common.FINALIZER_INSTANCE) {
125-
// Check if the deregistration operation id (database.Status.DeregistrationOperationId) is empty
126-
// If so, then make a deprovisionDatabase API call to NDB
127-
// else proceed check for the operation completion before removing finalizer.
126+
// Check if the deregistration operation id (database.Status.DeregistrationOperationId) is empty.
127+
// If so, make a deprovisionDatabase API call to NDB,
128+
// else poll for operation completion before removing the finalizer.
128129
deregistrationOperationId := database.Status.DeregistrationOperationId
129130
if deregistrationOperationId == "" {
130131
deregistrationOp, err := instanceManager.deregister(ctx, r, ndbClient, database)
131132
if err != nil {
133+
// If NDB reports the database is already gone (e.g. manually deleted via NDB UI),
134+
// skip deregistration and proceed to remove the finalizer.
135+
if strings.Contains(err.Error(), "ERA-ENT-0000001") {
136+
log.Info("Database already removed from NDB, skipping deregistration and removing finalizer " + common.FINALIZER_INSTANCE)
137+
r.recorder.Event(database, "Normal", EVENT_DEREGISTRATION_COMPLETED, "Database was already removed from NDB; skipping deregistration.")
138+
controllerutil.RemoveFinalizer(database, common.FINALIZER_INSTANCE)
139+
if err := r.Update(ctx, database); err != nil {
140+
return requeueOnErr(err)
141+
}
142+
return requeue()
143+
}
132144
// Not logging here, already done in the deregister function
133145
return requeueOnErr(err)
134146
}
@@ -215,6 +227,21 @@ func (r *DatabaseReconciler) handleSync(ctx context.Context, database *ndbv1alph
215227
databaseStatus.Id = taskResponse.EntityId
216228
databaseStatus.CreationOperationId = taskResponse.OperationId
217229
r.recorder.Event(database, "Normal", EVENT_CREATION_STARTED, "Database creation initiated on NDB")
230+
231+
// Persist CREATING status immediately and return — do not fall through to the
232+
// External Sync block below. The NDBServer CR won't have this DB yet (NDB just
233+
// started the operation), so the sync block would overwrite the status to
234+
// NOT_FOUND before it is ever written to the CR.
235+
database.Status = *databaseStatus
236+
base := database.DeepCopy()
237+
base.Status = ndbv1alpha1.DatabaseStatus{}
238+
if err := r.Status().Patch(ctx, database, client.MergeFrom(base)); err != nil {
239+
errStatement := "Failed to update status of database custom resource after provisioning"
240+
log.Error(err, errStatement)
241+
r.recorder.Eventf(database, "Warning", EVENT_CR_STATUS_UPDATE_FAILED, "Error: %s. %s.", err.Error())
242+
return requeueOnErr(err)
243+
}
244+
return requeueWithTimeout(common.DATABASE_RECONCILE_INTERVAL_SECONDS)
218245
}
219246

220247
// Handle External Sync
@@ -253,9 +280,9 @@ func (r *DatabaseReconciler) handleSync(ctx context.Context, database *ndbv1alph
253280
}
254281

255282
if !reflect.DeepEqual(database.Status, *databaseStatus) {
283+
base := database.DeepCopy()
256284
database.Status = *databaseStatus
257-
err := r.Status().Update(ctx, database)
258-
if err != nil {
285+
if err := r.Status().Patch(ctx, database, client.MergeFrom(base)); err != nil {
259286
errStatement := "Failed to update status of database custom resource"
260287
log.Error(err, errStatement)
261288
r.recorder.Eventf(database, "Warning", EVENT_CR_STATUS_UPDATE_FAILED, "Error: %s. %s.", err.Error())
@@ -314,14 +341,12 @@ func (r *DatabaseReconciler) handleSync(ctx context.Context, database *ndbv1alph
314341
// extra services (e.g. -ro-svc for Postgres/MySQL) via the HAConnectivityManager registry.
315342
//
316343
// ndbClient is used only when the read-only endpoint needs different IPs than the primary
317-
// (currently: MySQL HA router-disabled, where the -ro-svc targets Replica VMs instead of the Master).
318344
func (r *DatabaseReconciler) setupConnectivity(ctx context.Context, database *ndbv1alpha1.Database, ndbClient ndb_client.NDBClientHTTPInterface) (err error) {
319345
log := ctrllog.FromContext(ctx)
320346
log.Info("Entered database_reconciler_helpers.setupConnectivity")
321347

322348
ns := database.Namespace
323349

324-
// Primary IPs come from Status.IPAddress (set by the HAIPResolver during NDBServer sync).
325350
primaryIPs := strings.Split(database.Status.IPAddress, ",")
326351

327352
// Determine the port for the primary -svc and whether an HA manager is needed.
@@ -333,6 +358,20 @@ func (r *DatabaseReconciler) setupConnectivity(ctx context.Context, database *nd
333358
primaryPort = haManager.PrimaryPort(database.Spec.Instance.HAConfig)
334359
}
335360

361+
// For MySQL HA router-disabled, all nodes (Master and Replicas) listen on the same port.
362+
// If the user has overridden listener_port in additionalArguments, honour it.
363+
mysqlRouterDisabled := database.Spec.Instance != nil &&
364+
database.Spec.Instance.HAConfig != nil &&
365+
database.Spec.Instance.HAConfig.MySQL != nil &&
366+
!database.Spec.Instance.HAConfig.MySQL.DeployMySQLRouter
367+
if mysqlRouterDisabled {
368+
if portStr, ok := database.Spec.Instance.AdditionalArguments["listener_port"]; ok {
369+
if port, parseErr := strconv.ParseInt(portStr, 10, 32); parseErr == nil && port > 0 {
370+
primaryPort = int32(port)
371+
}
372+
}
373+
}
374+
336375
// All databases get a primary -svc pointing to the primary (RW) IPs.
337376
if err = r.setupServiceAndEndpoints(ctx, database, database.Name+"-svc", ns, primaryPort, primaryIPs); err != nil {
338377
return
@@ -342,23 +381,24 @@ func (r *DatabaseReconciler) setupConnectivity(ctx context.Context, database *nd
342381
if haManager != nil {
343382
for _, svcSpec := range haManager.AdditionalServices(database.Spec.Instance.HAConfig) {
344383
roIPs := primaryIPs
345-
346-
// MySQL HA router-disabled: the -ro-svc must target Replica VMs, not the Master.
347-
348-
if database.Spec.Instance.Type == common.DATABASE_TYPE_MYSQL &&
349-
database.Spec.Instance.HAConfig.MySQL != nil &&
350-
!database.Spec.Instance.HAConfig.MySQL.DeployMySQLRouter &&
351-
database.Status.DatabaseServerId != "" {
352-
replicaIPs, resolveErr := ndb_api.GetMySQLReplicaIPsFromDPC(ctx, ndbClient, database.Status.DatabaseServerId)
353-
if resolveErr != nil {
354-
log.Error(resolveErr, "Failed to resolve MySQL Replica IPs for -ro-svc; falling back to primary IPs")
355-
} else if len(replicaIPs) > 0 {
356-
roIPs = replicaIPs
357-
log.Info("MySQL HA router-disabled: using Replica IPs for -ro-svc", "ips", roIPs)
384+
roPort := svcSpec.Port
385+
386+
// MySQL HA router-disabled: the -ro-svc must target Replica VMs, not the Master,
387+
// and must use the same effective listener port as the primary -svc.
388+
if mysqlRouterDisabled {
389+
roPort = primaryPort
390+
if database.Status.DatabaseServerId != "" {
391+
replicaIPs, resolveErr := ndb_api.GetMySQLReplicaIPsFromDPC(ctx, ndbClient, database.Status.DatabaseServerId)
392+
if resolveErr != nil {
393+
log.Error(resolveErr, "Failed to resolve MySQL Replica IPs for -ro-svc; falling back to primary IPs")
394+
} else if len(replicaIPs) > 0 {
395+
roIPs = replicaIPs
396+
log.Info("MySQL HA router-disabled: using Replica IPs for -ro-svc", "ips", roIPs)
397+
}
358398
}
359399
}
360400

361-
if err = r.setupServiceAndEndpoints(ctx, database, database.Name+svcSpec.NameSuffix, ns, svcSpec.Port, roIPs); err != nil {
401+
if err = r.setupServiceAndEndpoints(ctx, database, database.Name+svcSpec.NameSuffix, ns, roPort, roIPs); err != nil {
362402
return
363403
}
364404
}
@@ -432,7 +472,6 @@ func (r *DatabaseReconciler) setupService(ctx context.Context, database *ndbv1al
432472

433473
// buildEndpointAddresses converts a slice of IP strings into Kubernetes EndpointAddresses.
434474
// Empty and whitespace-only entries are skipped. Each unique IP becomes a separate
435-
// EndpointAddress so kube-proxy load-balances across all of them.
436475
func buildEndpointAddresses(ips []string) []corev1.EndpointAddress {
437476
addrs := make([]corev1.EndpointAddress, 0, len(ips))
438477
for _, ip := range ips {

controllers/instance_manager.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package controllers
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
ndbv1alpha1 "github.com/nutanix-cloud-native/ndb-operator/api/v1alpha1"
89
"github.com/nutanix-cloud-native/ndb-operator/common"
@@ -122,6 +123,13 @@ func (dm *DatabaseManager) deleteDatabaseServer(ctx context.Context, r *Database
122123
// before giving up, making it robust against the common 409-Conflict case.
123124
opIds, apiErr := ndb_api.DeprovisionHADatabaseServers(ctx, ndbClient, database.Status.DatabaseServerId)
124125
if apiErr != nil {
126+
// If NDB reports the DB server is already gone (e.g. cluster manually deleted
127+
// via NDB UI), skip deprovision and signal done so the finalizer is removed.
128+
if strings.Contains(apiErr.Error(), "ERA-ENT-0000001") {
129+
log.Info("HA DB server already removed from NDB, skipping deprovision and removing finalizer " + common.FINALIZER_DATABASE_SERVER)
130+
r.recorder.Eventf(database, "Normal", EVENT_DEREGISTRATION_COMPLETED, "HA DB server was already removed from NDB; skipping deprovision.")
131+
return true, nil
132+
}
125133
log.Error(apiErr, "Failed to deprovision one or more HA database servers")
126134
r.recorder.Eventf(database, "Warning", EVENT_DEREGISTRATION_FAILED, "Error: %s", apiErr.Error())
127135
return false, apiErr

ndb_api/db_helpers.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,9 @@ func (a *MySqlRequestAppender) appendProvisioningRequest(req *DatabaseProvisionR
408408
"auto_tune_staging_drive": "true",
409409
}
410410

411-
// Appending/overwriting database actionArguments to actionArguments
412-
if err := setConfiguredActionArguments(database, actionArguments); err != nil {
413-
return nil, err
414-
}
415-
416-
// HA-specific request overrides (override req fields and merge HA action arguments)
411+
// HA-specific request overrides (override req fields and merge HA action arguments).
412+
// This runs before setConfiguredActionArguments so that user-provided additionalArguments
413+
// take final precedence over the HA defaults below.
417414
if database.IsMysqlHA() {
418415
haConfig := database.GetInstanceHAConfig()
419416

@@ -468,6 +465,12 @@ func (a *MySqlRequestAppender) appendProvisioningRequest(req *DatabaseProvisionR
468465
}
469466
}
470467

468+
// Apply user-provided additionalArguments last so they override both the base
469+
// defaults and the HA defaults merged above.
470+
if err := setConfiguredActionArguments(database, actionArguments); err != nil {
471+
return nil, err
472+
}
473+
471474
// Converting action arguments map to list and appending to req.ActionArguments
472475
req.ActionArguments = append(req.ActionArguments, convertMapToActionArguments(actionArguments)...)
473476

0 commit comments

Comments
 (0)