Skip to content

Commit 70176cd

Browse files
authored
support sort httproute when use gateway api (alibaba#622)
1 parent 7b1f538 commit 70176cd

File tree

2 files changed

+380
-3
lines changed

2 files changed

+380
-3
lines changed
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
diff -Naur istio/pilot/pkg/config/kube/gateway/conversion.go istio-new/pilot/pkg/config/kube/gateway/conversion.go
2+
--- istio/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 17:18:56.000000000 +0800
3+
+++ istio-new/pilot/pkg/config/kube/gateway/conversion.go 2023-11-03 17:14:50.000000000 +0800
4+
@@ -151,15 +151,113 @@
5+
}
6+
}
7+
8+
+ // for gateway routes, build one VS per gateway+host
9+
+ gatewayRoutes := make(map[string]map[string]*config.Config)
10+
+
11+
for _, obj := range r.HTTPRoute {
12+
- if vsConfig := buildHTTPVirtualServices(obj, gatewayMap, r.Domain); vsConfig != nil {
13+
+ buildHTTPVirtualServices(r, obj, gatewayMap, gatewayRoutes, r.Domain)
14+
+ }
15+
+ for _, vsByHost := range gatewayRoutes {
16+
+ for _, vsConfig := range vsByHost {
17+
result = append(result, *vsConfig)
18+
}
19+
}
20+
return result
21+
}
22+
23+
-func buildHTTPVirtualServices(obj config.Config, gateways map[parentKey]map[gatewayapiV1beta1.SectionName]*parentInfo, domain string) *config.Config {
24+
+// getURIRank ranks a URI match type. Exact > Prefix > Regex
25+
+func getURIRank(match *istio.HTTPMatchRequest) int {
26+
+ if match.Uri == nil {
27+
+ return -1
28+
+ }
29+
+ switch match.Uri.MatchType.(type) {
30+
+ case *istio.StringMatch_Exact:
31+
+ return 3
32+
+ case *istio.StringMatch_Prefix:
33+
+ return 2
34+
+ case *istio.StringMatch_Regex:
35+
+ // TODO optimize in new verison envoy
36+
+ if strings.HasSuffix(match.Uri.GetRegex(), prefixMatchRegex) &&
37+
+ !strings.ContainsAny(strings.TrimSuffix(match.Uri.GetRegex(), prefixMatchRegex), `\.+*?()|[]{}^$`) {
38+
+ return 2
39+
+ }
40+
+ return 1
41+
+ }
42+
+ // should not happen
43+
+ return -1
44+
+}
45+
+
46+
+func getURILength(match *istio.HTTPMatchRequest) int {
47+
+ if match.Uri == nil {
48+
+ return 0
49+
+ }
50+
+ switch match.Uri.MatchType.(type) {
51+
+ case *istio.StringMatch_Prefix:
52+
+ return len(match.Uri.GetPrefix())
53+
+ case *istio.StringMatch_Exact:
54+
+ return len(match.Uri.GetExact())
55+
+ case *istio.StringMatch_Regex:
56+
+ return len(match.Uri.GetRegex())
57+
+ }
58+
+ // should not happen
59+
+ return -1
60+
+}
61+
+
62+
+// sortHTTPRoutes sorts generated vs routes to meet gateway-api requirements
63+
+// see https://gateway-api.sigs.k8s.io/v1alpha2/references/spec/#gateway.networking.k8s.io/v1alpha2.HTTPRouteRule
64+
+func sortHTTPRoutes(routes []*istio.HTTPRoute) {
65+
+ sort.SliceStable(routes, func(i, j int) bool {
66+
+ if len(routes[i].Match) == 0 {
67+
+ return false
68+
+ } else if len(routes[j].Match) == 0 {
69+
+ return true
70+
+ }
71+
+ // Only look at match[0], we always generate only one match
72+
+ m1, m2 := routes[i].Match[0], routes[j].Match[0]
73+
+ r1, r2 := getURIRank(m1), getURIRank(m2)
74+
+ len1, len2 := getURILength(m1), getURILength(m2)
75+
+ switch {
76+
+ // 1: Exact/Prefix/Regex
77+
+ case r1 != r2:
78+
+ return r1 > r2
79+
+ case len1 != len2:
80+
+ return len1 > len2
81+
+ // 2: method math
82+
+ case (m1.Method == nil) != (m2.Method == nil):
83+
+ return m1.Method != nil
84+
+ // 3: number of header matches
85+
+ case len(m1.Headers) != len(m2.Headers):
86+
+ return len(m1.Headers) > len(m2.Headers)
87+
+ // 4: number of query matches
88+
+ default:
89+
+ return len(m1.QueryParams) > len(m2.QueryParams)
90+
+ }
91+
+ })
92+
+}
93+
+
94+
+func routeMeta(obj config.Config) map[string]string {
95+
+ m := parentMeta(obj, nil)
96+
+ m[constants.InternalRouteSemantics] = constants.RouteSemanticsGateway
97+
+ return m
98+
+}
99+
+
100+
+func filteredReferences(parents []routeParentReference) []routeParentReference {
101+
+ ret := make([]routeParentReference, 0, len(parents))
102+
+ for _, p := range parents {
103+
+ if p.DeniedReason != nil {
104+
+ // We should filter this out
105+
+ continue
106+
+ }
107+
+ ret = append(ret, p)
108+
+ }
109+
+ // To ensure deterministic order, sort them
110+
+ sort.Slice(ret, func(i, j int) bool {
111+
+ return ret[i].InternalName < ret[j].InternalName
112+
+ })
113+
+ return ret
114+
+}
115+
+
116+
+func buildHTTPVirtualServices(ctx *KubernetesResources, obj config.Config, gateways map[parentKey]map[gatewayapiV1beta1.SectionName]*parentInfo, gatewayRoutes map[string]map[string]*config.Config, domain string) {
117+
route := obj.Spec.(*gatewayapiV1beta1.HTTPRouteSpec)
118+
119+
parentRefs := extractParentReferenceInfo(gateways, route.ParentRefs, route.Hostnames, gvk.HTTPRoute, obj.Namespace)
120+
@@ -172,10 +270,7 @@
121+
})
122+
}
123+
124+
- name := fmt.Sprintf("%s-%s", obj.Name, constants.KubernetesGatewayName)
125+
-
126+
httproutes := []*istio.HTTPRoute{}
127+
- hosts := hostnameToStringList(route.Hostnames)
128+
for _, r := range route.Rules {
129+
// TODO: implement rewrite, timeout, mirror, corspolicy, retries
130+
vs := &istio.HTTPRoute{
131+
@@ -185,22 +280,22 @@
132+
uri, err := createURIMatch(match)
133+
if err != nil {
134+
reportError(err)
135+
- return nil
136+
+ return
137+
}
138+
headers, err := createHeadersMatch(match)
139+
if err != nil {
140+
reportError(err)
141+
- return nil
142+
+ return
143+
}
144+
qp, err := createQueryParamsMatch(match)
145+
if err != nil {
146+
reportError(err)
147+
- return nil
148+
+ return
149+
}
150+
method, err := createMethodMatch(match)
151+
if err != nil {
152+
reportError(err)
153+
- return nil
154+
+ return
155+
}
156+
vs.Match = append(vs.Match, &istio.HTTPMatchRequest{
157+
Uri: uri,
158+
@@ -219,7 +314,7 @@
159+
mirror, err := createMirrorFilter(filter.RequestMirror, obj.Namespace, domain)
160+
if err != nil {
161+
reportError(err)
162+
- return nil
163+
+ return
164+
}
165+
vs.Mirror = mirror
166+
default:
167+
@@ -227,7 +322,7 @@
168+
Reason: InvalidFilter,
169+
Message: fmt.Sprintf("unsupported filter type %q", filter.Type),
170+
})
171+
- return nil
172+
+ return
173+
}
174+
}
175+
176+
@@ -255,33 +350,65 @@
177+
route, err := buildHTTPDestination(r.BackendRefs, obj.Namespace, domain, zero, fallbackCluster)
178+
if err != nil {
179+
reportError(err)
180+
- return nil
181+
+ return
182+
}
183+
vs.Route = route
184+
185+
httproutes = append(httproutes, vs)
186+
}
187+
reportError(nil)
188+
- gatewayNames := referencesToInternalNames(parentRefs)
189+
- if len(gatewayNames) == 0 {
190+
- return nil
191+
+
192+
+ count := 0
193+
+ for _, parent := range filteredReferences(parentRefs) {
194+
+ // for gateway routes, build one VS per gateway+host
195+
+ routeMap := gatewayRoutes
196+
+ routeKey := parent.InternalName
197+
+ vsHosts := hostnameToStringList(route.Hostnames)
198+
+ routes := httproutes
199+
+ if len(routes) == 0 {
200+
+ continue
201+
+ }
202+
+ if _, f := routeMap[routeKey]; !f {
203+
+ routeMap[routeKey] = make(map[string]*config.Config)
204+
+ }
205+
+
206+
+ // Create one VS per hostname with a single hostname.
207+
+ // This ensures we can treat each hostname independently, as the spec requires
208+
+ for _, h := range vsHosts {
209+
+ if cfg := routeMap[routeKey][h]; cfg != nil {
210+
+ // merge http routes
211+
+ vs := cfg.Spec.(*istio.VirtualService)
212+
+ vs.Http = append(vs.Http, routes...)
213+
+ // append parents
214+
+ cfg.Annotations[constants.InternalParentNames] = fmt.Sprintf("%s,%s/%s.%s",
215+
+ cfg.Annotations[constants.InternalParentNames], obj.GroupVersionKind.Kind, obj.Name, obj.Namespace)
216+
+ } else {
217+
+ name := fmt.Sprintf("%s-%d-%s", obj.Name, count, constants.KubernetesGatewayName)
218+
+ routeMap[routeKey][h] = &config.Config{
219+
+ Meta: config.Meta{
220+
+ CreationTimestamp: obj.CreationTimestamp,
221+
+ GroupVersionKind: gvk.VirtualService,
222+
+ Name: name,
223+
+ Annotations: routeMeta(obj),
224+
+ Namespace: obj.Namespace,
225+
+ Domain: ctx.Domain,
226+
+ },
227+
+ Spec: &istio.VirtualService{
228+
+ Hosts: []string{h},
229+
+ Gateways: []string{parent.InternalName},
230+
+ Http: routes,
231+
+ },
232+
+ }
233+
+ count++
234+
+ }
235+
+ }
236+
}
237+
- vsConfig := config.Config{
238+
- Meta: config.Meta{
239+
- CreationTimestamp: obj.CreationTimestamp,
240+
- GroupVersionKind: gvk.VirtualService,
241+
- Name: name,
242+
- Annotations: parentMeta(obj, nil),
243+
- Namespace: obj.Namespace,
244+
- Domain: domain,
245+
- },
246+
- Spec: &istio.VirtualService{
247+
- Hosts: hosts,
248+
- Gateways: gatewayNames,
249+
- Http: httproutes,
250+
- },
251+
+ for _, vsByHost := range gatewayRoutes {
252+
+ for _, cfg := range vsByHost {
253+
+ vs := cfg.Spec.(*istio.VirtualService)
254+
+ sortHTTPRoutes(vs.Http)
255+
+ }
256+
}
257+
- return &vsConfig
258+
}
259+
260+
func parentMeta(obj config.Config, sectionName *gatewayapiV1beta1.SectionName) map[string]string {
261+
@@ -1155,9 +1282,11 @@
262+
}
263+
gs.Addresses = make([]gatewayapiV1beta1.GatewayAddress, 0, len(addressesToReport))
264+
for _, addr := range addressesToReport {
265+
+ addrPairs := strings.Split(addr, ":")
266+
gs.Addresses = append(gs.Addresses, gatewayapiV1beta1.GatewayAddress{
267+
- Type: &addrType,
268+
- Value: addr,
269+
+ Type: &addrType,
270+
+ // strip the port
271+
+ Value: addrPairs[0],
272+
})
273+
}
274+
return gs
275+
diff -Naur istio/pilot/pkg/model/push_context.go istio-new/pilot/pkg/model/push_context.go
276+
--- istio/pilot/pkg/model/push_context.go 2023-11-03 17:18:56.000000000 +0800
277+
+++ istio-new/pilot/pkg/model/push_context.go 2023-11-03 17:05:47.000000000 +0800
278+
@@ -841,7 +841,19 @@
279+
func (ps *PushContext) VirtualServicesForGateway(proxy *Proxy, gateway string) []config.Config {
280+
res := ps.virtualServiceIndex.privateByNamespaceAndGateway[proxy.ConfigNamespace][gateway]
281+
res = append(res, ps.virtualServiceIndex.exportedToNamespaceByGateway[proxy.ConfigNamespace][gateway]...)
282+
- res = append(res, ps.virtualServiceIndex.publicByGateway[gateway]...)
283+
+
284+
+ // Favor same-namespace Gateway routes, to give the "consumer override" preference.
285+
+ // We do 2 iterations here to avoid extra allocations.
286+
+ for _, vs := range ps.virtualServiceIndex.publicByGateway[gateway] {
287+
+ if UseGatewaySemantics(vs) && vs.Namespace == proxy.ConfigNamespace {
288+
+ res = append(res, vs)
289+
+ }
290+
+ }
291+
+ for _, vs := range ps.virtualServiceIndex.publicByGateway[gateway] {
292+
+ if !(UseGatewaySemantics(vs) && vs.Namespace == proxy.ConfigNamespace) {
293+
+ res = append(res, vs)
294+
+ }
295+
+ }
296+
return res
297+
}
298+
299+
diff -Naur istio/pilot/pkg/model/virtualservice.go istio-new/pilot/pkg/model/virtualservice.go
300+
--- istio/pilot/pkg/model/virtualservice.go 2023-11-03 17:18:55.000000000 +0800
301+
+++ istio-new/pilot/pkg/model/virtualservice.go 2023-11-03 15:19:08.000000000 +0800
302+
@@ -76,6 +76,11 @@
303+
}
304+
305+
func resolveVirtualServiceShortnames(rule *networking.VirtualService, meta config.Meta) {
306+
+ // Kubernetes Gateway API semantics support shortnames
307+
+ // if UseGatewaySemantics(config.Config{Meta: meta}) {
308+
+ // return
309+
+ // }
310+
+
311+
// resolve top level hosts
312+
for i, h := range rule.Hosts {
313+
rule.Hosts[i] = string(ResolveShortnameToFQDN(h, meta))
314+
@@ -524,3 +529,10 @@
315+
}
316+
return false
317+
}
318+
+
319+
+// UseGatewaySemantics determines which logic we should use for VirtualService
320+
+// This allows gateway-api and VS to both be represented by VirtualService, but have different
321+
+// semantics.
322+
+func UseGatewaySemantics(cfg config.Config) bool {
323+
+ return cfg.Annotations[constants.InternalRouteSemantics] == constants.RouteSemanticsGateway
324+
+}
325+
diff -Naur istio/pilot/pkg/networking/core/v1alpha3/route/route.go istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go
326+
--- istio/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-11-03 17:18:56.000000000 +0800
327+
+++ istio-new/pilot/pkg/networking/core/v1alpha3/route/route.go 2023-11-03 17:05:55.000000000 +0800
328+
@@ -408,7 +408,6 @@
329+
break
330+
}
331+
}
332+
-
333+
if len(out) == 0 {
334+
return nil, fmt.Errorf("no routes matched")
335+
}
336+
@@ -493,6 +492,14 @@
337+
},
338+
}
339+
340+
+ if model.UseGatewaySemantics(virtualService) {
341+
+ if uri, isPrefixReplace := cutPrefix(redirect.Uri, "%PREFIX()%"); isPrefixReplace {
342+
+ action.Redirect.PathRewriteSpecifier = &route.RedirectAction_PrefixRewrite{
343+
+ PrefixRewrite: uri,
344+
+ }
345+
+ }
346+
+ }
347+
+
348+
if redirect.Scheme != "" {
349+
action.Redirect.SchemeRewriteSpecifier = &route.RedirectAction_SchemeRedirect{SchemeRedirect: redirect.Scheme}
350+
}
351+
@@ -1616,3 +1623,10 @@
352+
isSupport = curVersion.GreaterThan(notSupportFallback)
353+
return
354+
}
355+
+
356+
+func cutPrefix(s, prefix string) (after string, found bool) {
357+
+ if !strings.HasPrefix(s, prefix) {
358+
+ return s, false
359+
+ }
360+
+ return s[len(prefix):], true
361+
+}
362+
diff -Naur istio/pkg/config/constants/constants.go istio-new/pkg/config/constants/constants.go
363+
--- istio/pkg/config/constants/constants.go 2023-11-03 17:18:54.000000000 +0800
364+
+++ istio-new/pkg/config/constants/constants.go 2023-11-03 14:29:27.000000000 +0800
365+
@@ -15,6 +15,12 @@
366+
package constants
367+
368+
const (
369+
+ InternalParentNames = "internal.istio.io/parents"
370+
+
371+
+ InternalRouteSemantics = "internal.istio.io/route-semantics"
372+
+
373+
+ RouteSemanticsGateway = "gateway"
374+
+
375+
// UnspecifiedIP constant for empty IP address
376+
UnspecifiedIP = "0.0.0.0"
377+

0 commit comments

Comments
 (0)