From 2142e7bc17e6961c5ab82449ebb45c09dde1fd17 Mon Sep 17 00:00:00 2001 From: Alex Leong Date: Wed, 3 Jul 2024 18:41:58 -0700 Subject: [PATCH] Add gprcroute support to inbound policy API (#12785) 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 --- policy-controller/core/src/inbound.rs | 31 +++- policy-controller/grpc/src/inbound.rs | 7 +- policy-controller/grpc/src/inbound/grpc.rs | 116 +++++++++++++ .../index/src/inbound/authorization_policy.rs | 8 + .../k8s/index/src/inbound/index.rs | 162 +++++++++++++++++- .../k8s/index/src/inbound/index/grpc.rs | 119 +++++++++++++ .../k8s/index/src/inbound/server.rs | 2 +- .../k8s/index/src/inbound/tests.rs | 12 +- .../k8s/index/src/inbound/tests/annotation.rs | 1 + .../src/inbound/tests/authorization_policy.rs | 7 + .../index/src/inbound/tests/grpc_routes.rs | 39 +++++ .../index/src/inbound/tests/http_routes.rs | 1 + .../src/inbound/tests/server_authorization.rs | 1 + policy-controller/src/admission.rs | 6 +- policy-controller/src/main.rs | 1 + 15 files changed, 501 insertions(+), 12 deletions(-) create mode 100644 policy-controller/grpc/src/inbound/grpc.rs create mode 100644 policy-controller/k8s/index/src/inbound/index/grpc.rs create mode 100644 policy-controller/k8s/index/src/inbound/tests/grpc_routes.rs diff --git a/policy-controller/core/src/inbound.rs b/policy-controller/core/src/inbound.rs index c50663552f5cc..d6ba1cc139f8c 100644 --- a/policy-controller/core/src/inbound.rs +++ b/policy-controller/core/src/inbound.rs @@ -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; @@ -90,9 +90,11 @@ pub struct InboundServer { pub protocol: ProxyProtocol, pub authorizations: HashMap, pub http_routes: HashMap>, + pub grpc_routes: HashMap>, } pub type HttpRoute = InboundRoute; +pub type GrpcRoute = InboundRoute; #[derive(Clone, Debug, PartialEq, Eq)] pub struct InboundRoute { @@ -145,6 +147,31 @@ impl Default for InboundRoute { } } +/// The default `InboundRoute` used for any `InboundServer` that +/// does not have routes. +impl Default for InboundRoute { + 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 { diff --git a/policy-controller/grpc/src/inbound.rs b/policy-controller/grpc/src/inbound.rs index 5c2ddbf94c1ce..1111bd11bf098 100644 --- a/policy-controller/grpc/src/inbound.rs +++ b/policy-controller/grpc/src/inbound.rs @@ -19,6 +19,7 @@ use maplit::*; use std::{num::NonZeroU16, str::FromStr, sync::Arc}; use tracing::trace; +mod grpc; mod http; #[derive(Clone, Debug)] @@ -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( diff --git a/policy-controller/grpc/src/inbound/grpc.rs b/policy-controller/grpc/src/inbound/grpc.rs new file mode 100644 index 0000000000000..cddc248fd833f --- /dev/null +++ b/policy-controller/grpc/src/inbound/grpc.rs @@ -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, + cluster_networks: &[IpNet], +) -> Vec { + // 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::>(); + 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 { + 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) }) +} diff --git a/policy-controller/k8s/index/src/inbound/authorization_policy.rs b/policy-controller/k8s/index/src/inbound/authorization_policy.rs index 0598c25fef784..5c03c912c45ab 100644 --- a/policy-controller/k8s/index/src/inbound/authorization_policy.rs +++ b/policy-controller/k8s/index/src/inbound/authorization_policy.rs @@ -15,6 +15,7 @@ pub(crate) struct Spec { #[derive(Debug, PartialEq)] pub(crate) enum Target { HttpRoute(GroupKindName), + GrpcRoute(GroupKindName), Server(String), Namespace, } @@ -73,6 +74,13 @@ fn target(t: LocalTargetRef) -> Result { name: t.name.into(), })) } + t if t.targets_kind::() => { + 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() diff --git a/policy-controller/k8s/index/src/inbound/index.rs b/policy-controller/k8s/index/src/inbound/index.rs index 5e90db4c2982c..a374010014f24 100644 --- a/policy-controller/k8s/index/src/inbound/index.rs +++ b/policy-controller/k8s/index/src/inbound/index.rs @@ -19,8 +19,8 @@ use ahash::{AHashMap as HashMap, AHashSet as HashSet}; use anyhow::{anyhow, bail, Result}; use linkerd_policy_controller_core::{ inbound::{ - AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, InboundRouteRule, - InboundServer, ProxyProtocol, RouteRef, ServerRef, + AuthorizationRef, ClientAuthentication, ClientAuthorization, GrpcRoute, HttpRoute, + InboundRouteRule, InboundServer, ProxyProtocol, RouteRef, ServerRef, }, routes::{GroupKindName, HttpRouteMatch, Method, PathMatch}, IdentityMatch, Ipv4Net, Ipv6Net, NetworkMatch, @@ -38,6 +38,7 @@ use std::{ use tokio::sync::watch; use tracing::info_span; +mod grpc; mod http; pub mod metrics; @@ -162,6 +163,7 @@ struct PolicyIndex { authorization_policies: HashMap, http_routes: HashMap>, + grpc_routes: HashMap>, } #[derive(Debug, Default)] @@ -360,6 +362,91 @@ impl Index { let _span = info_span!("delete httproute", %ns, route = ?gkn).entered(); self.ns_with_reindex(ns, |ns| ns.policy.http_routes.remove(&gkn).is_some()) } + + fn apply_grpc_route(&mut self, route: k8s_gateway_api::GrpcRoute) { + let ns = route.namespace().expect("GrpcRoute must have a namespace"); + let name = route.name_unchecked(); + let gkn = route.gkn(); + let _span = info_span!("apply grpcroute", %ns, %name).entered(); + + let route_binding = match route.try_into() { + Ok(binding) => binding, + Err(error) => { + tracing::info!(%ns, %name, %error, "Ignoring GrpcRoute"); + return; + } + }; + + self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_grpc_route(gkn, route_binding)) + } + + #[tracing::instrument(skip_all)] + fn reset_grpc_route( + &mut self, + routes: Vec, + deleted: HashMap>, + ) { + // Aggregate all of the updates by namespace so that we only reindex + // once per namespace. + type Ns = NsUpdate>; + let mut updates_by_ns = HashMap::::default(); + for route in routes.into_iter() { + let namespace = route.namespace().expect("GrpcRoute must be namespaced"); + let name = route.name_unchecked(); + let gkn = route.gkn(); + let route_binding = match route.try_into() { + Ok(binding) => binding, + Err(error) => { + tracing::info!(ns = %namespace, %name, %error, "Ignoring GrpcRoute"); + continue; + } + }; + updates_by_ns + .entry(namespace) + .or_default() + .added + .push((gkn, route_binding)); + } + for (ns, names) in deleted.into_iter() { + let removed = names + .into_iter() + .map(|name| name.gkn::()) + .collect(); + updates_by_ns.entry(ns).or_default().removed = removed; + } + + for (namespace, Ns { added, removed }) in updates_by_ns.into_iter() { + if added.is_empty() { + // If there are no live resources in the namespace, we do not + // want to create a default namespace instance, we just want to + // clear out all resources for the namespace (and then drop the + // whole namespace, if necessary). + self.ns_with_reindex(namespace, |ns| { + ns.policy.grpc_routes.clear(); + true + }); + } else { + // Otherwise, we take greater care to reindex only when the + // state actually changed. The vast majority of resets will see + // no actual data change. + self.ns_or_default_with_reindex(namespace, |ns| { + let mut changed = !removed.is_empty(); + for gkn in removed.into_iter() { + ns.policy.grpc_routes.remove(&gkn); + } + for (gkn, route_binding) in added.into_iter() { + changed = ns.policy.update_grpc_route(gkn, route_binding) || changed; + } + changed + }); + } + } + } + + fn delete_grpc_route(&mut self, ns: String, gkn: GroupKindName) { + let _span = info_span!("delete grpcroute", %ns, route = ?gkn).entered(); + self.ns_with_reindex(ns, |ns| ns.policy.grpc_routes.remove(&gkn).is_some()) + } } impl kubert::index::IndexNamespacedResource for Index { @@ -887,6 +974,25 @@ impl kubert::index::IndexNamespacedResource for Inde } } +impl kubert::index::IndexNamespacedResource for Index { + fn apply(&mut self, route: k8s_gateway_api::GrpcRoute) { + self.apply_grpc_route(route) + } + + fn delete(&mut self, ns: String, name: String) { + let gkn = name.gkn::(); + self.delete_grpc_route(ns, gkn) + } + + fn reset( + &mut self, + routes: Vec, + deleted: HashMap>, + ) { + self.reset_grpc_route(routes, deleted) + } +} + // === impl NemspaceIndex === impl NamespaceIndex { @@ -954,6 +1060,7 @@ impl Namespace { server_authorizations: HashMap::default(), authorization_policies: HashMap::default(), http_routes: HashMap::default(), + grpc_routes: HashMap::default(), }, } } @@ -1555,6 +1662,7 @@ impl PolicyIndex { protocol, authorizations, http_routes, + grpc_routes: HashMap::default(), } } @@ -1568,12 +1676,14 @@ impl PolicyIndex { tracing::trace!(%name, ?server, "Creating inbound server"); let authorizations = self.client_authzs(&name, server, authentications); let http_routes = self.http_routes(&name, authentications, probe_paths); + let grpc_routes = self.grpc_routes(&name, authentications); InboundServer { reference: ServerRef::Server(name), authorizations, protocol: server.protocol.clone(), http_routes, + grpc_routes, } } @@ -1609,7 +1719,8 @@ impl PolicyIndex { } } authorization_policy::Target::Namespace => {} - authorization_policy::Target::HttpRoute(_) => { + authorization_policy::Target::HttpRoute(_) + | authorization_policy::Target::GrpcRoute(_) => { // Policies which target routes will be attached to // the route authorizations and should not be included in // the server authorizations. @@ -1663,6 +1774,14 @@ impl PolicyIndex { "AuthorizationPolicy targets HttpRoute", ); } + authorization_policy::Target::GrpcRoute(n) if n.eq_ignore_ascii_case(gkn) => { + tracing::trace!( + ns = %self.namespace, + authorizationpolicy = %name, + route = ?gkn, + "AuthorizationPolicy targets GrpcRoute", + ); + } _ => { tracing::trace!( ns = %self.namespace, @@ -1720,6 +1839,28 @@ impl PolicyIndex { self.cluster_info.default_inbound_http_routes(probe_paths) } + fn grpc_routes( + &self, + server_name: &str, + authentications: &AuthenticationNsIndex, + ) -> HashMap { + let routes = self + .grpc_routes + .iter() + .filter(|(_, route)| route.selects_server(server_name)) + .filter(|(_, route)| route.accepted_by_server(server_name)) + .map(|(gkn, route)| { + let mut route = route.route.clone(); + route.authorizations = self.route_client_authzs(gkn, authentications); + (RouteRef::Resource(gkn.clone()), route) + }) + .collect::>(); + if !routes.is_empty() { + return routes; + } + [(RouteRef::Default("default"), GrpcRoute::default())].into() + } + fn policy_client_authz( &self, spec: &authorization_policy::Spec, @@ -1839,6 +1980,21 @@ impl PolicyIndex { } true } + + fn update_grpc_route(&mut self, gkn: GroupKindName, route: RouteBinding) -> bool { + match self.grpc_routes.entry(gkn) { + Entry::Vacant(entry) => { + entry.insert(route); + } + Entry::Occupied(mut entry) => { + if *entry.get() == route { + return false; + } + entry.insert(route); + } + } + true + } } // === impl AuthenticationNsIndex === diff --git a/policy-controller/k8s/index/src/inbound/index/grpc.rs b/policy-controller/k8s/index/src/inbound/index/grpc.rs new file mode 100644 index 0000000000000..7908816ac1a39 --- /dev/null +++ b/policy-controller/k8s/index/src/inbound/index/grpc.rs @@ -0,0 +1,119 @@ +use crate::inbound::routes::{ParentRef, RouteBinding, Status}; +use ahash::AHashMap as HashMap; +use anyhow::{bail, Error, Result}; +use linkerd_policy_controller_core::{ + inbound::{Filter, GrpcRoute, InboundRoute, InboundRouteRule}, + routes::{GrpcMethodMatch, GrpcRouteMatch}, +}; +use linkerd_policy_controller_k8s_api::{self as k8s, gateway}; + +impl TryFrom for RouteBinding { + type Error = Error; + + fn try_from(route: gateway::GrpcRoute) -> Result { + let route_ns = route.metadata.namespace.as_deref(); + let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t); + let parents = ParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?; + let hostnames = route + .spec + .hostnames + .into_iter() + .flatten() + .map(crate::routes::http::host_match) + .collect(); + + let rules = route + .spec + .rules + .into_iter() + .flatten() + .map( + |gateway::GrpcRouteRule { + matches, filters, .. + }| { try_grpc_rule(matches, filters, try_grpc_filter) }, + ) + .collect::>()?; + + let statuses = route + .status + .map_or_else(Vec::new, |status| Status::collect_from(status.inner)); + + Ok(RouteBinding { + parents, + route: InboundRoute { + hostnames, + rules, + authorizations: HashMap::default(), + creation_timestamp, + }, + statuses, + }) + } +} + +pub fn try_grpc_match( + gateway::GrpcRouteMatch { headers, method }: gateway::GrpcRouteMatch, +) -> Result { + let headers = headers + .into_iter() + .flatten() + .map(crate::routes::http::header_match) + .collect::>()?; + + let method = match method { + Some(gateway::GrpcMethodMatch::Exact { method, service }) => { + Some(GrpcMethodMatch { method, service }) + } + Some(gateway::GrpcMethodMatch::RegularExpression { .. }) => { + bail!("Regular expression gRPC method match is not supported") + } + None => None, + }; + + Ok(GrpcRouteMatch { headers, method }) +} + +fn try_grpc_rule( + matches: Option>, + filters: Option>, + try_filter: impl Fn(F) -> Result, +) -> Result> { + let matches = matches + .into_iter() + .flatten() + .map(try_grpc_match) + .collect::>()?; + + let filters = filters + .into_iter() + .flatten() + .map(try_filter) + .collect::>()?; + + Ok(InboundRouteRule { matches, filters }) +} + +fn try_grpc_filter(filter: gateway::GrpcRouteFilter) -> Result { + let filter = match filter { + gateway::GrpcRouteFilter::RequestHeaderModifier { + request_header_modifier, + } => { + let filter = crate::routes::http::header_modifier(request_header_modifier)?; + Filter::RequestHeaderModifier(filter) + } + + gateway::GrpcRouteFilter::ResponseHeaderModifier { + response_header_modifier, + } => { + let filter = crate::routes::http::header_modifier(response_header_modifier)?; + Filter::ResponseHeaderModifier(filter) + } + gateway::GrpcRouteFilter::RequestMirror { .. } => { + bail!("RequestMirror filter is not supported") + } + gateway::GrpcRouteFilter::ExtensionRef { .. } => { + bail!("ExtensionRef filter is not supported") + } + }; + Ok(filter) +} diff --git a/policy-controller/k8s/index/src/inbound/server.rs b/policy-controller/k8s/index/src/inbound/server.rs index d017655e0ac00..04c58d47e3729 100644 --- a/policy-controller/k8s/index/src/inbound/server.rs +++ b/policy-controller/k8s/index/src/inbound/server.rs @@ -34,7 +34,7 @@ fn proxy_protocol( }, Some(k8s::policy::server::ProxyProtocol::Http1) => ProxyProtocol::Http1, Some(k8s::policy::server::ProxyProtocol::Http2) => ProxyProtocol::Http2, - Some(k8s::policy::server::ProxyProtocol::Grpc) => ProxyProtocol::Http2, + Some(k8s::policy::server::ProxyProtocol::Grpc) => ProxyProtocol::Grpc, Some(k8s::policy::server::ProxyProtocol::Opaque) => ProxyProtocol::Opaque, Some(k8s::policy::server::ProxyProtocol::Tls) => ProxyProtocol::Tls, } diff --git a/policy-controller/k8s/index/src/inbound/tests.rs b/policy-controller/k8s/index/src/inbound/tests.rs index a14d31cdc8459..d9b835e8e8b41 100644 --- a/policy-controller/k8s/index/src/inbound/tests.rs +++ b/policy-controller/k8s/index/src/inbound/tests.rs @@ -1,5 +1,6 @@ mod annotation; mod authorization_policy; +mod grpc_routes; mod http_routes; mod server_authorization; @@ -13,8 +14,8 @@ use ahash::AHashMap as HashMap; use kubert::index::IndexNamespacedResource; use linkerd_policy_controller_core::{ inbound::{ - AuthorizationRef, ClientAuthentication, ClientAuthorization, HttpRoute, InboundServer, - ProxyProtocol, RouteRef, ServerRef, + AuthorizationRef, ClientAuthentication, ClientAuthorization, GrpcRoute, HttpRoute, + InboundServer, ProxyProtocol, RouteRef, ServerRef, }, IdentityMatch, IpNet, Ipv4Net, Ipv6Net, NetworkMatch, }; @@ -187,6 +188,12 @@ fn mk_default_http_routes() -> HashMap { .collect() } +fn mk_default_grpc_routes() -> HashMap { + Some((RouteRef::Default("default"), GrpcRoute::default())) + .into_iter() + .collect() +} + impl TestConfig { fn from_default_policy(default_policy: DefaultPolicy) -> Self { Self::from_default_policy_with_probes(default_policy, vec![]) @@ -227,6 +234,7 @@ impl TestConfig { timeout: self.detect_timeout, }, http_routes: mk_default_http_routes(), + grpc_routes: Default::default(), } } diff --git a/policy-controller/k8s/index/src/inbound/tests/annotation.rs b/policy-controller/k8s/index/src/inbound/tests/annotation.rs index ec26eb59e0d13..61a25a26f6767 100644 --- a/policy-controller/k8s/index/src/inbound/tests/annotation.rs +++ b/policy-controller/k8s/index/src/inbound/tests/annotation.rs @@ -117,6 +117,7 @@ fn authenticated_annotated() { timeout: test.detect_timeout, }, http_routes: mk_default_http_routes(), + grpc_routes: Default::default(), } }; diff --git a/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs b/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs index 018ab4aaa2a43..c1caec78133ef 100644 --- a/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs +++ b/policy-controller/k8s/index/src/inbound/tests/authorization_policy.rs @@ -35,6 +35,7 @@ fn links_authorization_policy_with_mtls_name() { authorizations: Default::default(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); @@ -89,6 +90,7 @@ fn links_authorization_policy_with_mtls_name() { .collect(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); } @@ -125,6 +127,7 @@ fn authorization_targets_namespace() { authorizations: Default::default(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); @@ -179,6 +182,7 @@ fn authorization_targets_namespace() { .collect(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); } @@ -215,6 +219,7 @@ fn links_authorization_policy_with_service_account() { authorizations: Default::default(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); @@ -263,6 +268,7 @@ fn links_authorization_policy_with_service_account() { .collect(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); } @@ -386,6 +392,7 @@ fn authorization_policy_prevents_index_deletion() { }) .into_iter() .collect(), + grpc_routes: mk_default_grpc_routes(), }, ); } diff --git a/policy-controller/k8s/index/src/inbound/tests/grpc_routes.rs b/policy-controller/k8s/index/src/inbound/tests/grpc_routes.rs new file mode 100644 index 0000000000000..02c26ab757fae --- /dev/null +++ b/policy-controller/k8s/index/src/inbound/tests/grpc_routes.rs @@ -0,0 +1,39 @@ +use super::*; + +#[test] +fn server_with_default_route() { + let test = TestConfig::default(); + // Create pod. + let mut pod = mk_pod("ns-0", "pod-0", Some(("container-0", None))); + pod.labels_mut() + .insert("app".to_string(), "app-0".to_string()); + test.index.write().apply(pod); + + let mut rx = test + .index + .write() + .pod_server_rx("ns-0", "pod-0", 8080.try_into().unwrap()) + .expect("pod-0.ns-0 should exist"); + assert_eq!(*rx.borrow_and_update(), test.default_server()); + + // Create server. + test.index.write().apply(mk_server( + "ns-0", + "srv-8080", + Port::Number(8080.try_into().unwrap()), + Some(("app", "app-0")), + Some(("app", "app-0")), + Some(k8s::policy::server::ProxyProtocol::Grpc), + )); + assert!(rx.has_changed().unwrap()); + assert_eq!( + *rx.borrow_and_update(), + InboundServer { + reference: ServerRef::Server("srv-8080".to_string()), + authorizations: Default::default(), + protocol: ProxyProtocol::Grpc, + http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), + }, + ); +} diff --git a/policy-controller/k8s/index/src/inbound/tests/http_routes.rs b/policy-controller/k8s/index/src/inbound/tests/http_routes.rs index a508458f2e7a1..c494bdc5ced20 100644 --- a/policy-controller/k8s/index/src/inbound/tests/http_routes.rs +++ b/policy-controller/k8s/index/src/inbound/tests/http_routes.rs @@ -41,6 +41,7 @@ fn route_attaches_to_server() { authorizations: Default::default(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); diff --git a/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs b/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs index b50ee5230474f..7bf6b681fadd3 100644 --- a/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs +++ b/policy-controller/k8s/index/src/inbound/tests/server_authorization.rs @@ -44,6 +44,7 @@ fn link_server_authz(selector: ServerSelector) { authorizations: Default::default(), protocol: ProxyProtocol::Http1, http_routes: mk_default_http_routes(), + grpc_routes: mk_default_grpc_routes(), }, ); test.index.write().apply(mk_server_authz( diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs index 0395538207ad8..237a4cdd5e803 100644 --- a/policy-controller/src/admission.rs +++ b/policy-controller/src/admission.rs @@ -11,7 +11,7 @@ use hyper::{body::Buf, http, Body, Request, Response}; use k8s_openapi::api::core::v1::{Namespace, ServiceAccount}; use kube::{core::DynamicObject, Resource, ResourceExt}; use linkerd_policy_controller_core as core; -use linkerd_policy_controller_k8s_api::gateway::{self as k8s_gateway_api}; +use linkerd_policy_controller_k8s_api::gateway::{self as k8s_gateway_api, GrpcRoute}; use linkerd_policy_controller_k8s_index as index; use serde::de::DeserializeOwned; use std::task; @@ -212,6 +212,10 @@ fn validate_policy_target(ns: &str, tgt: &LocalTargetRef) -> Result<()> { return Ok(()); } + if tgt.targets_kind::() { + return Ok(()); + } + if tgt.targets_kind::() { if tgt.name != ns { bail!("cannot target another namespace: {}", tgt.name); diff --git a/policy-controller/src/main.rs b/policy-controller/src/main.rs index 594bf2b46f470..999a196892e8c 100644 --- a/policy-controller/src/main.rs +++ b/policy-controller/src/main.rs @@ -263,6 +263,7 @@ async fn main() -> Result<()> { let gateway_grpc_routes = runtime.watch_all::(watcher::Config::default()); let gateway_grpc_routes_indexes = IndexList::new(outbound_index.clone()) + .push(inbound_index.clone()) .push(status_index.clone()) .shared(); tokio::spawn(