@@ -1110,6 +1110,80 @@ func formatFieldChange(field string, currentVal, desiredVal any) string {
11101110 field , formatValue (currentVal ), formatValue (desiredVal ))
11111111}
11121112
1113+ // validateStatefulSetUpdate checks for changes to immutable fields in a StatefulSet
1114+ // Returns the formatted error message and true if immutable fields were changed
1115+ func (sc * syncContext ) validateStatefulSetUpdate (current , desired * unstructured.Unstructured ) (string , bool ) {
1116+ currentSpec , _ , _ := unstructured .NestedMap (current .Object , "spec" )
1117+ desiredSpec , _ , _ := unstructured .NestedMap (desired .Object , "spec" )
1118+
1119+ changes := getImmutableFieldChanges (currentSpec , desiredSpec )
1120+ if len (changes ) == 0 {
1121+ return "" , false
1122+ }
1123+
1124+ sort .Strings (changes )
1125+ 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" ,
1126+ strings .Join (changes , "\n " ))
1127+ return message , true
1128+ }
1129+
1130+ // getImmutableFieldChanges compares specs and returns a list of changes to immutable fields
1131+ func getImmutableFieldChanges (currentSpec , desiredSpec map [string ]any ) []string {
1132+ mutableFields := map [string ]bool {
1133+ "replicas" : true , "ordinals" : true , "template" : true ,
1134+ "updateStrategy" : true , "persistentVolumeClaimRetentionPolicy" : true ,
1135+ "minReadySeconds" : true ,
1136+ }
1137+
1138+ var changes []string
1139+ for k , desiredVal := range desiredSpec {
1140+ if mutableFields [k ] {
1141+ continue
1142+ }
1143+
1144+ currentVal , exists := currentSpec [k ]
1145+ if ! exists {
1146+ changes = append (changes , formatFieldChange (k , nil , desiredVal ))
1147+ continue
1148+ }
1149+
1150+ if ! reflect .DeepEqual (currentVal , desiredVal ) {
1151+ if k == "volumeClaimTemplates" {
1152+ changes = append (changes , formatVolumeClaimChanges (currentVal , desiredVal )... )
1153+ } else {
1154+ changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1155+ }
1156+ }
1157+ }
1158+ return changes
1159+ }
1160+
1161+ // formatVolumeClaimChanges handles the special case of formatting changes to volumeClaimTemplates
1162+ func formatVolumeClaimChanges (currentVal , desiredVal any ) []string {
1163+ currentTemplates := currentVal .([]any )
1164+ desiredTemplates := desiredVal .([]any )
1165+
1166+ if len (currentTemplates ) != len (desiredTemplates ) {
1167+ return []string {formatFieldChange ("volumeClaimTemplates" , currentVal , desiredVal )}
1168+ }
1169+
1170+ var changes []string
1171+ for i := range desiredTemplates {
1172+ desiredTemplate := desiredTemplates [i ].(map [string ]any )
1173+ currentTemplate := currentTemplates [i ].(map [string ]any )
1174+
1175+ name := desiredTemplate ["metadata" ].(map [string ]any )["name" ].(string )
1176+ desiredStorage := getTemplateStorage (desiredTemplate )
1177+ currentStorage := getTemplateStorage (currentTemplate )
1178+
1179+ if currentStorage != desiredStorage {
1180+ changes = append (changes , fmt .Sprintf (" - volumeClaimTemplates.%s:\n from: %q\n to: %q" ,
1181+ name , currentStorage , desiredStorage ))
1182+ }
1183+ }
1184+ return changes
1185+ }
1186+
11131187func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
11141188 dryRunStrategy := cmdutil .DryRunNone
11151189 if dryRun {
@@ -1150,67 +1224,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
11501224 message , err = sc .resourceOps .ApplyResource (context .TODO (), t .targetObj , dryRunStrategy , force , validate , serverSideApply , sc .serverSideApplyManager )
11511225 }
11521226 if err != nil {
1153- // Check if this is a StatefulSet immutable field error
11541227 if strings .Contains (err .Error (), "updates to statefulset spec for fields other than" ) {
1155- current := t .liveObj
1156- desired := t .targetObj
1157-
1158- if current != nil && desired != nil {
1159- currentSpec , _ , _ := unstructured .NestedMap (current .Object , "spec" )
1160- desiredSpec , _ , _ := unstructured .NestedMap (desired .Object , "spec" )
1161-
1162- mutableFields := map [string ]bool {
1163- "replicas" : true ,
1164- "ordinals" : true ,
1165- "template" : true ,
1166- "updateStrategy" : true ,
1167- "persistentVolumeClaimRetentionPolicy" : true ,
1168- "minReadySeconds" : true ,
1169- }
1170-
1171- var changes []string
1172- for k , desiredVal := range desiredSpec {
1173- if ! mutableFields [k ] {
1174- currentVal , exists := currentSpec [k ]
1175- if ! exists {
1176- changes = append (changes , formatFieldChange (k , nil , desiredVal ))
1177- } else if ! reflect .DeepEqual (currentVal , desiredVal ) {
1178- if k == "volumeClaimTemplates" {
1179- // Handle volumeClaimTemplates specially
1180- currentTemplates := currentVal .([]any )
1181- desiredTemplates := desiredVal .([]any )
1182-
1183- // If template count differs or we're adding/removing templates,
1184- // use the standard array format
1185- if len (currentTemplates ) != len (desiredTemplates ) {
1186- changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1187- } else {
1188- // Compare each template
1189- for i , desired := range desiredTemplates {
1190- current := currentTemplates [i ]
1191- desiredTemplate := desired .(map [string ]any )
1192- currentTemplate := current .(map [string ]any )
1193-
1194- name := desiredTemplate ["metadata" ].(map [string ]any )["name" ].(string )
1195- desiredStorage := getTemplateStorage (desiredTemplate )
1196- currentStorage := getTemplateStorage (currentTemplate )
1197-
1198- if currentStorage != desiredStorage {
1199- changes = append (changes , fmt .Sprintf (" - volumeClaimTemplates.%s:\n from: %q\n to: %q" ,
1200- name , currentStorage , desiredStorage ))
1201- }
1202- }
1203- }
1204- } else {
1205- changes = append (changes , formatFieldChange (k , currentVal , desiredVal ))
1206- }
1207- }
1208- }
1209- }
1210- if len (changes ) > 0 {
1211- sort .Strings (changes )
1212- 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" ,
1213- strings .Join (changes , "\n " ))
1228+ if t .liveObj != nil && t .targetObj != nil {
1229+ if message , hasChanges := sc .validateStatefulSetUpdate (t .liveObj , t .targetObj ); hasChanges {
12141230 return common .ResultCodeSyncFailed , message
12151231 }
12161232 }
@@ -1223,6 +1239,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
12231239 sc .log .Error (err , fmt .Sprintf ("failed to ensure that CRD %s is ready" , crdName ))
12241240 }
12251241 }
1242+
12261243 return common .ResultCodeSynced , message
12271244}
12281245
0 commit comments