@@ -1009,91 +1009,28 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
10091009 defer c .mu .Unlock ()
10101010
10111011 // Block all spec changes when cluster is stopped or stopping
1012- if c .Status .Stopped () || c .Status .Stopping () {
1013- lifecyclePhase := ""
1014- if newSpec .Spec .Lifecycle != nil {
1015- lifecyclePhase = newSpec .Spec .Lifecycle .Phase
1016- }
1017- // During Stopping: block ALL spec changes (no cancellation allowed)
1018- // During Stopped: only block if keeping lifecycle.phase="stopped"
1019- if c .Status .Stopping () {
1020- return fmt .Errorf ("cannot update cluster while it is stopping. Wait for it to fully stop first" )
1021- }
1022- if lifecyclePhase == "stopped" {
1023- return fmt .Errorf ("cannot update cluster while stopped. Remove lifecycle.phase to wake up the cluster" )
1024- }
1012+ blocked , err := c .blockLifecycleUpdate (newSpec )
1013+ if err != nil {
1014+ return err
1015+ }
1016+ if blocked {
1017+ return nil
10251018 }
10261019
10271020 newSpec .Status .PostgresClusterStatus = acidv1 .ClusterStatusUpdating
10281021
1029- newSpec , err : = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), newSpec )
1022+ newSpec , err = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), newSpec )
10301023 if err != nil {
10311024 return fmt .Errorf ("could not set cluster status to updating: %w" , err )
10321025 }
10331026
1034- // Check if user is initiating hibernate (Running -> Stopping)
1035- if c .Status .Running () && newSpec .Spec .Lifecycle != nil && newSpec .Spec .Lifecycle .Phase == "stopped" {
1036- c .logger .Infof ("[lifecycle] initiating hibernate for cluster %s: current numberOfInstances=%d" , c .Name , c .Spec .NumberOfInstances )
1037-
1038- // Store previousNumberOfInstances BEFORE setting numberOfInstances to 0
1039- newSpec .Status .PreviousNumberOfInstances = c .Spec .NumberOfInstances
1040- newSpec .Spec .NumberOfInstances = 0
1041- newSpec .Status .PostgresClusterStatus = acidv1 .ClusterStatusStopping
1042-
1043- c .logger .Infof ("[lifecycle] hibernate initiated: setting numberOfInstances=0, previousNumberOfInstances=%d" , newSpec .Status .PreviousNumberOfInstances )
1044-
1045- // Update spec first (Update only updates spec when CR has status subresource)
1046- pgUpdated , err := c .KubeClient .UpdatePostgresCR (c .clusterName (), newSpec )
1047- if err != nil {
1048- return fmt .Errorf ("could not update spec during hibernate: %w" , err )
1049- }
1050- c .logger .Infof ("[lifecycle] hibernate: spec updated successfully" )
1051-
1052- // Update status separately - we need to preserve the status values we set
1053- // because UpdatePostgresCR returns object with status zeroed (subresource behavior)
1054- pgUpdated .Status .PreviousNumberOfInstances = newSpec .Status .PreviousNumberOfInstances
1055- pgUpdated .Status .PostgresClusterStatus = newSpec .Status .PostgresClusterStatus
1056-
1057- pgUpdated , err = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), pgUpdated )
1058- if err != nil {
1059- return fmt .Errorf ("could not update status during hibernate: %w" , err )
1060- }
1061- c .logger .Infof ("[lifecycle] hibernate: status updated successfully, previousNumberOfInstances=%d" , pgUpdated .Status .PreviousNumberOfInstances )
1062-
1063- c .setSpec (pgUpdated )
1064- return nil
1027+ // Handle lifecycle transitions (hibernate/wake-up)
1028+ handled , err := c .handleHibernateAndWakeUp (newSpec )
1029+ if err != nil {
1030+ return err
10651031 }
1066-
1067- // Check if user is waking up from stopped state (Stopped -> Running)
1068- // This is when user clears lifecycle.phase to wake up the cluster
1069- if c .Status .Stopped () && (newSpec .Spec .Lifecycle == nil || newSpec .Spec .Lifecycle .Phase != "stopped" ) {
1070- if newSpec .Status .PreviousNumberOfInstances > 0 {
1071- c .logger .Infof ("[lifecycle] waking up cluster %s: restoring numberOfInstances=%d" , c .Name , newSpec .Status .PreviousNumberOfInstances )
1072-
1073- // Restore numberOfInstances from previousNumberOfInstances
1074- newSpec .Spec .NumberOfInstances = newSpec .Status .PreviousNumberOfInstances
1075- newSpec .Status .PostgresClusterStatus = acidv1 .ClusterStatusUpdating
1076-
1077- // Update spec first
1078- pgUpdated , err := c .KubeClient .UpdatePostgresCR (c .clusterName (), newSpec )
1079- if err != nil {
1080- return fmt .Errorf ("could not update spec during wake-up: %w" , err )
1081- }
1082- c .logger .Infof ("[lifecycle] wake-up: spec updated successfully" )
1083-
1084- // Update status separately, and clear previousNumberOfInstances after restore
1085- pgUpdated .Status .PreviousNumberOfInstances = 0 // Clear after successful restore
1086- pgUpdated .Status .PostgresClusterStatus = newSpec .Status .PostgresClusterStatus
1087-
1088- pgUpdated , err = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), pgUpdated )
1089- if err != nil {
1090- return fmt .Errorf ("could not update status during wake-up: %w" , err )
1091- }
1092- c .logger .Infof ("[lifecycle] wake-up: status updated successfully, previousNumberOfInstances cleared" )
1093-
1094- c .setSpec (pgUpdated )
1095- return nil
1096- }
1032+ if handled {
1033+ return nil
10971034 }
10981035
10991036 if ! c .isInMaintenanceWindow (newSpec .Spec .MaintenanceWindows ) {
@@ -1301,6 +1238,106 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
13011238 return nil
13021239}
13031240
1241+ // blockLifecycleUpdate checks if an update should be blocked due to lifecycle state.
1242+ // Returns (blocked bool, err error):
1243+ // - (true, nil) if update is blocked and caller should return early
1244+ // - (false, nil) if update can proceed
1245+ // - (false, error) on error
1246+ func (c * Cluster ) blockLifecycleUpdate (newSpec * acidv1.Postgresql ) (bool , error ) {
1247+ if ! c .Status .Stopped () && ! c .Status .Stopping () {
1248+ return false , nil
1249+ }
1250+
1251+ lifecyclePhase := ""
1252+ if newSpec .Spec .Lifecycle != nil {
1253+ lifecyclePhase = newSpec .Spec .Lifecycle .Phase
1254+ }
1255+
1256+ // During Stopping: block ALL spec changes (no cancellation allowed)
1257+ if c .Status .Stopping () {
1258+ return true , fmt .Errorf ("cannot update cluster while it is stopping. Wait for it to fully stop first" )
1259+ }
1260+
1261+ // During Stopped: only block if keeping lifecycle.phase="stopped"
1262+ if lifecyclePhase == "stopped" {
1263+ return true , fmt .Errorf ("cannot update cluster while stopped. Remove lifecycle.phase to wake up the cluster" )
1264+ }
1265+
1266+ return false , nil
1267+ }
1268+
1269+ // handleHibernateAndWakeUp processes lifecycle hibernate/wake-up transitions.
1270+ // Returns (handled bool, err error):
1271+ // - (true, nil) if lifecycle transition was handled, Update() should return early
1272+ // - (false, nil) if no lifecycle transition, normal update continues
1273+ // - (false, error) on error
1274+ func (c * Cluster ) handleHibernateAndWakeUp (newSpec * acidv1.Postgresql ) (bool , error ) {
1275+ // === INITIATE HIBERNATE: Running -> Stopping ===
1276+ if c .Status .Running () && newSpec .Spec .Lifecycle != nil && newSpec .Spec .Lifecycle .Phase == "stopped" {
1277+ c .logger .Infof ("[lifecycle] initiating hibernate for cluster %s: current numberOfInstances=%d" , c .Name , c .Spec .NumberOfInstances )
1278+
1279+ // Store previousNumberOfInstances BEFORE setting numberOfInstances to 0
1280+ newSpec .Status .PreviousNumberOfInstances = c .Spec .NumberOfInstances
1281+ newSpec .Spec .NumberOfInstances = 0
1282+ newSpec .Status .PostgresClusterStatus = acidv1 .ClusterStatusStopping
1283+
1284+ c .logger .Infof ("[lifecycle] hibernate initiated: setting numberOfInstances=0, previousNumberOfInstances=%d" , newSpec .Status .PreviousNumberOfInstances )
1285+
1286+ // Update spec first (Update only updates spec when CR has status subresource)
1287+ pgUpdated , err := c .KubeClient .UpdatePostgresCR (c .clusterName (), newSpec )
1288+ if err != nil {
1289+ return false , fmt .Errorf ("could not update spec during hibernate: %w" , err )
1290+ }
1291+ c .logger .Infof ("[lifecycle] hibernate: spec updated successfully" )
1292+
1293+ // Update status separately - preserve status values since UpdatePostgresCR returns object with status zeroed
1294+ pgUpdated .Status .PreviousNumberOfInstances = newSpec .Status .PreviousNumberOfInstances
1295+ pgUpdated .Status .PostgresClusterStatus = newSpec .Status .PostgresClusterStatus
1296+
1297+ pgUpdated , err = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), pgUpdated )
1298+ if err != nil {
1299+ return false , fmt .Errorf ("could not update status during hibernate: %w" , err )
1300+ }
1301+ c .logger .Infof ("[lifecycle] hibernate: status updated successfully, previousNumberOfInstances=%d" , pgUpdated .Status .PreviousNumberOfInstances )
1302+
1303+ c .setSpec (pgUpdated )
1304+ return true , nil
1305+ }
1306+
1307+ // === WAKE-UP: Stopped -> Running ===
1308+ if c .Status .Stopped () && (newSpec .Spec .Lifecycle == nil || newSpec .Spec .Lifecycle .Phase != "stopped" ) {
1309+ if newSpec .Status .PreviousNumberOfInstances > 0 {
1310+ c .logger .Infof ("[lifecycle] waking up cluster %s: restoring numberOfInstances=%d" , c .Name , newSpec .Status .PreviousNumberOfInstances )
1311+
1312+ // Restore numberOfInstances from previousNumberOfInstances
1313+ newSpec .Spec .NumberOfInstances = newSpec .Status .PreviousNumberOfInstances
1314+ newSpec .Status .PostgresClusterStatus = acidv1 .ClusterStatusUpdating
1315+
1316+ // Update spec first
1317+ pgUpdated , err := c .KubeClient .UpdatePostgresCR (c .clusterName (), newSpec )
1318+ if err != nil {
1319+ return false , fmt .Errorf ("could not update spec during wake-up: %w" , err )
1320+ }
1321+ c .logger .Infof ("[lifecycle] wake-up: spec updated successfully" )
1322+
1323+ // Update status separately, and clear previousNumberOfInstances after restore
1324+ pgUpdated .Status .PreviousNumberOfInstances = 0
1325+ pgUpdated .Status .PostgresClusterStatus = newSpec .Status .PostgresClusterStatus
1326+
1327+ pgUpdated , err = c .KubeClient .SetPostgresCRDStatus (c .clusterName (), pgUpdated )
1328+ if err != nil {
1329+ return false , fmt .Errorf ("could not update status during wake-up: %w" , err )
1330+ }
1331+ c .logger .Infof ("[lifecycle] wake-up: status updated successfully, previousNumberOfInstances cleared" )
1332+
1333+ c .setSpec (pgUpdated )
1334+ return true , nil
1335+ }
1336+ }
1337+
1338+ return false , nil
1339+ }
1340+
13041341func syncResources (a , b * v1.ResourceRequirements ) bool {
13051342 for _ , res := range []v1.ResourceName {
13061343 v1 .ResourceCPU ,
0 commit comments