Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions pkg/conditions/conditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ package conditions

import (
"time"
"unicode/utf8"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// maxConditionMessageLength is the Kubernetes-enforced maximum byte length for
// a condition message field.
const maxConditionMessageLength = 32768

// Getter is an interface that allows getting conditions from a (sub)resource.
type Getter interface {
// GetConditions returns the conditions on the resource.
Expand Down Expand Up @@ -61,6 +66,11 @@ func Set(on Setter, conditions ...*metav1.Condition) {
continue
}

// Kubernetes enforces a 32768-byte limit on condition messages. Truncate
// to prevent status patch failures when upstream sources (e.g. ArgoCD)
// embed large payloads in error messages.
condition.Message = truncateMessage(condition.Message)

// Set ObservedGeneration if applicable
if objGeneration != 0 && condition.ObservedGeneration == 0 {
condition.ObservedGeneration = objGeneration
Expand Down Expand Up @@ -122,3 +132,19 @@ func Delete(on Setter, conditionType string) {
func Equal(a, b metav1.Condition) bool {
return a.Type == b.Type && a.Status == b.Status && a.Reason == b.Reason && a.Message == b.Message
}

// truncateMessage truncates msg to maxConditionMessageLength bytes, appending a
// suffix to indicate truncation. The truncation point is adjusted to avoid
// splitting a multi-byte UTF-8 character.
func truncateMessage(msg string) string {
if len(msg) <= maxConditionMessageLength {
return msg
}
const suffix = " ... (truncated)"
end := maxConditionMessageLength - len(suffix)
// Walk back to a valid UTF-8 rune boundary.
for end > 0 && !utf8.RuneStart(msg[end]) {
end--
}
return msg[:end] + suffix
}
64 changes: 64 additions & 0 deletions pkg/conditions/conditions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,23 @@ func TestSet(t *testing.T) {
}
}

func TestSet_TruncatesLongMessage(t *testing.T) {
t.Parallel()

longMsg := string(make([]byte, maxConditionMessageLength+100))
setter := &mockSetter{}
Set(setter, &metav1.Condition{
Type: "Test",
Status: metav1.ConditionTrue,
Reason: "Reason",
Message: longMsg,
})

conds := setter.GetConditions()
require.Len(t, conds, 1)
require.LessOrEqual(t, len(conds[0].Message), maxConditionMessageLength)
}

func TestDelete(t *testing.T) {
const (
mockType1 = "MockType1"
Expand Down Expand Up @@ -733,6 +750,53 @@ func TestEqual(t *testing.T) {
}
}

func TestTruncateMessage(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
input string
assert func(*testing.T, string)
}{
{
name: "short message unchanged",
input: "short",
assert: func(t *testing.T, got string) {
require.Equal(t, "short", got)
},
},
{
name: "exactly at limit unchanged",
input: string(make([]byte, maxConditionMessageLength)),
assert: func(t *testing.T, got string) {
require.Equal(t, maxConditionMessageLength, len(got))
},
},
{
name: "over limit gets truncated",
input: string(make([]byte, maxConditionMessageLength+1)),
assert: func(t *testing.T, got string) {
require.LessOrEqual(t, len(got), maxConditionMessageLength)
require.Contains(t, got, "(truncated)")
},
},
{
name: "multi-byte UTF-8 not split",
input: string(make([]byte, maxConditionMessageLength-1)) + "é", // 2-byte rune crosses boundary
Comment thread
EronWright marked this conversation as resolved.
Outdated
assert: func(t *testing.T, got string) {
require.LessOrEqual(t, len(got), maxConditionMessageLength)
require.Contains(t, got, "(truncated)")
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
tc.assert(t, truncateMessage(tc.input))
})
}
}

type deepCopyable interface {
DeepCopy() Setter
}
Expand Down
Loading