Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit 317527d

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 97ad5b5 commit 317527d

File tree

2 files changed

+445
-2
lines changed

2 files changed

+445
-2
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"
@@ -1155,6 +1156,85 @@ func (sc *syncContext) performClientSideApplyMigration(targetObj *unstructured.U
11551156
return nil
11561157
}
11571158

1159+
func formatValue(v interface{}) string {
1160+
if v == nil {
1161+
return "<nil>"
1162+
}
1163+
1164+
// Special case for volumeClaimTemplates
1165+
if templates, ok := v.([]interface{}); ok {
1166+
// For a single volumeClaimTemplate field change
1167+
if len(templates) == 1 {
1168+
if template, ok := templates[0].(map[string]interface{}); ok {
1169+
if storage := getTemplateStorage(template); storage != "" {
1170+
return fmt.Sprintf("%q", storage)
1171+
}
1172+
}
1173+
}
1174+
// For multiple templates or other array types format
1175+
var names []string
1176+
for _, t := range templates {
1177+
if template, ok := t.(map[string]interface{}); ok {
1178+
if metadata, ok := template["metadata"].(map[string]interface{}); ok {
1179+
if name, ok := metadata["name"].(string); ok {
1180+
if storage := getTemplateStorage(template); storage != "" {
1181+
names = append(names, fmt.Sprintf("%s(%s)", name, storage))
1182+
continue
1183+
}
1184+
names = append(names, name)
1185+
}
1186+
}
1187+
}
1188+
}
1189+
return fmt.Sprintf("[%s]", strings.Join(names, ", "))
1190+
}
1191+
1192+
// Special case for selector matchLabels
1193+
if m, ok := v.(map[string]interface{}); ok {
1194+
if matchLabels, exists := m["matchLabels"].(map[string]interface{}); exists {
1195+
var labels []string
1196+
for k, v := range matchLabels {
1197+
labels = append(labels, fmt.Sprintf("%s:%s", k, v))
1198+
}
1199+
sort.Strings(labels)
1200+
return fmt.Sprintf("{%s}", strings.Join(labels, ", "))
1201+
}
1202+
}
1203+
// Add quotes for string values
1204+
if str, ok := v.(string); ok {
1205+
return fmt.Sprintf("%q", str)
1206+
}
1207+
// For other types, use standard formatting
1208+
return fmt.Sprintf("%v", v)
1209+
}
1210+
1211+
// Get storage size from template
1212+
func getTemplateStorage(template map[string]interface{}) string {
1213+
spec, ok := template["spec"].(map[string]interface{})
1214+
if !ok {
1215+
return ""
1216+
}
1217+
resources, ok := spec["resources"].(map[string]interface{})
1218+
if !ok {
1219+
return ""
1220+
}
1221+
requests, ok := resources["requests"].(map[string]interface{})
1222+
if !ok {
1223+
return ""
1224+
}
1225+
storage, ok := requests["storage"].(string)
1226+
if !ok {
1227+
return ""
1228+
}
1229+
return storage
1230+
}
1231+
1232+
// Format field changes for error messages
1233+
func formatFieldChange(field string, currentVal, desiredVal interface{}) string {
1234+
return fmt.Sprintf(" - %s:\n from: %s\n to: %s",
1235+
field, formatValue(currentVal), formatValue(desiredVal))
1236+
}
1237+
11581238
func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
11591239
dryRunStrategy := cmdutil.DryRunNone
11601240
if dryRun {
@@ -1206,6 +1286,71 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
12061286
message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager)
12071287
}
12081288
if err != nil {
1289+
// Check if this is a StatefulSet immutable field error
1290+
if strings.Contains(err.Error(), "updates to statefulset spec for fields other than") {
1291+
current := t.liveObj
1292+
desired := t.targetObj
1293+
1294+
if current != nil && desired != nil {
1295+
currentSpec, _, _ := unstructured.NestedMap(current.Object, "spec")
1296+
desiredSpec, _, _ := unstructured.NestedMap(desired.Object, "spec")
1297+
1298+
mutableFields := map[string]bool{
1299+
"replicas": true,
1300+
"ordinals": true,
1301+
"template": true,
1302+
"updateStrategy": true,
1303+
"persistentVolumeClaimRetentionPolicy": true,
1304+
"minReadySeconds": true,
1305+
}
1306+
1307+
var changes []string
1308+
for k, desiredVal := range desiredSpec {
1309+
if !mutableFields[k] {
1310+
currentVal, exists := currentSpec[k]
1311+
if !exists {
1312+
changes = append(changes, formatFieldChange(k, nil, desiredVal))
1313+
} else if !reflect.DeepEqual(currentVal, desiredVal) {
1314+
if k == "volumeClaimTemplates" {
1315+
// Handle volumeClaimTemplates specially
1316+
currentTemplates := currentVal.([]interface{})
1317+
desiredTemplates := desiredVal.([]interface{})
1318+
1319+
// If template count differs or we're adding/removing templates,
1320+
// use the standard array format
1321+
if len(currentTemplates) != len(desiredTemplates) {
1322+
changes = append(changes, formatFieldChange(k, currentVal, desiredVal))
1323+
} else {
1324+
// Compare each template
1325+
for i, desired := range desiredTemplates {
1326+
current := currentTemplates[i]
1327+
desiredTemplate := desired.(map[string]interface{})
1328+
currentTemplate := current.(map[string]interface{})
1329+
1330+
name := desiredTemplate["metadata"].(map[string]interface{})["name"].(string)
1331+
desiredStorage := getTemplateStorage(desiredTemplate)
1332+
currentStorage := getTemplateStorage(currentTemplate)
1333+
1334+
if currentStorage != desiredStorage {
1335+
changes = append(changes, fmt.Sprintf(" - volumeClaimTemplates.%s:\n from: %q\n to: %q",
1336+
name, currentStorage, desiredStorage))
1337+
}
1338+
}
1339+
}
1340+
} else {
1341+
changes = append(changes, formatFieldChange(k, currentVal, desiredVal))
1342+
}
1343+
}
1344+
}
1345+
}
1346+
if len(changes) > 0 {
1347+
sort.Strings(changes)
1348+
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",
1349+
strings.Join(changes, "\n"))
1350+
return common.ResultCodeSyncFailed, message
1351+
}
1352+
}
1353+
}
12091354
return common.ResultCodeSyncFailed, err.Error()
12101355
}
12111356
if kubeutil.IsCRD(t.targetObj) && !dryRun {

0 commit comments

Comments
 (0)