Skip to content

Commit

Permalink
Add gprcroute support to inbound policy API (#12785)
Browse files Browse the repository at this point in the history
We add support for grpcroute in the inbound policy API.  When a Server resource has the proxy protocol set to grpc, we will now serve grpc as the protocol in the inbound policy API along with any GrpcRoutes which have been defined and attached to the Server.  If grpc is specified as the proxy protocol but no GrpcRoutes are attached, we return a default catch-all grpc route.

Signed-off-by: Alex Leong <[email protected]>
  • Loading branch information
adleong authored Jul 4, 2024
1 parent 5868d42 commit 2142e7b
Show file tree
Hide file tree
Showing 15 changed files with 501 additions and 12 deletions.
31 changes: 29 additions & 2 deletions policy-controller/core/src/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use crate::{
identity_match::IdentityMatch,
network_match::NetworkMatch,
routes::{
FailureInjectorFilter, GroupKindName, HeaderModifierFilter, HostMatch, HttpRouteMatch,
PathMatch, RequestRedirectFilter,
FailureInjectorFilter, GroupKindName, GrpcMethodMatch, GrpcRouteMatch,
HeaderModifierFilter, HostMatch, HttpRouteMatch, PathMatch, RequestRedirectFilter,
},
};
use ahash::AHashMap as HashMap;
Expand Down Expand Up @@ -90,9 +90,11 @@ pub struct InboundServer {
pub protocol: ProxyProtocol,
pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
pub http_routes: HashMap<RouteRef, InboundRoute<HttpRouteMatch>>,
pub grpc_routes: HashMap<RouteRef, InboundRoute<GrpcRouteMatch>>,
}

pub type HttpRoute = InboundRoute<HttpRouteMatch>;
pub type GrpcRoute = InboundRoute<GrpcRouteMatch>;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InboundRoute<M> {
Expand Down Expand Up @@ -145,6 +147,31 @@ impl Default for InboundRoute<HttpRouteMatch> {
}
}

/// The default `InboundRoute` used for any `InboundServer` that
/// does not have routes.
impl Default for InboundRoute<GrpcRouteMatch> {
fn default() -> Self {
Self {
hostnames: vec![],
rules: vec![InboundRouteRule {
matches: vec![GrpcRouteMatch {
headers: vec![],
method: Some(GrpcMethodMatch {
method: None,
service: None,
}),
}],
filters: vec![],
}],
// Default routes do not have authorizations; the default policy's
// authzs will be configured by the default `InboundServer`, not by
// the route.
authorizations: HashMap::new(),
creation_timestamp: None,
}
}
}

// === impl InboundHttpRouteRef ===

impl Ord for RouteRef {
Expand Down
7 changes: 4 additions & 3 deletions policy-controller/grpc/src/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use maplit::*;
use std::{num::NonZeroU16, str::FromStr, sync::Arc};
use tracing::trace;

mod grpc;
mod http;

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -158,9 +159,9 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
routes: http::to_route_list(&srv.http_routes, cluster_networks),
},
)),
ProxyProtocol::Grpc => Some(proto::proxy_protocol::Kind::Http2(
proto::proxy_protocol::Http2 {
routes: http::to_route_list(&srv.http_routes, cluster_networks),
ProxyProtocol::Grpc => Some(proto::proxy_protocol::Kind::Grpc(
proto::proxy_protocol::Grpc {
routes: grpc::to_route_list(&srv.grpc_routes, cluster_networks),
},
)),
ProxyProtocol::Opaque => Some(proto::proxy_protocol::Kind::Opaque(
Expand Down
116 changes: 116 additions & 0 deletions policy-controller/grpc/src/inbound/grpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use linkerd2_proxy_api::{inbound, meta};
use linkerd_policy_controller_core::{
inbound::{Filter, GrpcRoute, InboundRouteRule, RouteRef},
IpNet,
};

use crate::routes;

use super::to_authz;

pub(crate) fn to_route_list<'r>(
routes: impl IntoIterator<Item = (&'r RouteRef, &'r GrpcRoute)>,
cluster_networks: &[IpNet],
) -> Vec<inbound::GrpcRoute> {
// Per the Gateway API spec:
//
// > If ties still exist across multiple Routes, matching precedence MUST be
// > determined in order of the following criteria, continuing on ties:
// >
// > The oldest Route based on creation timestamp.
// > The Route appearing first in alphabetical order by
// > "{namespace}/{name}".
//
// Note that we don't need to include the route's namespace in this
// comparison, because all these routes will exist in the same
// namespace.
let mut route_list = routes.into_iter().collect::<Vec<_>>();
route_list.sort_by(|(a_ref, a), (b_ref, b)| {
let by_ts = match (&a.creation_timestamp, &b.creation_timestamp) {
(Some(a_ts), Some(b_ts)) => a_ts.cmp(b_ts),
(None, None) => std::cmp::Ordering::Equal,
// Routes with timestamps are preferred over routes without.
(Some(_), None) => return std::cmp::Ordering::Less,
(None, Some(_)) => return std::cmp::Ordering::Greater,
};
by_ts.then_with(|| a_ref.cmp(b_ref))
});

route_list
.into_iter()
.map(|(route_ref, route)| to_grpc_route(route_ref, route.clone(), cluster_networks))
.collect()
}

fn to_grpc_route(
reference: &RouteRef,
GrpcRoute {
hostnames,
rules,
authorizations,
creation_timestamp: _,
}: GrpcRoute,
cluster_networks: &[IpNet],
) -> inbound::GrpcRoute {
let metadata = match reference {
RouteRef::Resource(gkn) => meta::Metadata {
kind: Some(meta::metadata::Kind::Resource(meta::Resource {
group: gkn.group.to_string(),
kind: gkn.kind.to_string(),
name: gkn.name.to_string(),
..Default::default()
})),
},
RouteRef::Default(name) => meta::Metadata {
kind: Some(meta::metadata::Kind::Default(name.to_string())),
},
};

let hosts = hostnames
.into_iter()
.map(routes::convert_host_match)
.collect();

let rules = rules
.into_iter()
.map(
|InboundRouteRule { matches, filters }| inbound::grpc_route::Rule {
matches: matches
.into_iter()
.map(routes::grpc::convert_match)
.collect(),
filters: filters
.into_iter()
.filter_map(convert_grpc_filter)
.collect(),
},
)
.collect();

let authorizations = authorizations
.iter()
.map(|(n, c)| to_authz(n, c, cluster_networks))
.collect();

inbound::GrpcRoute {
metadata: Some(metadata),
hosts,
rules,
authorizations,
}
}

fn convert_grpc_filter(filter: Filter) -> Option<inbound::grpc_route::Filter> {
use inbound::grpc_route::filter::Kind;

let kind = match filter {
Filter::FailureInjector(_) => None,
Filter::RequestHeaderModifier(f) => Some(Kind::RequestHeaderModifier(
routes::convert_request_header_modifier_filter(f),
)),
Filter::ResponseHeaderModifier(_) => None,
Filter::RequestRedirect(_) => None,
};

kind.map(|kind| inbound::grpc_route::Filter { kind: Some(kind) })
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ pub(crate) struct Spec {
#[derive(Debug, PartialEq)]
pub(crate) enum Target {
HttpRoute(GroupKindName),
GrpcRoute(GroupKindName),
Server(String),
Namespace,
}
Expand Down Expand Up @@ -73,6 +74,13 @@ fn target(t: LocalTargetRef) -> Result<Target> {
name: t.name.into(),
}))
}
t if t.targets_kind::<k8s_gateway_api::GrpcRoute>() => {
Ok(Target::GrpcRoute(GroupKindName {
group: t.group.unwrap_or_default().into(),
kind: t.kind.into(),
name: t.name.into(),
}))
}
_ => anyhow::bail!(
"unsupported authorization target type: {}",
t.canonical_kind()
Expand Down
Loading

0 comments on commit 2142e7b

Please sign in to comment.