use crate::*;
#[derive(
Clone,
Debug,
Default,
kube::CustomResource,
serde::Deserialize,
serde::Serialize,
schemars::JsonSchema,
)]
#[kube(
group = "gateway.networking.k8s.io",
version = "v1alpha2",
kind = "GRPCRoute",
root = "GrpcRoute",
status = "GrpcRouteStatus",
namespaced
)]
pub struct GrpcRouteSpec {
#[serde(flatten)]
pub inner: CommonRouteSpec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub hostnames: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rules: Option<Vec<GrpcRouteRule>>,
}
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema)]
pub struct GrpcRouteStatus {
#[serde(flatten)]
pub inner: RouteStatus,
}
impl From<GrpcRouteStatus> for HttpRouteStatus {
fn from(route: GrpcRouteStatus) -> Self {
Self { inner: route.inner }
}
}
#[derive(
Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
)]
pub struct GrpcRouteRule {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filters: Option<Vec<GrpcRouteFilter>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub matches: Option<Vec<GrpcRouteMatch>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
rename = "backendRefs"
)]
pub backend_refs: Option<Vec<GrpcRouteBackendRef>>,
}
#[derive(
Clone, Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
)]
pub struct GrpcRouteMatch {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_method_match"
)]
pub method: Option<GrpcMethodMatch>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub headers: Option<Vec<GrpcHeaderMatch>>,
}
fn deserialize_method_match<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<GrpcMethodMatch>, D::Error> {
<Option<GrpcMethodMatch> as serde::Deserialize>::deserialize(deserializer).map(|value| {
match value.as_ref() {
Some(rule) if rule.is_empty() => None,
_ => value,
}
})
}
#[allow(unused_qualifications)]
pub type GrpcHeaderMatch = crate::httproute::HttpHeaderMatch;
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, schemars::JsonSchema)]
#[serde(tag = "type", rename_all = "PascalCase")]
pub enum GrpcMethodMatch {
#[serde(rename_all = "camelCase")]
Exact {
#[serde(default, skip_serializing_if = "Option::is_none")]
method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
service: Option<String>,
},
#[serde(rename_all = "camelCase")]
RegularExpression {
#[serde(default, skip_serializing_if = "Option::is_none")]
method: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
service: Option<String>,
},
}
impl GrpcMethodMatch {
fn is_empty(&self) -> bool {
let (method, service) = match self {
Self::Exact { method, service } => (method, service),
Self::RegularExpression { method, service } => (method, service),
};
method.as_deref().map(str::is_empty).unwrap_or(true)
&& service.as_deref().map(str::is_empty).unwrap_or(true)
}
}
fn empty_option_strings_are_none(value: Option<String>) -> Option<String> {
match value.as_ref() {
Some(string) if string.is_empty() => None,
_ => value,
}
}
impl<'de> serde::Deserialize<'de> for GrpcMethodMatch {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Type,
Method,
Service,
}
struct GrpcMethodMatchVisitor;
impl<'de> serde::de::Visitor<'de> for GrpcMethodMatchVisitor {
type Value = GrpcMethodMatch;
fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
formatter.write_str("GrpcMethodMatch")
}
fn visit_map<V>(self, mut map: V) -> Result<GrpcMethodMatch, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let (mut r#type, mut method, mut service) = (None, None, None);
while let Some(key) = map.next_key()? {
match key {
Field::Type => {
if r#type.is_some() {
return Err(serde::de::Error::duplicate_field("type"));
}
r#type = map
.next_value::<Option<String>>()
.map(empty_option_strings_are_none)?;
}
Field::Method => {
if method.is_some() {
return Err(serde::de::Error::duplicate_field("method"));
}
method = map
.next_value::<Option<String>>()
.map(empty_option_strings_are_none)?;
}
Field::Service => {
if service.is_some() {
return Err(serde::de::Error::duplicate_field("service"));
}
service = map
.next_value::<Option<String>>()
.map(empty_option_strings_are_none)?;
}
}
}
match r#type.as_deref() {
None | Some("Exact") => Ok(GrpcMethodMatch::Exact { method, service }),
Some("RegularExpression") => {
Ok(GrpcMethodMatch::RegularExpression { method, service })
}
Some(value) => Err(serde::de::Error::invalid_value(
serde::de::Unexpected::Str(value),
&r#"one of: {"Exact", "RegularExpression"}"#,
)),
}
}
}
const FIELDS: &[&str] = &["type", "method", "service"];
deserializer.deserialize_struct("GrpcMethodMatch", FIELDS, GrpcMethodMatchVisitor)
}
}
#[derive(
Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
)]
#[serde(tag = "type", rename_all = "PascalCase")]
pub enum GrpcRouteFilter {
#[serde(rename_all = "camelCase")]
ExtensionRef { extension_ref: LocalObjectReference },
#[serde(rename_all = "camelCase")]
RequestMirror {
request_mirror: HttpRequestMirrorFilter,
},
#[serde(rename_all = "camelCase")]
RequestHeaderModifier {
request_header_modifier: HttpRequestHeaderFilter,
},
#[serde(rename_all = "camelCase")]
ResponseHeaderModifier {
response_header_modifier: HttpRequestHeaderFilter,
},
}
impl From<GrpcRouteFilter> for HttpRouteFilter {
fn from(filter: GrpcRouteFilter) -> Self {
match filter {
GrpcRouteFilter::ExtensionRef { extension_ref } => Self::ExtensionRef { extension_ref },
GrpcRouteFilter::RequestMirror { request_mirror } => {
Self::RequestMirror { request_mirror }
}
GrpcRouteFilter::RequestHeaderModifier {
request_header_modifier,
} => Self::RequestHeaderModifier {
request_header_modifier,
},
GrpcRouteFilter::ResponseHeaderModifier {
response_header_modifier,
} => Self::ResponseHeaderModifier {
response_header_modifier,
},
}
}
}
#[derive(
Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, schemars::JsonSchema,
)]
pub struct GrpcRouteBackendRef {
#[serde(flatten)]
pub inner: BackendObjectReference,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filters: Option<Vec<GrpcRouteFilter>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub weight: Option<u16>,
}
impl From<GrpcRouteBackendRef> for HttpBackendRef {
fn from(backend: GrpcRouteBackendRef) -> Self {
let filters = backend
.filters
.map(|filters| filters.into_iter().map(Into::into).collect());
Self {
filters,
backend_ref: Some(BackendRef {
inner: backend.inner,
weight: backend.weight,
}),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_grpc_route_deserialization() {
let data = r#"{
"apiVersion": "gateway.networking.k8s.io/v1alpha2",
"kind": "GRPCRoute",
"metadata": {
"name": "grpc-app-1"
},
"spec": {
"parentRefs": [
{
"name": "my-gateway"
}
],
"hostnames": [
"example.com"
],
"rules": [
{
"matches": [
{
"method": {
"service": "com.example.User",
"method": "Login"
}
},
{
"method": {
"service": "com.example.User",
"method": "Logout",
"type": "Exact"
}
},
{
"method": {
"service": "com.example.User",
"method": "UpdateProfile",
"type": "RegularExpression"
}
}
],
"backendRefs": [
{
"name": "my-service1",
"port": 50051
}
]
},
{
"matches": [
{
"headers": [
{
"type": "Exact",
"name": "magic",
"value": "foo"
}
],
"method": {
"service": "com.example.Things",
"method": "DoThing"
}
}
],
"backendRefs": [
{
"name": "my-service2",
"port": 50051
}
]
}
]
}
}"#;
let route = serde_json::from_str::<GrpcRoute>(data);
assert!(route.is_ok());
}
}