This is an automated email from the ASF dual-hosted git repository.
moonming 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 9b8f7099 fix: preserve fractional seconds in upstream timeouts (#2894)
9b8f7099 is described below
commit 9b8f7099a9a1f723d175742b7e6a64d947e18bbb
Author: Efe <[email protected]>
AuthorDate: Mon Nov 9 06:40:45 2026 +0300
fix: preserve fractional seconds in upstream timeouts (#2894)
* Fix fractional upstream timeout translation
Fixes APISIX Ingress Controller issue #2879.
Signed-off-by: Efe Gökdemir <[email protected]>
* test: compare ingress timeouts as floats
Signed-off-by: Efe Gökdemir <[email protected]>
* fix: reject negative ApisixRoute timeouts
* test: cover valid and negative upstream timeouts
* fix: allow fractional backend policy timeouts
---------
Signed-off-by: Efe Gökdemir <[email protected]>
Signed-off-by: Efe Gökdemir <[email protected]>
---
api/adc/types.go | 6 +-
api/v1alpha1/backendtrafficpolicy_types.go | 6 +-
api/v2/apisixroute_types.go | 3 +
api/v2/apisixroute_types_test.go | 50 ++++++++
api/v2/backendtrafficpolicy_timeout_test.go | 47 +++++++
api/v2/crd_schema_validator_test.go | 9 +-
.../crd/bases/apisix.apache.org_apisixroutes.yaml | 9 ++
.../bases/apisix.apache.org_apisixupstreams.yaml | 18 +++
.../apisix.apache.org_backendtrafficpolicies.yaml | 12 +-
internal/adc/translator/apisixroute.go | 29 +++--
internal/adc/translator/apisixroute_test.go | 46 +++++++
internal/adc/translator/apisixupstream.go | 6 +-
internal/adc/translator/ingress.go | 6 +-
internal/adc/translator/policies.go | 7 +-
internal/adc/translator/timeouts_test.go | 138 +++++++++++++++++++++
test/e2e/ingress/annotations.go | 6 +-
16 files changed, 368 insertions(+), 30 deletions(-)
diff --git a/api/adc/types.go b/api/adc/types.go
index 667f29ec..f1d507ff 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -167,9 +167,9 @@ type Route struct {
}
type Timeout struct {
- Connect int `json:"connect"`
- Read int `json:"read"`
- Send int `json:"send"`
+ Connect float64 `json:"connect"`
+ Read float64 `json:"read"`
+ Send float64 `json:"send"`
}
// +k8s:deepcopy-gen=true
diff --git a/api/v1alpha1/backendtrafficpolicy_types.go
b/api/v1alpha1/backendtrafficpolicy_types.go
index b3dc24c3..b822d9b5 100644
--- a/api/v1alpha1/backendtrafficpolicy_types.go
+++ b/api/v1alpha1/backendtrafficpolicy_types.go
@@ -113,17 +113,17 @@ type LoadBalancer struct {
type Timeout struct {
// Connection timeout. Default is `60s`.
// +kubebuilder:default="60s"
- // +kubebuilder:validation:Pattern=`^[0-9]+s$`
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
// +kubebuilder:validation:Type=string
Connect metav1.Duration `json:"connect,omitempty"
yaml:"connect,omitempty"`
// Send timeout. Default is `60s`.
// +kubebuilder:default="60s"
- // +kubebuilder:validation:Pattern=`^[0-9]+s$`
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
// +kubebuilder:validation:Type=string
Send metav1.Duration `json:"send,omitempty" yaml:"send,omitempty"`
// Read timeout. Default is `60s`.
// +kubebuilder:default="60s"
- // +kubebuilder:validation:Pattern=`^[0-9]+s$`
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
// +kubebuilder:validation:Type=string
Read metav1.Duration `json:"read,omitempty" yaml:"read,omitempty"`
}
diff --git a/api/v2/apisixroute_types.go b/api/v2/apisixroute_types.go
index 0f6ad5a5..72a763e0 100644
--- a/api/v2/apisixroute_types.go
+++ b/api/v2/apisixroute_types.go
@@ -131,10 +131,13 @@ type ApisixRouteStream struct {
// UpstreamTimeout defines timeout settings for connecting, sending, and
reading from the upstream.
type UpstreamTimeout struct {
// Connect timeout for establishing a connection to the upstream.
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
Connect metav1.Duration `json:"connect,omitempty"
yaml:"connect,omitempty"`
// Send timeout for sending data to the upstream.
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
Send metav1.Duration `json:"send,omitempty" yaml:"send,omitempty"`
// Read timeout for reading data from the upstream.
+ // +kubebuilder:validation:XValidation:rule="duration(self) >=
duration('0s')",message="timeout must not be negative"
Read metav1.Duration `json:"read,omitempty" yaml:"read,omitempty"`
}
diff --git a/api/v2/apisixroute_types_test.go b/api/v2/apisixroute_types_test.go
index d5093477..1872e51b 100644
--- a/api/v2/apisixroute_types_test.go
+++ b/api/v2/apisixroute_types_test.go
@@ -19,9 +19,11 @@ import (
"path/filepath"
"runtime"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
apisixv2 "github.com/apache/apisix-ingress-controller/api/v2"
@@ -35,6 +37,14 @@ func loadApisixRouteSchema(t *testing.T) *crdSchemaValidator
{
return loadCRDSchema(t, crdPath)
}
+func loadApisixUpstreamSchema(t *testing.T) *crdSchemaValidator {
+ t.Helper()
+ _, thisFile, _, _ := runtime.Caller(0)
+ crdPath := filepath.Join(filepath.Dir(thisFile), "..", "..",
+ "config", "crd", "bases",
"apisix.apache.org_apisixupstreams.yaml")
+ return loadCRDSchema(t, crdPath)
+}
+
func strPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
func intPtr(i int) *int { return &i }
@@ -125,3 +135,43 @@ func TestApisixRoute_PathScope_EmptyName(t *testing.T) {
}
assert.NoError(t, v.Validate(t, ar))
}
+
+func TestApisixRoute_NegativeTimeoutRejected(t *testing.T) {
+ v := loadApisixRouteSchema(t)
+ tests := []struct {
+ name string
+ timeout apisixv2.UpstreamTimeout
+ }{
+ {name: "connect", timeout: apisixv2.UpstreamTimeout{Connect:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "read", timeout: apisixv2.UpstreamTimeout{Read:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "send", timeout: apisixv2.UpstreamTimeout{Send:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ar := &apisixv2.ApisixRoute{Spec:
apisixv2.ApisixRouteSpec{HTTP: []apisixv2.ApisixRouteHTTP{{
+ Name: "rule1", Websocket: boolPtr(false),
Match: apisixv2.ApisixRouteHTTPMatch{Paths: []string{"/*"}}, Timeout:
&tt.timeout,
+ }}}}
+ require.ErrorContains(t, v.Validate(t, ar), "timeout
must not be negative")
+ })
+ }
+}
+
+func TestApisixUpstream_NegativeTimeoutRejected(t *testing.T) {
+ v := loadApisixUpstreamSchema(t)
+ tests := []struct {
+ name string
+ timeout apisixv2.UpstreamTimeout
+ }{
+ {name: "connect", timeout: apisixv2.UpstreamTimeout{Connect:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "read", timeout: apisixv2.UpstreamTimeout{Read:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "send", timeout: apisixv2.UpstreamTimeout{Send:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ upstream := &apisixv2.ApisixUpstream{Spec:
apisixv2.ApisixUpstreamSpec{
+ ApisixUpstreamConfig:
apisixv2.ApisixUpstreamConfig{Timeout: &tt.timeout},
+ }}
+ require.ErrorContains(t, v.Validate(t, upstream),
"timeout must not be negative")
+ })
+ }
+}
diff --git a/api/v2/backendtrafficpolicy_timeout_test.go
b/api/v2/backendtrafficpolicy_timeout_test.go
new file mode 100644
index 00000000..dbe81096
--- /dev/null
+++ b/api/v2/backendtrafficpolicy_timeout_test.go
@@ -0,0 +1,47 @@
+// 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 v2_test
+
+import (
+ "path/filepath"
+ "runtime"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestBackendTrafficPolicyTimeoutSchema(t *testing.T) {
+ _, thisFile, _, _ := runtime.Caller(0)
+ schema := loadCRDSchemaVersion(t, filepath.Join(filepath.Dir(thisFile),
"..", "..",
+ "config", "crd", "bases",
"apisix.apache.org_backendtrafficpolicies.yaml"), "v1alpha1")
+ for _, field := range []string{"connect", "read", "send"} {
+ for _, value := range []string{"0s", "60s", "500ms", "1.5s",
"1m30s", "-500ms", "", "invalid", "9223372036854775808ns"} {
+ t.Run(field+"/"+value, func(t *testing.T) {
+ policy := map[string]any{"spec": map[string]any{
+ "targetRefs":
[]any{map[string]any{"group": "", "kind": "Service", "name": "backend"}},
+ "timeout": map[string]any{field:
value},
+ }}
+ err := schema.Validate(t, policy)
+ switch value {
+ case "-500ms", "", "invalid",
"9223372036854775808ns":
+ require.Error(t, err)
+ default:
+ require.NoError(t, err)
+ }
+ })
+ }
+ }
+}
diff --git a/api/v2/crd_schema_validator_test.go
b/api/v2/crd_schema_validator_test.go
index 0337b856..5204a2eb 100644
--- a/api/v2/crd_schema_validator_test.go
+++ b/api/v2/crd_schema_validator_test.go
@@ -67,6 +67,11 @@ func (v *crdSchemaValidator) Validate(t *testing.T, obj any)
error {
// loadCRDSchema reads a CRD YAML file and returns a validator for the "v2"
version schema.
func loadCRDSchema(t *testing.T, crdPath string) *crdSchemaValidator {
t.Helper()
+ return loadCRDSchemaVersion(t, crdPath, "v2")
+}
+
+func loadCRDSchemaVersion(t *testing.T, crdPath, version string)
*crdSchemaValidator {
+ t.Helper()
data, err := os.ReadFile(crdPath)
require.NoError(t, err, "failed to read CRD file: %s", crdPath)
@@ -79,12 +84,12 @@ func loadCRDSchema(t *testing.T, crdPath string)
*crdSchemaValidator {
var v1Schema *apiextensionsv1.JSONSchemaProps
for _, v := range crd.Spec.Versions {
- if v.Name == "v2" {
+ if v.Name == version {
v1Schema = v.Schema.OpenAPIV3Schema
break
}
}
- require.NotNil(t, v1Schema, "v2 schema not found in CRD")
+ require.NotNil(t, v1Schema, "%s schema not found in CRD", version)
var internal apiextensions.JSONSchemaProps
require.NoError(t,
diff --git a/config/crd/bases/apisix.apache.org_apisixroutes.yaml
b/config/crd/bases/apisix.apache.org_apisixroutes.yaml
index f692629e..4c2f89ab 100644
--- a/config/crd/bases/apisix.apache.org_apisixroutes.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixroutes.yaml
@@ -332,12 +332,21 @@ spec:
description: Connect timeout for establishing a
connection
to the upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
read:
description: Read timeout for reading data from the
upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
send:
description: Send timeout for sending data to the
upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
type: object
upstreams:
description: Upstreams references ApisixUpstream CRDs.
diff --git a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
index 8ac4c125..9e8c37a6 100644
--- a/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
+++ b/config/crd/bases/apisix.apache.org_apisixupstreams.yaml
@@ -633,12 +633,21 @@ spec:
description: Connect timeout for establishing a
connection
to the upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
read:
description: Read timeout for reading data from the
upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
send:
description: Send timeout for sending data to the
upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
type: object
tlsSecret:
description: |-
@@ -718,12 +727,21 @@ spec:
description: Connect timeout for establishing a connection
to
the upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
read:
description: Read timeout for reading data from the
upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
send:
description: Send timeout for sending data to the upstream.
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
type: object
tlsSecret:
description: |-
diff --git a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
index 3b070a78..b6be5dbe 100644
--- a/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
+++ b/config/crd/bases/apisix.apache.org_backendtrafficpolicies.yaml
@@ -365,18 +365,24 @@ spec:
connect:
default: 60s
description: Connection timeout. Default is `60s`.
- pattern: ^[0-9]+s$
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
read:
default: 60s
description: Read timeout. Default is `60s`.
- pattern: ^[0-9]+s$
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
send:
default: 60s
description: Send timeout. Default is `60s`.
- pattern: ^[0-9]+s$
type: string
+ x-kubernetes-validations:
+ - message: timeout must not be negative
+ rule: duration(self) >= duration('0s')
type: object
upstreamHost:
description: |-
diff --git a/internal/adc/translator/apisixroute.go
b/internal/adc/translator/apisixroute.go
index 5d03e8c1..56d56ffe 100644
--- a/internal/adc/translator/apisixroute.go
+++ b/internal/adc/translator/apisixroute.go
@@ -63,7 +63,10 @@ func (t *Translator) TranslateApisixRoute(tctx
*provider.TranslateContext, ar *a
}
func (t *Translator) translateHTTPRule(tctx *provider.TranslateContext, ar
*apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP, ruleIndex int) (*adc.Service,
error) {
- timeout := t.buildTimeout(rule)
+ timeout, err := t.buildTimeout(rule)
+ if err != nil {
+ return nil, err
+ }
plugins, err := t.buildPlugins(tctx, ar, rule)
if err != nil {
return nil, err
@@ -81,16 +84,28 @@ func (t *Translator) translateHTTPRule(tctx
*provider.TranslateContext, ar *apiv
return service, nil
}
-func (t *Translator) buildTimeout(rule apiv2.ApisixRouteHTTP) *adc.Timeout {
+func (t *Translator) buildTimeout(rule apiv2.ApisixRouteHTTP) (*adc.Timeout,
error) {
if rule.Timeout == nil {
- return nil
+ return nil, nil
+ }
+ for _, timeout := range []struct {
+ name string
+ duration metav1.Duration
+ }{
+ {name: "connect", duration: rule.Timeout.Connect},
+ {name: "read", duration: rule.Timeout.Read},
+ {name: "send", duration: rule.Timeout.Send},
+ } {
+ if timeout.duration.Duration < 0 {
+ return nil, fmt.Errorf("invalid value timeout.%s",
timeout.name)
+ }
}
defaultTimeout := metav1.Duration{Duration:
apiv2.DefaultUpstreamTimeout}
return &adc.Timeout{
- Connect: cmp.Or(int(rule.Timeout.Connect.Seconds()),
int(defaultTimeout.Seconds())),
- Read: cmp.Or(int(rule.Timeout.Read.Seconds()),
int(defaultTimeout.Seconds())),
- Send: cmp.Or(int(rule.Timeout.Send.Seconds()),
int(defaultTimeout.Seconds())),
- }
+ Connect: cmp.Or(rule.Timeout.Connect.Seconds(),
defaultTimeout.Seconds()),
+ Read: cmp.Or(rule.Timeout.Read.Seconds(),
defaultTimeout.Seconds()),
+ Send: cmp.Or(rule.Timeout.Send.Seconds(),
defaultTimeout.Seconds()),
+ }, nil
}
func (t *Translator) buildPlugins(tctx *provider.TranslateContext, ar
*apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP) (adc.Plugins, error) {
diff --git a/internal/adc/translator/apisixroute_test.go
b/internal/adc/translator/apisixroute_test.go
index 431e2d5b..8d646c89 100644
--- a/internal/adc/translator/apisixroute_test.go
+++ b/internal/adc/translator/apisixroute_test.go
@@ -20,6 +20,7 @@ package translator
import (
"context"
"testing"
+ "time"
"github.com/go-logr/logr"
"github.com/stretchr/testify/assert"
@@ -252,3 +253,48 @@ func TestTranslateApisixRouteStreamUpstreamScheme(t
*testing.T) {
})
}
}
+
+func TestTranslateApisixRouteRejectsNegativeTimeouts(t *testing.T) {
+ tests := []struct {
+ name string
+ timeout apiv2.UpstreamTimeout
+ }{
+ {
+ name: "connect",
+ timeout: apiv2.UpstreamTimeout{
+ Connect: metav1.Duration{Duration: -500 *
time.Millisecond},
+ },
+ },
+ {
+ name: "read",
+ timeout: apiv2.UpstreamTimeout{
+ Read: metav1.Duration{Duration: -500 *
time.Millisecond},
+ },
+ },
+ {
+ name: "send",
+ timeout: apiv2.UpstreamTimeout{
+ Send: metav1.Duration{Duration: -500 *
time.Millisecond},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ar := &apiv2.ApisixRoute{
+ Spec: apiv2.ApisixRouteSpec{
+ HTTP: []apiv2.ApisixRouteHTTP{{
+ Name: "rule1",
+ Timeout: &tt.timeout,
+ }},
+ },
+ }
+
+ _, err := NewTranslator(logr.Discard(),
"").TranslateApisixRoute(
+
provider.NewDefaultTranslateContext(context.Background()), ar,
+ )
+
+ require.ErrorContains(t, err, "invalid value
timeout."+tt.name)
+ })
+ }
+}
diff --git a/internal/adc/translator/apisixupstream.go
b/internal/adc/translator/apisixupstream.go
index 53b1b006..a552f1c7 100644
--- a/internal/adc/translator/apisixupstream.go
+++ b/internal/adc/translator/apisixupstream.go
@@ -164,9 +164,9 @@ func translateApisixUpstreamRetriesAndTimeout(config
*apiv2.ApisixUpstreamConfig
sendTimeout := cmp.Or(timeout.Send.Duration,
apiv2.DefaultUpstreamTimeout)
ups.Timeout = &adc.Timeout{
- Connect: int(connTimeout.Seconds()),
- Read: int(readTimeout.Seconds()),
- Send: int(sendTimeout.Seconds()),
+ Connect: connTimeout.Seconds(),
+ Read: readTimeout.Seconds(),
+ Send: sendTimeout.Seconds(),
}
return nil
diff --git a/internal/adc/translator/ingress.go
b/internal/adc/translator/ingress.go
index f3e121fe..0c0d90e8 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -207,9 +207,9 @@ func (t *Translator) resolveIngressUpstream(
}
if upConfig.TimeoutConnect > 0 || upConfig.TimeoutRead > 0 ||
upConfig.TimeoutSend > 0 {
upstream.Timeout = &adctypes.Timeout{
- Connect: cmp.Or(upConfig.TimeoutConnect, 60),
- Read: cmp.Or(upConfig.TimeoutRead, 60),
- Send: cmp.Or(upConfig.TimeoutSend, 60),
+ Connect:
float64(cmp.Or(upConfig.TimeoutConnect, 60)),
+ Read: float64(cmp.Or(upConfig.TimeoutRead,
60)),
+ Send: float64(cmp.Or(upConfig.TimeoutSend,
60)),
}
}
}
diff --git a/internal/adc/translator/policies.go
b/internal/adc/translator/policies.go
index f60bba21..d850d7d0 100644
--- a/internal/adc/translator/policies.go
+++ b/internal/adc/translator/policies.go
@@ -18,6 +18,7 @@
package translator
import (
+ "cmp"
"fmt"
corev1 "k8s.io/api/core/v1"
@@ -123,9 +124,9 @@ func (t *Translator)
attachBackendTrafficPolicyToUpstream(policy *v1alpha1.Backe
}
if policy.Spec.Timeout != nil {
upstream.Timeout = &adctypes.Timeout{
- Connect: int(policy.Spec.Timeout.Connect.Seconds()),
- Read: int(policy.Spec.Timeout.Read.Seconds()),
- Send: int(policy.Spec.Timeout.Send.Seconds()),
+ Connect: cmp.Or(policy.Spec.Timeout.Connect.Duration,
apiv2.DefaultUpstreamTimeout).Seconds(),
+ Read: cmp.Or(policy.Spec.Timeout.Read.Duration,
apiv2.DefaultUpstreamTimeout).Seconds(),
+ Send: cmp.Or(policy.Spec.Timeout.Send.Duration,
apiv2.DefaultUpstreamTimeout).Seconds(),
}
}
if policy.Spec.LoadBalancer != nil {
diff --git a/internal/adc/translator/timeouts_test.go
b/internal/adc/translator/timeouts_test.go
new file mode 100644
index 00000000..6479f10a
--- /dev/null
+++ b/internal/adc/translator/timeouts_test.go
@@ -0,0 +1,138 @@
+// 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 translator
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/apache/apisix-ingress-controller/api/adc"
+ "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+ apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+)
+
+func TestTranslateApisixUpstreamRetriesAndTimeoutPreservesSubsecondValues(t
*testing.T) {
+ config := &apiv2.ApisixUpstreamConfig{
+ Timeout: &apiv2.UpstreamTimeout{
+ Connect: metav1.Duration{Duration: 500 *
time.Millisecond},
+ Read: metav1.Duration{Duration: 1500 *
time.Millisecond},
+ Send: metav1.Duration{Duration: 2500 *
time.Millisecond},
+ },
+ }
+ upstream := adc.NewDefaultUpstream()
+
+ err := translateApisixUpstreamRetriesAndTimeout(config, upstream)
+
+ require.NoError(t, err)
+ assert.Equal(t, 0.5, upstream.Timeout.Connect)
+ assert.Equal(t, 1.5, upstream.Timeout.Read)
+ assert.Equal(t, 2.5, upstream.Timeout.Send)
+}
+
+func TestTranslateApisixUpstreamRetriesAndTimeoutRejectsNegativeValues(t
*testing.T) {
+ tests := []struct {
+ name string
+ timeout apiv2.UpstreamTimeout
+ }{
+ {name: "connect", timeout: apiv2.UpstreamTimeout{Connect:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "read", timeout: apiv2.UpstreamTimeout{Read:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ {name: "send", timeout: apiv2.UpstreamTimeout{Send:
metav1.Duration{Duration: -500 * time.Millisecond}}},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ config := &apiv2.ApisixUpstreamConfig{Timeout:
&tt.timeout}
+ upstream := adc.NewDefaultUpstream()
+
+ err := translateApisixUpstreamRetriesAndTimeout(config,
upstream)
+
+ require.ErrorContains(t, err, "invalid value
timeout."+tt.name)
+ })
+ }
+}
+
+func TestBuildTimeoutPreservesSubsecondValues(t *testing.T) {
+ rule := apiv2.ApisixRouteHTTP{
+ Timeout: &apiv2.UpstreamTimeout{
+ Connect: metav1.Duration{Duration: 500 *
time.Millisecond},
+ Read: metav1.Duration{Duration: 1500 *
time.Millisecond},
+ Send: metav1.Duration{Duration: 2500 *
time.Millisecond},
+ },
+ }
+
+ timeout, err := (&Translator{}).buildTimeout(rule)
+
+ require.NoError(t, err)
+ assert.Equal(t, 0.5, timeout.Connect)
+ assert.Equal(t, 1.5, timeout.Read)
+ assert.Equal(t, 2.5, timeout.Send)
+}
+
+func TestBuildTimeoutDefaultsZeroValuesAndPreservesWholeSeconds(t *testing.T) {
+ tests := []struct {
+ name string
+ timeout apiv2.UpstreamTimeout
+ want adc.Timeout
+ }{
+ {
+ name: "zero values use the default",
+ want: adc.Timeout{Connect: 60, Read: 60, Send: 60},
+ },
+ {
+ name: "whole-second values are preserved",
+ timeout: apiv2.UpstreamTimeout{
+ Connect: metav1.Duration{Duration: time.Second},
+ Read: metav1.Duration{Duration: 2 *
time.Second},
+ Send: metav1.Duration{Duration: 3 *
time.Second},
+ },
+ want: adc.Timeout{Connect: 1, Read: 2, Send: 3},
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ rule := apiv2.ApisixRouteHTTP{Timeout: &tt.timeout}
+
+ got, err := (&Translator{}).buildTimeout(rule)
+
+ require.NoError(t, err)
+ assert.Equal(t, tt.want, *got)
+ })
+ }
+}
+
+func TestAttachBackendTrafficPolicyToUpstreamPreservesSubsecondValues(t
*testing.T) {
+ policy := &v1alpha1.BackendTrafficPolicy{
+ Spec: v1alpha1.BackendTrafficPolicySpec{
+ Timeout: &v1alpha1.Timeout{
+ Connect: metav1.Duration{},
+ Read: metav1.Duration{Duration: 1500 *
time.Millisecond},
+ Send: metav1.Duration{Duration: 2500 *
time.Millisecond},
+ },
+ },
+ }
+ upstream := &adc.Upstream{}
+
+ (&Translator{}).attachBackendTrafficPolicyToUpstream(policy, upstream)
+
+ assert.Equal(t, 60.0, upstream.Timeout.Connect)
+ assert.Equal(t, 1.5, upstream.Timeout.Read)
+ assert.Equal(t, 2.5, upstream.Timeout.Send)
+}
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 2f813c88..5dc0e3ec 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -210,9 +210,9 @@ spec:
Expect(err).NotTo(HaveOccurred(), "listing Upstream")
Expect(upstreams).To(HaveLen(1), "checking Upstream
length")
Expect(upstreams[0].Timeout).ToNot(BeNil(), "checking
Upstream timeout")
- Expect(upstreams[0].Timeout.Read).To(Equal(2),
"checking Upstream read timeout")
- Expect(upstreams[0].Timeout.Send).To(Equal(3),
"checking Upstream send timeout")
- Expect(upstreams[0].Timeout.Connect).To(Equal(4),
"checking Upstream connect timeout")
+ Expect(upstreams[0].Timeout.Read).To(Equal(2.0),
"checking Upstream read timeout")
+ Expect(upstreams[0].Timeout.Send).To(Equal(3.0),
"checking Upstream send timeout")
+ Expect(upstreams[0].Timeout.Connect).To(Equal(4.0),
"checking Upstream connect timeout")
})
It("cors annotations", func() {