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