Skip to content

Commit 3fd05a8

Browse files
Fix: Truncate large error messages in status conditions
When upgrading operators, CRD validation errors can be very large (50KB+). Kubernetes rejects status updates over 32KB with "Too long: may not be more than 32768 bytes". This causes ClusterExtension upgrades to fail and get stuck. Added `truncateMessage()` function that cuts messages over 30KB. Applied to status condition functions that handle large errors: - `setStatusProgressing()` - handles CRD validation errors - `ensureAllConditionsWithReason()` - handles resolution errors - `setInstalledStatusConditionUnknown()` - handles bundle errors Messages keep important info at the start and add "... [message truncated]" suffix. Now upgrades complete successfully even with large CRD validation errors. Added unit tests for truncation logic and CRD error scenarios. Assisted-by: Cursor
1 parent 43caaae commit 3fd05a8

File tree

3 files changed

+121
-3
lines changed

3 files changed

+121
-3
lines changed

internal/operator-controller/controllers/clusterextension_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func ensureAllConditionsWithReason(ext *ocv1.ClusterExtension, reason v1alpha1.C
160160
Type: condType,
161161
Status: metav1.ConditionFalse,
162162
Reason: string(reason),
163-
Message: message,
163+
Message: truncateMessage(message),
164164
ObservedGeneration: ext.GetGeneration(),
165165
LastTransitionTime: metav1.NewTime(time.Now()),
166166
}

internal/operator-controller/controllers/common_controller.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ import (
2727
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2828
)
2929

30+
const (
31+
// maxConditionMessageLength is the Kubernetes limit minus some buffer for safety
32+
maxConditionMessageLength = 30000
33+
// truncationSuffix is added when messages are too long
34+
truncationSuffix = "\n\n... [message truncated]"
35+
)
36+
37+
// truncateMessage cuts long messages to fit Kubernetes condition limits
38+
func truncateMessage(message string) string {
39+
if len(message) <= maxConditionMessageLength {
40+
return message
41+
}
42+
43+
maxContent := maxConditionMessageLength - len(truncationSuffix)
44+
return message[:maxContent] + truncationSuffix
45+
}
46+
3047
// setInstalledStatusFromBundle sets the installed status based on the given installedBundle.
3148
func setInstalledStatusFromBundle(ext *ocv1.ClusterExtension, installedBundle *InstalledBundle) {
3249
// Nothing is installed
@@ -71,7 +88,7 @@ func setInstalledStatusConditionUnknown(ext *ocv1.ClusterExtension, message stri
7188
Type: ocv1.TypeInstalled,
7289
Status: metav1.ConditionUnknown,
7390
Reason: ocv1.ReasonFailed,
74-
Message: message,
91+
Message: truncateMessage(message),
7592
ObservedGeneration: ext.GetGeneration(),
7693
})
7794
}
@@ -91,7 +108,7 @@ func setStatusProgressing(ext *ocv1.ClusterExtension, err error) {
91108

92109
if err != nil {
93110
progressingCond.Reason = ocv1.ReasonRetrying
94-
progressingCond.Message = err.Error()
111+
progressingCond.Message = truncateMessage(err.Error())
95112
}
96113

97114
if errors.Is(err, reconcile.TerminalError(nil)) {

internal/operator-controller/controllers/common_controller_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package controllers
22

33
import (
44
"errors"
5+
"fmt"
6+
"strings"
57
"testing"
68

79
"github.com/google/go-cmp/cmp"
@@ -64,3 +66,102 @@ func TestSetStatusProgressing(t *testing.T) {
6466
})
6567
}
6668
}
69+
70+
func TestTruncateMessage(t *testing.T) {
71+
tests := []struct {
72+
name string
73+
message string
74+
expected string
75+
}{
76+
{
77+
name: "short message unchanged",
78+
message: "This is a short message",
79+
expected: "This is a short message",
80+
},
81+
{
82+
name: "empty message unchanged",
83+
message: "",
84+
expected: "",
85+
},
86+
{
87+
name: "exact max length message unchanged",
88+
message: strings.Repeat("a", maxConditionMessageLength),
89+
expected: strings.Repeat("a", maxConditionMessageLength),
90+
},
91+
{
92+
name: "message just over limit gets truncated",
93+
message: strings.Repeat("a", maxConditionMessageLength+1),
94+
expected: strings.Repeat("a", maxConditionMessageLength-len(truncationSuffix)) + truncationSuffix,
95+
},
96+
{
97+
name: "very long message gets truncated",
98+
message: strings.Repeat("word ", 10000) + "finalword",
99+
expected: strings.Repeat("word ", 10000)[:maxConditionMessageLength-len(truncationSuffix)] + truncationSuffix,
100+
},
101+
}
102+
103+
for _, tc := range tests {
104+
t.Run(tc.name, func(t *testing.T) {
105+
result := truncateMessage(tc.message)
106+
require.Equal(t, tc.expected, result)
107+
108+
// Verify the result is within the limit
109+
require.LessOrEqual(t, len(result), maxConditionMessageLength,
110+
"truncated message should not exceed max length")
111+
112+
// If the original message was over the limit, verify truncation occurred
113+
if len(tc.message) > maxConditionMessageLength {
114+
require.Contains(t, result, truncationSuffix,
115+
"long messages should contain truncation suffix")
116+
require.Less(t, len(result), len(tc.message),
117+
"truncated message should be shorter than original")
118+
}
119+
})
120+
}
121+
}
122+
123+
func TestSetStatusProgressingWithLongMessage(t *testing.T) {
124+
// Create a very long CRD validation error message
125+
longCRDError := fmt.Sprintf("validating upgrade for CRD \"applications.argoproj.io\": %s",
126+
strings.Repeat("v1alpha1: ^.spec.generators: unhandled changes found\n", 500))
127+
128+
ext := &ocv1.ClusterExtension{}
129+
err := errors.New(longCRDError)
130+
setStatusProgressing(ext, err)
131+
132+
cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing)
133+
require.NotNil(t, cond)
134+
require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength)
135+
require.Contains(t, cond.Message, truncationSuffix)
136+
require.Contains(t, cond.Message, "validating upgrade for CRD")
137+
}
138+
139+
func TestSetInstalledStatusConditionUnknownWithLongMessage(t *testing.T) {
140+
longError := strings.Repeat("some long error message ", 2000)
141+
142+
ext := &ocv1.ClusterExtension{}
143+
setInstalledStatusConditionUnknown(ext, longError)
144+
145+
cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeInstalled)
146+
require.NotNil(t, cond)
147+
require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength)
148+
require.Contains(t, cond.Message, truncationSuffix)
149+
}
150+
151+
func TestCRDValidationErrorScenario(t *testing.T) {
152+
// Simulate a large CRD validation error like the one in the user's issue
153+
crdError := "validating upgrade for CRD \"applications.argoproj.io\": " +
154+
strings.Repeat("v1alpha1: ^.spec.generators: unhandled changes found in JSONSchemaProps\n", 1000)
155+
156+
ext := &ocv1.ClusterExtension{ObjectMeta: metav1.ObjectMeta{Name: "test-extension"}}
157+
err := errors.New(crdError)
158+
setStatusProgressing(ext, err)
159+
160+
cond := meta.FindStatusCondition(ext.Status.Conditions, ocv1.TypeProgressing)
161+
require.NotNil(t, cond)
162+
require.LessOrEqual(t, len(cond.Message), maxConditionMessageLength)
163+
require.Contains(t, cond.Message, truncationSuffix)
164+
require.Contains(t, cond.Message, "validating upgrade for CRD")
165+
require.Equal(t, metav1.ConditionTrue, cond.Status)
166+
require.Equal(t, ocv1.ReasonRetrying, cond.Reason)
167+
}

0 commit comments

Comments
 (0)