@@ -1110,6 +1110,80 @@ func formatFieldChange(field string, currentVal, desiredVal any) string {
1110
1110
field , formatValue (currentVal ), formatValue (desiredVal ))
1111
1111
}
1112
1112
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
+
1113
1187
func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
1114
1188
dryRunStrategy := cmdutil .DryRunNone
1115
1189
if dryRun {
@@ -1150,67 +1224,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1150
1224
message , err = sc .resourceOps .ApplyResource (context .TODO (), t .targetObj , dryRunStrategy , force , validate , serverSideApply , sc .serverSideApplyManager )
1151
1225
}
1152
1226
if err != nil {
1153
- // Check if this is a StatefulSet immutable field error
1154
1227
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 {
1214
1230
return common .ResultCodeSyncFailed , message
1215
1231
}
1216
1232
}
@@ -1223,6 +1239,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1223
1239
sc .log .Error (err , fmt .Sprintf ("failed to ensure that CRD %s is ready" , crdName ))
1224
1240
}
1225
1241
}
1242
+
1226
1243
return common .ResultCodeSynced , message
1227
1244
}
1228
1245
0 commit comments