Skip to content

Commit c6972ed

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 54992bf commit c6972ed

File tree

2 files changed

+440
-1
lines changed

2 files changed

+440
-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"
@@ -965,6 +966,85 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
965966
return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
966967
}
967968

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+
9681048
func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
9691049
dryRunStrategy := cmdutil.DryRunNone
9701050
if dryRun {
@@ -1005,6 +1085,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
10051085
message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager, false)
10061086
}
10071087
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+
}
10081153
return common.ResultCodeSyncFailed, err.Error()
10091154
}
10101155
if kube.IsCRD(t.targetObj) && !dryRun {

0 commit comments

Comments
 (0)