Skip to content

Commit 58d4d7c

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 093aef0 commit 58d4d7c

File tree

2 files changed

+444
-2
lines changed

2 files changed

+444
-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"
@@ -1146,6 +1147,85 @@ func (sc *syncContext) performClientSideApplyMigration(targetObj *unstructured.U
11461147
return nil
11471148
}
11481149

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

0 commit comments

Comments
 (0)