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}