|
4 | 4 | "context"
|
5 | 5 | "encoding/json"
|
6 | 6 | "fmt"
|
| 7 | + "reflect" |
7 | 8 | "sort"
|
8 | 9 | "strings"
|
9 | 10 | "sync"
|
@@ -1155,6 +1156,85 @@ func (sc *syncContext) performClientSideApplyMigration(targetObj *unstructured.U
|
1155 | 1156 | return nil
|
1156 | 1157 | }
|
1157 | 1158 |
|
| 1159 | +func formatValue(v interface{}) string { |
| 1160 | + if v == nil { |
| 1161 | + return "<nil>" |
| 1162 | + } |
| 1163 | + |
| 1164 | + // Special case for volumeClaimTemplates |
| 1165 | + if templates, ok := v.([]interface{}); ok { |
| 1166 | + // For a single volumeClaimTemplate field change |
| 1167 | + if len(templates) == 1 { |
| 1168 | + if template, ok := templates[0].(map[string]interface{}); ok { |
| 1169 | + if storage := getTemplateStorage(template); storage != "" { |
| 1170 | + return fmt.Sprintf("%q", storage) |
| 1171 | + } |
| 1172 | + } |
| 1173 | + } |
| 1174 | + // For multiple templates or other array types format |
| 1175 | + var names []string |
| 1176 | + for _, t := range templates { |
| 1177 | + if template, ok := t.(map[string]interface{}); ok { |
| 1178 | + if metadata, ok := template["metadata"].(map[string]interface{}); ok { |
| 1179 | + if name, ok := metadata["name"].(string); ok { |
| 1180 | + if storage := getTemplateStorage(template); storage != "" { |
| 1181 | + names = append(names, fmt.Sprintf("%s(%s)", name, storage)) |
| 1182 | + continue |
| 1183 | + } |
| 1184 | + names = append(names, name) |
| 1185 | + } |
| 1186 | + } |
| 1187 | + } |
| 1188 | + } |
| 1189 | + return fmt.Sprintf("[%s]", strings.Join(names, ", ")) |
| 1190 | + } |
| 1191 | + |
| 1192 | + // Special case for selector matchLabels |
| 1193 | + if m, ok := v.(map[string]interface{}); ok { |
| 1194 | + if matchLabels, exists := m["matchLabels"].(map[string]interface{}); exists { |
| 1195 | + var labels []string |
| 1196 | + for k, v := range matchLabels { |
| 1197 | + labels = append(labels, fmt.Sprintf("%s:%s", k, v)) |
| 1198 | + } |
| 1199 | + sort.Strings(labels) |
| 1200 | + return fmt.Sprintf("{%s}", strings.Join(labels, ", ")) |
| 1201 | + } |
| 1202 | + } |
| 1203 | + // Add quotes for string values |
| 1204 | + if str, ok := v.(string); ok { |
| 1205 | + return fmt.Sprintf("%q", str) |
| 1206 | + } |
| 1207 | + // For other types, use standard formatting |
| 1208 | + return fmt.Sprintf("%v", v) |
| 1209 | +} |
| 1210 | + |
| 1211 | +// Get storage size from template |
| 1212 | +func getTemplateStorage(template map[string]interface{}) string { |
| 1213 | + spec, ok := template["spec"].(map[string]interface{}) |
| 1214 | + if !ok { |
| 1215 | + return "" |
| 1216 | + } |
| 1217 | + resources, ok := spec["resources"].(map[string]interface{}) |
| 1218 | + if !ok { |
| 1219 | + return "" |
| 1220 | + } |
| 1221 | + requests, ok := resources["requests"].(map[string]interface{}) |
| 1222 | + if !ok { |
| 1223 | + return "" |
| 1224 | + } |
| 1225 | + storage, ok := requests["storage"].(string) |
| 1226 | + if !ok { |
| 1227 | + return "" |
| 1228 | + } |
| 1229 | + return storage |
| 1230 | +} |
| 1231 | + |
| 1232 | +// Format field changes for error messages |
| 1233 | +func formatFieldChange(field string, currentVal, desiredVal interface{}) string { |
| 1234 | + return fmt.Sprintf(" - %s:\n from: %s\n to: %s", |
| 1235 | + field, formatValue(currentVal), formatValue(desiredVal)) |
| 1236 | +} |
| 1237 | + |
1158 | 1238 | func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
|
1159 | 1239 | dryRunStrategy := cmdutil.DryRunNone
|
1160 | 1240 | if dryRun {
|
@@ -1206,6 +1286,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
|
1206 | 1286 | message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager)
|
1207 | 1287 | }
|
1208 | 1288 | if err != nil {
|
| 1289 | + // Check if this is a StatefulSet immutable field error |
| 1290 | + 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.([]interface{}) |
| 1317 | + desiredTemplates := desiredVal.([]interface{}) |
| 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]interface{}) |
| 1328 | + currentTemplate := current.(map[string]interface{}) |
| 1329 | + |
| 1330 | + name := desiredTemplate["metadata"].(map[string]interface{})["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\nForbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", |
| 1349 | + strings.Join(changes, "\n")) |
| 1350 | + return common.ResultCodeSyncFailed, message |
| 1351 | + } |
| 1352 | + } |
| 1353 | + } |
1209 | 1354 | return common.ResultCodeSyncFailed, err.Error()
|
1210 | 1355 | }
|
1211 | 1356 | if kubeutil.IsCRD(t.targetObj) && !dryRun {
|
|
0 commit comments