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