diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 35981ebaa..fc32c85d4 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "reflect" "sort" "strings" "sync" @@ -965,6 +966,46 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply) } +//=============================================================================================================== + +func FormatStatefulSetError(err error, targetObj *unstructured.Unstructured, liveObj *unstructured.Unstructured) error { + if err == nil { + return nil + } + + errMsg := err.Error() + if !strings.Contains(errMsg, "updates to statefulset spec for fields other than") { + return err + } + + // Get the specs to compare + targetSpec, _, _ := unstructured.NestedMap(targetObj.Object, "spec") + liveSpec, _, _ := unstructured.NestedMap(liveObj.Object, "spec") + if targetSpec == nil || liveSpec == nil { + return err + } + + // Known immutable StatefulSet fields + immutableFields := []string{"serviceName", "podManagementPolicy", "volumeClaimTemplates", "selector"} + + // Find which field changed + for _, field := range immutableFields { + targetVal, targetExists := targetSpec[field] + liveVal, liveExists := liveSpec[field] + + if targetExists && liveExists && !reflect.DeepEqual(targetVal, liveVal) { + // Return exact Kubernetes error with field information appended + return fmt.Errorf("The StatefulSet \"%s\" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden (field: spec.%s was modified)", + targetObj.GetName(), + field) + } + } + + return err +} + +//=============================================================================================================== + func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) { dryRunStrategy := cmdutil.DryRunNone if dryRun { @@ -1005,6 +1046,9 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager, false) } if err != nil { + if t.targetObj != nil && t.targetObj.GetKind() == "StatefulSet" && t.liveObj != nil { + err = FormatStatefulSetError(err, t.targetObj, t.liveObj) + } return common.ResultCodeSyncFailed, err.Error() } if kube.IsCRD(t.targetObj) && !dryRun { diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index 7e416d20b..89fe33457 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -2039,3 +2039,68 @@ func TestWaitForCleanUpBeforeNextWave(t *testing.T) { assert.Equal(t, synccommon.ResultCodePruned, result[2].Status) } + +func TestFormatStatefulSetError(t *testing.T) { + tests := []struct { + name string + err error + liveObj *unstructured.Unstructured + targetObj *unstructured.Unstructured + wantErr string + }{ + { + name: "service name change", + err: fmt.Errorf("The StatefulSet \"test-sts\" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden"), + liveObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": map[string]interface{}{ + "name": "test-sts", + }, + "spec": map[string]interface{}{ + "serviceName": "old-service", + }, + }, + }, + targetObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "metadata": map[string]interface{}{ + "name": "test-sts", + }, + "spec": map[string]interface{}{ + "serviceName": "new-service", + }, + }, + }, + wantErr: "The StatefulSet \"test-sts\" is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden (field: spec.serviceName was modified)", + }, + { + name: "non-statefulset error", + err: fmt.Errorf("some other error"), + liveObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + }, + }, + targetObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + }, + }, + wantErr: "some other error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := FormatStatefulSetError(tt.err, tt.targetObj, tt.liveObj) + if err.Error() != tt.wantErr { + t.Errorf("FormatStatefulSetError() error = %v, want %v", err, tt.wantErr) + } + }) + } +}