This is an automated email from the ASF dual-hosted git repository.
AlinsRan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 08f8508c fix: cap condition message to Kubernetes 32768-byte limit
(#2816)
08f8508c is described below
commit 08f8508c0638e315874bfaa6c77be16691bd0d84
Author: AlinsRan <[email protected]>
AuthorDate: Thu Jul 23 11:57:51 2026 +0800
fix: cap condition message to Kubernetes 32768-byte limit (#2816)
---
internal/controller/status.go | 7 +--
internal/controller/utils/utils.go | 38 +++++++++++++++-
internal/controller/utils/utils_test.go | 80 +++++++++++++++++++++++++++++++++
3 files changed, 121 insertions(+), 4 deletions(-)
diff --git a/internal/controller/status.go b/internal/controller/status.go
index 7281836b..29cf971a 100644
--- a/internal/controller/status.go
+++ b/internal/controller/status.go
@@ -24,6 +24,7 @@ import (
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
"github.com/apache/apisix-ingress-controller/internal/controller/status"
+ cutils
"github.com/apache/apisix-ingress-controller/internal/controller/utils"
"github.com/apache/apisix-ingress-controller/internal/provider"
)
@@ -47,7 +48,7 @@ func NewCondition(observedGeneration int64, status bool,
message string) metav1.
Type: ConditionTypeAvailable,
Reason: reason,
Status: condition,
- Message: message,
+ Message: cutils.TruncateConditionMessage(message),
ObservedGeneration: observedGeneration,
}
}
@@ -79,7 +80,7 @@ func NewPolicyCondition(observedGeneration int64, status
bool, message string) m
Type:
string(gatewayv1alpha2.PolicyConditionAccepted),
Reason: reason,
Status: conditionStatus,
- Message: message,
+ Message: cutils.TruncateConditionMessage(message),
ObservedGeneration: observedGeneration,
LastTransitionTime: metav1.Now(),
}
@@ -90,7 +91,7 @@ func NewPolicyConflictCondition(observedGeneration int64,
message string) metav1
Type:
string(gatewayv1alpha2.PolicyConditionAccepted),
Reason:
string(gatewayv1alpha2.PolicyReasonConflicted),
Status: metav1.ConditionFalse,
- Message: message,
+ Message: cutils.TruncateConditionMessage(message),
ObservedGeneration: observedGeneration,
LastTransitionTime: metav1.Now(),
}
diff --git a/internal/controller/utils/utils.go
b/internal/controller/utils/utils.go
index a22babc1..b1ac1bf1 100644
--- a/internal/controller/utils/utils.go
+++ b/internal/controller/utils/utils.go
@@ -18,12 +18,48 @@
package utils
import (
+ "unicode/utf8"
+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
"github.com/apache/apisix-ingress-controller/internal/utils"
)
+const (
+ // maxConditionMessageBytes is the hard limit Kubernetes enforces on a
+ // status condition Message (status.conditions[].message). Updates whose
+ // message exceeds this are rejected with "Too long: may not be more
than
+ // 32768 bytes".
+ maxConditionMessageBytes = 32768
+
+ // conditionMessageTruncationMarker is appended to a message that had
to be
+ // truncated to fit within maxConditionMessageBytes.
+ conditionMessageTruncationMarker = " ... (truncated)"
+)
+
+// TruncateConditionMessage caps a condition Message so a status update is
never
+// rejected for exceeding Kubernetes' 32768-byte limit. Truncation is
rune-safe:
+// it trims back to a UTF-8 rune boundary (never splitting a multi-byte rune)
and
+// appends conditionMessageTruncationMarker to signal that content was dropped.
+func TruncateConditionMessage(msg string) string {
+ if len(msg) <= maxConditionMessageBytes {
+ return msg
+ }
+
+ budget := maxConditionMessageBytes -
len(conditionMessageTruncationMarker)
+ truncated := msg[:budget]
+ // Back off any partial trailing rune left by the byte-wise cut.
+ for len(truncated) > 0 {
+ if r, size := utf8.DecodeLastRuneInString(truncated); r ==
utf8.RuneError && size <= 1 {
+ truncated = truncated[:len(truncated)-1]
+ continue
+ }
+ break
+ }
+ return truncated + conditionMessageTruncationMarker
+}
+
func SetApisixCRDConditionWithGeneration(status *apiv2.ApisixStatus,
generation int64, condition metav1.Condition) {
condition.ObservedGeneration = generation
SetApisixCRDCondition(status, condition)
@@ -51,7 +87,7 @@ func NewConditionTypeAccepted(reason
apiv2.ApisixRouteConditionReason, status bo
ObservedGeneration: generation,
LastTransitionTime: metav1.Now(),
Reason: string(reason),
- Message: msg,
+ Message: TruncateConditionMessage(msg),
}
return condition
}
diff --git a/internal/controller/utils/utils_test.go
b/internal/controller/utils/utils_test.go
new file mode 100644
index 00000000..02af4f7a
--- /dev/null
+++ b/internal/controller/utils/utils_test.go
@@ -0,0 +1,80 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package utils
+
+import (
+ "strings"
+ "testing"
+ "unicode/utf8"
+)
+
+func TestTruncateConditionMessage(t *testing.T) {
+ t.Run("message under limit is unchanged", func(t *testing.T) {
+ msg := strings.Repeat("a", maxConditionMessageBytes)
+ if got := TruncateConditionMessage(msg); got != msg {
+ t.Fatalf("expected message to be unchanged, got
len=%d", len(got))
+ }
+ })
+
+ t.Run("ascii message over limit is capped and marked", func(t
*testing.T) {
+ msg := strings.Repeat("a", maxConditionMessageBytes+100)
+ got := TruncateConditionMessage(msg)
+ if len(got) > maxConditionMessageBytes {
+ t.Fatalf("truncated message len=%d exceeds limit %d",
len(got), maxConditionMessageBytes)
+ }
+ if !strings.HasSuffix(got, conditionMessageTruncationMarker) {
+ t.Fatalf("truncated message does not end with marker:
%q", got[len(got)-len(conditionMessageTruncationMarker):])
+ }
+ if !utf8.ValidString(got) {
+ t.Fatalf("truncated message is not valid UTF-8")
+ }
+ })
+
+ // Build a message from 3-byte runes so the byte budget lands in the
middle
+ // of a rune, proving the cut backs off to a rune boundary.
+ t.Run("multi-byte rune straddling the boundary is rune-safe", func(t
*testing.T) {
+ const rune3 = "δΈ" // 3
bytes
+ msg := strings.Repeat(rune3, maxConditionMessageBytes) // ~3x
over the limit
+ got := TruncateConditionMessage(msg)
+
+ if len(got) > maxConditionMessageBytes {
+ t.Fatalf("truncated message len=%d exceeds limit %d",
len(got), maxConditionMessageBytes)
+ }
+ if !utf8.ValidString(got) {
+ t.Fatalf("truncated message is not valid UTF-8 (a rune
was split)")
+ }
+ if !strings.HasSuffix(got, conditionMessageTruncationMarker) {
+ t.Fatalf("truncated message does not end with marker")
+ }
+ // The content before the marker must consist only of whole
3-byte runes.
+ content := strings.TrimSuffix(got,
conditionMessageTruncationMarker)
+ if strings.Trim(content, rune3) != "" {
+ t.Fatalf("truncated content contains a partial rune")
+ }
+ })
+}
+
+// TestNewConditionTypeAcceptedTruncates ensures the constructor routes its
+// Message through the cap so no status update can exceed the Kubernetes limit.
+func TestNewConditionTypeAcceptedTruncates(t *testing.T) {
+ huge := strings.Repeat("x", maxConditionMessageBytes*2)
+ cond := NewConditionTypeAccepted("SyncFailed", false, 1, huge)
+ if len(cond.Message) > maxConditionMessageBytes {
+ t.Fatalf("condition message len=%d exceeds limit %d",
len(cond.Message), maxConditionMessageBytes)
+ }
+}