|
4 | 4 | "context"
|
5 | 5 | "encoding/json"
|
6 | 6 | "fmt"
|
| 7 | + "reflect" |
7 | 8 | "sort"
|
8 | 9 | "strings"
|
9 | 10 | "sync"
|
@@ -1030,6 +1031,85 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
|
1030 | 1031 | return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
|
1031 | 1032 | }
|
1032 | 1033 |
|
| 1034 | +func formatValue(v interface{}) string { |
| 1035 | + if v == nil { |
| 1036 | + return "<nil>" |
| 1037 | + } |
| 1038 | + |
| 1039 | + // Special case for volumeClaimTemplates |
| 1040 | + if templates, ok := v.([]interface{}); ok { |
| 1041 | + // For a single volumeClaimTemplate field change |
| 1042 | + if len(templates) == 1 { |
| 1043 | + if template, ok := templates[0].(map[string]interface{}); ok { |
| 1044 | + if storage := getTemplateStorage(template); storage != "" { |
| 1045 | + return fmt.Sprintf("%q", storage) |
| 1046 | + } |
| 1047 | + } |
| 1048 | + } |
| 1049 | + // For multiple templates or other array types format |
| 1050 | + var names []string |
| 1051 | + for _, t := range templates { |
| 1052 | + if template, ok := t.(map[string]interface{}); ok { |
| 1053 | + if metadata, ok := template["metadata"].(map[string]interface{}); ok { |
| 1054 | + if name, ok := metadata["name"].(string); ok { |
| 1055 | + if storage := getTemplateStorage(template); storage != "" { |
| 1056 | + names = append(names, fmt.Sprintf("%s(%s)", name, storage)) |
| 1057 | + continue |
| 1058 | + } |
| 1059 | + names = append(names, name) |
| 1060 | + } |
| 1061 | + } |
| 1062 | + } |
| 1063 | + } |
| 1064 | + return fmt.Sprintf("[%s]", strings.Join(names, ", ")) |
| 1065 | + } |
| 1066 | + |
| 1067 | + // Special case for selector matchLabels |
| 1068 | + if m, ok := v.(map[string]interface{}); ok { |
| 1069 | + if matchLabels, exists := m["matchLabels"].(map[string]interface{}); exists { |
| 1070 | + var labels []string |
| 1071 | + for k, v := range matchLabels { |
| 1072 | + labels = append(labels, fmt.Sprintf("%s:%s", k, v)) |
| 1073 | + } |
| 1074 | + sort.Strings(labels) |
| 1075 | + return fmt.Sprintf("{%s}", strings.Join(labels, ", ")) |
| 1076 | + } |
| 1077 | + } |
| 1078 | + // Add quotes for string values |
| 1079 | + if str, ok := v.(string); ok { |
| 1080 | + return fmt.Sprintf("%q", str) |
| 1081 | + } |
| 1082 | + // For other types, use standard formatting |
| 1083 | + return fmt.Sprintf("%v", v) |
| 1084 | +} |
| 1085 | + |
| 1086 | +// Get storage size from template |
| 1087 | +func getTemplateStorage(template map[string]interface{}) string { |
| 1088 | + spec, ok := template["spec"].(map[string]interface{}) |
| 1089 | + if !ok { |
| 1090 | + return "" |
| 1091 | + } |
| 1092 | + resources, ok := spec["resources"].(map[string]interface{}) |
| 1093 | + if !ok { |
| 1094 | + return "" |
| 1095 | + } |
| 1096 | + requests, ok := resources["requests"].(map[string]interface{}) |
| 1097 | + if !ok { |
| 1098 | + return "" |
| 1099 | + } |
| 1100 | + storage, ok := requests["storage"].(string) |
| 1101 | + if !ok { |
| 1102 | + return "" |
| 1103 | + } |
| 1104 | + return storage |
| 1105 | +} |
| 1106 | + |
| 1107 | +// Format field changes for error messages |
| 1108 | +func formatFieldChange(field string, currentVal, desiredVal interface{}) string { |
| 1109 | + return fmt.Sprintf(" - %s:\n from: %s\n to: %s", |
| 1110 | + field, formatValue(currentVal), formatValue(desiredVal)) |
| 1111 | +} |
| 1112 | + |
1033 | 1113 | func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
|
1034 | 1114 | dryRunStrategy := cmdutil.DryRunNone
|
1035 | 1115 | if dryRun {
|
@@ -1070,6 +1150,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
|
1070 | 1150 | message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager)
|
1071 | 1151 | }
|
1072 | 1152 | if err != nil {
|
| 1153 | + // Check if this is a StatefulSet immutable field error |
| 1154 | + 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.([]interface{}) |
| 1181 | + desiredTemplates := desiredVal.([]interface{}) |
| 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]interface{}) |
| 1192 | + currentTemplate := current.(map[string]interface{}) |
| 1193 | + |
| 1194 | + name := desiredTemplate["metadata"].(map[string]interface{})["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\nForbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", |
| 1213 | + strings.Join(changes, "\n")) |
| 1214 | + return common.ResultCodeSyncFailed, message |
| 1215 | + } |
| 1216 | + } |
| 1217 | + } |
1073 | 1218 | return common.ResultCodeSyncFailed, err.Error()
|
1074 | 1219 | }
|
1075 | 1220 | if kubeutil.IsCRD(t.targetObj) && !dryRun {
|
|
0 commit comments