Skip to content

Commit 5403648

Browse files
committed
This is a combination of 8 commits.
feat: enhance StatefulSet immutable field error message to show specific field changes Signed-off-by: Atif Ali <[email protected]> formatting && added tests Signed-off-by: Atif Ali <[email protected]>
1 parent 65db274 commit 5403648

File tree

2 files changed

+444
-1
lines changed

2 files changed

+444
-1
lines changed

pkg/sync/sync_context.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"reflect"
78
"sort"
89
"strings"
910
"sync"
@@ -1030,6 +1031,85 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
10301031
return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
10311032
}
10321033

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+
10331113
func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
10341114
dryRunStrategy := cmdutil.DryRunNone
10351115
if dryRun {
@@ -1070,6 +1150,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
10701150
message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager)
10711151
}
10721152
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+
}
10731218
return common.ResultCodeSyncFailed, err.Error()
10741219
}
10751220
if kubeutil.IsCRD(t.targetObj) && !dryRun {

0 commit comments

Comments
 (0)