@@ -1235,6 +1235,80 @@ func formatFieldChange(field string, currentVal, desiredVal any) string {
1235
1235
field , formatValue (currentVal ), formatValue (desiredVal ))
1236
1236
}
1237
1237
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
+
1238
1312
func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
1239
1313
dryRunStrategy := cmdutil .DryRunNone
1240
1314
if dryRun {
@@ -1286,67 +1360,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1286
1360
message , err = sc .resourceOps .ApplyResource (context .TODO (), t .targetObj , dryRunStrategy , force , validate , serverSideApply , sc .serverSideApplyManager )
1287
1361
}
1288
1362
if err != nil {
1289
- // Check if this is a StatefulSet immutable field error
1290
1363
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 {
1350
1366
return common .ResultCodeSyncFailed , message
1351
1367
}
1352
1368
}
@@ -1359,6 +1375,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1359
1375
sc .log .Error (err , fmt .Sprintf ("failed to ensure that CRD %s is ready" , crdName ))
1360
1376
}
1361
1377
}
1378
+
1362
1379
return common .ResultCodeSynced , message
1363
1380
}
1364
1381
0 commit comments