|
4 | 4 | "context"
|
5 | 5 | "encoding/json"
|
6 | 6 | "fmt"
|
| 7 | + "reflect" |
7 | 8 | "sort"
|
8 | 9 | "strings"
|
9 | 10 | "sync"
|
@@ -965,6 +966,85 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
|
965 | 966 | return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
|
966 | 967 | }
|
967 | 968 |
|
| 969 | +func formatValue(v interface{}) string { |
| 970 | + if v == nil { |
| 971 | + return "<nil>" |
| 972 | + } |
| 973 | + |
| 974 | + // Special case for volumeClaimTemplates |
| 975 | + if templates, ok := v.([]interface{}); ok { |
| 976 | + // For a single volumeClaimTemplate field change |
| 977 | + if len(templates) == 1 { |
| 978 | + if template, ok := templates[0].(map[string]interface{}); ok { |
| 979 | + if storage := getTemplateStorage(template); storage != "" { |
| 980 | + return fmt.Sprintf("%q", storage) |
| 981 | + } |
| 982 | + } |
| 983 | + } |
| 984 | + // For multiple templates or other array types format |
| 985 | + var names []string |
| 986 | + for _, t := range templates { |
| 987 | + if template, ok := t.(map[string]interface{}); ok { |
| 988 | + if metadata, ok := template["metadata"].(map[string]interface{}); ok { |
| 989 | + if name, ok := metadata["name"].(string); ok { |
| 990 | + if storage := getTemplateStorage(template); storage != "" { |
| 991 | + names = append(names, fmt.Sprintf("%s(%s)", name, storage)) |
| 992 | + continue |
| 993 | + } |
| 994 | + names = append(names, name) |
| 995 | + } |
| 996 | + } |
| 997 | + } |
| 998 | + } |
| 999 | + return fmt.Sprintf("[%s]", strings.Join(names, ", ")) |
| 1000 | + } |
| 1001 | + |
| 1002 | + // Special case for selector matchLabels |
| 1003 | + if m, ok := v.(map[string]interface{}); ok { |
| 1004 | + if matchLabels, exists := m["matchLabels"].(map[string]interface{}); exists { |
| 1005 | + var labels []string |
| 1006 | + for k, v := range matchLabels { |
| 1007 | + labels = append(labels, fmt.Sprintf("%s:%s", k, v)) |
| 1008 | + } |
| 1009 | + sort.Strings(labels) |
| 1010 | + return fmt.Sprintf("{%s}", strings.Join(labels, ", ")) |
| 1011 | + } |
| 1012 | + } |
| 1013 | + // Add quotes for string values |
| 1014 | + if str, ok := v.(string); ok { |
| 1015 | + return fmt.Sprintf("%q", str) |
| 1016 | + } |
| 1017 | + // For other types, use standard formatting |
| 1018 | + return fmt.Sprintf("%v", v) |
| 1019 | +} |
| 1020 | + |
| 1021 | +// Get storage size from template |
| 1022 | +func getTemplateStorage(template map[string]interface{}) string { |
| 1023 | + spec, ok := template["spec"].(map[string]interface{}) |
| 1024 | + if !ok { |
| 1025 | + return "" |
| 1026 | + } |
| 1027 | + resources, ok := spec["resources"].(map[string]interface{}) |
| 1028 | + if !ok { |
| 1029 | + return "" |
| 1030 | + } |
| 1031 | + requests, ok := resources["requests"].(map[string]interface{}) |
| 1032 | + if !ok { |
| 1033 | + return "" |
| 1034 | + } |
| 1035 | + storage, ok := requests["storage"].(string) |
| 1036 | + if !ok { |
| 1037 | + return "" |
| 1038 | + } |
| 1039 | + return storage |
| 1040 | +} |
| 1041 | + |
| 1042 | +// Format field changes for error messages |
| 1043 | +func formatFieldChange(field string, currentVal, desiredVal interface{}) string { |
| 1044 | + return fmt.Sprintf(" - %s:\n from: %s\n to: %s", |
| 1045 | + field, formatValue(currentVal), formatValue(desiredVal)) |
| 1046 | +} |
| 1047 | + |
968 | 1048 | func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
|
969 | 1049 | dryRunStrategy := cmdutil.DryRunNone
|
970 | 1050 | if dryRun {
|
@@ -1005,6 +1085,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
|
1005 | 1085 | message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager, false)
|
1006 | 1086 | }
|
1007 | 1087 | if err != nil {
|
| 1088 | + // Check if this is a StatefulSet immutable field error |
| 1089 | + if strings.Contains(err.Error(), "updates to statefulset spec for fields other than") { |
| 1090 | + current := t.liveObj |
| 1091 | + desired := t.targetObj |
| 1092 | + |
| 1093 | + if current != nil && desired != nil { |
| 1094 | + currentSpec, _, _ := unstructured.NestedMap(current.Object, "spec") |
| 1095 | + desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec") |
| 1096 | + |
| 1097 | + mutableFields := map[string]bool{ |
| 1098 | + "replicas": true, |
| 1099 | + "ordinals": true, |
| 1100 | + "template": true, |
| 1101 | + "updateStrategy": true, |
| 1102 | + "persistentVolumeClaimRetentionPolicy": true, |
| 1103 | + "minReadySeconds": true, |
| 1104 | + } |
| 1105 | + |
| 1106 | + var changes []string |
| 1107 | + for k, desiredVal := range desiredSpec { |
| 1108 | + if !mutableFields[k] { |
| 1109 | + currentVal, exists := currentSpec[k] |
| 1110 | + if !exists { |
| 1111 | + changes = append(changes, formatFieldChange(k, nil, desiredVal)) |
| 1112 | + } else if !reflect.DeepEqual(currentVal, desiredVal) { |
| 1113 | + if k == "volumeClaimTemplates" { |
| 1114 | + // Handle volumeClaimTemplates specially |
| 1115 | + currentTemplates := currentVal.([]interface{}) |
| 1116 | + desiredTemplates := desiredVal.([]interface{}) |
| 1117 | + |
| 1118 | + // If template count differs or we're adding/removing templates, |
| 1119 | + // use the standard array format |
| 1120 | + if len(currentTemplates) != len(desiredTemplates) { |
| 1121 | + changes = append(changes, formatFieldChange(k, currentVal, desiredVal)) |
| 1122 | + } else { |
| 1123 | + // Compare each template |
| 1124 | + for i, desired := range desiredTemplates { |
| 1125 | + current := currentTemplates[i] |
| 1126 | + desiredTemplate := desired.(map[string]interface{}) |
| 1127 | + currentTemplate := current.(map[string]interface{}) |
| 1128 | + |
| 1129 | + name := desiredTemplate["metadata"].(map[string]interface{})["name"].(string) |
| 1130 | + desiredStorage := getTemplateStorage(desiredTemplate) |
| 1131 | + currentStorage := getTemplateStorage(currentTemplate) |
| 1132 | + |
| 1133 | + if currentStorage != desiredStorage { |
| 1134 | + changes = append(changes, fmt.Sprintf(" - volumeClaimTemplates.%s:\n from: %q\n to: %q", |
| 1135 | + name, currentStorage, desiredStorage)) |
| 1136 | + } |
| 1137 | + } |
| 1138 | + } |
| 1139 | + } else { |
| 1140 | + changes = append(changes, formatFieldChange(k, currentVal, desiredVal)) |
| 1141 | + } |
| 1142 | + } |
| 1143 | + } |
| 1144 | + } |
| 1145 | + if len(changes) > 0 { |
| 1146 | + sort.Strings(changes) |
| 1147 | + 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", |
| 1148 | + strings.Join(changes, "\n")) |
| 1149 | + return common.ResultCodeSyncFailed, message |
| 1150 | + } |
| 1151 | + } |
| 1152 | + } |
1008 | 1153 | return common.ResultCodeSyncFailed, err.Error()
|
1009 | 1154 | }
|
1010 | 1155 | if kube.IsCRD(t.targetObj) && !dryRun {
|
|
0 commit comments