policy: Provide a default route for servers with no HTTPRoutes (#9036)

Currently, if no `HTTPRoute` resources reference a `Server`, the policy
controller returns an empty list of routes. This results in the proxy
404ing HTTP traffic. Instead, we should return a default empty route in
this case.

This branch changes the policy controller to return a default route for
`Server`s which are not referenced by any `HTTPRoute`s. The default
route defines a single `HttpRouteRule` with no matches and no filters,
so it will match any HTTP request. The default route does *not* define
any authorizations, as they would potentially clobber authorizations
defined by other resources that define authorization policies targeting
that `Server` --- if it is not targeted by any other resource defining
authorization policies, the `Server` itself will still get the default
authz policy.

In addition, this branch changes the various `${Resource}Ref` enums so
that their `Default` variants take a `&'static str` rather than a
`String`, to minimize string copying, since the default names are always
pre-defined. It also adds an `InboundHttpRouteRef` type which is used as
a key in the maps of `HTTPRoute`s (instead of `String`s), since we now
define default routes as well.

Signed-off-by: Eliza Weisman <eliza@buoyant.io>
Co-authored-by: Oliver Gould <ver@buoyant.io>
This commit is contained in:
Oliver Gould 2022-07-29 14:24:52 -07:00 committed by GitHub
parent 18716ca206
commit 75bbbb9146
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 308 additions and 177 deletions

View File

@ -18,7 +18,7 @@ pub struct InboundHttpRoute {
/// This is required for ordering returned `HttpRoute`s by their creation
/// timestamp.
pub creation_timestamp: DateTime<Utc>,
pub creation_timestamp: Option<DateTime<Utc>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -102,6 +102,32 @@ pub enum QueryParamMatch {
Regex(String, Regex),
}
// === impl InboundHttpRoute ===
/// The default `InboundHttpRoute` used for any `InboundServer` that
/// does not have routes.
impl Default for InboundHttpRoute {
fn default() -> Self {
Self {
hostnames: vec![],
rules: vec![InboundHttpRouteRule {
matches: vec![HttpRouteMatch {
path: Some(PathMatch::Prefix("/".to_string())),
headers: vec![],
query_params: vec![],
method: 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 PathMatch ===
impl PartialEq for PathMatch {

View File

@ -31,22 +31,28 @@ pub struct InboundServer {
pub protocol: ProxyProtocol,
pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
pub http_routes: HashMap<String, InboundHttpRoute>,
pub http_routes: HashMap<InboundHttpRouteRef, InboundHttpRoute>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ServerRef {
Default(String),
Default(&'static str),
Server(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum AuthorizationRef {
Default(String),
Default(&'static str),
ServerAuthorization(String),
AuthorizationPolicy(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum InboundHttpRouteRef {
Default(&'static str),
Linkerd(String),
}
/// Describes how a proxy should handle inbound connections.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ProxyProtocol {
@ -87,3 +93,24 @@ pub enum ClientAuthentication {
/// Indicates that clients must use mutually-authenticated TLS.
TlsAuthenticated(Vec<IdentityMatch>),
}
// === impl InboundHttpRouteRef ===
impl Ord for InboundHttpRouteRef {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Self::Default(a), Self::Default(b)) => a.cmp(b),
(Self::Linkerd(a), Self::Linkerd(b)) => a.cmp(b),
// Route resources are always preferred over default resources, so they should sort
// first in a list.
(Self::Linkerd(_), Self::Default(_)) => std::cmp::Ordering::Less,
(Self::Default(_), Self::Linkerd(_)) => std::cmp::Ordering::Greater,
}
}
}
impl PartialOrd for InboundHttpRouteRef {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

View File

@ -15,8 +15,8 @@ use linkerd2_proxy_api::{
use linkerd_policy_controller_core::{
http_route::{InboundFilter, InboundHttpRoute, InboundHttpRouteRule},
AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer,
IdentityMatch, InboundServer, InboundServerStream, IpNet, NetworkMatch, ProxyProtocol,
ServerRef,
IdentityMatch, InboundHttpRouteRef, InboundServer, InboundServerStream, IpNet, NetworkMatch,
ProxyProtocol, ServerRef,
};
use maplit::*;
use std::{num::NonZeroU16, sync::Arc};
@ -211,7 +211,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
ServerRef::Default(name) => convert_args!(hashmap!(
"group" => "",
"kind" => "default",
"name" => name,
"name" => *name,
)),
ServerRef::Server(name) => convert_args!(hashmap!(
"group" => "policy.linkerd.io",
@ -239,12 +239,12 @@ fn to_authz(
) -> proto::Authz {
let meta = Metadata {
kind: Some(match reference {
AuthorizationRef::Default(name) => metadata::Kind::Default(name.clone()),
AuthorizationRef::Default(name) => metadata::Kind::Default(name.to_string()),
AuthorizationRef::AuthorizationPolicy(name) => {
metadata::Kind::Resource(api::meta::Resource {
group: "policy.linkerd.io".to_string(),
kind: "authorizationpolicy".to_string(),
name: name.clone(),
name: name.to_string(),
})
}
AuthorizationRef::ServerAuthorization(name) => {
@ -263,7 +263,7 @@ fn to_authz(
AuthorizationRef::Default(name) => convert_args!(hashmap!(
"group" => "",
"kind" => "default",
"name" => name,
"name" => *name,
)),
AuthorizationRef::ServerAuthorization(name) => convert_args!(hashmap!(
"group" => "policy.linkerd.io",
@ -353,7 +353,7 @@ fn to_authz(
}
fn to_http_route_list<'r>(
routes: impl IntoIterator<Item = (&'r String, &'r InboundHttpRoute)>,
routes: impl IntoIterator<Item = (&'r InboundHttpRouteRef, &'r InboundHttpRoute)>,
cluster_networks: &[IpNet],
) -> Vec<proto::HttpRoute> {
// Per the Gateway API spec:
@ -369,20 +369,25 @@ fn to_http_route_list<'r>(
// 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_name, a), (b_name, b)| {
a.creation_timestamp
.cmp(&b.creation_timestamp)
.then_with(|| a_name.cmp(b_name))
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(|(name, route)| to_http_route(name, route.clone(), cluster_networks))
.map(|(route_ref, route)| to_http_route(route_ref, route.clone(), cluster_networks))
.collect()
}
fn to_http_route(
name: impl ToString,
name: &InboundHttpRouteRef,
InboundHttpRoute {
hostnames,
rules,
@ -392,11 +397,14 @@ fn to_http_route(
cluster_networks: &[IpNet],
) -> proto::HttpRoute {
let metadata = Metadata {
kind: Some(metadata::Kind::Resource(api::meta::Resource {
group: "policy.linkerd.io".to_string(),
kind: "HTTPRoute".to_string(),
name: name.to_string(),
})),
kind: Some(match name {
InboundHttpRouteRef::Default(name) => metadata::Kind::Default(name.to_string()),
InboundHttpRouteRef::Linkerd(name) => metadata::Kind::Resource(api::meta::Resource {
group: "policy.linkerd.io".to_string(),
kind: "HTTPRoute".to_string(),
name: name.to_string(),
}),
}),
};
let hosts = hostnames

View File

@ -1,6 +1,12 @@
use ahash::AHashMap as HashMap;
use anyhow::{anyhow, Error, Result};
use linkerd_policy_controller_core::{
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, IpNet,
};
use std::hash::Hash;
use crate::ClusterInfo;
/// Indicates the default behavior to apply when no Server is found for a port.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum DefaultPolicy {
@ -45,28 +51,68 @@ impl std::str::FromStr for DefaultPolicy {
}
}
impl std::fmt::Display for DefaultPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl DefaultPolicy {
pub(crate) fn as_str(&self) -> &'static str {
match self {
Self::Allow {
authenticated_only: true,
cluster_only: false,
} => "all-authenticated".fmt(f),
} => "all-authenticated",
Self::Allow {
authenticated_only: false,
cluster_only: false,
} => "all-unauthenticated".fmt(f),
} => "all-unauthenticated",
Self::Allow {
authenticated_only: true,
cluster_only: true,
} => "cluster-authenticated".fmt(f),
} => "cluster-authenticated",
Self::Allow {
authenticated_only: false,
cluster_only: true,
} => "cluster-unauthenticated".fmt(f),
Self::Deny => "deny".fmt(f),
} => "cluster-unauthenticated",
Self::Deny => "deny",
}
}
pub(crate) fn default_authzs(
self,
config: &ClusterInfo,
) -> HashMap<AuthorizationRef, ClientAuthorization> {
let mut authzs = HashMap::default();
if let DefaultPolicy::Allow {
authenticated_only,
cluster_only,
} = self
{
let authentication = if authenticated_only {
ClientAuthentication::TlsAuthenticated(vec![IdentityMatch::Suffix(vec![])])
} else {
ClientAuthentication::Unauthenticated
};
let networks = if cluster_only {
config.networks.iter().copied().map(Into::into).collect()
} else {
vec![
"0.0.0.0/0".parse::<IpNet>().unwrap().into(),
"::/0".parse::<IpNet>().unwrap().into(),
]
};
authzs.insert(
AuthorizationRef::Default(self.as_str()),
ClientAuthorization {
authentication,
networks,
},
);
};
authzs
}
}
impl std::fmt::Display for DefaultPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_str().fmt(f)
}
}
#[cfg(test)]

View File

@ -39,16 +39,7 @@ impl TryFrom<api::HttpRoute> for InboundRouteBinding {
fn try_from(route: api::HttpRoute) -> Result<Self, Self::Error> {
let route_ns = route.metadata.namespace.as_deref();
let creation_timestamp = route
.metadata
.creation_timestamp
.map(|k8s::Time(t)| t)
.unwrap_or_else(|| {
tracing::warn!("HTTPRoute resource did not have a creation timestamp!");
// If the resource is missing a creation timestamp, we'll use the current time so
// that existing resources have precedence over newly added ones.
std::time::SystemTime::now().into()
});
let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
let parents = InboundParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
let hostnames = route
.spec
@ -89,16 +80,7 @@ impl TryFrom<policy::HttpRoute> for InboundRouteBinding {
fn try_from(route: policy::HttpRoute) -> Result<Self, Self::Error> {
let route_ns = route.metadata.namespace.as_deref();
let creation_timestamp = route
.metadata
.creation_timestamp
.map(|k8s::Time(t)| t)
.unwrap_or_else(|| {
tracing::warn!("HTTPRoute resource did not have a creation timestamp!");
// If the resource is missing a creation timestamp, we'll use the current time so
// that existing resources have precedence over newly added ones.
std::time::SystemTime::now().into()
});
let creation_timestamp = route.metadata.creation_timestamp.map(|k8s::Time(t)| t);
let parents = InboundParentRef::collect_from(route_ns, route.spec.inner.parent_refs)?;
let hostnames = route
.spec

View File

@ -18,7 +18,7 @@ use ahash::{AHashMap as HashMap, AHashSet as HashSet};
use anyhow::{anyhow, bail, Result};
use linkerd_policy_controller_core::{
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundHttpRoute,
InboundServer, IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef,
InboundHttpRouteRef, InboundServer, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef,
};
use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port, ResourceExt};
use parking_lot::RwLock;
@ -1042,39 +1042,20 @@ impl Pod {
}
}
let mut authorizations = HashMap::default();
if let DefaultPolicy::Allow {
authenticated_only,
cluster_only,
} = policy
{
let authentication = if authenticated_only {
ClientAuthentication::TlsAuthenticated(vec![IdentityMatch::Suffix(vec![])])
} else {
ClientAuthentication::Unauthenticated
};
let networks = if cluster_only {
config.networks.iter().copied().map(Into::into).collect()
} else {
vec![
"0.0.0.0/0".parse::<IpNet>().unwrap().into(),
"::/0".parse::<IpNet>().unwrap().into(),
]
};
authorizations.insert(
AuthorizationRef::Default(policy.to_string()),
ClientAuthorization {
authentication,
networks,
},
);
};
let authorizations = policy.default_authzs(config);
let http_routes = Some((
InboundHttpRouteRef::Default("default"),
InboundHttpRoute::default(),
))
.into_iter()
.collect();
InboundServer {
reference: ServerRef::Default(policy.to_string()),
reference: ServerRef::Default(policy.as_str()),
protocol,
authorizations,
http_routes: HashMap::default(),
http_routes,
}
}
}
@ -1282,16 +1263,29 @@ impl PolicyIndex {
&self,
server_name: &str,
authentications: &AuthenticationNsIndex,
) -> HashMap<String, InboundHttpRoute> {
self.http_routes
) -> HashMap<InboundHttpRouteRef, InboundHttpRoute> {
let mut routes = self
.http_routes
.iter()
.filter(|(_, route)| route.selects_server(server_name))
.map(|(name, route)| {
let mut route = route.route.clone();
route.authorizations = self.route_client_authzs(name, authentications);
(name.clone(), route)
(InboundHttpRouteRef::Linkerd(name.clone()), route)
})
.collect()
.collect::<HashMap<_, _>>();
if routes.is_empty() {
// If no routes are defined for the server, use a default route that
// matches all requests. Default authorizations are instrumented on
// the server.
routes.insert(
InboundHttpRouteRef::Default("default"),
InboundHttpRoute::default(),
);
}
routes
}
fn policy_client_authz(

View File

@ -7,8 +7,9 @@ use crate::{defaults::DefaultPolicy, index::*, server_authorization::ServerSelec
use ahash::AHashMap as HashMap;
use kubert::index::IndexNamespacedResource;
use linkerd_policy_controller_core::{
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer,
IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef,
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundHttpRoute,
InboundHttpRouteRef, InboundServer, IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol,
ServerRef,
};
use linkerd_policy_controller_k8s_api::{
self as k8s,
@ -126,7 +127,7 @@ fn mk_default_policy(
authenticated_only: true,
cluster_only: false,
} => Some((
AuthorizationRef::Default("all-authenticated".to_string()),
AuthorizationRef::Default("all-authenticated"),
ClientAuthorization {
authentication: authed,
networks: all_nets,
@ -136,7 +137,7 @@ fn mk_default_policy(
authenticated_only: false,
cluster_only: false,
} => Some((
AuthorizationRef::Default("all-unauthenticated".to_string()),
AuthorizationRef::Default("all-unauthenticated"),
ClientAuthorization {
authentication: ClientAuthentication::Unauthenticated,
networks: all_nets,
@ -146,7 +147,7 @@ fn mk_default_policy(
authenticated_only: true,
cluster_only: true,
} => Some((
AuthorizationRef::Default("cluster-authenticated".to_string()),
AuthorizationRef::Default("cluster-authenticated"),
ClientAuthorization {
authentication: authed,
networks: cluster_nets,
@ -156,7 +157,7 @@ fn mk_default_policy(
authenticated_only: false,
cluster_only: true,
} => Some((
AuthorizationRef::Default("cluster-unauthenticated".to_string()),
AuthorizationRef::Default("cluster-unauthenticated"),
ClientAuthorization {
authentication: ClientAuthentication::Unauthenticated,
networks: cluster_nets,
@ -167,6 +168,15 @@ fn mk_default_policy(
.collect()
}
fn mk_default_routes() -> HashMap<InboundHttpRouteRef, InboundHttpRoute> {
Some((
InboundHttpRouteRef::Default("default"),
InboundHttpRoute::default(),
))
.into_iter()
.collect()
}
impl TestConfig {
fn from_default_policy(default_policy: DefaultPolicy) -> Self {
let _tracing = Self::init_tracing();
@ -191,12 +201,12 @@ impl TestConfig {
fn default_server(&self) -> InboundServer {
InboundServer {
reference: ServerRef::Default(self.default_policy.to_string()),
reference: ServerRef::Default(self.default_policy.as_str()),
authorizations: mk_default_policy(self.default_policy, self.cluster.networks.clone()),
protocol: ProxyProtocol::Detect {
timeout: self.detect_timeout,
},
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
}
}

View File

@ -31,7 +31,7 @@ fn default_policy_annotated() {
.expect("pod-0.ns-0 should exist");
assert_eq!(
rx.borrow_and_update().reference,
ServerRef::Default(test.default_policy.to_string()),
ServerRef::Default(test.default_policy.as_str()),
);
// Update the annotation on the pod and check that the watch is updated
@ -42,10 +42,7 @@ fn default_policy_annotated() {
);
test.index.write().apply(pod);
assert!(rx.has_changed().unwrap());
assert_eq!(
rx.borrow().reference,
ServerRef::Default(default.to_string())
);
assert_eq!(rx.borrow().reference, ServerRef::Default(default.as_str()));
}
}
@ -114,12 +111,12 @@ fn authenticated_annotated() {
DefaultPolicy::Deny => DefaultPolicy::Deny,
};
InboundServer {
reference: ServerRef::Default(policy.to_string()),
reference: ServerRef::Default(policy.as_str()),
authorizations: mk_default_policy(policy, test.cluster.networks),
protocol: ProxyProtocol::Detect {
timeout: test.detect_timeout,
},
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
}
};

View File

@ -31,7 +31,7 @@ fn links_authorization_policy_with_mtls_name() {
reference: ServerRef::Server("srv-8080".to_string()),
authorizations: Default::default(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
@ -85,7 +85,7 @@ fn links_authorization_policy_with_mtls_name() {
.into_iter()
.collect(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
}
@ -121,7 +121,7 @@ fn authorization_targets_namespace() {
reference: ServerRef::Server("srv-8080".to_string()),
authorizations: Default::default(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
@ -175,7 +175,7 @@ fn authorization_targets_namespace() {
.into_iter()
.collect(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
}
@ -211,7 +211,7 @@ fn links_authorization_policy_with_service_account() {
reference: ServerRef::Server("srv-8080".to_string()),
authorizations: Default::default(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
@ -259,7 +259,7 @@ fn links_authorization_policy_with_service_account() {
.into_iter()
.collect(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
}

View File

@ -1,4 +1,5 @@
use super::*;
use linkerd_policy_controller_core::InboundHttpRouteRef;
const POLICY_API_GROUP: &str = "policy.linkerd.io";
@ -34,7 +35,7 @@ fn route_attaches_to_server() {
reference: ServerRef::Server("srv-8080".to_string()),
authorizations: Default::default(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
@ -47,7 +48,10 @@ fn route_attaches_to_server() {
rx.borrow().reference,
ServerRef::Server("srv-8080".to_string())
);
assert!(rx.borrow_and_update().http_routes.contains_key("route-foo"));
assert!(rx
.borrow_and_update()
.http_routes
.contains_key(&InboundHttpRouteRef::Linkerd("route-foo".to_string())));
// Create authz policy.
test.index.write().apply(mk_authorization_policy(
@ -63,11 +67,13 @@ fn route_attaches_to_server() {
));
assert!(rx.has_changed().unwrap());
assert!(rx.borrow().http_routes["route-foo"]
.authorizations
.contains_key(&AuthorizationRef::AuthorizationPolicy(
"authz-foo".to_string()
)));
assert!(
rx.borrow().http_routes[&InboundHttpRouteRef::Linkerd("route-foo".to_string())]
.authorizations
.contains_key(&AuthorizationRef::AuthorizationPolicy(
"authz-foo".to_string()
))
);
}
fn mk_route(

View File

@ -43,7 +43,7 @@ fn link_server_authz(selector: ServerSelector) {
reference: ServerRef::Server("srv-8080".to_string()),
authorizations: Default::default(),
protocol: ProxyProtocol::Http1,
http_routes: HashMap::default(),
http_routes: mk_default_routes(),
},
);
test.index.write().apply(mk_server_authz(

View File

@ -11,6 +11,14 @@ use tokio::io;
#[macro_export]
macro_rules! assert_is_default_all_unauthenticated {
($config:expr) => {
assert_default_all_unauthenticated_labels!($config);
assert_eq!($config.authorizations.len(), 1);
};
}
#[macro_export]
macro_rules! assert_default_all_unauthenticated_labels {
($config:expr) => {
assert_eq!(
$config.labels,
@ -22,7 +30,6 @@ macro_rules! assert_is_default_all_unauthenticated {
.into_iter()
.collect()
);
assert_eq!($config.authorizations.len(), 1);
};
}
@ -37,7 +44,7 @@ macro_rules! assert_protocol_detect {
kind: Some(inbound::proxy_protocol::Kind::Detect(
inbound::proxy_protocol::Detect {
timeout: Some(time::Duration::from_secs(10).into()),
http_routes: Default::default(),
http_routes: vec![$crate::grpc::defaults::http_route()],
}
)),
}),
@ -173,3 +180,38 @@ impl hyper::service::Service<hyper::Request<tonic::body::BoxBody>> for GrpcHttp
self.tx.call(hyper::Request::from_parts(parts, body))
}
}
pub mod defaults {
use super::*;
pub fn proxy_protocol() -> inbound::ProxyProtocol {
use inbound::proxy_protocol::{Http1, Kind};
inbound::ProxyProtocol {
kind: Some(Kind::Http1(Http1 {
routes: vec![http_route()],
})),
}
}
pub fn http_route() -> inbound::HttpRoute {
use http_route::{path_match, HttpRouteMatch, PathMatch};
use inbound::{http_route::Rule, HttpRoute};
use meta::{metadata, Metadata};
HttpRoute {
metadata: Some(Metadata {
kind: Some(metadata::Kind::Default("default".to_owned())),
}),
rules: vec![Rule {
matches: vec![HttpRouteMatch {
path: Some(PathMatch {
kind: Some(path_match::Kind::Prefix("/".to_owned())),
}),
..HttpRouteMatch::default()
}],
..Rule::default()
}],
..HttpRoute::default()
}
}
}

View File

@ -5,8 +5,8 @@ use kube::ResourceExt;
use linkerd_policy_controller_core::{Ipv4Net, Ipv6Net};
use linkerd_policy_controller_k8s_api as k8s;
use linkerd_policy_test::{
assert_is_default_all_unauthenticated, assert_protocol_detect, create, create_ready_pod, grpc,
with_temp_ns,
assert_default_all_unauthenticated_labels, assert_is_default_all_unauthenticated,
assert_protocol_detect, create, create_ready_pod, grpc, with_temp_ns,
};
use maplit::{btreemap, convert_args, hashmap};
use tokio::time;
@ -34,14 +34,7 @@ async fn server_with_server_authorization() {
// that the update now uses this server, which has no authorizations
let server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
let config = next_config(&mut rx).await;
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(config.authorizations, vec![]);
assert_eq!(
config.labels,
@ -82,14 +75,7 @@ async fn server_with_server_authorization() {
.expect("watch must not fail")
.expect("watch must return an updated config");
tracing::trace!(?config);
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(
config.authorizations.first().unwrap().labels,
convert_args!(hashmap!(
@ -160,14 +146,7 @@ async fn server_with_authorization_policy() {
// that the update now uses this server, which has no authorizations
let server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
let config = next_config(&mut rx).await;
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(config.authorizations, vec![]);
assert_eq!(
config.labels,
@ -223,14 +202,8 @@ async fn server_with_authorization_policy() {
let config = time::timeout(time::Duration::from_secs(10), next_config(&mut rx))
.await
.expect("watch must update within 10s");
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(config.authorizations.len(), 1);
assert_eq!(
config.authorizations.first().unwrap().labels,
@ -288,14 +261,7 @@ async fn server_with_http_route() {
// and no routes.
let _server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
let config = next_config(&mut rx).await;
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(config.authorizations, vec![]);
assert_eq!(
config.labels,
@ -409,14 +375,7 @@ async fn server_with_http_route() {
.await
.expect("HttpRoute must be deleted");
let config = next_config(&mut rx).await;
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
})
.await
}
@ -461,14 +420,7 @@ async fn http_routes_ordered_by_creation() {
// and no routes.
let _server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
let config = next_config(&mut rx).await;
assert_eq!(
config.protocol,
Some(grpc::inbound::ProxyProtocol {
kind: Some(grpc::inbound::proxy_protocol::Kind::Http1(
grpc::inbound::proxy_protocol::Http1::default()
)),
}),
);
assert_eq!(config.protocol, Some(grpc::defaults::proxy_protocol()));
assert_eq!(config.authorizations, vec![]);
assert_eq!(
config.labels,
@ -540,6 +492,31 @@ async fn http_routes_ordered_by_creation() {
.await
}
#[tokio::test(flavor = "current_thread")]
async fn default_http_routes() {
with_temp_ns(|client, ns| async move {
// Create a pod that does nothing. It's injected with a proxy, so we can
// attach policies to its admin server.
let pod = create_ready_pod(&client, mk_pause(&ns, "pause")).await;
let mut rx = retry_watch_server(&client, &ns, &pod.name_unchecked()).await;
let config = rx
.next()
.await
.expect("watch must not fail")
.expect("watch must return an initial config");
tracing::trace!(?config);
assert_is_default_all_unauthenticated!(config);
assert_protocol_detect!(config);
let routes = detect_routes(&config);
assert_eq!(routes.len(), 1);
let route_authzs = &routes[0].authorizations;
assert_eq!(route_authzs.len(), 0);
})
.await
}
/// Returns an `HttpRoute` resource in the provdied namespace and with the
/// provided name, which attaches to the `linkerd-admin` `Server` resource and
/// matches `GET` requests with the path `/metrics`.
@ -686,18 +663,34 @@ async fn next_config(rx: &mut tonic::Streaming<grpc::inbound::Server>) -> grpc::
config
}
fn http1_routes(config: &grpc::inbound::Server) -> &[grpc::inbound::HttpRoute] {
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(ref http1) = config
fn detect_routes(config: &grpc::inbound::Server) -> &[grpc::inbound::HttpRoute] {
let kind = config
.protocol
.as_ref()
.expect("must have proxy protocol")
.kind
.as_ref()
.expect("must have kind")
{
.expect("must have kind");
let detect = if let grpc::inbound::proxy_protocol::Kind::Detect(ref detect) = kind {
detect
} else {
panic!("proxy protocol must be Detect; actually got:\n{kind:#?}")
};
&detect.http_routes[..]
}
fn http1_routes(config: &grpc::inbound::Server) -> &[grpc::inbound::HttpRoute] {
let kind = config
.protocol
.as_ref()
.expect("must have proxy protocol")
.kind
.as_ref()
.expect("must have kind");
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(ref http1) = kind {
http1
} else {
panic!("proxy protocol must be HTTP1")
panic!("proxy protocol must be HTTP1; actually got:\n{kind:#?}")
};
&http1.routes[..]
}