Skip to content

Commit fe57c75

Browse files
authored
fix: use not strictly translation when remote ApisixRoute (#544)
* fix: use not strictly translation when remote ApisixRoute * fix: lint & comments * add comments * fix: add upstreams to ctx when translation * use upstraemId instead of upstream in tcproute * fix: check upstream exist * test: add e2e test * CI: [Temporary changes]use APISIX:2.6 instead of APISIX:dev * use Apisix:dev image & use intstring type for counter * fix: counter * fix: use json.Unmarshal instead * fix: the count in cache should be modify too * recover test case * fix: style * fix: count compare * fix: style * fix: remove ginkgo focus test case
1 parent b4e2e2a commit fe57c75

8 files changed

Lines changed: 255 additions & 14 deletions

File tree

pkg/apisix/resource.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"encoding/json"
1919
"errors"
2020
"fmt"
21+
"strconv"
2122
"strings"
2223

2324
"github.com/apache/apisix-ingress-controller/pkg/log"
@@ -30,8 +31,23 @@ type getResponse struct {
3031

3132
// listResponse is the unified LIST response mapping of APISIX.
3233
type listResponse struct {
33-
Count string `json:"count"`
34-
Node node `json:"node"`
34+
Count IntOrString `json:"count"`
35+
Node node `json:"node"`
36+
}
37+
38+
// IntOrString processing number and string types, after json deserialization will output int
39+
type IntOrString struct {
40+
IntValue int `json:"int_value"`
41+
}
42+
43+
func (ios *IntOrString) UnmarshalJSON(p []byte) error {
44+
result := strings.Trim(string(p), "\"")
45+
count, err := strconv.Atoi(result)
46+
if err != nil {
47+
return err
48+
}
49+
ios.IntValue = count
50+
return nil
3551
}
3652

3753
type createResponse struct {

pkg/ingress/apisix_route.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,14 @@ func (c *apisixRouteController) sync(ctx context.Context, ev *types.Event) error
139139
return err
140140
}
141141
} else {
142-
tctx, err = c.controller.translator.TranslateRouteV2alpha1(ar.V2alpha1())
142+
if ev.Type != types.EventDelete {
143+
tctx, err = c.controller.translator.TranslateRouteV2alpha1(ar.V2alpha1())
144+
} else {
145+
// Use TranslateRouteV2alpha1NotStrictly in EventDelete.
146+
// if K8S service has been removed before ApisixRoute resource, the translation about nodes
147+
// of upstream will be failed.
148+
tctx, err = c.controller.translator.TranslateRouteV2alpha1NotStrictly(ar.V2alpha1())
149+
}
143150
if err != nil {
144151
log.Errorw("failed to translate ApisixRoute v2alpha1",
145152
zap.Error(err),

pkg/kube/translation/apisix_route.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,21 @@ func (t *translator) TranslateRouteV1(ar *configv1.ApisixRoute) (*TranslateConte
7878
return ctx, nil
7979
}
8080

81+
// TranslateRouteV2alpha1NotStrictly translates route v2alpha1 with a loose way, only generate ID and Name for delete Event.
82+
func (t *translator) TranslateRouteV2alpha1NotStrictly(ar *configv2alpha1.ApisixRoute) (*TranslateContext, error) {
83+
ctx := &TranslateContext{
84+
upstreamMap: make(map[string]struct{}),
85+
}
86+
87+
if err := t.translateHTTPRouteNotStrictly(ctx, ar); err != nil {
88+
return nil, err
89+
}
90+
if err := t.translateTCPRouteNotStrictly(ctx, ar); err != nil {
91+
return nil, err
92+
}
93+
return ctx, nil
94+
}
95+
8196
func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) (*TranslateContext, error) {
8297
ctx := &TranslateContext{
8398
upstreamMap: make(map[string]struct{}),
@@ -92,6 +107,32 @@ func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) (*Tr
92107
return ctx, nil
93108
}
94109

110+
// translateHTTPRouteNotStrictly translates http route with a loose way, only generate ID and Name for delete Event.
111+
func (t *translator) translateHTTPRouteNotStrictly(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute) error {
112+
for _, part := range ar.Spec.HTTP {
113+
backends := part.Backends
114+
backend := part.Backend
115+
if len(backends) > 0 {
116+
// Use the first backend as the default backend in Route,
117+
// others will be configured in traffic-split plugin.
118+
backend = backends[0]
119+
} // else use the deprecated Backend.
120+
upstreamName := apisixv1.ComposeUpstreamName(ar.Namespace, backend.ServiceName, backend.Subset, backend.ServicePort.IntVal)
121+
route := apisixv1.NewDefaultRoute()
122+
route.Name = apisixv1.ComposeRouteName(ar.Namespace, ar.Name, part.Name)
123+
route.ID = id.GenID(route.Name)
124+
ctx.addRoute(route)
125+
if !ctx.checkUpstreamExist(upstreamName) {
126+
ups, err := t.translateUpstreamNotStrictly(ar.Namespace, backend.ServiceName, backend.Subset, backend.ServicePort.IntVal)
127+
if err != nil {
128+
return err
129+
}
130+
ctx.addUpstream(ups)
131+
}
132+
}
133+
return nil
134+
}
135+
95136
func (t *translator) translateHTTPRoute(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute) error {
96137
ruleNameMap := make(map[string]struct{})
97138
for _, part := range ar.Spec.HTTP {
@@ -304,6 +345,27 @@ func (t *translator) translateRouteMatchExprs(nginxVars []configv2alpha1.ApisixR
304345
return vars, nil
305346
}
306347

348+
// translateTCPRouteNotStrictly translates tcp route with a loose way, only generate ID and Name for delete Event.
349+
func (t *translator) translateTCPRouteNotStrictly(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute) error {
350+
for _, part := range ar.Spec.TCP {
351+
backend := &part.Backend
352+
sr := apisixv1.NewDefaultStreamRoute()
353+
name := apisixv1.ComposeStreamRouteName(ar.Namespace, ar.Name, part.Name)
354+
sr.ID = id.GenID(name)
355+
sr.ServerPort = part.Match.IngressPort
356+
ups, err := t.translateUpstreamNotStrictly(ar.Namespace, backend.ServiceName, backend.Subset, backend.ServicePort.IntVal)
357+
if err != nil {
358+
return err
359+
}
360+
sr.UpstreamId = ups.ID
361+
ctx.addStreamRoute(sr)
362+
if !ctx.checkUpstreamExist(ups.Name) {
363+
ctx.addUpstream(ups)
364+
}
365+
}
366+
return nil
367+
}
368+
307369
func (t *translator) translateTCPRoute(ctx *TranslateContext, ar *configv2alpha1.ApisixRoute) error {
308370
ruleNameMap := make(map[string]struct{})
309371
for _, part := range ar.Spec.TCP {
@@ -329,8 +391,12 @@ func (t *translator) translateTCPRoute(ctx *TranslateContext, ar *configv2alpha1
329391
if err != nil {
330392
return err
331393
}
332-
sr.Upstream = ups
394+
sr.UpstreamId = ups.ID
333395
ctx.addStreamRoute(sr)
396+
if !ctx.checkUpstreamExist(ups.Name) {
397+
ctx.addUpstream(ups)
398+
}
399+
334400
}
335401
return nil
336402
}

pkg/kube/translation/apisix_route_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package translation
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"testing"
2021

2122
"github.com/stretchr/testify/assert"
@@ -26,6 +27,7 @@ import (
2627
"k8s.io/client-go/kubernetes/fake"
2728
"k8s.io/client-go/tools/cache"
2829

30+
"github.com/apache/apisix-ingress-controller/pkg/id"
2931
configv2alpha1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v2alpha1"
3032
fakeapisix "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/clientset/versioned/fake"
3133
apisixinformers "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/client/informers/externalversions"
@@ -320,3 +322,63 @@ func TestTranslateApisixRouteV2alpha1WithDuplicatedName(t *testing.T) {
320322
_, err = tr.TranslateRouteV2alpha1(ar)
321323
assert.Equal(t, err.Error(), "duplicated route rule name")
322324
}
325+
326+
func TestTranslateApisixRouteV2alpha1NotStrictly(t *testing.T) {
327+
tr := &translator{
328+
&TranslatorOptions{},
329+
}
330+
ar := &configv2alpha1.ApisixRoute{
331+
ObjectMeta: metav1.ObjectMeta{
332+
Name: "ar",
333+
Namespace: "test",
334+
},
335+
Spec: &configv2alpha1.ApisixRouteSpec{
336+
HTTP: []*configv2alpha1.ApisixRouteHTTP{
337+
{
338+
Name: "rule1",
339+
Match: &configv2alpha1.ApisixRouteHTTPMatch{
340+
Paths: []string{
341+
"/*",
342+
},
343+
},
344+
Backend: &configv2alpha1.ApisixRouteHTTPBackend{
345+
ServiceName: "svc1",
346+
ServicePort: intstr.IntOrString{
347+
IntVal: 81,
348+
},
349+
},
350+
},
351+
{
352+
Name: "rule2",
353+
Match: &configv2alpha1.ApisixRouteHTTPMatch{
354+
Paths: []string{
355+
"/*",
356+
},
357+
},
358+
Backend: &configv2alpha1.ApisixRouteHTTPBackend{
359+
ServiceName: "svc2",
360+
ServicePort: intstr.IntOrString{
361+
IntVal: 82,
362+
},
363+
},
364+
},
365+
},
366+
},
367+
}
368+
369+
tx, err := tr.TranslateRouteV2alpha1NotStrictly(ar)
370+
fmt.Println(tx)
371+
assert.NoError(t, err, "translateRoute not strictly should be no error")
372+
assert.Equal(t, len(tx.Routes), 2, "There should be 2 routes")
373+
assert.Equal(t, len(tx.Upstreams), 2, "There should be 2 upstreams")
374+
assert.Equal(t, tx.Routes[0].Name, "test_ar_rule1", "route1 name error")
375+
assert.Equal(t, tx.Routes[1].Name, "test_ar_rule2", "route2 name error")
376+
assert.Equal(t, tx.Upstreams[0].Name, "test_svc1_81", "upstream1 name error")
377+
assert.Equal(t, tx.Upstreams[1].Name, "test_svc2_82", "upstream2 name error")
378+
379+
assert.Equal(t, tx.Routes[0].ID, id.GenID("test_ar_rule1"), "route1 id error")
380+
assert.Equal(t, tx.Routes[1].ID, id.GenID("test_ar_rule2"), "route2 id error")
381+
assert.Equal(t, tx.Upstreams[0].ID, id.GenID("test_svc1_81"), "upstream1 id error")
382+
assert.Equal(t, tx.Upstreams[1].ID, id.GenID("test_svc2_82"), "upstream2 id error")
383+
384+
}

pkg/kube/translation/translator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ type Translator interface {
7070
// TranslateRouteV2alpha1 translates the configv2alpha1.ApisixRoute object into several Route
7171
// and Upstream resources.
7272
TranslateRouteV2alpha1(*configv2alpha1.ApisixRoute) (*TranslateContext, error)
73+
// TranslateRouteV2alpha1NotStrictly translates the configv2alpha1.ApisixRoute object into several Route
74+
// and Upstream resources not strictly, only used for delete event.
75+
TranslateRouteV2alpha1NotStrictly(*configv2alpha1.ApisixRoute) (*TranslateContext, error)
7376
// TranslateSSL translates the configv2alpha1.ApisixTls object into the APISIX SSL resource.
7477
TranslateSSL(*configv1.ApisixTls) (*apisixv1.Ssl, error)
7578
// TranslateClusterConfig translates the configv2alpha1.ApisixClusterConfig object into the APISIX

pkg/kube/translation/util.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ loop:
110110
return svc.Spec.ClusterIP, svcPort, nil
111111
}
112112

113+
// translateUpstreamNotStrictly translates Upstream nodes with a loose way, only generate ID and Name for delete Event.
114+
func (t *translator) translateUpstreamNotStrictly(namespace, svcName, subset string, svcPort int32) (*apisixv1.Upstream, error) {
115+
ups := &apisixv1.Upstream{}
116+
ups.Name = apisixv1.ComposeUpstreamName(namespace, svcName, subset, svcPort)
117+
ups.ID = id.GenID(ups.Name)
118+
return ups, nil
119+
}
120+
113121
func (t *translator) translateUpstream(namespace, svcName, subset, svcResolveGranularity, svcClusterIP string, svcPort int32) (*apisixv1.Upstream, error) {
114122
ups, err := t.TranslateUpstream(namespace, svcName, subset, svcPort)
115123
if err != nil {

test/e2e/ingress/resourcepushing.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,86 @@ spec:
150150
assert.Contains(ginkgo.GinkgoT(), body, "404 Route Not Found")
151151
})
152152

153+
ginkgo.It("create, update, remove k8s service, remove ApisixRoute", func() {
154+
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
155+
apisixRoute := fmt.Sprintf(`
156+
apiVersion: apisix.apache.org/v2alpha1
157+
kind: ApisixRoute
158+
metadata:
159+
name: httpbin-route
160+
spec:
161+
http:
162+
- name: rule1
163+
match:
164+
hosts:
165+
- httpbin.com
166+
paths:
167+
- /ip
168+
backend:
169+
serviceName: %s
170+
servicePort: %d
171+
`, backendSvc, backendSvcPort[0])
172+
173+
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute), "creating ApisixRoute")
174+
err := s.EnsureNumApisixRoutesCreated(1)
175+
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of routes")
176+
err = s.EnsureNumApisixUpstreamsCreated(1)
177+
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of upstreams")
178+
179+
s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.com").Expect().Status(http.StatusOK)
180+
181+
// update
182+
apisixRoute = fmt.Sprintf(`
183+
apiVersion: apisix.apache.org/v2alpha1
184+
kind: ApisixRoute
185+
metadata:
186+
name: httpbin-route
187+
spec:
188+
http:
189+
- name: rule1
190+
match:
191+
hosts:
192+
- httpbin.com
193+
paths:
194+
- /ip
195+
exprs:
196+
- subject:
197+
scope: Header
198+
name: X-Foo
199+
op: Equal
200+
value: "barbaz"
201+
backend:
202+
serviceName: %s
203+
servicePort: %d
204+
`, backendSvc, backendSvcPort[0])
205+
206+
assert.Nil(ginkgo.GinkgoT(), s.CreateResourceFromString(apisixRoute))
207+
// TODO When ingress controller can feedback the lifecycle of CRDs to the
208+
// status field, we can poll it rather than sleeping.
209+
time.Sleep(10 * time.Second)
210+
211+
err = s.EnsureNumApisixRoutesCreated(1)
212+
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of routes")
213+
err = s.EnsureNumApisixUpstreamsCreated(1)
214+
assert.Nil(ginkgo.GinkgoT(), err, "Checking number of upstreams")
215+
216+
s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.com").Expect().Status(http.StatusNotFound)
217+
s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.com").WithHeader("X-Foo", "barbaz").Expect().Status(http.StatusOK)
218+
// remove k8s service first
219+
s.DeleteHTTPBINService()
220+
// remove
221+
assert.Nil(ginkgo.GinkgoT(), s.RemoveResourceByString(apisixRoute))
222+
// TODO When ingress controller can feedback the lifecycle of CRDs to the
223+
// status field, we can poll it rather than sleeping.
224+
time.Sleep(10 * time.Second)
225+
ups, err := s.ListApisixUpstreams()
226+
assert.Nil(ginkgo.GinkgoT(), err, "list upstreams error")
227+
assert.Len(ginkgo.GinkgoT(), ups, 0, "upstreams nodes not expect")
228+
229+
body := s.NewAPISIXClient().GET("/ip").WithHeader("Host", "httpbin.com").Expect().Status(http.StatusNotFound).Body().Raw()
230+
assert.Contains(ginkgo.GinkgoT(), body, "404 Route Not Found")
231+
})
232+
153233
ginkgo.It("change route rule name", func() {
154234
backendSvc, backendSvcPort := s.DefaultHTTPBackend()
155235
apisixRoute := fmt.Sprintf(`

test/e2e/scaffold/k8s.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ package scaffold
1717
import (
1818
"context"
1919
"encoding/json"
20+
"io/ioutil"
2021
"net/http"
2122
"net/url"
22-
"strconv"
2323
"time"
2424

2525
"github.com/apache/apisix-ingress-controller/pkg/apisix"
@@ -33,7 +33,7 @@ import (
3333
)
3434

3535
type counter struct {
36-
Count string `json:"count"`
36+
Count apisix.IntOrString `json:"count"`
3737
}
3838

3939
// ApisixRoute is the ApisixRoute CRD definition.
@@ -150,19 +150,18 @@ func (s *Scaffold) ensureNumApisixCRDsCreated(url string, desired int) error {
150150
ginkgo.GinkgoT().Logf("got status code %d from APISIX", resp.StatusCode)
151151
return false, nil
152152
}
153-
var c counter
154-
dec := json.NewDecoder(resp.Body)
155-
if err := dec.Decode(&c); err != nil {
153+
c := &counter{}
154+
b, err := ioutil.ReadAll(resp.Body)
155+
if err != nil {
156156
return false, err
157157
}
158-
// NOTE count field is a string.
159-
count, err := strconv.Atoi(c.Count)
158+
err = json.Unmarshal(b, c)
160159
if err != nil {
161160
return false, err
162161
}
163-
// 1 for dir.
164-
if count != desired+1 {
165-
ginkgo.GinkgoT().Logf("mismatched number of items, expected %d but found %d", desired, count-1)
162+
count := c.Count.IntValue
163+
if count != desired {
164+
ginkgo.GinkgoT().Logf("mismatched number of items, expected %d but found %d", desired, count)
166165
return false, nil
167166
}
168167
return true, nil

0 commit comments

Comments
 (0)