@@ -1226,6 +1226,80 @@ func formatFieldChange(field string, currentVal, desiredVal any) string {
1226
1226
field , formatValue (currentVal ), formatValue (desiredVal ))
1227
1227
}
1228
1228
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
+
1229
1303
func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
1230
1304
dryRunStrategy := cmdutil .DryRunNone
1231
1305
if dryRun {
@@ -1277,67 +1351,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1277
1351
message , err = sc .resourceOps .ApplyResource (context .TODO (), t .targetObj , dryRunStrategy , force , validate , serverSideApply , sc .serverSideApplyManager )
1278
1352
}
1279
1353
if err != nil {
1280
- // Check if this is a StatefulSet immutable field error
1281
1354
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 {
1341
1357
return common .ResultCodeSyncFailed , message
1342
1358
}
1343
1359
}
@@ -1350,6 +1366,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1350
1366
sc .log .Error (err , fmt .Sprintf ("failed to ensure that CRD %s is ready" , crdName ))
1351
1367
}
1352
1368
}
1369
+
1353
1370
return common .ResultCodeSynced , message
1354
1371
}
1355
1372
0 commit comments