mirror of https://github.com/linkerd/linkerd2.git
policy: Add HTTPRoute indexing (#8795)
linkerd2-proxy-api v0.6.0 adds support for inbound proxies to discover route configurations based on the Gateway API HTTPRoute types. This change updates the policy controller to index `gateway.networking.k8s.io/v1beta` `HTTPRoute` types to discover these policies from the Kubernetes API. `HTTPRoute` resources may target `Server` resources (as a `parentRef`) to attach policies to an inbound proxy. When no routes are configured, a default route is synthesized to allow traffic; but when at least one route attaches to a server, only requests that match a route are permitted (other requests are failed with a 404). Only the *core* subset of the `HTTPRoute` filters are supported: `RequestRedirect` and `RequestHeaderModifier`. Backends may *not* be configured on these routes (since they may only apply to inbound/server- side proxies). No `status` updates are currently performed on these `HTTPRoute` resources. This change does not yet allow `AuthorizationPolicy` resources to target `HTTPRoute` resources. This will be added in a follow-up change. Signed-off-by: Alex Leong <alex@buoyant.io> Co-authored-by: Oliver Gould <ver@buoyant.io>
This commit is contained in:
parent
698c0593c6
commit
f04edae759
15
Cargo.lock
15
Cargo.lock
|
@ -13,6 +13,15 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
|
@ -973,7 +982,9 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-trait",
|
||||
"futures",
|
||||
"http",
|
||||
"ipnet",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1016,11 +1027,13 @@ dependencies = [
|
|||
"ahash",
|
||||
"anyhow",
|
||||
"futures",
|
||||
"k8s-gateway-api",
|
||||
"kubert",
|
||||
"linkerd-policy-controller-core",
|
||||
"linkerd-policy-controller-k8s-api",
|
||||
"maplit",
|
||||
"parking_lot",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-test",
|
||||
|
@ -1452,6 +1465,8 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
|
|
|
@ -207,6 +207,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -202,6 +202,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -202,6 +202,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -202,6 +202,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -202,6 +202,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -207,6 +207,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -210,6 +210,14 @@ rules:
|
|||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- gateway.networking.k8s.io
|
||||
resources:
|
||||
- httproutes
|
||||
verbs:
|
||||
- list
|
||||
- get
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
|
|
|
@ -49,3 +49,7 @@ unknown-registry = "deny"
|
|||
unknown-git = "deny"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
github = []
|
||||
|
||||
|
|
|
@ -13,7 +13,11 @@ rustls-tls = ["kube/rustls-tls"]
|
|||
[dependencies]
|
||||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
clap = { version = "3", default-features = false, features = ["derive", "env", "std"] }
|
||||
clap = { version = "3", default-features = false, features = [
|
||||
"derive",
|
||||
"env",
|
||||
"std",
|
||||
] }
|
||||
drain = "0.1"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
k8s-gateway-api = "0.6"
|
||||
|
@ -34,30 +38,16 @@ tracing = "0.1"
|
|||
[dependencies.kube]
|
||||
version = "0.74"
|
||||
default-features = false
|
||||
features = [
|
||||
"admission",
|
||||
"derive",
|
||||
]
|
||||
features = ["admission", "derive"]
|
||||
|
||||
[dependencies.kubert]
|
||||
version = "0.9"
|
||||
default-features = false
|
||||
features = [
|
||||
"clap",
|
||||
"index",
|
||||
"runtime",
|
||||
"server",
|
||||
]
|
||||
features = ["clap", "index", "runtime", "server"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1"
|
||||
features = [
|
||||
"macros",
|
||||
"parking_lot",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
]
|
||||
features = ["macros", "parking_lot", "rt", "rt-multi-thread", "signal"]
|
||||
|
||||
[target.x86_64-unknown-linux-gnu.dependencies]
|
||||
jemallocator = "0.5"
|
||||
|
|
|
@ -10,4 +10,6 @@ ahash = "0.7"
|
|||
anyhow = "1"
|
||||
async-trait = "0.1"
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
http = "0.2"
|
||||
ipnet = "2"
|
||||
regex = "1"
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
use anyhow::Result;
|
||||
pub use http::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
uri::Scheme,
|
||||
Method, StatusCode,
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InboundHttpRoute {
|
||||
pub hostnames: Vec<HostMatch>,
|
||||
pub rules: Vec<InboundHttpRouteRule>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum HostMatch {
|
||||
Exact(String),
|
||||
Suffix { reverse_labels: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InboundHttpRouteRule {
|
||||
pub matches: Vec<HttpRouteMatch>,
|
||||
pub filters: Vec<InboundFilter>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum InboundFilter {
|
||||
RequestHeaderModifier(RequestHeaderModifierFilter),
|
||||
RequestRedirect(RequestRedirectFilter),
|
||||
FailureInjector(FailureInjectorFilter),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RequestHeaderModifierFilter {
|
||||
pub add: Vec<(HeaderName, HeaderValue)>,
|
||||
pub set: Vec<(HeaderName, HeaderValue)>,
|
||||
pub remove: Vec<HeaderName>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RequestRedirectFilter {
|
||||
pub scheme: Option<Scheme>,
|
||||
pub host: Option<String>,
|
||||
pub path: Option<PathModifier>,
|
||||
pub port: Option<u32>,
|
||||
pub status: Option<StatusCode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FailureInjectorFilter {
|
||||
pub status: StatusCode,
|
||||
pub message: String,
|
||||
pub ratio: Ratio,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum PathModifier {
|
||||
Full(String),
|
||||
Prefix(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Ratio {
|
||||
pub numerator: u32,
|
||||
pub denominator: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct HttpRouteMatch {
|
||||
pub path: Option<PathMatch>,
|
||||
pub headers: Vec<HeaderMatch>,
|
||||
pub query_params: Vec<QueryParamMatch>,
|
||||
pub method: Option<Method>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PathMatch {
|
||||
Exact(String),
|
||||
Prefix(String),
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HeaderMatch {
|
||||
Exact(HeaderName, HeaderValue),
|
||||
Regex(HeaderName, Regex),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum QueryParamMatch {
|
||||
Exact(String, String),
|
||||
Regex(String, Regex),
|
||||
}
|
||||
|
||||
// === impl PathMatch ===
|
||||
|
||||
impl PartialEq for PathMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Exact(l0), Self::Exact(r0)) => l0 == r0,
|
||||
(Self::Prefix(l0), Self::Prefix(r0)) => l0 == r0,
|
||||
(Self::Regex(l0), Self::Regex(r0)) => l0.as_str() == r0.as_str(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PathMatch {}
|
||||
|
||||
impl PathMatch {
|
||||
pub fn regex(s: &str) -> Result<Self> {
|
||||
Ok(Self::Regex(Regex::new(s)?))
|
||||
}
|
||||
}
|
||||
|
||||
// === impl HeaderMatch ===
|
||||
|
||||
impl PartialEq for HeaderMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Exact(n0, v0), Self::Exact(n1, v1)) => n0 == n1 && v0 == v1,
|
||||
(Self::Regex(n0, r0), Self::Regex(n1, r1)) => n0 == n1 && r0.as_str() == r1.as_str(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for HeaderMatch {}
|
||||
|
||||
// === impl QueryParamMatch ===
|
||||
|
||||
impl PartialEq for QueryParamMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Exact(n0, v0), Self::Exact(n1, v1)) => n0 == n1 && v0 == v1,
|
||||
(Self::Regex(n0, r0), Self::Regex(n1, r1)) => n0 == n1 && r0.as_str() == r1.as_str(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for QueryParamMatch {}
|
|
@ -1,10 +1,13 @@
|
|||
#![deny(warnings, rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod http_route;
|
||||
mod identity_match;
|
||||
mod network_match;
|
||||
|
||||
pub use self::{identity_match::IdentityMatch, network_match::NetworkMatch};
|
||||
pub use self::{
|
||||
http_route::InboundHttpRoute, identity_match::IdentityMatch, network_match::NetworkMatch,
|
||||
};
|
||||
use ahash::AHashMap as HashMap;
|
||||
use anyhow::Result;
|
||||
use futures::prelude::*;
|
||||
|
@ -28,6 +31,7 @@ pub struct InboundServer {
|
|||
|
||||
pub protocol: ProxyProtocol,
|
||||
pub authorizations: HashMap<AuthorizationRef, ClientAuthorization>,
|
||||
pub http_routes: HashMap<String, InboundHttpRoute>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
use linkerd2_proxy_api::{http_route as proto, http_types};
|
||||
use linkerd_policy_controller_core::http_route::{
|
||||
FailureInjectorFilter, HeaderMatch, HostMatch, HttpRouteMatch, PathMatch, PathModifier,
|
||||
QueryParamMatch, RequestHeaderModifierFilter, RequestRedirectFilter,
|
||||
};
|
||||
|
||||
pub(crate) fn convert_host_match(h: HostMatch) -> proto::HostMatch {
|
||||
proto::HostMatch {
|
||||
r#match: Some(match h {
|
||||
HostMatch::Exact(host) => proto::host_match::Match::Exact(host),
|
||||
HostMatch::Suffix { reverse_labels } => {
|
||||
proto::host_match::Match::Suffix(proto::host_match::Suffix {
|
||||
reverse_labels: reverse_labels.to_vec(),
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_match(
|
||||
HttpRouteMatch {
|
||||
headers,
|
||||
path,
|
||||
query_params,
|
||||
method,
|
||||
}: HttpRouteMatch,
|
||||
) -> proto::HttpRouteMatch {
|
||||
let headers = headers
|
||||
.into_iter()
|
||||
.map(|hm| match hm {
|
||||
HeaderMatch::Exact(name, value) => proto::HeaderMatch {
|
||||
name: name.to_string(),
|
||||
value: Some(proto::header_match::Value::Exact(value.as_bytes().to_vec())),
|
||||
},
|
||||
HeaderMatch::Regex(name, re) => proto::HeaderMatch {
|
||||
name: name.to_string(),
|
||||
value: Some(proto::header_match::Value::Regex(re.to_string())),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
let path = path.map(|path| proto::PathMatch {
|
||||
kind: Some(match path {
|
||||
PathMatch::Exact(path) => proto::path_match::Kind::Exact(path),
|
||||
PathMatch::Prefix(prefix) => proto::path_match::Kind::Prefix(prefix),
|
||||
PathMatch::Regex(regex) => proto::path_match::Kind::Regex(regex.to_string()),
|
||||
}),
|
||||
});
|
||||
|
||||
let query_params = query_params
|
||||
.into_iter()
|
||||
.map(|qpm| match qpm {
|
||||
QueryParamMatch::Exact(name, value) => proto::QueryParamMatch {
|
||||
name,
|
||||
value: Some(proto::query_param_match::Value::Exact(value)),
|
||||
},
|
||||
QueryParamMatch::Regex(name, re) => proto::QueryParamMatch {
|
||||
name,
|
||||
value: Some(proto::query_param_match::Value::Regex(re.to_string())),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
proto::HttpRouteMatch {
|
||||
headers,
|
||||
path,
|
||||
query_params,
|
||||
method: method.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_failure_injector_filter(
|
||||
FailureInjectorFilter {
|
||||
status,
|
||||
message,
|
||||
ratio,
|
||||
}: FailureInjectorFilter,
|
||||
) -> proto::HttpFailureInjector {
|
||||
proto::HttpFailureInjector {
|
||||
status: u32::from(status.as_u16()),
|
||||
message,
|
||||
ratio: Some(proto::Ratio {
|
||||
numerator: ratio.numerator,
|
||||
denominator: ratio.denominator,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_header_modifier_filter(
|
||||
RequestHeaderModifierFilter { add, set, remove }: RequestHeaderModifierFilter,
|
||||
) -> proto::RequestHeaderModifier {
|
||||
proto::RequestHeaderModifier {
|
||||
add: Some(http_types::Headers {
|
||||
headers: add
|
||||
.into_iter()
|
||||
.map(|(n, v)| http_types::headers::Header {
|
||||
name: n.to_string(),
|
||||
value: v.as_bytes().to_owned(),
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
set: Some(http_types::Headers {
|
||||
headers: set
|
||||
.into_iter()
|
||||
.map(|(n, v)| http_types::headers::Header {
|
||||
name: n.to_string(),
|
||||
value: v.as_bytes().to_owned(),
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
remove: remove.into_iter().map(|n| n.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn convert_redirect_filter(
|
||||
RequestRedirectFilter {
|
||||
scheme,
|
||||
host,
|
||||
path,
|
||||
port,
|
||||
status,
|
||||
}: RequestRedirectFilter,
|
||||
) -> proto::RequestRedirect {
|
||||
proto::RequestRedirect {
|
||||
scheme: scheme.map(|ref s| s.into()),
|
||||
host: host.unwrap_or_default(),
|
||||
path: path.map(|pm| proto::PathModifier {
|
||||
replace: Some(match pm {
|
||||
PathModifier::Full(p) => proto::path_modifier::Replace::Full(p),
|
||||
PathModifier::Prefix(p) => proto::path_modifier::Replace::Prefix(p),
|
||||
}),
|
||||
}),
|
||||
port: port.unwrap_or_default(),
|
||||
status: u32::from(status.unwrap_or_default().as_u16()),
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
#![deny(warnings, rust_2018_idioms)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod http_route;
|
||||
|
||||
use futures::prelude::*;
|
||||
use linkerd2_proxy_api::{
|
||||
self as api,
|
||||
|
@ -11,6 +13,7 @@ use linkerd2_proxy_api::{
|
|||
meta::{metadata, Metadata},
|
||||
};
|
||||
use linkerd_policy_controller_core::{
|
||||
http_route::{InboundFilter, InboundHttpRoute, InboundHttpRouteRule},
|
||||
AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer,
|
||||
IdentityMatch, InboundServer, InboundServerStream, IpNet, NetworkMatch, ProxyProtocol,
|
||||
ServerRef,
|
||||
|
@ -177,14 +180,30 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server {
|
|||
ProxyProtocol::Detect { timeout } => Some(proto::proxy_protocol::Kind::Detect(
|
||||
proto::proxy_protocol::Detect {
|
||||
timeout: Some(timeout.into()),
|
||||
http_routes: Default::default(),
|
||||
http_routes: srv
|
||||
.http_routes
|
||||
.iter()
|
||||
.map(|(name, route)| to_http_route(name, route.clone()))
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
ProxyProtocol::Http1 => Some(proto::proxy_protocol::Kind::Http1(
|
||||
proto::proxy_protocol::Http1::default(),
|
||||
proto::proxy_protocol::Http1 {
|
||||
routes: srv
|
||||
.http_routes
|
||||
.iter()
|
||||
.map(|(name, route)| to_http_route(name, route.clone()))
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
ProxyProtocol::Http2 => Some(proto::proxy_protocol::Kind::Http2(
|
||||
proto::proxy_protocol::Http2::default(),
|
||||
proto::proxy_protocol::Http2 {
|
||||
routes: srv
|
||||
.http_routes
|
||||
.iter()
|
||||
.map(|(name, route)| to_http_route(name, route.clone()))
|
||||
.collect(),
|
||||
},
|
||||
)),
|
||||
ProxyProtocol::Grpc => Some(proto::proxy_protocol::Kind::Grpc(
|
||||
proto::proxy_protocol::Grpc::default(),
|
||||
|
@ -344,9 +363,62 @@ fn to_authz(
|
|||
};
|
||||
|
||||
proto::Authz {
|
||||
networks,
|
||||
labels,
|
||||
authentication: Some(authn),
|
||||
metadata: Some(meta),
|
||||
labels,
|
||||
networks,
|
||||
authentication: Some(authn),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_http_route(
|
||||
name: impl ToString,
|
||||
InboundHttpRoute { hostnames, rules }: InboundHttpRoute,
|
||||
) -> proto::HttpRoute {
|
||||
let metadata = Metadata {
|
||||
kind: Some(metadata::Kind::Resource(api::meta::Resource {
|
||||
group: "gateway.networking.k8s.io".to_string(),
|
||||
kind: "HTTPRoute".to_string(),
|
||||
name: name.to_string(),
|
||||
})),
|
||||
};
|
||||
|
||||
let hosts = hostnames
|
||||
.into_iter()
|
||||
.map(http_route::convert_host_match)
|
||||
.collect();
|
||||
|
||||
let rules = rules
|
||||
.into_iter()
|
||||
.map(
|
||||
|InboundHttpRouteRule { matches, filters }| proto::http_route::Rule {
|
||||
matches: matches.into_iter().map(http_route::convert_match).collect(),
|
||||
filters: filters.into_iter().map(convert_filter).collect(),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
proto::HttpRoute {
|
||||
metadata: Some(metadata),
|
||||
hosts,
|
||||
rules,
|
||||
authorizations: Vec::default(), // TODO populate per-route authorizations
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_filter(filter: InboundFilter) -> proto::http_route::Filter {
|
||||
use proto::http_route::filter::Kind;
|
||||
|
||||
proto::http_route::Filter {
|
||||
kind: Some(match filter {
|
||||
InboundFilter::FailureInjector(f) => {
|
||||
Kind::FailureInjector(http_route::convert_failure_injector_filter(f))
|
||||
}
|
||||
InboundFilter::RequestHeaderModifier(f) => {
|
||||
Kind::RequestHeaderModifier(http_route::convert_header_modifier_filter(f))
|
||||
}
|
||||
InboundFilter::RequestRedirect(f) => {
|
||||
Kind::Redirect(http_route::convert_redirect_filter(f))
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ publish = false
|
|||
ahash = "0.7"
|
||||
anyhow = "1"
|
||||
futures = { version = "0.3", default-features = false }
|
||||
k8s-gateway-api = "0.6"
|
||||
kubert = { version = "0.9", default-features = false, features = ["index"] }
|
||||
linkerd-policy-controller-core = { path = "../../core" }
|
||||
linkerd-policy-controller-k8s-api = { path = "../api" }
|
||||
parking_lot = "0.12"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["macros", "rt", "sync"] }
|
||||
tracing = "0.1"
|
||||
|
||||
|
|
|
@ -0,0 +1,260 @@
|
|||
use anyhow::{bail, Error, Result};
|
||||
use k8s_gateway_api as api;
|
||||
use linkerd_policy_controller_core::http_route;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct InboundRouteBinding {
|
||||
pub parents: Vec<InboundParentRef>,
|
||||
pub route: http_route::InboundHttpRoute,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum InboundParentRef {
|
||||
Server(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, thiserror::Error)]
|
||||
pub enum InvalidParentRef {
|
||||
#[error("HTTPRoute resource does not reference a Server resource")]
|
||||
DoesNotSelectServer,
|
||||
|
||||
#[error("HTTPRoute resource may not reference a parent Server in an other namespace")]
|
||||
ServerInAnotherNamespace,
|
||||
|
||||
#[error("HTTPRoute resource may not reference a parent by port")]
|
||||
SpecifiesPort,
|
||||
|
||||
#[error("HTTPRoute resource may not reference a parent by section name")]
|
||||
SpecifiesSection,
|
||||
}
|
||||
|
||||
impl TryFrom<api::HttpRoute> for InboundRouteBinding {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(route: api::HttpRoute) -> Result<Self, Self::Error> {
|
||||
let parents = route
|
||||
.spec
|
||||
.inner
|
||||
.parent_refs
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(
|
||||
|api::ParentReference {
|
||||
group,
|
||||
kind,
|
||||
namespace,
|
||||
name,
|
||||
section_name,
|
||||
port,
|
||||
}| {
|
||||
// Ignore parents that are not a Server.
|
||||
if let Some(g) = group {
|
||||
if let Some(k) = kind {
|
||||
if !g.eq_ignore_ascii_case("policy.linkerd.io")
|
||||
|| !k.eq_ignore_ascii_case("server")
|
||||
|| name.is_empty()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if namespace.is_some() && namespace != route.metadata.namespace {
|
||||
return Some(Err(InvalidParentRef::ServerInAnotherNamespace));
|
||||
}
|
||||
if port.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesPort));
|
||||
}
|
||||
if section_name.is_some() {
|
||||
return Some(Err(InvalidParentRef::SpecifiesSection));
|
||||
}
|
||||
|
||||
Some(Ok(InboundParentRef::Server(name)))
|
||||
},
|
||||
)
|
||||
.collect::<Result<Vec<_>, InvalidParentRef>>()?;
|
||||
// If there are no valid parents, then the route is invalid.
|
||||
if parents.is_empty() {
|
||||
return Err(InvalidParentRef::DoesNotSelectServer.into());
|
||||
}
|
||||
|
||||
let hostnames = route
|
||||
.spec
|
||||
.hostnames
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|hostname| {
|
||||
if hostname.starts_with("*.") {
|
||||
let mut reverse_labels = hostname
|
||||
.split('.')
|
||||
.skip(1)
|
||||
.map(|label| label.to_string())
|
||||
.collect::<Vec<String>>();
|
||||
reverse_labels.reverse();
|
||||
http_route::HostMatch::Suffix { reverse_labels }
|
||||
} else {
|
||||
http_route::HostMatch::Exact(hostname)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let rules = route
|
||||
.spec
|
||||
.rules
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_rule)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(InboundRouteBinding {
|
||||
parents,
|
||||
route: http_route::InboundHttpRoute { hostnames, rules },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl InboundRouteBinding {
|
||||
#[inline]
|
||||
pub fn selects_server(&self, name: &str) -> bool {
|
||||
self.parents
|
||||
.iter()
|
||||
.any(|p| matches!(p, InboundParentRef::Server(n) if n == name))
|
||||
}
|
||||
|
||||
fn try_match(
|
||||
api::HttpRouteMatch {
|
||||
path,
|
||||
headers,
|
||||
query_params,
|
||||
method,
|
||||
}: api::HttpRouteMatch,
|
||||
) -> Result<http_route::HttpRouteMatch> {
|
||||
let path = path
|
||||
.map(|pm| match pm {
|
||||
api::HttpPathMatch::Exact { value } => Ok(http_route::PathMatch::Exact(value)),
|
||||
api::HttpPathMatch::PathPrefix { value } => {
|
||||
Ok(http_route::PathMatch::Prefix(value))
|
||||
}
|
||||
api::HttpPathMatch::RegularExpression { value } => {
|
||||
value.parse().map(http_route::PathMatch::Regex)
|
||||
}
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let headers = headers
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|hm| match hm {
|
||||
api::HttpHeaderMatch::Exact { name, value } => Ok(http_route::HeaderMatch::Exact(
|
||||
name.parse()?,
|
||||
value.parse()?,
|
||||
)),
|
||||
api::HttpHeaderMatch::RegularExpression { name, value } => Ok(
|
||||
http_route::HeaderMatch::Regex(name.parse()?, value.parse()?),
|
||||
),
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let query_params = query_params
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|query_param| match query_param {
|
||||
api::HttpQueryParamMatch::Exact { name, value } => {
|
||||
Ok(http_route::QueryParamMatch::Exact(name, value))
|
||||
}
|
||||
api::HttpQueryParamMatch::RegularExpression { name, value } => {
|
||||
Ok(http_route::QueryParamMatch::Exact(name, value.parse()?))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let method = method
|
||||
.as_deref()
|
||||
.map(http_route::Method::try_from)
|
||||
.transpose()?;
|
||||
|
||||
Ok(http_route::HttpRouteMatch {
|
||||
path,
|
||||
headers,
|
||||
query_params,
|
||||
method,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_rule(rule: api::HttpRouteRule) -> Result<http_route::InboundHttpRouteRule> {
|
||||
let matches = rule
|
||||
.matches
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_match)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
let filters = rule
|
||||
.filters
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(Self::try_filter)
|
||||
.collect::<Result<_>>()?;
|
||||
|
||||
Ok(http_route::InboundHttpRouteRule { matches, filters })
|
||||
}
|
||||
|
||||
fn try_filter(filter: api::HttpRouteFilter) -> Result<http_route::InboundFilter> {
|
||||
let filter = match filter {
|
||||
api::HttpRouteFilter::RequestHeaderModifier {
|
||||
request_header_modifier: api::HttpRequestHeaderFilter { set, add, remove },
|
||||
} => http_route::InboundFilter::RequestHeaderModifier(
|
||||
http_route::RequestHeaderModifierFilter {
|
||||
add: add
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
set: set
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?)))
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
remove: remove
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(http_route::HeaderName::try_from)
|
||||
.collect::<Result<_, _>>()?,
|
||||
},
|
||||
),
|
||||
|
||||
api::HttpRouteFilter::RequestRedirect {
|
||||
request_redirect:
|
||||
api::HttpRequestRedirectFilter {
|
||||
scheme,
|
||||
hostname,
|
||||
path,
|
||||
port,
|
||||
status_code,
|
||||
},
|
||||
} => http_route::InboundFilter::RequestRedirect(http_route::RequestRedirectFilter {
|
||||
scheme: scheme.as_deref().map(TryInto::try_into).transpose()?,
|
||||
host: hostname,
|
||||
path: path.map(|path_mod| match path_mod {
|
||||
api::HttpPathModifier::ReplaceFullPath(s) => http_route::PathModifier::Full(s),
|
||||
api::HttpPathModifier::ReplacePrefixMatch(s) => {
|
||||
http_route::PathModifier::Prefix(s)
|
||||
}
|
||||
}),
|
||||
port: port.map(Into::into),
|
||||
status: status_code.map(TryFrom::try_from).transpose()?,
|
||||
}),
|
||||
|
||||
api::HttpRouteFilter::RequestMirror { .. } => {
|
||||
bail!("RequestMirror filter is not supported")
|
||||
}
|
||||
api::HttpRouteFilter::URLRewrite { .. } => {
|
||||
bail!("URLRewrite filter is not supported")
|
||||
}
|
||||
api::HttpRouteFilter::ExtensionRef { .. } => {
|
||||
bail!("ExtensionRef filter is not supported")
|
||||
}
|
||||
};
|
||||
Ok(filter)
|
||||
}
|
||||
}
|
|
@ -7,14 +7,14 @@
|
|||
//! kubernetes resources.
|
||||
|
||||
use crate::{
|
||||
authorization_policy, defaults::DefaultPolicy, meshtls_authentication, network_authentication,
|
||||
pod, server, server_authorization, ClusterInfo,
|
||||
authorization_policy, defaults::DefaultPolicy, http_route::InboundRouteBinding,
|
||||
meshtls_authentication, network_authentication, pod, server, server_authorization, ClusterInfo,
|
||||
};
|
||||
use ahash::{AHashMap as HashMap, AHashSet as HashSet};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use linkerd_policy_controller_core::{
|
||||
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundServer,
|
||||
IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef,
|
||||
AuthorizationRef, ClientAuthentication, ClientAuthorization, IdentityMatch, InboundHttpRoute,
|
||||
InboundServer, IpNet, Ipv4Net, Ipv6Net, NetworkMatch, ProxyProtocol, ServerRef,
|
||||
};
|
||||
use linkerd_policy_controller_k8s_api::{self as k8s, policy::server::Port, ResourceExt};
|
||||
use parking_lot::RwLock;
|
||||
|
@ -110,6 +110,7 @@ struct PolicyIndex {
|
|||
server_authorizations: HashMap<String, server_authorization::ServerAuthz>,
|
||||
|
||||
authorization_policies: HashMap<String, authorization_policy::Spec>,
|
||||
http_routes: HashMap<String, InboundRouteBinding>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
@ -609,6 +610,88 @@ impl kubert::index::IndexNamespacedResource<k8s::policy::NetworkAuthentication>
|
|||
}
|
||||
}
|
||||
|
||||
impl kubert::index::IndexNamespacedResource<k8s_gateway_api::HttpRoute> for Index {
|
||||
fn apply(&mut self, route: k8s_gateway_api::HttpRoute) {
|
||||
let ns = route.namespace().expect("HttpRoute must have a namespace");
|
||||
let name = route.name_unchecked();
|
||||
let _span = info_span!("apply", %ns, %name).entered();
|
||||
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(%ns, %name, %error, "Ignoring HTTPRoute");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.ns_or_default_with_reindex(ns, |ns| ns.policy.update_http_route(name, route_binding))
|
||||
}
|
||||
|
||||
fn delete(&mut self, ns: String, name: String) {
|
||||
let _span = info_span!("delete", %ns, %name).entered();
|
||||
self.ns_with_reindex(ns, |ns| ns.policy.http_routes.remove(&name).is_some())
|
||||
}
|
||||
|
||||
fn reset(
|
||||
&mut self,
|
||||
routes: Vec<k8s_gateway_api::HttpRoute>,
|
||||
deleted: HashMap<String, HashSet<String>>,
|
||||
) {
|
||||
let _span = info_span!("reset").entered();
|
||||
|
||||
// Aggregate all of the updates by namespace so that we only reindex
|
||||
// once per namespace.
|
||||
type Ns = NsUpdate<InboundRouteBinding>;
|
||||
let mut updates_by_ns = HashMap::<String, Ns>::default();
|
||||
for route in routes.into_iter() {
|
||||
let namespace = route.namespace().expect("HttpRoute must be namespaced");
|
||||
let name = route.name_unchecked();
|
||||
let route_binding = match route.try_into() {
|
||||
Ok(binding) => binding,
|
||||
Err(error) => {
|
||||
tracing::info!(ns = %namespace, %name, %error, "Ignoring HTTPRoute");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
updates_by_ns
|
||||
.entry(namespace)
|
||||
.or_default()
|
||||
.added
|
||||
.push((name, route_binding));
|
||||
}
|
||||
for (ns, names) in deleted.into_iter() {
|
||||
updates_by_ns.entry(ns).or_default().removed = names;
|
||||
}
|
||||
|
||||
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.http_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 name in removed.into_iter() {
|
||||
ns.policy.http_routes.remove(&name);
|
||||
}
|
||||
for (name, route_binding) in added.into_iter() {
|
||||
changed = ns.policy.update_http_route(name, route_binding) || changed;
|
||||
}
|
||||
changed
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === impl NemspaceIndex ===
|
||||
|
||||
impl NamespaceIndex {
|
||||
|
@ -670,6 +753,7 @@ impl Namespace {
|
|||
servers: HashMap::default(),
|
||||
server_authorizations: HashMap::default(),
|
||||
authorization_policies: HashMap::default(),
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -941,6 +1025,7 @@ impl Pod {
|
|||
reference: ServerRef::Default(policy.to_string()),
|
||||
protocol,
|
||||
authorizations,
|
||||
http_routes: HashMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1015,10 +1100,13 @@ impl PolicyIndex {
|
|||
) -> InboundServer {
|
||||
tracing::trace!(%name, ?server, "Creating inbound server");
|
||||
let authorizations = self.client_authzs(&name, server, authentications);
|
||||
let routes = self.http_routes(&name);
|
||||
|
||||
InboundServer {
|
||||
reference: ServerRef::Server(name),
|
||||
authorizations,
|
||||
protocol: server.protocol.clone(),
|
||||
http_routes: routes,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1084,6 +1172,14 @@ impl PolicyIndex {
|
|||
authzs
|
||||
}
|
||||
|
||||
fn http_routes(&self, server_name: &str) -> HashMap<String, InboundHttpRoute> {
|
||||
self.http_routes
|
||||
.iter()
|
||||
.filter(|(_, route)| route.selects_server(server_name))
|
||||
.map(|(name, route)| (name.clone(), route.route.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn policy_client_authz(
|
||||
&self,
|
||||
spec: &authorization_policy::Spec,
|
||||
|
@ -1188,6 +1284,21 @@ impl PolicyIndex {
|
|||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn update_http_route(&mut self, name: String, route: InboundRouteBinding) -> bool {
|
||||
match self.http_routes.entry(name) {
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(route);
|
||||
}
|
||||
Entry::Occupied(mut entry) => {
|
||||
if *entry.get() == route {
|
||||
return false;
|
||||
}
|
||||
entry.insert(route);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// === impl AuthenticationNsIndex ===
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
pub mod authorization_policy;
|
||||
mod defaults;
|
||||
mod http_route;
|
||||
mod index;
|
||||
mod meshtls_authentication;
|
||||
mod network_authentication;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
mod annotation;
|
||||
mod authorization_policy;
|
||||
mod http_routes;
|
||||
mod server_authorization;
|
||||
|
||||
use crate::{defaults::DefaultPolicy, index::*, server_authorization::ServerSelector, ClusterInfo};
|
||||
|
@ -195,6 +196,7 @@ impl TestConfig {
|
|||
protocol: ProxyProtocol::Detect {
|
||||
timeout: self.detect_timeout,
|
||||
},
|
||||
http_routes: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -119,6 +119,7 @@ fn authenticated_annotated() {
|
|||
protocol: ProxyProtocol::Detect {
|
||||
timeout: test.detect_timeout,
|
||||
},
|
||||
http_routes: HashMap::default(),
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -31,6 +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(),
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -84,6 +85,7 @@ fn links_authorization_policy_with_mtls_name() {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
protocol: ProxyProtocol::Http1,
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -119,6 +121,7 @@ fn authorization_targets_namespace() {
|
|||
reference: ServerRef::Server("srv-8080".to_string()),
|
||||
authorizations: Default::default(),
|
||||
protocol: ProxyProtocol::Http1,
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -172,6 +175,7 @@ fn authorization_targets_namespace() {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
protocol: ProxyProtocol::Http1,
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -207,6 +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(),
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -254,6 +259,7 @@ fn links_authorization_policy_with_service_account() {
|
|||
.into_iter()
|
||||
.collect(),
|
||||
protocol: ProxyProtocol::Http1,
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn route_attaches_to_server() {
|
||||
let test = TestConfig::default();
|
||||
|
||||
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)
|
||||
.expect("pod-0.ns-0 should exist");
|
||||
assert_eq!(*rx.borrow_and_update(), test.default_server());
|
||||
|
||||
test.index.write().apply(mk_server(
|
||||
"ns-0",
|
||||
"srv-8080",
|
||||
Port::Number(8080),
|
||||
Some(("app", "app-0")),
|
||||
Some(("app", "app-0")),
|
||||
Some(k8s::policy::server::ProxyProtocol::Http1),
|
||||
));
|
||||
assert!(rx.has_changed().unwrap());
|
||||
assert_eq!(
|
||||
*rx.borrow_and_update(),
|
||||
InboundServer {
|
||||
reference: ServerRef::Server("srv-8080".to_string()),
|
||||
authorizations: Default::default(),
|
||||
protocol: ProxyProtocol::Http1,
|
||||
http_routes: HashMap::default(),
|
||||
},
|
||||
);
|
||||
test.index
|
||||
.write()
|
||||
.apply(mk_http_route("ns-0", "route-foo", "srv-8080"));
|
||||
assert!(rx.has_changed().unwrap());
|
||||
assert_eq!(
|
||||
rx.borrow().reference,
|
||||
ServerRef::Server("srv-8080".to_string())
|
||||
);
|
||||
assert!(rx.borrow().http_routes.contains_key("route-foo"));
|
||||
}
|
||||
|
||||
fn mk_http_route(
|
||||
ns: impl ToString,
|
||||
name: impl ToString,
|
||||
server: impl ToString,
|
||||
) -> k8s_gateway_api::HttpRoute {
|
||||
k8s_gateway_api::HttpRoute {
|
||||
metadata: k8s::ObjectMeta {
|
||||
namespace: Some(ns.to_string()),
|
||||
name: Some(name.to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: server.to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::PathPrefix {
|
||||
value: "/foo/bar".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
}]),
|
||||
},
|
||||
status: None,
|
||||
}
|
||||
}
|
|
@ -43,6 +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(),
|
||||
},
|
||||
);
|
||||
test.index.write().apply(mk_server_authz(
|
||||
|
|
|
@ -450,30 +450,14 @@ fn validate_http_route_rule(rule: &HttpRouteRule) -> Result<()> {
|
|||
}
|
||||
|
||||
fn validate_http_route_filters(filters: &[HttpRouteFilter]) -> Result<()> {
|
||||
// for filter in filters.iter() {
|
||||
// match filter {
|
||||
// HttpRouteFilter::ExtensionRef{ .. } => bail!("ExtensionRef filters are not supported"),
|
||||
// HttpRouteFilter::RequestHeaderModifier { .. } => bail!("RequestHeaderModifier filters are not supported"),
|
||||
// HttpRouteFilter::RequestMirror { .. } => bail!("RequestMirror filters are not supported"),
|
||||
// HttpRouteFilter::RequestRedirect { .. } => bail!("RequestRedirect filters are not supported"),
|
||||
// HttpRouteFilter::URLRewrite { .. } => bail!("URLRewrite filters are not supported"),
|
||||
// }
|
||||
// }
|
||||
// Since we don't support any filter types yet, we will always fail on the
|
||||
// first filter. Once it becomes possible for this validation to pass, we
|
||||
// should iterate through all filters, as in the commented-out code above.
|
||||
if let Some(filter) = filters.iter().next() {
|
||||
for filter in filters.iter() {
|
||||
match filter {
|
||||
HttpRouteFilter::ExtensionRef { .. } => bail!("ExtensionRef filters are not supported"),
|
||||
HttpRouteFilter::RequestHeaderModifier { .. } => {
|
||||
bail!("RequestHeaderModifier filters are not supported")
|
||||
}
|
||||
HttpRouteFilter::RequestHeaderModifier { .. } => {}
|
||||
HttpRouteFilter::RequestMirror { .. } => {
|
||||
bail!("RequestMirror filters are not supported")
|
||||
}
|
||||
HttpRouteFilter::RequestRedirect { .. } => {
|
||||
bail!("RequestRedirect filters are not supported")
|
||||
}
|
||||
HttpRouteFilter::RequestRedirect { .. } => {}
|
||||
HttpRouteFilter::URLRewrite { .. } => bail!("URLRewrite filters are not supported"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,6 +146,11 @@ async fn main() -> Result<()> {
|
|||
.instrument(info_span!("networkauthentications")),
|
||||
);
|
||||
|
||||
let http_routes = runtime.watch_all::<k8s_gateway_api::HttpRoute>(ListParams::default());
|
||||
tokio::spawn(
|
||||
kubert::index::namespaced(index.clone(), http_routes).instrument(info_span!("httproutes")),
|
||||
);
|
||||
|
||||
// Run the gRPC server, serving results by looking up against the index handle.
|
||||
tokio::spawn(grpc(
|
||||
grpc_addr,
|
||||
|
|
|
@ -58,7 +58,7 @@ async fn server_with_server_authorization() {
|
|||
)),
|
||||
);
|
||||
|
||||
// Create a server authorizaation that refers to the `linkerd-admin`
|
||||
// Create a server authorization that refers to the `linkerd-admin`
|
||||
// server (by name) and ensure that the update now reflects this
|
||||
// authorization.
|
||||
create(
|
||||
|
@ -286,6 +286,207 @@ async fn server_with_authorization_policy() {
|
|||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn server_with_http_route() {
|
||||
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;
|
||||
tracing::trace!(?pod);
|
||||
|
||||
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);
|
||||
|
||||
// Create a server that selects the pod's proxy admin server and ensure
|
||||
// that the update now uses this server, which has no authorizations
|
||||
// and no routes.
|
||||
let _server = create(&client, mk_admin_server(&ns, "linkerd-admin")).await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.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.authorizations, vec![]);
|
||||
assert_eq!(
|
||||
config.labels,
|
||||
convert_args!(hashmap!(
|
||||
"group" => "policy.linkerd.io",
|
||||
"kind" => "server",
|
||||
"name" => "linkerd-admin"
|
||||
)),
|
||||
);
|
||||
|
||||
// Create an http route that refers to the `linkerd-admin` server (by
|
||||
// name) and ensure that the update now reflects this route.
|
||||
create(
|
||||
&client,
|
||||
k8s_gateway_api::HttpRoute {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("metrics-route".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s_gateway_api::HttpRouteSpec {
|
||||
inner: k8s_gateway_api::CommonRouteSpec {
|
||||
parent_refs: Some(vec![k8s_gateway_api::ParentReference {
|
||||
group: Some("policy.linkerd.io".to_string()),
|
||||
kind: Some("Server".to_string()),
|
||||
namespace: None,
|
||||
name: "linkerd-admin".to_string(),
|
||||
section_name: None,
|
||||
port: None,
|
||||
}]),
|
||||
},
|
||||
hostnames: None,
|
||||
rules: Some(vec![k8s_gateway_api::HttpRouteRule {
|
||||
matches: Some(vec![k8s_gateway_api::HttpRouteMatch {
|
||||
path: Some(k8s_gateway_api::HttpPathMatch::Exact {
|
||||
value: "/metrics".to_string(),
|
||||
}),
|
||||
headers: None,
|
||||
query_params: None,
|
||||
method: Some("GET".to_string()),
|
||||
}]),
|
||||
filters: None,
|
||||
backend_refs: None,
|
||||
}]),
|
||||
},
|
||||
status: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
};
|
||||
let route = http1.routes.first().expect("must have route");
|
||||
let rule_match = route
|
||||
.rules
|
||||
.first()
|
||||
.expect("must have rule")
|
||||
.matches
|
||||
.first()
|
||||
.expect("must have match");
|
||||
// Route has no authorizations by default.
|
||||
assert_eq!(route.authorizations, Vec::default());
|
||||
// Route has appropriate metadata.
|
||||
assert_eq!(
|
||||
route.metadata.to_owned().expect("route must have metadata"),
|
||||
grpc::meta::Metadata {
|
||||
kind: Some(grpc::meta::metadata::Kind::Resource(grpc::meta::Resource {
|
||||
group: "gateway.networking.k8s.io".to_string(),
|
||||
kind: "HTTPRoute".to_string(),
|
||||
name: "metrics-route".to_string(),
|
||||
}))
|
||||
}
|
||||
);
|
||||
// Route has path match.
|
||||
assert_eq!(
|
||||
rule_match
|
||||
.path
|
||||
.to_owned()
|
||||
.expect("must have path match")
|
||||
.kind
|
||||
.expect("must have kind"),
|
||||
grpc::http_route::path_match::Kind::Exact("/metrics".to_string()),
|
||||
);
|
||||
|
||||
// Create a server authorization that refers to the `linkerd-admin`
|
||||
// server (by name) and ensure that the authorization is copied onto
|
||||
// the route.
|
||||
create(
|
||||
&client,
|
||||
k8s::policy::ServerAuthorization {
|
||||
metadata: kube::api::ObjectMeta {
|
||||
namespace: Some(ns.clone()),
|
||||
name: Some("all-admin".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
spec: k8s::policy::ServerAuthorizationSpec {
|
||||
server: k8s::policy::server_authorization::Server {
|
||||
name: Some("linkerd-admin".to_string()),
|
||||
selector: None,
|
||||
},
|
||||
client: k8s::policy::server_authorization::Client {
|
||||
unauthenticated: true,
|
||||
..k8s::policy::server_authorization::Client::default()
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.expect("watch must not fail")
|
||||
.expect("watch must return an updated config");
|
||||
tracing::trace!(?config);
|
||||
let http1 = if let grpc::inbound::proxy_protocol::Kind::Http1(http1) = config
|
||||
.protocol
|
||||
.expect("must have proxy protocol")
|
||||
.kind
|
||||
.expect("must have kind")
|
||||
{
|
||||
http1
|
||||
} else {
|
||||
panic!("proxy protocol must be HTTP1")
|
||||
};
|
||||
|
||||
assert_eq!(http1.routes.len(), 1, "must have routes");
|
||||
|
||||
// Delete the `HttpRoute` and ensure that the update reverts to the
|
||||
// default.
|
||||
kube::Api::<k8s_gateway_api::HttpRoute>::namespaced(client.clone(), &ns)
|
||||
.delete("metrics-route", &kube::api::DeleteParams::default())
|
||||
.await
|
||||
.expect("HttpRoute must be deleted");
|
||||
let config = rx
|
||||
.next()
|
||||
.await
|
||||
.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()
|
||||
)),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
fn mk_pause(ns: &str, name: &str) -> k8s::Pod {
|
||||
k8s::Pod {
|
||||
metadata: k8s::ObjectMeta {
|
||||
|
|
Loading…
Reference in New Issue