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)
+       }
+}

Reply via email to