mirror of https://github.com/linkerd/linkerd2.git
Add support for HTTP filters in outbound policy (#11083)
We add support for the RequestHeaderModifier and RequestRedirect HTTP filters. The policy controller reads these filters in any HttpRoute resource that it indexes (both policy.linkerd.io and gateway.networking.k8s.io) and returns them in the outbound policy API. These filters may be added at the route rule level and at the backend level. We add outbound api tests for this behavior for both types of HttpRoute. Incidentally we also fix a flaky test in the outbound api tests where a watch was being recreated partway through a test, leading to a race condition. Signed-off-by: Alex Leong <alex@buoyant.io>
This commit is contained in:
parent
a96139892e
commit
f19c74b960
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,6 @@
|
||||||
use crate::http_route::{GroupKindName, HostMatch, HttpRouteMatch};
|
use crate::http_route::{
|
||||||
|
GroupKindName, HostMatch, HttpRouteMatch, RequestHeaderModifierFilter, RequestRedirectFilter,
|
||||||
|
};
|
||||||
use ahash::AHashMap as HashMap;
|
use ahash::AHashMap as HashMap;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use chrono::{offset::Utc, DateTime};
|
use chrono::{offset::Utc, DateTime};
|
||||||
|
@ -44,6 +46,7 @@ pub struct HttpRouteRule {
|
||||||
pub backends: Vec<Backend>,
|
pub backends: Vec<Backend>,
|
||||||
pub request_timeout: Option<time::Duration>,
|
pub request_timeout: Option<time::Duration>,
|
||||||
pub backend_request_timeout: Option<time::Duration>,
|
pub backend_request_timeout: Option<time::Duration>,
|
||||||
|
pub filters: Vec<Filter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
@ -67,6 +70,7 @@ pub struct WeightedService {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub namespace: String,
|
pub namespace: String,
|
||||||
pub port: NonZeroU16,
|
pub port: NonZeroU16,
|
||||||
|
pub filters: Vec<Filter>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
@ -80,3 +84,9 @@ pub struct Backoff {
|
||||||
pub max_penalty: time::Duration,
|
pub max_penalty: time::Duration,
|
||||||
pub jitter: f32,
|
pub jitter: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Filter {
|
||||||
|
RequestHeaderModifier(RequestHeaderModifierFilter),
|
||||||
|
RequestRedirect(RequestRedirectFilter),
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ use linkerd2_proxy_api::{
|
||||||
use linkerd_policy_controller_core::{
|
use linkerd_policy_controller_core::{
|
||||||
http_route::GroupKindName,
|
http_route::GroupKindName,
|
||||||
outbound::{
|
outbound::{
|
||||||
Backend, DiscoverOutboundPolicy, HttpRoute, HttpRouteRule, OutboundPolicy,
|
Backend, DiscoverOutboundPolicy, Filter, HttpRoute, HttpRouteRule, OutboundPolicy,
|
||||||
OutboundPolicyStream,
|
OutboundPolicyStream,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -323,6 +323,7 @@ fn convert_outbound_http_route(
|
||||||
backends,
|
backends,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
backend_request_timeout,
|
backend_request_timeout,
|
||||||
|
filters,
|
||||||
}| {
|
}| {
|
||||||
let backend_request_timeout = backend_request_timeout
|
let backend_request_timeout = backend_request_timeout
|
||||||
.and_then(|d| convert_duration("backend request_timeout", d));
|
.and_then(|d| convert_duration("backend request_timeout", d));
|
||||||
|
@ -348,7 +349,7 @@ fn convert_outbound_http_route(
|
||||||
outbound::http_route::Rule {
|
outbound::http_route::Rule {
|
||||||
matches: matches.into_iter().map(http_route::convert_match).collect(),
|
matches: matches.into_iter().map(http_route::convert_match).collect(),
|
||||||
backends: Some(outbound::http_route::Distribution { kind: Some(dist) }),
|
backends: Some(outbound::http_route::Distribution { kind: Some(dist) }),
|
||||||
filters: Default::default(),
|
filters: filters.into_iter().map(convert_filter).collect(),
|
||||||
request_timeout: request_timeout
|
request_timeout: request_timeout
|
||||||
.and_then(|d| convert_duration("request timeout", d)),
|
.and_then(|d| convert_duration("request timeout", d)),
|
||||||
}
|
}
|
||||||
|
@ -389,7 +390,9 @@ fn convert_http_backend(
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Backend::Service(svc) => outbound::http_route::WeightedRouteBackend {
|
Backend::Service(svc) => {
|
||||||
|
let filters = svc.filters.into_iter().map(convert_filter).collect();
|
||||||
|
outbound::http_route::WeightedRouteBackend {
|
||||||
weight: svc.weight,
|
weight: svc.weight,
|
||||||
backend: Some(outbound::http_route::RouteBackend {
|
backend: Some(outbound::http_route::RouteBackend {
|
||||||
backend: Some(outbound::Backend {
|
backend: Some(outbound::Backend {
|
||||||
|
@ -417,10 +420,11 @@ fn convert_http_backend(
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
}),
|
}),
|
||||||
filters: Default::default(),
|
filters,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
}),
|
}),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
Backend::Invalid { weight, message } => outbound::http_route::WeightedRouteBackend {
|
Backend::Invalid { weight, message } => outbound::http_route::WeightedRouteBackend {
|
||||||
weight,
|
weight,
|
||||||
backend: Some(outbound::http_route::RouteBackend {
|
backend: Some(outbound::http_route::RouteBackend {
|
||||||
|
@ -551,3 +555,16 @@ fn convert_duration(name: &'static str, duration: time::Duration) -> Option<pros
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn convert_filter(filter: Filter) -> outbound::http_route::Filter {
|
||||||
|
use outbound::http_route::filter::Kind;
|
||||||
|
|
||||||
|
outbound::http_route::Filter {
|
||||||
|
kind: Some(match filter {
|
||||||
|
Filter::RequestHeaderModifier(f) => {
|
||||||
|
Kind::RequestHeaderModifier(http_route::convert_header_modifier_filter(f))
|
||||||
|
}
|
||||||
|
Filter::RequestRedirect(f) => Kind::Redirect(http_route::convert_redirect_filter(f)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,7 +9,8 @@ use k8s_gateway_api::{BackendObjectReference, HttpBackendRef, ParentReference};
|
||||||
use linkerd_policy_controller_core::{
|
use linkerd_policy_controller_core::{
|
||||||
http_route::GroupKindName,
|
http_route::GroupKindName,
|
||||||
outbound::{
|
outbound::{
|
||||||
Backend, Backoff, FailureAccrual, HttpRoute, HttpRouteRule, OutboundPolicy, WeightedService,
|
Backend, Backoff, FailureAccrual, Filter, HttpRoute, HttpRouteRule, OutboundPolicy,
|
||||||
|
WeightedService,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use linkerd_policy_controller_k8s_api::{policy as api, ResourceExt, Service, Time};
|
use linkerd_policy_controller_k8s_api::{policy as api, ResourceExt, Service, Time};
|
||||||
|
@ -384,6 +385,13 @@ impl Namespace {
|
||||||
.filter_map(|b| convert_backend(&self.namespace, b, cluster, service_info))
|
.filter_map(|b| convert_backend(&self.namespace, b, cluster, service_info))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let filters = rule
|
||||||
|
.filters
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(convert_linkerd_filter)
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
let request_timeout = rule.timeouts.as_ref().and_then(|timeouts| {
|
let request_timeout = rule.timeouts.as_ref().and_then(|timeouts| {
|
||||||
let timeout = time::Duration::from(timeouts.request?);
|
let timeout = time::Duration::from(timeouts.request?);
|
||||||
|
|
||||||
|
@ -414,6 +422,7 @@ impl Namespace {
|
||||||
backends,
|
backends,
|
||||||
request_timeout,
|
request_timeout,
|
||||||
backend_request_timeout,
|
backend_request_timeout,
|
||||||
|
filters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,11 +446,19 @@ impl Namespace {
|
||||||
.filter_map(|b| convert_backend(&self.namespace, b, cluster, service_info))
|
.filter_map(|b| convert_backend(&self.namespace, b, cluster, service_info))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
let filters = rule
|
||||||
|
.filters
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(convert_gateway_filter)
|
||||||
|
.collect::<Result<_>>()?;
|
||||||
|
|
||||||
Ok(HttpRouteRule {
|
Ok(HttpRouteRule {
|
||||||
matches,
|
matches,
|
||||||
backends,
|
backends,
|
||||||
request_timeout: None,
|
request_timeout: None,
|
||||||
backend_request_timeout: None,
|
backend_request_timeout: None,
|
||||||
|
filters,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,16 +469,17 @@ fn convert_backend(
|
||||||
cluster: &ClusterInfo,
|
cluster: &ClusterInfo,
|
||||||
services: &HashMap<ServiceRef, ServiceInfo>,
|
services: &HashMap<ServiceRef, ServiceInfo>,
|
||||||
) -> Option<Backend> {
|
) -> Option<Backend> {
|
||||||
backend.backend_ref.map(|backend| {
|
let filters = backend.filters;
|
||||||
|
let backend = backend.backend_ref?;
|
||||||
if !is_backend_service(&backend.inner) {
|
if !is_backend_service(&backend.inner) {
|
||||||
return Backend::Invalid {
|
return Some(Backend::Invalid {
|
||||||
weight: backend.weight.unwrap_or(1).into(),
|
weight: backend.weight.unwrap_or(1).into(),
|
||||||
message: format!(
|
message: format!(
|
||||||
"unsupported backend type {group} {kind}",
|
"unsupported backend type {group} {kind}",
|
||||||
group = backend.inner.group.as_deref().unwrap_or("core"),
|
group = backend.inner.group.as_deref().unwrap_or("core"),
|
||||||
kind = backend.inner.kind.as_deref().unwrap_or("<empty>"),
|
kind = backend.inner.kind.as_deref().unwrap_or("<empty>"),
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = backend.inner.name;
|
let name = backend.inner.name;
|
||||||
|
@ -477,10 +495,10 @@ fn convert_backend(
|
||||||
{
|
{
|
||||||
Some(port) => port,
|
Some(port) => port,
|
||||||
None => {
|
None => {
|
||||||
return Backend::Invalid {
|
return Some(Backend::Invalid {
|
||||||
weight: weight.into(),
|
weight: weight.into(),
|
||||||
message: format!("missing port for backend Service {name}"),
|
message: format!("missing port for backend Service {name}"),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let service_ref = ServiceRef {
|
let service_ref = ServiceRef {
|
||||||
|
@ -488,20 +506,78 @@ fn convert_backend(
|
||||||
namespace: backend.inner.namespace.unwrap_or_else(|| ns.to_string()),
|
namespace: backend.inner.namespace.unwrap_or_else(|| ns.to_string()),
|
||||||
};
|
};
|
||||||
if !services.contains_key(&service_ref) {
|
if !services.contains_key(&service_ref) {
|
||||||
return Backend::Invalid {
|
return Some(Backend::Invalid {
|
||||||
weight: weight.into(),
|
weight: weight.into(),
|
||||||
message: format!("Service not found {name}"),
|
message: format!("Service not found {name}"),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Backend::Service(WeightedService {
|
let filters = match filters
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.map(convert_gateway_filter)
|
||||||
|
.collect::<Result<_>>()
|
||||||
|
{
|
||||||
|
Ok(filters) => filters,
|
||||||
|
Err(error) => {
|
||||||
|
return Some(Backend::Invalid {
|
||||||
|
weight: backend.weight.unwrap_or(1).into(),
|
||||||
|
message: format!("unsupported backend filter: {error}", error = error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Backend::Service(WeightedService {
|
||||||
weight: weight.into(),
|
weight: weight.into(),
|
||||||
authority: cluster.service_dns_authority(&service_ref.namespace, &name, port),
|
authority: cluster.service_dns_authority(&service_ref.namespace, &name, port),
|
||||||
name,
|
name,
|
||||||
namespace: ns.to_string(),
|
namespace: ns.to_string(),
|
||||||
port,
|
port,
|
||||||
})
|
filters,
|
||||||
})
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_linkerd_filter(filter: api::httproute::HttpRouteFilter) -> Result<Filter> {
|
||||||
|
let filter = match filter {
|
||||||
|
api::httproute::HttpRouteFilter::RequestHeaderModifier {
|
||||||
|
request_header_modifier,
|
||||||
|
} => {
|
||||||
|
let filter = http_route::req_header_modifier(request_header_modifier)?;
|
||||||
|
Filter::RequestHeaderModifier(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
api::httproute::HttpRouteFilter::RequestRedirect { request_redirect } => {
|
||||||
|
let filter = http_route::req_redirect(request_redirect)?;
|
||||||
|
Filter::RequestRedirect(filter)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_gateway_filter(filter: k8s_gateway_api::HttpRouteFilter) -> Result<Filter> {
|
||||||
|
let filter = match filter {
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestHeaderModifier {
|
||||||
|
request_header_modifier,
|
||||||
|
} => {
|
||||||
|
let filter = http_route::req_header_modifier(request_header_modifier)?;
|
||||||
|
Filter::RequestHeaderModifier(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestRedirect { request_redirect } => {
|
||||||
|
let filter = http_route::req_redirect(request_redirect)?;
|
||||||
|
Filter::RequestRedirect(filter)
|
||||||
|
}
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestMirror { .. } => {
|
||||||
|
bail!("RequestMirror filter is not supported")
|
||||||
|
}
|
||||||
|
k8s_gateway_api::HttpRouteFilter::URLRewrite { .. } => {
|
||||||
|
bail!("URLRewrite filter is not supported")
|
||||||
|
}
|
||||||
|
k8s_gateway_api::HttpRouteFilter::ExtensionRef { .. } => {
|
||||||
|
bail!("ExtensionRef filter is not supported")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
|
|
@ -72,7 +72,6 @@ async fn service_with_http_route_without_rules() {
|
||||||
|
|
||||||
let _route = create(&client, mk_empty_http_route(&ns, "foo-route", &svc, 4191)).await;
|
let _route = create(&client, mk_empty_http_route(&ns, "foo-route", &svc, 4191)).await;
|
||||||
|
|
||||||
let mut rx = retry_watch_outbound_policy(&client, &ns, &svc).await;
|
|
||||||
let config = rx
|
let config = rx
|
||||||
.next()
|
.next()
|
||||||
.await
|
.await
|
||||||
|
@ -109,11 +108,7 @@ async fn service_with_http_routes_without_backends() {
|
||||||
assert_route_is_default(route, &svc, 4191);
|
assert_route_is_default(route, &svc, 4191);
|
||||||
});
|
});
|
||||||
|
|
||||||
let _route = create(
|
let _route = create(&client, mk_http_route(&ns, "foo-route", &svc, 4191).build()).await;
|
||||||
&client,
|
|
||||||
mk_http_route(&ns, "foo-route", &svc, 4191, None, None),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let config = rx
|
let config = rx
|
||||||
.next()
|
.next()
|
||||||
|
@ -156,11 +151,9 @@ async fn service_with_http_routes_with_backend() {
|
||||||
let backend_name = "backend";
|
let backend_name = "backend";
|
||||||
let backend_svc = create_service(&client, &ns, backend_name, 8888).await;
|
let backend_svc = create_service(&client, &ns, backend_name, 8888).await;
|
||||||
let backends = [backend_name];
|
let backends = [backend_name];
|
||||||
let _route = create(
|
let route =
|
||||||
&client,
|
mk_http_route(&ns, "foo-route", &svc, 4191).with_backends(Some(&backends), None, None);
|
||||||
mk_http_route(&ns, "foo-route", &svc, 4191, Some(&backends), None),
|
let _route = create(&client, route.build()).await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let config = rx
|
let config = rx
|
||||||
.next()
|
.next()
|
||||||
|
@ -220,18 +213,12 @@ async fn service_with_http_routes_with_cross_namespace_backend() {
|
||||||
let backend_name = "backend";
|
let backend_name = "backend";
|
||||||
let backend_svc = create_service(&client, &backend_ns_name, backend_name, 8888).await;
|
let backend_svc = create_service(&client, &backend_ns_name, backend_name, 8888).await;
|
||||||
let backends = [backend_name];
|
let backends = [backend_name];
|
||||||
let _route = create(
|
let route = mk_http_route(&ns, "foo-route", &svc, 4191).with_backends(
|
||||||
&client,
|
|
||||||
mk_http_route(
|
|
||||||
&ns,
|
|
||||||
"foo-route",
|
|
||||||
&svc,
|
|
||||||
4191,
|
|
||||||
Some(&backends),
|
Some(&backends),
|
||||||
Some(backend_ns_name),
|
Some(backend_ns_name),
|
||||||
),
|
None,
|
||||||
)
|
);
|
||||||
.await;
|
let _route = create(&client, route.build()).await;
|
||||||
|
|
||||||
let config = rx
|
let config = rx
|
||||||
.next()
|
.next()
|
||||||
|
@ -277,11 +264,9 @@ async fn service_with_http_routes_with_invalid_backend() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let backends = ["invalid-backend"];
|
let backends = ["invalid-backend"];
|
||||||
let _route = create(
|
let route =
|
||||||
&client,
|
mk_http_route(&ns, "foo-route", &svc, 4191).with_backends(Some(&backends), None, None);
|
||||||
mk_http_route(&ns, "foo-route", &svc, 4191, Some(&backends), None),
|
let _route = create(&client, route.build()).await;
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let config = rx
|
let config = rx
|
||||||
.next()
|
.next()
|
||||||
|
@ -326,11 +311,7 @@ async fn service_with_multiple_http_routes() {
|
||||||
// Routes should be returned in sorted order by creation timestamp then
|
// Routes should be returned in sorted order by creation timestamp then
|
||||||
// name. To ensure that this test isn't timing dependant, routes should
|
// name. To ensure that this test isn't timing dependant, routes should
|
||||||
// be created in alphabetical order.
|
// be created in alphabetical order.
|
||||||
let _a_route = create(
|
let _a_route = create(&client, mk_http_route(&ns, "a-route", &svc, 4191).build()).await;
|
||||||
&client,
|
|
||||||
mk_http_route(&ns, "a-route", &svc, 4191, None, None),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// First route update.
|
// First route update.
|
||||||
let config = rx
|
let config = rx
|
||||||
|
@ -340,11 +321,7 @@ async fn service_with_multiple_http_routes() {
|
||||||
.expect("watch must return an updated config");
|
.expect("watch must return an updated config");
|
||||||
tracing::trace!(?config);
|
tracing::trace!(?config);
|
||||||
|
|
||||||
let _b_route = create(
|
let _b_route = create(&client, mk_http_route(&ns, "b-route", &svc, 4191).build()).await;
|
||||||
&client,
|
|
||||||
mk_http_route(&ns, "b-route", &svc, 4191, None, None),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Second route update.
|
// Second route update.
|
||||||
let config = rx
|
let config = rx
|
||||||
|
@ -623,8 +600,234 @@ async fn opaque_service() {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn route_with_filters() {
|
||||||
|
with_temp_ns(|client, ns| async move {
|
||||||
|
// Create a service
|
||||||
|
let svc = create_service(&client, &ns, "my-svc", 4191).await;
|
||||||
|
|
||||||
|
let mut rx = retry_watch_outbound_policy(&client, &ns, &svc).await;
|
||||||
|
let config = rx
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.expect("watch must not fail")
|
||||||
|
.expect("watch must return an initial config");
|
||||||
|
tracing::trace!(?config);
|
||||||
|
|
||||||
|
// There should be a default route.
|
||||||
|
detect_http_routes(&config, |routes| {
|
||||||
|
let route = assert_singleton(routes);
|
||||||
|
assert_route_is_default(route, &svc, 4191);
|
||||||
|
});
|
||||||
|
|
||||||
|
let backend_name = "backend";
|
||||||
|
let backends = [backend_name];
|
||||||
|
let route = mk_http_route(&ns, "foo-route", &svc, 4191)
|
||||||
|
.with_backends(Some(&backends), None, None)
|
||||||
|
.with_filters(Some(vec![
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestHeaderModifier {
|
||||||
|
request_header_modifier: k8s_gateway_api::HttpRequestHeaderFilter {
|
||||||
|
set: Some(vec![k8s_gateway_api::HttpHeader {
|
||||||
|
name: "set".to_string(),
|
||||||
|
value: "set-value".to_string(),
|
||||||
|
}]),
|
||||||
|
add: Some(vec![k8s_gateway_api::HttpHeader {
|
||||||
|
name: "add".to_string(),
|
||||||
|
value: "add-value".to_string(),
|
||||||
|
}]),
|
||||||
|
remove: Some(vec!["remove".to_string()]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestRedirect {
|
||||||
|
request_redirect: k8s_gateway_api::HttpRequestRedirectFilter {
|
||||||
|
scheme: Some("http".to_string()),
|
||||||
|
hostname: Some("host".to_string()),
|
||||||
|
path: Some(k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch {
|
||||||
|
replace_prefix_match: "/path".to_string(),
|
||||||
|
}),
|
||||||
|
port: Some(5555),
|
||||||
|
status_code: Some(302),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
let _route = create(&client, route.build()).await;
|
||||||
|
|
||||||
|
let config = rx
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.expect("watch must not fail")
|
||||||
|
.expect("watch must return an updated config");
|
||||||
|
tracing::trace!(?config);
|
||||||
|
|
||||||
|
// There should be a route with filters.
|
||||||
|
detect_http_routes(&config, |routes| {
|
||||||
|
let route = assert_singleton(routes);
|
||||||
|
let rule = assert_singleton(&route.rules);
|
||||||
|
let filters = &rule.filters;
|
||||||
|
assert_eq!(
|
||||||
|
*filters,
|
||||||
|
vec![
|
||||||
|
grpc::outbound::http_route::Filter {
|
||||||
|
kind: Some(
|
||||||
|
grpc::outbound::http_route::filter::Kind::RequestHeaderModifier(
|
||||||
|
grpc::http_route::RequestHeaderModifier {
|
||||||
|
add: Some(grpc::http_types::Headers {
|
||||||
|
headers: vec![grpc::http_types::headers::Header {
|
||||||
|
name: "add".to_string(),
|
||||||
|
value: "add-value".into(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
set: Some(grpc::http_types::Headers {
|
||||||
|
headers: vec![grpc::http_types::headers::Header {
|
||||||
|
name: "set".to_string(),
|
||||||
|
value: "set-value".into(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
remove: vec!["remove".to_string()],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
grpc::outbound::http_route::Filter {
|
||||||
|
kind: Some(grpc::outbound::http_route::filter::Kind::Redirect(
|
||||||
|
grpc::http_route::RequestRedirect {
|
||||||
|
scheme: Some(grpc::http_types::Scheme {
|
||||||
|
r#type: Some(grpc::http_types::scheme::Type::Registered(
|
||||||
|
grpc::http_types::scheme::Registered::Http.into(),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
host: "host".to_string(),
|
||||||
|
path: Some(linkerd2_proxy_api::http_route::PathModifier { replace: Some(linkerd2_proxy_api::http_route::path_modifier::Replace::Prefix("/path".to_string())) }),
|
||||||
|
port: 5555,
|
||||||
|
status: 302,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "current_thread")]
|
||||||
|
async fn backend_with_filters() {
|
||||||
|
with_temp_ns(|client, ns| async move {
|
||||||
|
// Create a service
|
||||||
|
let svc = create_service(&client, &ns, "my-svc", 4191).await;
|
||||||
|
|
||||||
|
let mut rx = retry_watch_outbound_policy(&client, &ns, &svc).await;
|
||||||
|
let config = rx
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.expect("watch must not fail")
|
||||||
|
.expect("watch must return an initial config");
|
||||||
|
tracing::trace!(?config);
|
||||||
|
|
||||||
|
// There should be a default route.
|
||||||
|
detect_http_routes(&config, |routes| {
|
||||||
|
let route = assert_singleton(routes);
|
||||||
|
assert_route_is_default(route, &svc, 4191);
|
||||||
|
});
|
||||||
|
|
||||||
|
let backend_name = "backend";
|
||||||
|
let backend_svc = create_service(&client, &ns, backend_name, 8888).await;
|
||||||
|
let backends = [backend_name];
|
||||||
|
let route = mk_http_route(&ns, "foo-route", &svc, 4191)
|
||||||
|
.with_backends(Some(&backends), None, Some(vec![
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestHeaderModifier {
|
||||||
|
request_header_modifier: k8s_gateway_api::HttpRequestHeaderFilter {
|
||||||
|
set: Some(vec![k8s_gateway_api::HttpHeader {
|
||||||
|
name: "set".to_string(),
|
||||||
|
value: "set-value".to_string(),
|
||||||
|
}]),
|
||||||
|
add: Some(vec![k8s_gateway_api::HttpHeader {
|
||||||
|
name: "add".to_string(),
|
||||||
|
value: "add-value".to_string(),
|
||||||
|
}]),
|
||||||
|
remove: Some(vec!["remove".to_string()]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
k8s_gateway_api::HttpRouteFilter::RequestRedirect {
|
||||||
|
request_redirect: k8s_gateway_api::HttpRequestRedirectFilter {
|
||||||
|
scheme: Some("http".to_string()),
|
||||||
|
hostname: Some("host".to_string()),
|
||||||
|
path: Some(k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch {
|
||||||
|
replace_prefix_match: "/path".to_string(),
|
||||||
|
}),
|
||||||
|
port: Some(5555),
|
||||||
|
status_code: Some(302),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
let _route = create(&client, route.build()).await;
|
||||||
|
|
||||||
|
let config = rx
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.expect("watch must not fail")
|
||||||
|
.expect("watch must return an updated config");
|
||||||
|
tracing::trace!(?config);
|
||||||
|
|
||||||
|
// There should be a route without rule filters.
|
||||||
|
detect_http_routes(&config, |routes| {
|
||||||
|
let route = assert_singleton(routes);
|
||||||
|
let rule = assert_singleton(&route.rules);
|
||||||
|
assert_eq!(rule.filters.len(), 0);
|
||||||
|
let backends = route_backends_random_available(route);
|
||||||
|
let backend = assert_singleton(backends);
|
||||||
|
assert_backend_matches_service(backend.backend.as_ref().unwrap(), &backend_svc, 8888);
|
||||||
|
let filters = &backend.backend.as_ref().unwrap().filters;
|
||||||
|
assert_eq!(
|
||||||
|
*filters,
|
||||||
|
vec![
|
||||||
|
grpc::outbound::http_route::Filter {
|
||||||
|
kind: Some(
|
||||||
|
grpc::outbound::http_route::filter::Kind::RequestHeaderModifier(
|
||||||
|
grpc::http_route::RequestHeaderModifier {
|
||||||
|
add: Some(grpc::http_types::Headers {
|
||||||
|
headers: vec![grpc::http_types::headers::Header {
|
||||||
|
name: "add".to_string(),
|
||||||
|
value: "add-value".into(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
set: Some(grpc::http_types::Headers {
|
||||||
|
headers: vec![grpc::http_types::headers::Header {
|
||||||
|
name: "set".to_string(),
|
||||||
|
value: "set-value".into(),
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
remove: vec!["remove".to_string()],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
grpc::outbound::http_route::Filter {
|
||||||
|
kind: Some(grpc::outbound::http_route::filter::Kind::Redirect(
|
||||||
|
grpc::http_route::RequestRedirect {
|
||||||
|
scheme: Some(grpc::http_types::Scheme {
|
||||||
|
r#type: Some(grpc::http_types::scheme::Type::Registered(
|
||||||
|
grpc::http_types::scheme::Registered::Http.into(),
|
||||||
|
))
|
||||||
|
}),
|
||||||
|
host: "host".to_string(),
|
||||||
|
path: Some(linkerd2_proxy_api::http_route::PathModifier { replace: Some(linkerd2_proxy_api::http_route::path_modifier::Replace::Prefix("/path".to_string())) }),
|
||||||
|
port: 5555,
|
||||||
|
status: 302,
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
/* Helpers */
|
/* Helpers */
|
||||||
|
|
||||||
|
struct HttpRouteBuilder(k8s_gateway_api::HttpRoute);
|
||||||
|
|
||||||
async fn retry_watch_outbound_policy(
|
async fn retry_watch_outbound_policy(
|
||||||
client: &kube::Client,
|
client: &kube::Client,
|
||||||
ns: &str,
|
ns: &str,
|
||||||
|
@ -649,34 +852,10 @@ async fn retry_watch_outbound_policy(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mk_http_route(
|
fn mk_http_route(ns: &str, name: &str, svc: &k8s::Service, port: u16) -> HttpRouteBuilder {
|
||||||
ns: &str,
|
use k8s_gateway_api as api;
|
||||||
name: &str,
|
|
||||||
svc: &k8s::Service,
|
HttpRouteBuilder(api::HttpRoute {
|
||||||
port: u16,
|
|
||||||
backends: Option<&[&str]>,
|
|
||||||
backends_ns: Option<String>,
|
|
||||||
) -> k8s::policy::HttpRoute {
|
|
||||||
use k8s::policy::httproute as api;
|
|
||||||
let backend_refs = backends.map(|names| {
|
|
||||||
names
|
|
||||||
.iter()
|
|
||||||
.map(|name| api::HttpBackendRef {
|
|
||||||
backend_ref: Some(k8s_gateway_api::BackendRef {
|
|
||||||
weight: None,
|
|
||||||
inner: k8s_gateway_api::BackendObjectReference {
|
|
||||||
name: name.to_string(),
|
|
||||||
port: Some(8888),
|
|
||||||
group: None,
|
|
||||||
kind: None,
|
|
||||||
namespace: backends_ns.clone(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
filters: None,
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
});
|
|
||||||
api::HttpRoute {
|
|
||||||
metadata: kube::api::ObjectMeta {
|
metadata: kube::api::ObjectMeta {
|
||||||
namespace: Some(ns.to_string()),
|
namespace: Some(ns.to_string()),
|
||||||
name: Some(name.to_string()),
|
name: Some(name.to_string()),
|
||||||
|
@ -704,11 +883,58 @@ fn mk_http_route(
|
||||||
method: Some("GET".to_string()),
|
method: Some("GET".to_string()),
|
||||||
}]),
|
}]),
|
||||||
filters: None,
|
filters: None,
|
||||||
backend_refs,
|
backend_refs: None,
|
||||||
timeouts: None,
|
|
||||||
}]),
|
}]),
|
||||||
},
|
},
|
||||||
status: None,
|
status: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpRouteBuilder {
|
||||||
|
fn with_backends(
|
||||||
|
self,
|
||||||
|
backends: Option<&[&str]>,
|
||||||
|
backends_ns: Option<String>,
|
||||||
|
backend_filters: Option<Vec<k8s_gateway_api::HttpRouteFilter>>,
|
||||||
|
) -> Self {
|
||||||
|
let mut route = self.0;
|
||||||
|
let backend_refs = backends.map(|names| {
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|name| k8s_gateway_api::HttpBackendRef {
|
||||||
|
backend_ref: Some(k8s_gateway_api::BackendRef {
|
||||||
|
weight: None,
|
||||||
|
inner: k8s_gateway_api::BackendObjectReference {
|
||||||
|
name: name.to_string(),
|
||||||
|
port: Some(8888),
|
||||||
|
group: None,
|
||||||
|
kind: None,
|
||||||
|
namespace: backends_ns.clone(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
filters: backend_filters.clone(),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
route.spec.rules.iter_mut().flatten().for_each(|rule| {
|
||||||
|
rule.backend_refs = backend_refs.clone();
|
||||||
|
});
|
||||||
|
Self(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_filters(self, filters: Option<Vec<k8s_gateway_api::HttpRouteFilter>>) -> Self {
|
||||||
|
let mut route = self.0;
|
||||||
|
route
|
||||||
|
.spec
|
||||||
|
.rules
|
||||||
|
.iter_mut()
|
||||||
|
.flatten()
|
||||||
|
.for_each(|rule| rule.filters = filters.clone());
|
||||||
|
Self(route)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self) -> k8s_gateway_api::HttpRoute {
|
||||||
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -717,8 +943,8 @@ fn mk_empty_http_route(
|
||||||
name: &str,
|
name: &str,
|
||||||
svc: &k8s::Service,
|
svc: &k8s::Service,
|
||||||
port: u16,
|
port: u16,
|
||||||
) -> k8s::policy::HttpRoute {
|
) -> k8s_gateway_api::HttpRoute {
|
||||||
use k8s::policy::httproute as api;
|
use k8s_gateway_api as api;
|
||||||
api::HttpRoute {
|
api::HttpRoute {
|
||||||
metadata: kube::api::ObjectMeta {
|
metadata: kube::api::ObjectMeta {
|
||||||
namespace: Some(ns.to_string()),
|
namespace: Some(ns.to_string()),
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue