Skip to content

Commit 2dc7ae6

Browse files
authored
feat: support upstream scheme/retries/timeouts via ingress annotations (#2614)
1 parent 16f4328 commit 2dc7ae6

File tree

8 files changed

+504
-21
lines changed

8 files changed

+504
-21
lines changed

internal/adc/translator/annotations.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,24 @@ import (
2222
"github.com/imdario/mergo"
2323

2424
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
2526
)
2627

2728
// Structure extracted by Ingress Resource
28-
type Ingress struct{}
29+
type IngressConfig struct {
30+
Upstream upstream.Upstream
31+
}
2932

3033
// parsers registered for ingress annotations
31-
var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{}
34+
var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
35+
"upstream": upstream.NewParser(),
36+
}
3237

33-
func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *Ingress {
34-
ing := &Ingress{}
38+
func (t *Translator) TranslateIngressAnnotations(anno map[string]string) *IngressConfig {
39+
if len(anno) == 0 {
40+
return nil
41+
}
42+
ing := &IngressConfig{}
3543
if err := translateAnnotations(anno, ing); err != nil {
3644
t.Log.Error(err, "failed to translate ingress annotations", "annotations", anno)
3745
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package upstream
17+
18+
import (
19+
"fmt"
20+
"strconv"
21+
"strings"
22+
23+
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
24+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
)
26+
27+
func NewParser() annotations.IngressAnnotationsParser {
28+
return &Upstream{}
29+
}
30+
31+
type Upstream struct {
32+
Scheme string
33+
Retries int
34+
TimeoutRead int
35+
TimeoutConnect int
36+
TimeoutSend int
37+
}
38+
39+
var validSchemes = map[string]struct{}{
40+
apiv2.SchemeHTTP: {},
41+
apiv2.SchemeHTTPS: {},
42+
apiv2.SchemeGRPC: {},
43+
apiv2.SchemeGRPCS: {},
44+
}
45+
46+
func (u Upstream) Parse(e annotations.Extractor) (any, error) {
47+
if scheme := strings.ToLower(e.GetStringAnnotation(annotations.AnnotationsUpstreamScheme)); scheme != "" {
48+
if _, ok := validSchemes[scheme]; ok {
49+
u.Scheme = scheme
50+
} else {
51+
return nil, fmt.Errorf("invalid upstream scheme: %s", scheme)
52+
}
53+
}
54+
55+
if retry := e.GetStringAnnotation(annotations.AnnotationsUpstreamRetry); retry != "" {
56+
t, err := strconv.Atoi(retry)
57+
if err != nil {
58+
return nil, fmt.Errorf("could not parse retry as an integer: %s", err.Error())
59+
}
60+
u.Retries = t
61+
}
62+
63+
if timeoutConnect := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutConnect), "s"); timeoutConnect != "" {
64+
t, err := strconv.Atoi(timeoutConnect)
65+
if err != nil {
66+
return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
67+
}
68+
u.TimeoutConnect = t
69+
}
70+
71+
if timeoutRead := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutRead), "s"); timeoutRead != "" {
72+
t, err := strconv.Atoi(timeoutRead)
73+
if err != nil {
74+
return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
75+
}
76+
u.TimeoutRead = t
77+
}
78+
79+
if timeoutSend := strings.TrimSuffix(e.GetStringAnnotation(annotations.AnnotationsUpstreamTimeoutSend), "s"); timeoutSend != "" {
80+
t, err := strconv.Atoi(timeoutSend)
81+
if err != nil {
82+
return nil, fmt.Errorf("could not parse timeout as an integer: %s", err.Error())
83+
}
84+
u.TimeoutSend = t
85+
}
86+
87+
return u, nil
88+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one or more
2+
// contributor license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright ownership.
4+
// The ASF licenses this file to You under the Apache License, Version 2.0
5+
// (the "License"); you may not use this file except in compliance with
6+
// the License. You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package upstream
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
23+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
24+
)
25+
26+
func TestIPRestrictionHandler(t *testing.T) {
27+
anno := map[string]string{
28+
annotations.AnnotationsUpstreamScheme: "grpcs",
29+
}
30+
u := NewParser()
31+
32+
out, err := u.Parse(annotations.NewExtractor(anno))
33+
assert.Nil(t, err, "checking given error")
34+
35+
ups, ok := out.(Upstream)
36+
if !ok {
37+
t.Fatalf("could not parse upstream")
38+
}
39+
assert.Equal(t, "grpcs", ups.Scheme)
40+
41+
anno[annotations.AnnotationsUpstreamScheme] = "gRPC"
42+
out, err = u.Parse(annotations.NewExtractor(anno))
43+
ups, ok = out.(Upstream)
44+
if !ok {
45+
t.Fatalf("could not parse upstream")
46+
}
47+
assert.Nil(t, err, "checking given error")
48+
assert.Equal(t, "grpc", ups.Scheme)
49+
50+
anno[annotations.AnnotationsUpstreamScheme] = "nothing"
51+
out, err = u.Parse(annotations.NewExtractor(anno))
52+
assert.NotNil(t, err, "checking given error")
53+
assert.Nil(t, out, "checking given output")
54+
}
55+
56+
func TestRetryParsing(t *testing.T) {
57+
anno := map[string]string{
58+
annotations.AnnotationsUpstreamRetry: "2",
59+
}
60+
u := NewParser()
61+
out, err := u.Parse(annotations.NewExtractor(anno))
62+
assert.Nil(t, err, "checking given error")
63+
ups, ok := out.(Upstream)
64+
if !ok {
65+
t.Fatalf("could not parse upstream")
66+
}
67+
assert.Nil(t, err, "checking given error")
68+
assert.Equal(t, 2, ups.Retries)
69+
70+
anno[annotations.AnnotationsUpstreamRetry] = "asdf"
71+
out, err = u.Parse(annotations.NewExtractor(anno))
72+
assert.NotNil(t, err, "checking given error")
73+
assert.Nil(t, out, "checking given output")
74+
}
75+
76+
func TestTimeoutParsing(t *testing.T) {
77+
anno := map[string]string{
78+
annotations.AnnotationsUpstreamTimeoutConnect: "2s",
79+
annotations.AnnotationsUpstreamTimeoutRead: "3s",
80+
annotations.AnnotationsUpstreamTimeoutSend: "4s",
81+
}
82+
u := NewParser()
83+
out, err := u.Parse(annotations.NewExtractor(anno))
84+
assert.Nil(t, err, "checking given error")
85+
86+
ups, ok := out.(Upstream)
87+
if !ok {
88+
t.Fatalf("could not parse upstream")
89+
}
90+
assert.Nil(t, err, "checking given error")
91+
assert.Equal(t, 2, ups.TimeoutConnect)
92+
assert.Equal(t, 3, ups.TimeoutRead)
93+
assert.Equal(t, 4, ups.TimeoutSend)
94+
anno[annotations.AnnotationsUpstreamRetry] = "asdf"
95+
out, err = u.Parse(annotations.NewExtractor(anno))
96+
assert.NotNil(t, err, "checking given error")
97+
assert.Nil(t, out, "checking given output")
98+
}

internal/adc/translator/annotations_test.go

Lines changed: 92 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/stretchr/testify/assert"
2323

2424
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
25+
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
2526
)
2627

2728
type mockParser struct {
@@ -63,7 +64,10 @@ func TestTranslateAnnotations(t *testing.T) {
6364

6465
for _, tt := range tests {
6566
t.Run(tt.name, func(t *testing.T) {
66-
// Set up mock parsers
67+
orig := ingressAnnotationParsers
68+
defer func() { ingressAnnotationParsers = orig }()
69+
70+
ingressAnnotationParsers = make(map[string]annotations.IngressAnnotationsParser)
6771
for key, parser := range tt.parsers {
6872
ingressAnnotationParsers[key] = parser
6973
}
@@ -77,11 +81,94 @@ func TestTranslateAnnotations(t *testing.T) {
7781
assert.NoError(t, err)
7882
}
7983
assert.Equal(t, tt.expected, dst)
84+
})
85+
}
86+
}
8087

81-
// Clean up mock parsers
82-
for key := range tt.parsers {
83-
delete(ingressAnnotationParsers, key)
84-
}
88+
func TestTranslateIngressAnnotations(t *testing.T) {
89+
tests := []struct {
90+
name string
91+
anno map[string]string
92+
expected *IngressConfig
93+
}{
94+
{
95+
name: "no matching annotations",
96+
anno: map[string]string{"upstream": "value1"},
97+
expected: &IngressConfig{},
98+
},
99+
{
100+
name: "invalid scheme",
101+
anno: map[string]string{annotations.AnnotationsUpstreamScheme: "invalid"},
102+
expected: &IngressConfig{},
103+
},
104+
{
105+
name: "http scheme",
106+
anno: map[string]string{annotations.AnnotationsUpstreamScheme: "https"},
107+
expected: &IngressConfig{
108+
Upstream: upstream.Upstream{
109+
Scheme: "https",
110+
},
111+
},
112+
},
113+
{
114+
name: "retries",
115+
anno: map[string]string{annotations.AnnotationsUpstreamRetry: "3"},
116+
expected: &IngressConfig{
117+
Upstream: upstream.Upstream{
118+
Retries: 3,
119+
},
120+
},
121+
},
122+
{
123+
name: "read timeout",
124+
anno: map[string]string{
125+
annotations.AnnotationsUpstreamTimeoutRead: "5s",
126+
},
127+
expected: &IngressConfig{
128+
Upstream: upstream.Upstream{
129+
TimeoutRead: 5,
130+
},
131+
},
132+
},
133+
{
134+
name: "timeouts",
135+
anno: map[string]string{
136+
annotations.AnnotationsUpstreamTimeoutRead: "5s",
137+
annotations.AnnotationsUpstreamTimeoutSend: "6s",
138+
annotations.AnnotationsUpstreamTimeoutConnect: "7s",
139+
},
140+
expected: &IngressConfig{
141+
Upstream: upstream.Upstream{
142+
TimeoutRead: 5,
143+
TimeoutSend: 6,
144+
TimeoutConnect: 7,
145+
},
146+
},
147+
},
148+
{
149+
name: "timeout/scheme/retries",
150+
anno: map[string]string{
151+
annotations.AnnotationsUpstreamTimeoutRead: "5s",
152+
annotations.AnnotationsUpstreamScheme: "http",
153+
annotations.AnnotationsUpstreamRetry: "2",
154+
},
155+
expected: &IngressConfig{
156+
Upstream: upstream.Upstream{
157+
TimeoutRead: 5,
158+
Scheme: "http",
159+
Retries: 2,
160+
},
161+
},
162+
},
163+
}
164+
165+
for _, tt := range tests {
166+
t.Run(tt.name, func(t *testing.T) {
167+
translator := &Translator{}
168+
result := translator.TranslateIngressAnnotations(tt.anno)
169+
170+
assert.NotNil(t, result)
171+
assert.Equal(t, tt.expected, result)
85172
})
86173
}
87174
}

0 commit comments

Comments
 (0)