Delegating with Route Tables
The Gloo Edge Virtual Service makes it possible to define all routes for a domain on a single configuration resource.
However, condensing all routing config onto a single object can be cumbersome when dealing with a large number of routes.
Gloo Edge provides a feature referred to as delegation. Delegation allows a complete routing configuration to be assembled from separate config objects. The root config object delegates responsibility to other objects, forming a tree of config objects. The tree always has a Virtual Service as its root, which delegates to any number of Route Tables. Route Tables can further delegate to other Route Tables.
Motivation
Use cases for delegation include:
-
Allowing multiple tenants to own add, remove, and update routes without requiring shared access to the root-level Virtual Service
-
Sharing route configuration between Virtual Services
-
Simplifying blue-green routing configurations by swapping the target Route Table for a delegated route.
-
Simplifying very large routing configurations for a single Virtual Service
-
Restricting ownership of routing configuration for a tenant to a subset of the whole Virtual Service.
Using delegation, organizations can delegate ownership of routing config to individuals or teams. Those individuals or teams can then further delegate routes to others.
Config Model
A configuration using Delegation can be understood as a tree.
The root node in the tree is a Virtual Service while every child node is a RouteTable:
*.petclinic.com
] -->|delegate /api
prefix | rt1(Route Table /api/pets
/api/vets
)
vs -->|delegate /site
prefix | rt2(Route Table /site/login
/site/logout
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
Route Tables can be nested for any level of granularity:
*.petclinic.com
] -->|delegate /api
prefix | rt1(Route Table /api/pets
/api/vets
)
rt1 -->|delegate /api/pets/.*
prefix | rt3(Route Table /api/pets/.*/records
/api/pets/.*/billing
)
vs -->|delegate /site
prefix | rt2(Route Table /site/login
/site/logout
)
rt1 -->|delegate /api/vets
prefix | rt4(Route Table GET /api/vets
POST /api/vets
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
Non-delegating routes can be defined at every level of the config tree:
*.petclinic.com
] -->|delegate /api
prefix | rt1(Route Table /api/pets
/api/vets
)
vs -->|delegate /site
prefix | rt2(Route Table /site/login
/site/logout
)
vs -->|route /pharmacy
| us1(Upstream pharmacy-svc.petstore.cluster.svc.local:80
)
rt1 -->|route /api/pets
| us2(Upstream pet-svc.petstore.cluster.svc.local:80
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
Routes defined at any level must inherit the prefix delegated to them, else Gloo Edge will not consider the config tree valid:
*.petclinic.com
] -->|delegate /api
prefix | rt1(Route Table /api/v1
/v2
)
style vs fill:#f54,stroke:#233,stroke-width:4px
end
subgraph valid
vsValid[Virtual Service *.petclinic.com
] -->|delegate /api
prefix | rt1Valid(Route Table /api/v1
/api/v2
)
style vsValid fill:#0DFF00,stroke:#233,stroke-width:4px
end
Gloo Edge will flatten the non-delegated routes defined in config tree down to a single Proxy object, such that:
*.petclinic.com
] -->|delegate /api
prefix | rt1(Route Table /api/pets
/api/vets
)
vs -->|route /pharmacy
| us1(Upstream pharmacy-svc.petstore.cluster.svc.local:80
)
rt1 -->|route /api/pets
| us2(Upstream pet-svc.petstore.cluster.svc.local:80
)
rt1 -->|route /api/vets
| us3(Upstream vet-svc.petstore.cluster.svc.local:80
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
style us3 fill:#f9f,stroke:#333,stroke-width:4px
Would become:
*.petclinic.com
} --> |route /api/pets
| us2(Upstream pet-svc.petstore.cluster.svc.local:80
)
px -->|route /api/vets
| us3(Upstream vet-svc.petstore.cluster.svc.local:80
)
px --> |route /pharmacy
| us1(Upstream pharmacy-svc.petstore.cluster.svc.local:80
)
style px fill:#0DFFDD,stroke:#333,stroke-width:4px
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
style us3 fill:#f9f,stroke:#333,stroke-width:4px
Example Configuration
The delegateAction
object (which can be defined on routes, both on VirtualServices
and RouteTables
) can assume
one of two forms:
ref
: delegates to a specific route table;selector
: delegates to all the route tables that match the selection criteria.
In the next two sections we will see examples of both these delegation actions.
Delegation via direct reference
A complete configuration that uses a delegateAction
which references specific route tables might look as follows:
A root-level VirtualService which delegates routing decisions to the a-routes
and b-routes
RouteTables.
Please note that routes with delegateActions
can only use a prefix
matcher.
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: 'example'
namespace: 'gloo-system'
spec:
virtualHost:
domains:
- 'example.com'
routes:
- matchers:
- prefix: '/a' # delegate ownership of routes for `example.com/a`
delegateAction:
ref:
name: 'a-routes'
namespace: 'a'
- matchers:
- prefix: '/b' # delegate ownership of routes for `example.com/b`
delegateAction:
ref:
name: 'b-routes'
namespace: 'b'
A RouteTable which defines two routes.
apiVersion: gateway.solo.io/v1
kind: RouteTable
metadata:
name: 'a-routes'
namespace: 'a'
spec:
routes:
- matchers:
# the path matchers in this RouteTable must begin with the prefix `/a/`
- prefix: '/a/1'
routeAction:
single:
upstream:
name: 'foo-upstream'
- matchers:
- prefix: '/a/2'
routeAction:
single:
upstream:
name: 'bar-upstream'
A RouteTable which both defines a route and delegates to another RouteTable.
apiVersion: gateway.solo.io/v1
kind: RouteTable
metadata:
name: 'b-routes'
namespace: 'b'
spec:
routes:
- matchers:
# the path matchers in this RouteTable must begin with the prefix `/b/`
- regex: '/b/3'
routeAction:
single:
upstream:
name: 'baz-upstream'
- matchers:
- prefix: '/b/c/'
# routes in the RouteTable can perform any action, including a delegateAction
delegateAction:
ref:
name: 'c-routes'
namespace: 'c'
A RouteTable which is a child of another route table.
apiVersion: gateway.solo.io/v1
kind: RouteTable
metadata:
name: 'c-routes'
namespace: 'c'
spec:
routes:
- matchers:
- exact: '/b/c/4'
routeAction:
single:
upstream:
name: 'qux-upstream'
The above configuration can be visualized as:
example.com
]
vs -->|delegate /a
prefix | rt1(Route Table /a/1
/a/2
)
vs -->|delegate /b
prefix | rt2(Route Table /b/1
/b/2
)
rt1 -->|route /a/1
| us1(Upstream foo-upstream
)
rt1 -->|route /a/2
| us2(Upstream bar-upstream
)
rt2 -->|route /b/3
| us3(Upstream baz-upstream
)
rt2 -->|route /b/c
| rt3(Route Table /b/c/4
)
rt3 -->|route /b/c/4
| us4(Upstream qux-upstream
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
style us3 fill:#f9f,stroke:#333,stroke-width:4px
style us4 fill:#f9f,stroke:#333,stroke-width:4px
And would result in the following Proxy:
example.com
}
style px fill:#0DFFDD,stroke:#333,stroke-width:4px
px -->|route /a/1
| us1(Upstream foo-upstream
)
px -->|route /a/2
| us2(Upstream bar-upstream
)
px -->|route /b/3
| us3(Upstream baz-upstream
)
px -->|route /b/c/4
| us4(Upstream qux-upstream
)
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
style us3 fill:#f9f,stroke:#333,stroke-width:4px
style us4 fill:#f9f,stroke:#333,stroke-width:4px
Delegation via route table selector
By using a RouteTableSelector
, a route can delegate to multiple route tables.
You can specify three types of selection criteria (labels
and expressions
cannot be used together):
labels
: if present, Gloo Edge will select route tables whose labels match the specified ones;expressions
: if present, Gloo Edge will select according to the expression (adhering to the same semantics as kubernetes label selectors). An example config for this selection model follows here;namespaces
: if present, Gloo Edge will select route tables in these namespaces. If omitted, Gloo Edge will only select route tables in the same namespace as the resource (Virtual Service or Route Table) that owns this selector. The reserved value*
can be used to select Route Tables in all namespaces watched by Gloo Edge.
If a RouteTableSelector
matches multiple route tables and the route tables do not specify different weights
,
Gloo Edge will sort the routes which belong to those tables to avoid short-circuiting (e.g. having a route with a /foo
prefix matcher coming before a route with a /foo/bar
one). The sorting occurs by descending specificity:
routes with longer paths will come first, and in case of equal paths, precedence will be given to the route that defines
the more restrictive matchers. The algorithm used for sorting the routes can be found
here.
In this scenario, Gloo Edge will also alert the user by adding a warning to the status of the parent resource (the one that
specifies the RouteTableSelector
).
Please see the Route Table weight section below for more information about how to control the ordering of your delegated routes.
A complete configuration might look as follows:
A root-level VirtualService which delegates routing decisions to any RouteTables in the gloo-system
namespace that
contain the domain: example.com
label.
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: 'example'
namespace: 'gloo-system'
spec:
virtualHost:
domains:
- 'example.com'
routes:
- matchers:
- prefix: '/' # delegate ownership of all routes for `example.com`
delegateAction:
selector:
labels:
domain: example.com
namespaces:
- gloo-system
Two RouteTables which match the selection criteria:
apiVersion: gateway.solo.io/v1
kind: RouteTable
metadata:
name: 'a-routes'
namespace: 'gloo-system'
labels:
domain: example.com
spec:
weight: 20
routes:
- matchers:
# the path matchers in this RouteTable can begin with any prefix
- prefix: '/a'
routeAction:
single:
upstream:
name: 'foo-upstream'
apiVersion: gateway.solo.io/v1
kind: RouteTable
metadata:
name: 'a-b-routes'
namespace: 'gloo-system'
labels:
domain: example.com
spec:
weight: 10
routes:
- matchers:
# the path matchers in this RouteTable can begin with any prefix
- regex: '/a/b'
routeAction:
single:
upstream:
name: 'bar-upstream'
The above configuration can be visualized as:
example.com
]
vs -->|delegate /
prefix | rt1(Route Table /a
)
vs -->|delegate /
prefix | rt2(Route Table /a/b/
)
rt1 -->|route /a
| us1(Upstream foo-upstream
)
rt2 -->|route /a/b
| us3(Upstream bar-upstream
)
style vs fill:#0DFF00,stroke:#233,stroke-width:4px
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us3 fill:#f9f,stroke:#333,stroke-width:4px
And would result in the following Proxy:
example.com
}
style px fill:#0DFFDD,stroke:#333,stroke-width:4px
px -->|route /a/b
| us1(Upstream bar-upstream
)
px -->|route /a
| us2(Upstream foo-upstream
)
style us1 fill:#f9f,stroke:#333,stroke-width:4px
style us2 fill:#f9f,stroke:#333,stroke-width:4px
Route Table Selector Expression
To allow for flexible route table label matching in more complex architecture scenarios, the expressions
attribute of the delegateAction.selector
field can be used as such:
apiVersion: gateway.solo.io/v1
kind: VirtualService
metadata:
name: 'example'
namespace: 'gloo-system'
spec:
virtualHost:
domains:
- 'example.com'
routes:
- matchers:
- prefix: '/'
delegateAction:
selector:
# 'expressions' and 'labels' cannot coexist within a single selector
expressions:
- key: domain
operator: In
values:
- example.com
# Do not include route tables exposing the 'shouldnotexist' label
- key: shouldnotexist
operator: !
namespaces:
- gloo-system
Note that candidate route tables must match all selector expressions (logical AND) to be selected.
Route Table weight
As you might have noticed, we specified a weight
attribute on the above route tables. This attribute can be used
to determine the order in which the routes will appear on the final Proxy
resource when multiple route tables match
a RouteTableSelector
. The field is optional; if no value is specified, the weight
defaults to 0 (zero).
Gloo Edge will process the route tables matched by a selector in ascending order by weight and collect the routes of each
route table in the order they are defined.
In the above example, we want the /a/b
route to come before the /a
route, to avoid the latter one short-circuiting
the former; hence, we set the weight of the a-b-routes
table to 10
and the weight of the a-routes
table to 20
.
As you can see in the diagram above, the resulting Proxy
object defines the routes in the desired order.
Matcher restrictions
The Gloo Edge route delegation model imposes some restrictions on the virtual service and parent route table’s matchers (i.e., any resource delegating routing config to another route table). Most notably, parent matchers must have only a single prefix matcher. Further, any parent matcher must have its own prefix start with the same prefix as its parent (if any).
Further, in versions prior to Gloo Edge 1.5.0-beta21, parent matchers cannot use header, query parameter, or method matchers.
In more recent Gloo Edge versions, parent matchers can now use those matchers so long as their child route tables have
headers, query parameters, and methods that are a superset of those defined on the parent. In Gloo Edge versions
Gloo Edge 1.5.0-beta25 and higher, the inheritableMatchers
boolean field was added to virtual services and route
tables, which allows users to enable matcher inheritance for non-path matchers from parents (rather than requiring the
whole subset enumerated on any chlidren).
In all versions of Gloo Edge, the leaf route table can use any kind of path matcher, so long as it begins with the same prefix as its parent.
Learn more
Explore Gloo Edge’s Routing API in the API documentation:
Please submit questions and feedback to the solo.io slack channel, or open an issue on GitHub.