@@ -1226,6 +1226,80 @@ func formatFieldChange(field string, currentVal, desiredVal any) string {
12261226 field , formatValue (currentVal ), formatValue (desiredVal ))
12271227}
12281228
1229+ // validateStatefulSetUpdate checks for changes to immutable fields in a StatefulSet
1230+ // Returns the formatted error message and true if immutable fields were changed
1231+ func (sc * syncContext ) validateStatefulSetUpdate (current , desired * unstructured.Unstructured ) (string , bool ) {
1232+ currentSpec , _ , _ := unstructured .NestedMap (current .Object , "spec" )
1233+ desiredSpec , _ , _ := unstructured .NestedMap (desired .Object , "spec" )
1234+
1235+ changes := getImmutableFieldChanges (currentSpec , desiredSpec )
1236+ if len (changes ) == 0 {
1237+ return "" , false
1238+ }
1239+
1240+ sort .Strings (changes )
1241+ message := fmt .Sprintf ("attempting to change immutable fields:\n %s\n \n Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden" ,
1242+ strings .Join (changes , "\n " ))
1243+ return message , true
1244+ }
1245+
1246+ // getImmutableFieldChanges compares specs and returns a list of changes to immutable fields
1247+ func getImmutableFieldChanges (currentSpec , desiredSpec map [string ]any ) []string {
1248+ mutableFields := map [string ]bool {
1249+ "replicas" : true , "ordinals" : true , "template" : true ,
1250+ "updateStrategy" : true , "persistentVolumeClaimRetentionPolicy" : true ,
1251+ "minReadySeconds" : true ,
1252+ }
1253+
1254+ var changes []string
1255+ for k , desiredVal := range desiredSpec {
1256+ if mutableFields [k ] {
1257+ continue
1258+ }
1259+
1260+ currentVal , exists := currentSpec [k ]
1261+ if ! exists {
1262+ changes = append (changes , formatFieldChange (k , nil , desiredVal ))
1263+ continue
1264+ }
1265+
1266+ if ! reflect .DeepEqual (currentVal , desiredVal ) {
1267+ if k == "volumeClaimTemplates" {
1268+ changes = append (changes , formatVolumeClaimChanges (currentVal , desiredVal )... )
1269+ } else {
1270+ changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1271+ }
1272+ }
1273+ }
1274+ return changes
1275+ }
1276+
1277+ // formatVolumeClaimChanges handles the special case of formatting changes to volumeClaimTemplates
1278+ func formatVolumeClaimChanges (currentVal , desiredVal any ) []string {
1279+ currentTemplates := currentVal .([]any )
1280+ desiredTemplates := desiredVal .([]any )
1281+
1282+ if len (currentTemplates ) != len (desiredTemplates ) {
1283+ return []string {formatFieldChange ("volumeClaimTemplates" , currentVal , desiredVal )}
1284+ }
1285+
1286+ var changes []string
1287+ for i := range desiredTemplates {
1288+ desiredTemplate := desiredTemplates [i ].(map [string ]any )
1289+ currentTemplate := currentTemplates [i ].(map [string ]any )
1290+
1291+ name := desiredTemplate ["metadata" ].(map [string ]any )["name" ].(string )
1292+ desiredStorage := getTemplateStorage (desiredTemplate )
1293+ currentStorage := getTemplateStorage (currentTemplate )
1294+
1295+ if currentStorage != desiredStorage {
1296+ changes = append (changes , fmt .Sprintf (" - volumeClaimTemplates.%s:\n from: %q\n to: %q" ,
1297+ name , currentStorage , desiredStorage ))
1298+ }
1299+ }
1300+ return changes
1301+ }
1302+
12291303func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
12301304 dryRunStrategy := cmdutil .DryRunNone
12311305 if dryRun {
@@ -1277,67 +1351,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
12771351 message , err = sc .resourceOps .ApplyResource (context .TODO (), t .targetObj , dryRunStrategy , force , validate , serverSideApply , sc .serverSideApplyManager )
12781352 }
12791353 if err != nil {
1280- // Check if this is a StatefulSet immutable field error
12811354 if strings .Contains (err .Error (), "updates to statefulset spec for fields other than" ) {
1282- current := t .liveObj
1283- desired := t .targetObj
1284-
1285- if current != nil && desired != nil {
1286- currentSpec , _ , _ := unstructured .NestedMap (current .Object , "spec" )
1287- desiredSpec , _ , _ := unstructured .NestedMap (desired .Object , "spec" )
1288-
1289- mutableFields := map [string ]bool {
1290- "replicas" : true ,
1291- "ordinals" : true ,
1292- "template" : true ,
1293- "updateStrategy" : true ,
1294- "persistentVolumeClaimRetentionPolicy" : true ,
1295- "minReadySeconds" : true ,
1296- }
1297-
1298- var changes []string
1299- for k , desiredVal := range desiredSpec {
1300- if ! mutableFields [k ] {
1301- currentVal , exists := currentSpec [k ]
1302- if ! exists {
1303- changes = append (changes , formatFieldChange (k , nil , desiredVal ))
1304- } else if ! reflect .DeepEqual (currentVal , desiredVal ) {
1305- if k == "volumeClaimTemplates" {
1306- // Handle volumeClaimTemplates specially
1307- currentTemplates := currentVal .([]any )
1308- desiredTemplates := desiredVal .([]any )
1309-
1310- // If template count differs or we're adding/removing templates,
1311- // use the standard array format
1312- if len (currentTemplates ) != len (desiredTemplates ) {
1313- changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1314- } else {
1315- // Compare each template
1316- for i , desired := range desiredTemplates {
1317- current := currentTemplates [i ]
1318- desiredTemplate := desired .(map [string ]any )
1319- currentTemplate := current .(map [string ]any )
1320-
1321- name := desiredTemplate ["metadata" ].(map [string ]any )["name" ].(string )
1322- desiredStorage := getTemplateStorage (desiredTemplate )
1323- currentStorage := getTemplateStorage (currentTemplate )
1324-
1325- if currentStorage != desiredStorage {
1326- changes = append (changes , fmt .Sprintf (" - volumeClaimTemplates.%s:\n from: %q\n to: %q" ,
1327- name , currentStorage , desiredStorage ))
1328- }
1329- }
1330- }
1331- } else {
1332- changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1333- }
1334- }
1335- }
1336- }
1337- if len (changes ) > 0 {
1338- sort .Strings (changes )
1339- message := fmt .Sprintf ("attempting to change immutable fields:\n %s\n \n Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden" ,
1340- strings .Join (changes , "\n " ))
1355+ if t .liveObj != nil && t .targetObj != nil {
1356+ if message , hasChanges := sc .validateStatefulSetUpdate (t .liveObj , t .targetObj ); hasChanges {
13411357 return common .ResultCodeSyncFailed , message
13421358 }
13431359 }
@@ -1350,6 +1366,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
13501366 sc .log .Error (err , fmt .Sprintf ("failed to ensure that CRD %s is ready" , crdName ))
13511367 }
13521368 }
1369+
13531370 return common .ResultCodeSynced , message
13541371}
13551372
0 commit comments