@@ -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).
318344func (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.
436475func buildEndpointAddresses (ips []string ) []corev1.EndpointAddress {
437476 addrs := make ([]corev1.EndpointAddress , 0 , len (ips ))
438477 for _ , ip := range ips {
0 commit comments