k8s_gateway_api/exp/grpcroute.rs
1use crate::*;
2
3/// Spec defines the desired state of GrpcRoute.
4#[derive(
5 Clone,
6 Debug,
7 Default,
8 kube::CustomResource,
9 serde::Deserialize,
10 serde::Serialize,
11 schemars::JsonSchema,
12)]
13#[kube(
14 group = "gateway.networking.k8s.io",
15 version = "v1alpha2",
16 kind = "GRPCRoute",
17 root = "GrpcRoute",
18 status = "GrpcRouteStatus",
19 namespaced
20)]
21pub struct GrpcRouteSpec {
22 /// Common route information.
23 #[serde(flatten)]
24 pub inner: CommonRouteSpec,
25 /// Hostnames defines a set of hostnames to match against the GRPC
26 /// Host header to select a GRPCRoute to process the request. This matches
27 /// the RFC 1123 definition of a hostname with 2 notable exceptions:
28 ///
29 /// 1. IPs are not allowed.
30 /// 2. A hostname may be prefixed with a wildcard label (`*.`). The wildcard
31 /// label MUST appear by itself as the first label.
32 ///
33 /// If a hostname is specified by both the Listener and GRPCRoute, there
34 /// MUST be at least one intersecting hostname for the GRPCRoute to be
35 /// attached to the Listener. For example:
36 ///
37 /// * A Listener with `test.example.com` as the hostname matches GRPCRoutes
38 /// that have either not specified any hostnames, or have specified at
39 /// least one of `test.example.com` or `*.example.com`.
40 /// * A Listener with `*.example.com` as the hostname matches GRPCRoutes
41 /// that have either not specified any hostnames or have specified at least
42 /// one hostname that matches the Listener hostname. For example,
43 /// `test.example.com` and `*.example.com` would both match. On the other
44 /// hand, `example.com` and `test.example.net` would not match.
45 ///
46 /// Hostnames that are prefixed with a wildcard label (`*.`) are interpreted
47 /// as a suffix match. That means that a match for `*.example.com` would match
48 /// both `test.example.com`, and `foo.test.example.com`, but not `example.com`.
49 ///
50 /// If both the Listener and GRPCRoute have specified hostnames, any
51 /// GRPCRoute hostnames that do not match the Listener hostname MUST be
52 /// ignored. For example, if a Listener specified `*.example.com`, and the
53 /// GRPCRoute specified `test.example.com` and `test.example.net`,
54 /// `test.example.net` MUST NOT be considered for a match.
55 ///
56 /// If both the Listener and GRPCRoute have specified hostnames, and none
57 /// match with the criteria above, then the GRPCRoute MUST NOT be accepted by
58 /// the implementation. The implementation MUST raise an 'Accepted' Condition
59 /// with a status of `False` in the corresponding RouteParentStatus.
60 ///
61 /// If a Route (A) of type HTTPRoute or GRPCRoute is attached to a
62 /// Listener and that listener already has another Route (B) of the other
63 /// type attached and the intersection of the hostnames of A and B is
64 /// non-empty, then the implementation MUST accept exactly one of these two
65 /// routes, determined by the following criteria, in order:
66 ///
67 /// * The oldest Route based on creation timestamp.
68 /// * The Route appearing first in alphabetical order by
69 /// "{namespace}/{name}".
70 ///
71 /// The rejected Route MUST raise an 'Accepted' condition with a status of
72 /// 'False' in the corresponding RouteParentStatus.
73 ///
74 /// Support: Core
75 #[serde(default, skip_serializing_if = "Option::is_none")]
76 pub hostnames: Option<Vec<String>>,
77 /// Rules are a list of Grpc matchers, filters and actions.
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub rules: Option<Vec<GrpcRouteRule>>,
80}
81
82/// Status defines the current state of GrpcRoute.
83#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
84pub struct GrpcRouteStatus {
85 /// Common route status information.
86 #[serde(flatten)]
87 pub inner: RouteStatus,
88}
89
90impl From<GrpcRouteStatus> for HttpRouteStatus {
91 fn from(route: GrpcRouteStatus) -> Self {
92 Self { inner: route.inner }
93 }
94}
95
96/// GrpcRouteRule defines the semantics for matching a gRPC request based on
97/// conditions (matches), processing it (filters), and forwarding the request to
98/// an API object (backendRefs).
99#[derive(
100 Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
101)]
102pub struct GrpcRouteRule {
103 /// Filters define the filters that are applied to requests that match
104 /// this rule.
105 ///
106 /// The effects of ordering of multiple behaviors are currently unspecified.
107 /// This can change in the future based on feedback during the alpha stage.
108 ///
109 /// Conformance-levels at this level are defined based on the type of filter:
110 ///
111 /// - ALL core filters MUST be supported by all implementations that support
112 /// GRPCRoute.
113 /// - Implementers are encouraged to support extended filters.
114 /// - Implementation-specific custom filters have no API guarantees across
115 /// implementations.
116 ///
117 /// Specifying the same filter multiple times is not supported unless explicitly
118 /// indicated in the filter.
119 ///
120 /// If an implementation can not support a combination of filters, it must clearly
121 /// document that limitation. In cases where incompatible or unsupported
122 /// filters are specified and cause the `Accepted` condition to be set to status
123 /// `False`, implementations may use the `IncompatibleFilters` reason to specify
124 /// this configuration error.
125 ///
126 /// Support: Core
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub filters: Option<Vec<GrpcRouteFilter>>,
129 /// Matches define conditions used for matching the rule against incoming
130 /// gRPC requests. Each match is independent, i.e. this rule will be matched
131 /// if **any** one of the matches is satisfied.
132 ///
133 /// For example, take the following `matches` configuration:
134 ///
135 /// ```yaml
136 /// matches:
137 /// - method:
138 /// service: foo.bar
139 /// headers:
140 /// values:
141 /// version: 2
142 /// - method:
143 /// service: foo.bar.v2
144 /// ```
145 ///
146 /// For a request to match against this rule, it MUST satisfy
147 /// EITHER of the two conditions:
148 ///
149 /// - service of `foo.bar` AND contains the header `version: 2`
150 /// - service of `foo.bar.v2`
151 ///
152 /// See the documentation for GRPCRouteMatch on how to specify multiple
153 /// match conditions to be ANDed together.
154 ///
155 /// If no matches are specified, the implementation MUST match every gRPC request.
156 ///
157 /// Proxy or Load Balancer routing configuration generated from GRPCRoutes
158 /// MUST prioritize rules based on the following criteria, continuing on
159 /// ties. Merging MUST not be done between GRPCRoutes and HTTPRoutes.
160 /// Precedence MUST be given to the rule with the largest number of:
161 ///
162 /// * Characters in a matching non-wildcard hostname.
163 /// * Characters in a matching hostname.
164 /// * Characters in a matching service.
165 /// * Characters in a matching method.
166 /// * Header matches.
167 ///
168 /// If ties still exist across multiple Routes, matching precedence MUST be
169 /// determined in order of the following criteria, continuing on ties:
170 ///
171 /// * The oldest Route based on creation timestamp.
172 /// * The Route appearing first in alphabetical order by
173 /// "{namespace}/{name}".
174 ///
175 /// If ties still exist within the Route that has been given precedence,
176 /// matching precedence MUST be granted to the first matching rule meeting
177 /// the above criteria.
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub matches: Option<Vec<GrpcRouteMatch>>,
180 /// BackendRefs defines the backend(s) where matching requests should be
181 /// sent.
182 ///
183 /// Failure behavior here depends on how many BackendRefs are specified and
184 /// how many are invalid.
185 ///
186 /// If *all* entries in BackendRefs are invalid, and there are also no filters
187 /// specified in this route rule, *all* traffic which matches this rule MUST
188 /// receive an `UNAVAILABLE` status.
189 ///
190 /// See the GRPCBackendRef definition for the rules about what makes a single
191 /// GRPCBackendRef invalid.
192 ///
193 /// When a GRPCBackendRef is invalid, `UNAVAILABLE` statuses MUST be returned for
194 /// requests that would have otherwise been routed to an invalid backend. If
195 /// multiple backends are specified, and some are invalid, the proportion of
196 /// requests that would otherwise have been routed to an invalid backend
197 /// MUST receive an `UNAVAILABLE` status.
198 ///
199 /// For example, if two backends are specified with equal weights, and one is
200 /// invalid, 50 percent of traffic MUST receive an `UNAVAILABLE` status.
201 /// Implementations may choose how that 50 percent is determined.
202 ///
203 /// Support: Core for Kubernetes Service
204 ///
205 /// Support: Implementation-specific for any other resource
206 ///
207 /// Support for weight: Core
208 #[serde(
209 default,
210 skip_serializing_if = "Option::is_none",
211 rename = "backendRefs"
212 )]
213 pub backend_refs: Option<Vec<GrpcRouteBackendRef>>,
214}
215
216/// GrpcRouteMatch defines the predicate used to match requests to a given
217/// action. Multiple match types are ANDed together, i.e. the match will
218/// evaluate to true only if all conditions are satisfied.
219///
220///
221/// For example, the match below will match a gRPC request only if its service
222/// is `foo` AND it contains the `version: v1` header:
223///
224///
225/// ```yaml
226/// matches:
227/// - method:
228/// type: Exact
229/// service: "foo"
230/// headers:
231/// - name: "version"
232/// value "v1"
233///
234///
235/// ```
236#[derive(
237 Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
238)]
239pub struct GrpcRouteMatch {
240 /// Method specifies a gRPC request service/method matcher. If this field is
241 /// not specified, all services and methods will match.
242 #[serde(
243 default,
244 skip_serializing_if = "Option::is_none",
245 deserialize_with = "deserialize_method_match"
246 )]
247 pub method: Option<GrpcMethodMatch>,
248 /// Headers specifies gRPC request header matchers. Multiple match values are
249 /// ANDed together, meaning, a request MUST match all the specified headers
250 /// to select the route.
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub headers: Option<Vec<GrpcHeaderMatch>>,
253}
254
255fn deserialize_method_match<'de, D: serde::Deserializer<'de>>(
256 deserializer: D,
257) -> Result<Option<GrpcMethodMatch>, D::Error> {
258 <Option<GrpcMethodMatch> as serde::Deserialize>::deserialize(deserializer).map(|value| {
259 match value.as_ref() {
260 Some(rule) if rule.is_empty() => None,
261 _ => value,
262 }
263 })
264}
265
266#[allow(unused_qualifications)]
267pub type GrpcHeaderMatch = crate::httproute::HttpHeaderMatch;
268
269/// Method specifies a gRPC request service/method matcher. If this field is
270/// not specified, all services and methods will match.
271#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, schemars::JsonSchema)]
272#[serde(tag = "type", rename_all = "PascalCase")]
273pub enum GrpcMethodMatch {
274 #[serde(rename_all = "camelCase")]
275 Exact {
276 /// Value of the method to match against. If left empty or omitted, will
277 /// match all services.
278 ///
279 /// At least one of Service and Method MUST be a non-empty string.
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 method: Option<String>,
282 /// Value of the service to match against. If left empty or omitted, will
283 /// match any service.
284 ///
285 /// At least one of Service and Method MUST be a non-empty string.
286 #[serde(default, skip_serializing_if = "Option::is_none")]
287 service: Option<String>,
288 },
289
290 #[serde(rename_all = "camelCase")]
291 RegularExpression {
292 /// Value of the method to match against. If left empty or omitted, will
293 /// match all services.
294 ///
295 /// At least one of Service and Method MUST be a non-empty string.
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 method: Option<String>,
298 /// Value of the service to match against. If left empty or omitted, will
299 /// match any service.
300 ///
301 /// At least one of Service and Method MUST be a non-empty string.
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 service: Option<String>,
304 },
305}
306
307impl GrpcMethodMatch {
308 fn is_empty(&self) -> bool {
309 let (method, service) = match self {
310 Self::Exact { method, service } => (method, service),
311 Self::RegularExpression { method, service } => (method, service),
312 };
313
314 method.as_deref().map(str::is_empty).unwrap_or(true)
315 && service.as_deref().map(str::is_empty).unwrap_or(true)
316 }
317}
318
319fn empty_option_strings_are_none(value: Option<String>) -> Option<String> {
320 match value.as_ref() {
321 Some(string) if string.is_empty() => None,
322 _ => value,
323 }
324}
325
326impl<'de> serde::Deserialize<'de> for GrpcMethodMatch {
327 // NOTE: This custom deserialization exists to ensure the deserialization
328 // behavior matches the behavior prescribed by the gateway api docs
329 // for how the "type" field on `GRPCRouteMatch` is expected to work.
330 //
331 // ref: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1alpha2.GRPCMethodMatch
332 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
333 #[derive(serde::Deserialize)]
334 #[serde(field_identifier, rename_all = "lowercase")]
335 enum Field {
336 Type,
337 Method,
338 Service,
339 }
340
341 struct GrpcMethodMatchVisitor;
342
343 impl<'de> serde::de::Visitor<'de> for GrpcMethodMatchVisitor {
344 type Value = GrpcMethodMatch;
345
346 fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
347 formatter.write_str("GrpcMethodMatch")
348 }
349
350 fn visit_map<V>(self, mut map: V) -> Result<GrpcMethodMatch, V::Error>
351 where
352 V: serde::de::MapAccess<'de>,
353 {
354 let (mut r#type, mut method, mut service) = (None, None, None);
355
356 while let Some(key) = map.next_key()? {
357 match key {
358 Field::Type => {
359 if r#type.is_some() {
360 return Err(serde::de::Error::duplicate_field("type"));
361 }
362 r#type = map
363 .next_value::<Option<String>>()
364 .map(empty_option_strings_are_none)?;
365 }
366 Field::Method => {
367 if method.is_some() {
368 return Err(serde::de::Error::duplicate_field("method"));
369 }
370 method = map
371 .next_value::<Option<String>>()
372 .map(empty_option_strings_are_none)?;
373 }
374 Field::Service => {
375 if service.is_some() {
376 return Err(serde::de::Error::duplicate_field("service"));
377 }
378 service = map
379 .next_value::<Option<String>>()
380 .map(empty_option_strings_are_none)?;
381 }
382 }
383 }
384
385 match r#type.as_deref() {
386 None | Some("Exact") => Ok(GrpcMethodMatch::Exact { method, service }),
387 Some("RegularExpression") => {
388 Ok(GrpcMethodMatch::RegularExpression { method, service })
389 }
390 Some(value) => Err(serde::de::Error::invalid_value(
391 serde::de::Unexpected::Str(value),
392 &r#"one of: {"Exact", "RegularExpression"}"#,
393 )),
394 }
395 }
396 }
397
398 const FIELDS: &[&str] = &["type", "method", "service"];
399 deserializer.deserialize_struct("GrpcMethodMatch", FIELDS, GrpcMethodMatchVisitor)
400 }
401}
402
403/// GrpcRouteFilter defines processing steps that must be completed during the
404/// request or response lifecycle. GrpcRouteFilters are meant as an extension
405/// point to express processing that may be done in Gateway implementations. Some
406/// examples include request or response modification, implementing
407/// authentication strategies, rate-limiting, and traffic shaping. API
408/// guarantee/conformance is defined based on the type of the filter.
409#[derive(
410 Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
411)]
412#[serde(tag = "type", rename_all = "PascalCase")]
413pub enum GrpcRouteFilter {
414 /// ExtensionRef is an optional, implementation-specific extension to the
415 /// "filter" behavior. For example, resource "myroutefilter" in group
416 /// "networking.example.net"). ExtensionRef MUST NOT be used for core and
417 /// extended filters.
418 ///
419 /// Support: Implementation-specific
420 ///
421 /// This filter can be used multiple times within the same rule.
422 #[serde(rename_all = "camelCase")]
423 ExtensionRef { extension_ref: LocalObjectReference },
424
425 /// RequestMirror defines a schema for a filter that mirrors requests.
426 /// Requests are sent to the specified destination, but responses from
427 /// that destination are ignored.
428 ///
429 /// This filter can be used multiple times within the same rule. Note that
430 /// not all implementations will be able to support mirroring to multiple
431 /// backends.
432 ///
433 /// Support: Extended
434 #[serde(rename_all = "camelCase")]
435 RequestMirror {
436 request_mirror: HttpRequestMirrorFilter,
437 },
438
439 /// RequestHeaderModifier defines a schema for a filter that modifies request
440 /// headers.
441 ///
442 /// Support: Core
443 #[serde(rename_all = "camelCase")]
444 RequestHeaderModifier {
445 request_header_modifier: HttpRequestHeaderFilter,
446 },
447
448 /// ResponseHeaderModifier defines a schema for a filter that modifies
449 /// response headers.
450 ///
451 /// Support: Extended
452 #[serde(rename_all = "camelCase")]
453 ResponseHeaderModifier {
454 response_header_modifier: HttpRequestHeaderFilter,
455 },
456}
457
458impl From<GrpcRouteFilter> for HttpRouteFilter {
459 fn from(filter: GrpcRouteFilter) -> Self {
460 match filter {
461 GrpcRouteFilter::ExtensionRef { extension_ref } => Self::ExtensionRef { extension_ref },
462 GrpcRouteFilter::RequestMirror { request_mirror } => {
463 Self::RequestMirror { request_mirror }
464 }
465 GrpcRouteFilter::RequestHeaderModifier {
466 request_header_modifier,
467 } => Self::RequestHeaderModifier {
468 request_header_modifier,
469 },
470 GrpcRouteFilter::ResponseHeaderModifier {
471 response_header_modifier,
472 } => Self::ResponseHeaderModifier {
473 response_header_modifier,
474 },
475 }
476 }
477}
478
479/// GrpcBackendRef defines how a GrpcRoute forwards a gRPC request.
480///
481/// Note that when a namespace different from the local namespace is specified, a
482/// ReferenceGrant object is required in the referent namespace to allow that
483/// namespace's owner to accept the reference. See the ReferenceGrant
484/// documentation for details.
485///
486/// <gateway:experimental:description>
487///
488/// When the BackendRef points to a Kubernetes Service, implementations SHOULD
489/// honor the appProtocol field if it is set for the target Service Port.
490///
491/// Implementations supporting appProtocol SHOULD recognize the Kubernetes
492/// Standard Application Protocols defined in KEP-3726.
493///
494/// If a Service appProtocol isn't specified, an implementation MAY infer the
495/// backend protocol through its own means. Implementations MAY infer the
496/// protocol from the Route type referring to the backend Service.
497///
498/// If a Route is not able to send traffic to the backend using the specified
499/// protocol then the backend is considered invalid. Implementations MUST set the
500/// "ResolvedRefs" condition to "False" with the "UnsupportedProtocol" reason.
501///
502/// </gateway:experimental:description>
503#[derive(
504 Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
505)]
506pub struct GrpcRouteBackendRef {
507 /// BackendObjectReference references a Kubernetes object.
508 #[serde(flatten)]
509 pub inner: BackendObjectReference,
510 /// Filters defined at this level MUST be executed if and only if the
511 /// request is being forwarded to the backend defined here.
512 ///
513 /// Support: Implementation-specific (For broader support of filters, use the
514 /// Filters field in GrpcRouteRule.)
515 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub filters: Option<Vec<GrpcRouteFilter>>,
517 /// Weight specifies the proportion of requests forwarded to the referenced
518 /// backend. This is computed as weight/(sum of all weights in this
519 /// BackendRefs list). For non-zero values, there may be some epsilon from
520 /// the exact proportion defined here depending on the precision an
521 /// implementation supports. Weight is not a percentage and the sum of
522 /// weights does not need to equal 100.
523 ///
524 /// If only one backend is specified, and it has a weight greater than 0, 100%
525 /// of the traffic is forwarded to that backend. If weight is set to 0, no
526 /// traffic should be forwarded for this entry. If unspecified, weight
527 /// defaults to 1.
528 ///
529 /// Support for this field varies based on the context where used.
530 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub weight: Option<u16>,
532}
533
534impl From<GrpcRouteBackendRef> for HttpBackendRef {
535 fn from(backend: GrpcRouteBackendRef) -> Self {
536 let filters = backend
537 .filters
538 .map(|filters| filters.into_iter().map(Into::into).collect());
539
540 Self {
541 filters,
542 backend_ref: Some(BackendRef {
543 inner: backend.inner,
544 weight: backend.weight,
545 }),
546 }
547 }
548}
549
550#[cfg(test)]
551mod test {
552 use super::*;
553
554 #[test]
555 fn test_grpc_route_deserialization() {
556 // Test deserialization against upstream example
557 // ref: https://gateway-api.sigs.k8s.io/api-types/grpcroute/#backendrefs-optional
558 let data = r#"{
559 "apiVersion": "gateway.networking.k8s.io/v1alpha2",
560 "kind": "GRPCRoute",
561 "metadata": {
562 "name": "grpc-app-1"
563 },
564 "spec": {
565 "parentRefs": [
566 {
567 "name": "my-gateway"
568 }
569 ],
570 "hostnames": [
571 "example.com"
572 ],
573 "rules": [
574 {
575 "matches": [
576 {
577 "method": {
578 "service": "com.example.User",
579 "method": "Login"
580 }
581 },
582 {
583 "method": {
584 "service": "com.example.User",
585 "method": "Logout",
586 "type": "Exact"
587 }
588 },
589 {
590 "method": {
591 "service": "com.example.User",
592 "method": "UpdateProfile",
593 "type": "RegularExpression"
594 }
595 }
596 ],
597 "backendRefs": [
598 {
599 "name": "my-service1",
600 "port": 50051
601 }
602 ]
603 },
604 {
605 "matches": [
606 {
607 "headers": [
608 {
609 "type": "Exact",
610 "name": "magic",
611 "value": "foo"
612 }
613 ],
614 "method": {
615 "service": "com.example.Things",
616 "method": "DoThing"
617 }
618 }
619 ],
620 "backendRefs": [
621 {
622 "name": "my-service2",
623 "port": 50051
624 }
625 ]
626 }
627 ]
628 }
629 }"#;
630 let route = serde_json::from_str::<GrpcRoute>(data);
631 assert!(route.is_ok());
632 }
633}