Skip to content

Commit 06a0ed2

Browse files
networking: support pathTemplate URI matching in VirtualService
Add support for the pathTemplate match type in VirtualService HTTPMatchRequest.uri, using Envoy's URI template path matching extension (UriTemplateMatchConfig). This is consistent with the existing path template support in AuthorizationPolicy. - Route conversion: StringMatch_PathTemplate maps to RouteMatch_PathMatchPolicy with the uri_template_matcher extension, reusing the existing PathTemplateMatcher() from the authz matcher package for sanitization ({*}->*, {**}->**) - Validation: pathTemplate is validated via CheckValidPathTemplate() (same as AuthorizationPolicy) and is restricted to uri matches only - Delegate conflict detection: pathTemplate is treated as incompatible with delegate VirtualService composition - Gateway API: getURIRank/getURILength updated to handle pathTemplate Depends on istio/api#3675 Fixes istio#59533 Signed-off-by: Antonio <antoniomorales9711@gmail.com>
1 parent 1020787 commit 06a0ed2

9 files changed

Lines changed: 125 additions & 1 deletion

File tree

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,5 @@ require (
237237
sigs.k8s.io/randfill v1.0.0 // indirect
238238
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
239239
)
240+
241+
replace istio.io/api => ../api

pilot/pkg/config/kube/gateway/conversion.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ func parentMeta(obj controllers.Object, sectionName *k8s.SectionName) map[string
462462
}
463463
}
464464

465-
// getURIRank ranks a URI match type. Exact > Prefix > Regex
465+
// getURIRank ranks a URI match type. Exact > Prefix > Regex > PathTemplate
466466
func getURIRank(match *istio.HTTPMatchRequest) int {
467467
if match.Uri == nil {
468468
return -1
@@ -474,6 +474,8 @@ func getURIRank(match *istio.HTTPMatchRequest) int {
474474
return 2
475475
case *istio.StringMatch_Regex:
476476
return 1
477+
case *istio.StringMatch_PathTemplate:
478+
return 1
477479
}
478480
// should not happen
479481
return -1
@@ -490,6 +492,8 @@ func getURILength(match *istio.HTTPMatchRequest) int {
490492
return len(match.Uri.GetExact())
491493
case *istio.StringMatch_Regex:
492494
return len(match.Uri.GetRegex())
495+
case *istio.StringMatch_PathTemplate:
496+
return len(match.Uri.GetPathTemplate())
493497
}
494498
// should not happen
495499
return -1

pilot/pkg/model/virtualservice.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ func stringMatchConflict(root, leaf *networking.StringMatch) bool {
410410
if root == nil || leaf == nil {
411411
return false
412412
}
413+
// pathTemplate cannot be composed with delegate VirtualServices.
414+
if root.GetPathTemplate() != "" || leaf.GetPathTemplate() != "" {
415+
return true
416+
}
413417
// If root regex match is specified, delegate should not have other matches.
414418
if root.GetRegex() != "" {
415419
if leaf.GetRegex() != "" || leaf.GetPrefix() != "" || leaf.GetExact() != "" {

pilot/pkg/networking/core/route/route.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"istio.io/istio/pilot/pkg/networking/telemetry"
4545
"istio.io/istio/pilot/pkg/networking/util"
4646
authz "istio.io/istio/pilot/pkg/security/authz/model"
47+
authmatcher "istio.io/istio/pilot/pkg/security/authz/matcher"
4748
"istio.io/istio/pilot/pkg/util/protoconv"
4849
"istio.io/istio/pkg/config"
4950
"istio.io/istio/pkg/config/constants"
@@ -1109,6 +1110,13 @@ func TranslateRouteMatch(vs config.Config, in *networking.HTTPMatchRequest) *rou
11091110
Regex: m.Regex,
11101111
},
11111112
}
1113+
case *networking.StringMatch_PathTemplate:
1114+
out.PathSpecifier = &route.RouteMatch_PathMatchPolicy{
1115+
PathMatchPolicy: &core.TypedExtensionConfig{
1116+
Name: "envoy.path.match.uri_template.uri_template_matcher",
1117+
TypedConfig: protoconv.MessageToAny(authmatcher.PathTemplateMatcher(m.PathTemplate)),
1118+
},
1119+
}
11121120
}
11131121
}
11141122

pilot/pkg/networking/core/route/route_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1210,6 +1210,26 @@ func TestBuildHTTPRoutes(t *testing.T) {
12101210
}))
12111211
})
12121212

1213+
t.Run("for URI path template match", func(t *testing.T) {
1214+
g := NewWithT(t)
1215+
cg := core.NewConfigGenTest(t, core.TestOptions{})
1216+
1217+
routeOptsWithPush := routeOpts
1218+
routeOptsWithPush.Push = cg.PushContext()
1219+
routes, err := route.BuildHTTPRoutesForVirtualService(node(cg),
1220+
virtualServiceWithPathTemplateMatch,
1221+
8080, gatewayNames, routeOptsWithPush)
1222+
xdstest.ValidateRoutes(t, routes)
1223+
g.Expect(err).NotTo(HaveOccurred())
1224+
g.Expect(len(routes)).To(Equal(1))
1225+
1226+
// Verify the route match uses PathMatchPolicy (URI template extension)
1227+
pathMatchPolicy, ok := routes[0].Match.PathSpecifier.(*envoyroute.RouteMatch_PathMatchPolicy)
1228+
g.Expect(ok).To(BeTrue(), "expected PathMatchPolicy specifier for path template match")
1229+
g.Expect(pathMatchPolicy.PathMatchPolicy.Name).To(Equal("envoy.path.match.uri_template.uri_template_matcher"))
1230+
g.Expect(pathMatchPolicy.PathMatchPolicy.TypedConfig).NotTo(BeNil())
1231+
})
1232+
12131233
t.Run("for host rewrite with XForwardedHost enabled", func(t *testing.T) {
12141234
g := NewWithT(t)
12151235
cg := core.NewConfigGenTest(t, core.TestOptions{})
@@ -2395,6 +2415,39 @@ var virtualServiceWithPathRegexMatchRegexRewrite = config.Config{
23952415
},
23962416
}
23972417

2418+
var virtualServiceWithPathTemplateMatch = config.Config{
2419+
Meta: config.Meta{
2420+
GroupVersionKind: gvk.VirtualService,
2421+
Name: "acme",
2422+
},
2423+
Spec: &networking.VirtualService{
2424+
Hosts: []string{},
2425+
Gateways: []string{"some-gateway"},
2426+
Http: []*networking.HTTPRoute{
2427+
{
2428+
Match: []*networking.HTTPMatchRequest{
2429+
{
2430+
Name: "path-template",
2431+
Uri: &networking.StringMatch{
2432+
MatchType: &networking.StringMatch_PathTemplate{
2433+
PathTemplate: "/users/{*}/orders/{**}",
2434+
},
2435+
},
2436+
},
2437+
},
2438+
Route: []*networking.HTTPRouteDestination{
2439+
{
2440+
Destination: &networking.Destination{
2441+
Host: "foo.example.org",
2442+
},
2443+
Weight: 100,
2444+
},
2445+
},
2446+
},
2447+
},
2448+
},
2449+
}
2450+
23982451
var virtualServiceWithRedirectAndSetHeader = config.Config{
23992452
Meta: config.Meta{
24002453
GroupVersionKind: gvk.VirtualService,

pkg/config/validation/validation.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2310,6 +2310,16 @@ func validateStringMatch(sm *networking.StringMatch, where string) error {
23102310
}
23112311
case *networking.StringMatch_Regex:
23122312
return validateStringMatchRegexp(sm, where)
2313+
case *networking.StringMatch_PathTemplate:
2314+
return fmt.Errorf("%q: pathTemplate is only supported for uri matches", where)
2315+
}
2316+
return nil
2317+
}
2318+
2319+
func validatePathTemplateMatch(sm *networking.StringMatch, where string) error {
2320+
switch sm.GetMatchType().(type) {
2321+
case *networking.StringMatch_PathTemplate:
2322+
return security.CheckValidPathTemplate(where, []string{sm.GetPathTemplate()})
23132323
}
23142324
return nil
23152325
}

pkg/config/validation/virtualservice.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ func validateHTTPRouteMatchRequest(http *networking.HTTPRoute) (errs error) {
159159
// whereas scheme/method/authority does not:
160160
// https://github.com/envoyproxy/envoy/blob/v1.29.2/api/envoy/type/matcher/string.proto#L38
161161
errs = appendErrors(errs, validateStringMatchRegexp(match.GetUri(), "uri"))
162+
errs = appendErrors(errs, validatePathTemplateMatch(match.GetUri(), "uri"))
162163
errs = appendErrors(errs, validateStringMatch(match.GetScheme(), "scheme"))
163164
errs = appendErrors(errs, validateStringMatch(match.GetMethod(), "method"))
164165
errs = appendErrors(errs, validateStringMatch(match.GetAuthority(), "authority"))

pkg/config/validation/virtualservice_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,39 @@ func TestValidateRootHTTPRoute(t *testing.T) {
348348
},
349349
}},
350350
}, valid: true},
351+
{name: "path template uri match", route: &networking.HTTPRoute{
352+
Delegate: &networking.Delegate{
353+
Name: "test",
354+
Namespace: "test",
355+
},
356+
Match: []*networking.HTTPMatchRequest{{
357+
Uri: &networking.StringMatch{
358+
MatchType: &networking.StringMatch_PathTemplate{PathTemplate: "/users/{*}/orders/{**}"},
359+
},
360+
}},
361+
}, valid: true},
362+
{name: "invalid path template uri match with multiple {**}", route: &networking.HTTPRoute{
363+
Delegate: &networking.Delegate{
364+
Name: "test",
365+
Namespace: "test",
366+
},
367+
Match: []*networking.HTTPMatchRequest{{
368+
Uri: &networking.StringMatch{
369+
MatchType: &networking.StringMatch_PathTemplate{PathTemplate: "/users/{**}/{**}"},
370+
},
371+
}},
372+
}, valid: false},
373+
{name: "path template on non-uri field is invalid", route: &networking.HTTPRoute{
374+
Delegate: &networking.Delegate{
375+
Name: "test",
376+
Namespace: "test",
377+
},
378+
Match: []*networking.HTTPMatchRequest{{
379+
Method: &networking.StringMatch{
380+
MatchType: &networking.StringMatch_PathTemplate{PathTemplate: "/users/{*}"},
381+
},
382+
}},
383+
}, valid: false},
351384
{
352385
name: "prefix queryParams match", route: &networking.HTTPRoute{
353386
Delegate: &networking.Delegate{

releasenotes/notes/59533.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: release-notes/v2
2+
kind: feature
3+
area: traffic-management
4+
issue:
5+
- 59533
6+
7+
releaseNotes:
8+
- |
9+
**Added** support for `pathTemplate` URI matching in VirtualService `HTTPMatchRequest`, using `{*}` (matches one path segment) and `{**}` (matches one or more path segments) operators. This is backed by Envoy's URI template path matching extension and is consistent with the existing `pathTemplate` support in `AuthorizationPolicy`. See https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/path/match/uri_template/v3/uri_template_match.proto

0 commit comments

Comments
 (0)