diff --git a/Cargo.lock b/Cargo.lock index b3db16bee2391..f1779c38c323f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -982,7 +982,7 @@ dependencies = [ "anyhow", "async-trait", "futures", - "hyper", + "http", "ipnet", "regex", ] @@ -1033,6 +1033,7 @@ dependencies = [ "linkerd-policy-controller-k8s-api", "maplit", "parking_lot", + "thiserror", "tokio", "tokio-stream", "tokio-test", @@ -1069,7 +1070,7 @@ dependencies = [ [[package]] name = "linkerd2-proxy-api" version = "0.5.0" -source = "git+https://github.com/linkerd/linkerd2-proxy-api?branch=ver/rautz#c8d0904fc73ab1f4279878b2b7e5113d7b1a1169" +source = "git+https://github.com/linkerd/linkerd2-proxy-api?branch=ver/rautz#6e97a203d8245fc258cf14ae4304738aa555aa90" dependencies = [ "http", "ipnet", diff --git a/deny.toml b/deny.toml index 7620fe6232367..7cc5f616d8bc4 100644 --- a/deny.toml +++ b/deny.toml @@ -49,3 +49,9 @@ unknown-registry = "deny" unknown-git = "deny" allow-registry = ["https://github.com/rust-lang/crates.io-index"] allow-git = [] + +[sources.allow-org] +github = [ + "linkerd", +] + diff --git a/go.mod b/go.mod index 5a305be888dff..1683f363dd6b0 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( go.opencensus.io v0.23.0 golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/tools v0.1.11 - google.golang.org/grpc v1.47.0 + google.golang.org/grpc v1.48.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 google.golang.org/protobuf v1.28.0 helm.sh/helm/v3 v3.9.0 diff --git a/go.sum b/go.sum index 0c22a5ca60e9a..c06ca41a7a5c2 100644 --- a/go.sum +++ b/go.sum @@ -1583,8 +1583,8 @@ google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 h1:TLkBREm4nIsEcexnCjgQd5GQWaHcqMzwQV0TX9pq8S0= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= diff --git a/policy-controller/core/Cargo.toml b/policy-controller/core/Cargo.toml index fc2c434e007d8..7c14538c17cf0 100644 --- a/policy-controller/core/Cargo.toml +++ b/policy-controller/core/Cargo.toml @@ -10,6 +10,6 @@ ahash = "0.7" anyhow = "1" async-trait = "0.1" futures = { version = "0.3", default-features = false, features = ["std"] } -hyper = { version = "0.14" } +http = "0.2" ipnet = "2" regex = "1" diff --git a/policy-controller/core/src/http_route.rs b/policy-controller/core/src/http_route.rs index 8787b5c006dbc..3e2cc87fa290c 100644 --- a/policy-controller/core/src/http_route.rs +++ b/policy-controller/core/src/http_route.rs @@ -1,45 +1,57 @@ -use ahash::AHashMap as HashMap; use anyhow::Result; -pub use hyper::http::{uri::Scheme, Method, StatusCode}; +pub use http::{ + header::{HeaderName, HeaderValue}, + uri::Scheme, + Method, StatusCode, +}; use regex::Regex; #[derive(Clone, Debug, PartialEq, Eq)] -pub struct HttpRoute { - pub hostnames: Vec, - pub rules: Vec, +pub struct InboundHttpRoute { + pub hostnames: Vec, + pub rules: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Hostname { +pub enum HostMatch { Exact(String), Suffix { reverse_labels: Vec }, } #[derive(Clone, Debug, PartialEq, Eq)] -pub struct HttpRouteRule { +pub struct InboundHttpRouteRule { pub matches: Vec, - pub filters: Vec, + pub filters: Vec, } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum HttpFilter { - RequestHeaderModifier { - add: HashMap, - set: HashMap, - remove: Vec, - }, - RequestRedirect { - scheme: Option, - host: Option, - path: Option, - port: Option, - status: Option, - }, - HttpFailureInjector { - status: StatusCode, - message: String, - ratio: Ratio, - }, +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, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RequestRedirectFilter { + pub scheme: Option, + pub host: Option, + pub path: Option, + pub port: Option, + pub status: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FailureInjectorFilter { + pub status: StatusCode, + pub message: String, + pub ratio: Ratio, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -69,23 +81,19 @@ pub enum PathMatch { Regex(Regex), } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct HeaderMatch { - pub name: String, - pub value: Value, +#[derive(Clone, Debug)] +pub enum HeaderMatch { + Exact(HeaderName, HeaderValue), + Regex(HeaderName, Regex), } #[derive(Clone, Debug)] -pub enum Value { - Exact(String), - Regex(Regex), +pub enum QueryParamMatch { + Exact(String, String), + Regex(String, Regex), } -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct QueryParamMatch { - pub name: String, - pub value: Value, -} +// === impl PathMatch === impl PartialEq for PathMatch { fn eq(&self, other: &Self) -> bool { @@ -106,20 +114,30 @@ impl PathMatch { } } -impl PartialEq for Value { +// === impl HeaderMatch === + +impl PartialEq for HeaderMatch { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Exact(l0), Self::Exact(r0)) => l0 == r0, - (Self::Regex(l0), Self::Regex(r0)) => l0.as_str() == r0.as_str(), + (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 Value {} +impl Eq for HeaderMatch {} -impl Value { - pub fn regex(s: &str) -> Result { - Ok(Self::Regex(Regex::new(s)?)) +// === 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 {} diff --git a/policy-controller/core/src/lib.rs b/policy-controller/core/src/lib.rs index 8d7496e58abbc..be28938ebb99a 100644 --- a/policy-controller/core/src/lib.rs +++ b/policy-controller/core/src/lib.rs @@ -5,7 +5,9 @@ pub mod http_route; mod identity_match; mod network_match; -pub use self::{http_route::HttpRoute, 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::*; @@ -29,7 +31,7 @@ pub struct InboundServer { pub protocol: ProxyProtocol, pub authorizations: HashMap, - pub http_routes: HashMap, + pub http_routes: HashMap, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/policy-controller/grpc/src/http_route.rs b/policy-controller/grpc/src/http_route.rs new file mode 100644 index 0000000000000..72f6249ffe014 --- /dev/null +++ b/policy-controller/grpc/src/http_route.rs @@ -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()), + } +} diff --git a/policy-controller/grpc/src/lib.rs b/policy-controller/grpc/src/lib.rs index a60dcf354dc7e..fa66b95201b7d 100644 --- a/policy-controller/grpc/src/lib.rs +++ b/policy-controller/grpc/src/lib.rs @@ -1,9 +1,11 @@ #![deny(warnings, rust_2018_idioms)] #![forbid(unsafe_code)] +mod http_route; + use futures::prelude::*; use linkerd2_proxy_api::{ - http_route, http_types, + self as api, inbound::{ self as proto, inbound_server_policies_server::{InboundServerPolicies, InboundServerPoliciesServer}, @@ -11,9 +13,8 @@ use linkerd2_proxy_api::{ meta::{metadata, Metadata}, }; use linkerd_policy_controller_core::{ - http_route::Hostname, - http_route::{HttpFilter, HttpRouteMatch, PathModifier, Value}, - AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer, HttpRoute, + http_route::{InboundFilter, InboundHttpRoute, InboundHttpRouteRule}, + AuthorizationRef, ClientAuthentication, ClientAuthorization, DiscoverInboundServer, IdentityMatch, InboundServer, InboundServerStream, IpNet, NetworkMatch, ProxyProtocol, ServerRef, }; @@ -182,7 +183,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server { http_routes: srv .http_routes .iter() - .map(|(name, route)| to_http_route(name, route)) + .map(|(name, route)| to_http_route(name, route.clone())) .collect(), }, )), @@ -191,7 +192,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server { routes: srv .http_routes .iter() - .map(|(name, route)| to_http_route(name, route)) + .map(|(name, route)| to_http_route(name, route.clone())) .collect(), }, )), @@ -200,7 +201,7 @@ fn to_server(srv: &InboundServer, cluster_networks: &[IpNet]) -> proto::Server { routes: srv .http_routes .iter() - .map(|(name, route)| to_http_route(name, route)) + .map(|(name, route)| to_http_route(name, route.clone())) .collect(), }, )), @@ -347,179 +348,55 @@ fn to_authz( } } -fn to_http_route(name: impl ToString, route: &HttpRoute) -> proto::HttpRoute { +fn to_http_route( + name: impl ToString, + InboundHttpRoute { hostnames, rules }: InboundHttpRoute, +) -> proto::HttpRoute { let metadata = Metadata { - kind: Some(metadata::Kind::Resource( - linkerd2_proxy_api::meta::Resource { - group: "gateway.networking.k8s.io".to_string(), - kind: "HTTPRoute".to_string(), - name: name.to_string(), - }, - )), + kind: Some(metadata::Kind::Resource(api::meta::Resource { + group: "gateway.networking.k8s.io".to_string(), + kind: "HTTPRoute".to_string(), + name: name.to_string(), + })), }; - let route = route.clone(); - - let hosts = route - .hostnames + let hosts = hostnames .into_iter() - .map(|hostname| match hostname { - Hostname::Exact(host) => http_route::HostMatch { - r#match: Some(http_route::host_match::Match::Exact(host)), - }, - Hostname::Suffix { reverse_labels } => http_route::HostMatch { - r#match: Some(http_route::host_match::Match::Suffix( - http_route::host_match::Suffix { - reverse_labels: reverse_labels.to_vec(), - }, - )), - }, - }) + .map(http_route::convert_host_match) .collect(); - let rules = route - .rules + let rules = rules .into_iter() - .map(|rule| { - let matches = rule.matches.into_iter().map(to_match).collect(); - let filters = rule.filters.into_iter().map(to_filter).collect(); - proto::http_route::Rule { matches, filters } - }) + .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, - authorizations: Vec::default(), rules, + authorizations: Vec::default(), // TODO populate per-route authorizations } } -fn to_match(route_match: HttpRouteMatch) -> http_route::HttpRouteMatch { - let headers = route_match - .headers - .into_iter() - .map(|header_match| { - let value = match header_match.value { - Value::Exact(value) => http_route::header_match::Value::Exact(value), - Value::Regex(value) => http_route::header_match::Value::Regex(value.to_string()), - }; - http_route::HeaderMatch { - name: header_match.name, - value: Some(value), - } - }) - .collect(); +fn convert_filter(filter: InboundFilter) -> proto::http_route::Filter { + use proto::http_route::filter::Kind; - let path = route_match.path.map(|path| match path { - linkerd_policy_controller_core::http_route::PathMatch::Exact(path) => { - http_route::PathMatch { - kind: Some(http_route::path_match::Kind::Exact(path)), + proto::http_route::Filter { + kind: Some(match filter { + InboundFilter::FailureInjector(f) => { + Kind::FailureInjector(http_route::convert_failure_injector_filter(f)) } - } - linkerd_policy_controller_core::http_route::PathMatch::Prefix(prefix) => { - http_route::PathMatch { - kind: Some(http_route::path_match::Kind::Prefix(prefix)), - } - } - linkerd_policy_controller_core::http_route::PathMatch::Regex(regex) => { - http_route::PathMatch { - kind: Some(http_route::path_match::Kind::Regex(regex.to_string())), + InboundFilter::RequestHeaderModifier(f) => { + Kind::RequestHeaderModifier(http_route::convert_header_modifier_filter(f)) } - } - }); - - let query_params = route_match - .query_params - .into_iter() - .map(|query_param| { - let value = match query_param.value { - Value::Exact(value) => http_route::query_param_match::Value::Exact(value), - Value::Regex(value) => { - http_route::query_param_match::Value::Regex(value.to_string()) - } - }; - http_route::QueryParamMatch { - name: query_param.name, - value: Some(value), + InboundFilter::RequestRedirect(f) => { + Kind::Redirect(http_route::convert_redirect_filter(f)) } - }) - .collect(); - - let method = route_match.method.map(|method| http_types::HttpMethod { - r#type: Some(method.into()), - }); - - http_route::HttpRouteMatch { - headers, - path, - query_params, - method, - } -} - -fn to_filter(filter: HttpFilter) -> proto::http_route::Filter { - let kind = match filter { - HttpFilter::HttpFailureInjector { - status, - message, - ratio, - } => proto::http_route::filter::Kind::FailureInjector(http_route::HttpFailureInjector { - status: u32::from(status.as_u16()), - message, - ratio: Some(http_route::Ratio { - numerator: ratio.numerator, - denominator: ratio.denominator, - }), - }), - HttpFilter::RequestHeaderModifier { add, set, remove } => { - let add_headers = add - .into_iter() - .map(|(k, v)| http_types::headers::Header { - name: k, - value: v.into(), - }) - .collect(); - let set_headers = set - .into_iter() - .map(|(k, v)| http_types::headers::Header { - name: k, - value: v.into(), - }) - .collect(); - proto::http_route::filter::Kind::RequestHeaderModifier( - http_route::RequestHeaderModifier { - add: Some(http_types::Headers { - headers: add_headers, - }), - set: Some(http_types::Headers { - headers: set_headers, - }), - remove, - }, - ) - } - HttpFilter::RequestRedirect { - scheme, - host, - path, - port, - status, - } => proto::http_route::filter::Kind::Redirect(http_route::RequestRedirect { - scheme: scheme.map(|ref s| s.into()), - host: host.unwrap_or_default(), - path: path.map(|pm| { - let replace = match pm { - PathModifier::Full(p) => http_route::path_modifier::Replace::Full(p), - PathModifier::Prefix(p) => http_route::path_modifier::Replace::Prefix(p), - }; - http_route::PathModifier { - replace: Some(replace), - } - }), - port: port.unwrap_or_default(), - status: u32::from(status.unwrap_or_default().as_u16()), }), - }; - proto::http_route::Filter { kind: Some(kind) } + } } diff --git a/policy-controller/k8s/index/Cargo.toml b/policy-controller/k8s/index/Cargo.toml index 7071357f34b87..648516ab252a7 100644 --- a/policy-controller/k8s/index/Cargo.toml +++ b/policy-controller/k8s/index/Cargo.toml @@ -14,6 +14,7 @@ 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" diff --git a/policy-controller/k8s/index/src/authorization_policy.rs b/policy-controller/k8s/index/src/authorization_policy.rs index cbf9dfd707389..10dcbe68f39a2 100644 --- a/policy-controller/k8s/index/src/authorization_policy.rs +++ b/policy-controller/k8s/index/src/authorization_policy.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::Result; use linkerd_policy_controller_k8s_api::{ self as k8s, policy::{LocalTargetRef, NamespacedTargetRef}, @@ -33,6 +33,12 @@ pub(crate) enum AuthenticationTarget { }, } +#[inline] +pub fn validate(ap: k8s::policy::AuthorizationPolicySpec) -> Result<()> { + Spec::try_from(ap)?; + Ok(()) +} + impl TryFrom for Spec { type Error = anyhow::Error; @@ -44,9 +50,6 @@ impl TryFrom for Spec { .into_iter() .map(authentication_ref) .collect::>>()?; - if authentications.is_empty() { - bail!("No authentication targets"); - } Ok(Self { target, diff --git a/policy-controller/k8s/index/src/http_route.rs b/policy-controller/k8s/index/src/http_route.rs index 220c73592487a..da5647656ee32 100644 --- a/policy-controller/k8s/index/src/http_route.rs +++ b/policy-controller/k8s/index/src/http_route.rs @@ -1,20 +1,79 @@ use anyhow::{bail, Error, Result}; -use k8s_gateway_api::ParentReference; -use linkerd_policy_controller_core::http_route::{ - HeaderMatch, Hostname, HttpFilter, HttpRoute, HttpRouteMatch, HttpRouteRule, PathMatch, - PathModifier, QueryParamMatch, Value, -}; +use k8s_gateway_api as api; +use linkerd_policy_controller_core::http_route; #[derive(Clone, Debug, PartialEq)] -pub struct RouteBinding { - pub route: HttpRoute, - pub parent_refs: Vec, +pub struct InboundRouteBinding { + pub parents: Vec, + pub route: http_route::InboundHttpRoute, } -impl TryFrom for RouteBinding { +#[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 for InboundRouteBinding { type Error = Error; - fn try_from(route: k8s_gateway_api::HttpRoute) -> Result { + fn try_from(route: api::HttpRoute) -> Result { + 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 group.as_deref() != Some("policy.linkerd.io") + || kind.as_deref() != Some("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::, 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 @@ -28,9 +87,9 @@ impl TryFrom for RouteBinding { .map(|label| label.to_string()) .collect::>(); reverse_labels.reverse(); - Hostname::Suffix { reverse_labels } + http_route::HostMatch::Suffix { reverse_labels } } else { - Hostname::Exact(hostname) + http_route::HostMatch::Exact(hostname) } }) .collect(); @@ -43,41 +102,37 @@ impl TryFrom for RouteBinding { .map(Self::try_rule) .collect::>()?; - Ok(RouteBinding { - route: HttpRoute { hostnames, rules }, - parent_refs: route.spec.inner.parent_refs.unwrap_or_default(), + Ok(InboundRouteBinding { + parents, + route: http_route::InboundHttpRoute { hostnames, rules }, }) } } -impl RouteBinding { +impl InboundRouteBinding { + #[inline] pub fn selects_server(&self, name: &str) -> bool { - for parent_ref in self.parent_refs.iter() { - if parent_ref.group.as_deref() == Some("policy.linkerd.io") - && parent_ref.kind.as_deref() == Some("Server") - && parent_ref.name == name - { - return true; - } - } - false + self.parents + .iter() + .any(|p| matches!(p, InboundParentRef::Server(n) if n == name)) } - fn try_match(route_match: k8s_gateway_api::HttpRouteMatch) -> Result { - let k8s_gateway_api::HttpRouteMatch { + fn try_match( + api::HttpRouteMatch { path, headers, query_params, method, - } = route_match; + }: api::HttpRouteMatch, + ) -> Result { let path = path - .map(|path_match| match path_match { - k8s_gateway_api::HttpPathMatch::Exact { value } => Ok(PathMatch::Exact(value)), - k8s_gateway_api::HttpPathMatch::PathPrefix { value } => { - Ok(PathMatch::Prefix(value)) + .map(|pm| match pm { + api::HttpPathMatch::Exact { value } => Ok(http_route::PathMatch::Exact(value)), + api::HttpPathMatch::PathPrefix { value } => { + Ok(http_route::PathMatch::Prefix(value)) } - k8s_gateway_api::HttpPathMatch::RegularExpression { value } => { - PathMatch::regex(&value) + api::HttpPathMatch::RegularExpression { value } => { + value.parse().map(http_route::PathMatch::Regex) } }) .transpose()?; @@ -85,17 +140,14 @@ impl RouteBinding { let headers = headers .into_iter() .flatten() - .map(|header_match| match header_match { - k8s_gateway_api::HttpHeaderMatch::Exact { name, value } => Ok(HeaderMatch { - name, - value: Value::Exact(value), - }), - k8s_gateway_api::HttpHeaderMatch::RegularExpression { name, value } => { - Ok(HeaderMatch { - name, - value: Value::regex(&value)?, - }) - } + .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::>()?; @@ -103,24 +155,21 @@ impl RouteBinding { .into_iter() .flatten() .map(|query_param| match query_param { - k8s_gateway_api::HttpQueryParamMatch::Exact { name, value } => { - Ok(QueryParamMatch { - name, - value: Value::Exact(value), - }) + api::HttpQueryParamMatch::Exact { name, value } => { + Ok(http_route::QueryParamMatch::Exact(name, value)) } - k8s_gateway_api::HttpQueryParamMatch::RegularExpression { name, value } => { - Ok(QueryParamMatch { - name, - value: Value::regex(&value)?, - }) + api::HttpQueryParamMatch::RegularExpression { name, value } => { + Ok(http_route::QueryParamMatch::Exact(name, value.parse()?)) } }) .collect::>()?; - let method = method.as_deref().map(TryInto::try_into).transpose()?; + let method = method + .as_deref() + .map(http_route::Method::try_from) + .transpose()?; - Ok(HttpRouteMatch { + Ok(http_route::HttpRouteMatch { path, headers, query_params, @@ -128,7 +177,7 @@ impl RouteBinding { }) } - fn try_rule(rule: k8s_gateway_api::HttpRouteRule) -> Result { + fn try_rule(rule: api::HttpRouteRule) -> Result { let matches = rule .matches .into_iter() @@ -143,55 +192,62 @@ impl RouteBinding { .map(Self::try_filter) .collect::>()?; - Ok(HttpRouteRule { matches, filters }) + Ok(http_route::InboundHttpRouteRule { matches, filters }) } - fn try_filter(filter: k8s_gateway_api::HttpRouteFilter) -> Result { + fn try_filter(filter: api::HttpRouteFilter) -> Result { let filter = match filter { - k8s_gateway_api::HttpRouteFilter::RequestHeaderModifier { - request_header_modifier: - k8s_gateway_api::HttpRequestHeaderFilter { set, add, remove }, - } => HttpFilter::RequestHeaderModifier { - add: add - .into_iter() - .flatten() - .map(|header| (header.name, header.value)) - .collect(), - set: set - .into_iter() - .flatten() - .map(|header| (header.name, header.value)) - .collect(), - remove: remove.unwrap_or_default(), - }, - k8s_gateway_api::HttpRouteFilter::RequestMirror { .. } => { - bail!("RequestMirror filter is not supported") - } - k8s_gateway_api::HttpRouteFilter::RequestRedirect { + 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::>>()?, + set: set + .into_iter() + .flatten() + .map(|api::HttpHeader { name, value }| Ok((name.parse()?, value.parse()?))) + .collect::>>()?, + remove: remove + .into_iter() + .flatten() + .map(http_route::HeaderName::try_from) + .collect::>()?, + }, + ), + + api::HttpRouteFilter::RequestRedirect { request_redirect: - k8s_gateway_api::HttpRequestRedirectFilter { + api::HttpRequestRedirectFilter { scheme, hostname, path, port, status_code, }, - } => HttpFilter::RequestRedirect { + } => 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 { - k8s_gateway_api::HttpPathModifier::ReplaceFullPath(s) => PathModifier::Full(s), - k8s_gateway_api::HttpPathModifier::ReplacePrefixMatch(s) => { - PathModifier::Prefix(s) + 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()?, - }, - k8s_gateway_api::HttpRouteFilter::URLRewrite { .. } => { + }), + + api::HttpRouteFilter::RequestMirror { .. } => { + bail!("RequestMirror filter is not supported") + } + api::HttpRouteFilter::URLRewrite { .. } => { bail!("URLRewrite filter is not supported") } - k8s_gateway_api::HttpRouteFilter::ExtensionRef { .. } => { + api::HttpRouteFilter::ExtensionRef { .. } => { bail!("ExtensionRef filter is not supported") } }; diff --git a/policy-controller/k8s/index/src/index.rs b/policy-controller/k8s/index/src/index.rs index 6be3e29e55f28..0b12a21ffa508 100644 --- a/policy-controller/k8s/index/src/index.rs +++ b/policy-controller/k8s/index/src/index.rs @@ -7,13 +7,13 @@ //! kubernetes resources. use crate::{ - authorization_policy, defaults::DefaultPolicy, http_route::RouteBinding, + 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, HttpRoute, IdentityMatch, + 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}; @@ -110,7 +110,7 @@ struct PolicyIndex { server_authorizations: HashMap, authorization_policies: HashMap, - http_routes: HashMap, + http_routes: HashMap, } #[derive(Debug, Default)] @@ -619,7 +619,7 @@ impl kubert::index::IndexNamespacedResource for Inde let route_binding = match route.try_into() { Ok(binding) => binding, Err(error) => { - tracing::warn!(%ns, %name, %error, "Invalid HttpRoute"); + tracing::info!(%ns, %name, %error, "Ignoring HTTPRoute"); return; } }; @@ -641,7 +641,7 @@ impl kubert::index::IndexNamespacedResource for Inde // Aggregate all of the updates by namespace so that we only reindex // once per namespace. - type Ns = NsUpdate; + type Ns = NsUpdate; let mut updates_by_ns = HashMap::::default(); for route in routes.into_iter() { let namespace = route.namespace().expect("HttpRoute must be namespaced"); @@ -649,8 +649,8 @@ impl kubert::index::IndexNamespacedResource for Inde let route_binding = match route.try_into() { Ok(binding) => binding, Err(error) => { - tracing::warn!(ns = %namespace, %name, %error, "Invalid HttpRoute"); - return; + tracing::info!(ns = %namespace, %name, %error, "Ignoring HTTPRoute"); + continue; } }; updates_by_ns @@ -1172,7 +1172,7 @@ impl PolicyIndex { authzs } - fn http_routes(&self, server_name: &str) -> HashMap { + fn http_routes(&self, server_name: &str) -> HashMap { self.http_routes .iter() .filter(|(_, route)| route.selects_server(server_name)) @@ -1285,7 +1285,7 @@ impl PolicyIndex { }) } - fn update_http_route(&mut self, name: String, route: RouteBinding) -> bool { + fn update_http_route(&mut self, name: String, route: InboundRouteBinding) -> bool { match self.http_routes.entry(name) { Entry::Vacant(entry) => { entry.insert(route); diff --git a/policy-controller/k8s/index/src/lib.rs b/policy-controller/k8s/index/src/lib.rs index 6587d5425de60..7cc2fbb2f7ac6 100644 --- a/policy-controller/k8s/index/src/lib.rs +++ b/policy-controller/k8s/index/src/lib.rs @@ -23,7 +23,7 @@ #![deny(warnings, rust_2018_idioms)] #![forbid(unsafe_code)] -mod authorization_policy; +pub mod authorization_policy; mod defaults; mod http_route; mod index; diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs index fd5697af407b0..ec5fed1b7680e 100644 --- a/policy-controller/src/admission.rs +++ b/policy-controller/src/admission.rs @@ -2,8 +2,9 @@ use crate::k8s::{ labels, policy::{ AuthorizationPolicy, AuthorizationPolicySpec, LocalTargetRef, MeshTLSAuthentication, - MeshTLSAuthenticationSpec, NetworkAuthentication, NetworkAuthenticationSpec, Server, - ServerAuthorization, ServerAuthorizationSpec, ServerSpec, + MeshTLSAuthenticationSpec, NamespacedTargetRef, NetworkAuthentication, + NetworkAuthenticationSpec, Server, ServerAuthorization, ServerAuthorizationSpec, + ServerSpec, }, }; use anyhow::{anyhow, bail, Result}; @@ -12,7 +13,7 @@ use hyper::{body::Buf, http, Body, Request, Response}; use k8s_gateway_api::{HttpRoute, HttpRouteFilter, HttpRouteRule, HttpRouteSpec}; use k8s_openapi::api::core::v1::{Namespace, ServiceAccount}; use kube::{core::DynamicObject, Resource, ResourceExt}; -use linkerd_policy_controller_k8s_api::policy::NamespacedTargetRef; +use linkerd_policy_controller_k8s_index as index; use serde::de::DeserializeOwned; use std::task; use thiserror::Error; @@ -261,6 +262,9 @@ impl Validate for Admission { bail!("unsupported authentication kind(s): {}", kinds.join(", ")); } + // Confirm that the index will be able to read this spec. + index::authorization_policy::validate(spec)?; + Ok(()) } } diff --git a/policy-test/tests/api.rs b/policy-test/tests/api.rs index 8c287ed88236f..ab214e5aac4dd 100644 --- a/policy-test/tests/api.rs +++ b/policy-test/tests/api.rs @@ -460,20 +460,8 @@ async fn server_with_http_route() { } else { panic!("proxy protocol must be HTTP1") }; - let route = http1.routes.first().expect("must have route"); - // Authorizations are copied onto the route. - assert_eq!( - route - .authorizations - .first() - .expect("route must have authorizations") - .labels, - convert_args!(hashmap!( - "group" => "policy.linkerd.io", - "kind" => "serverauthorization", - "name" => "all-admin", - )), - ); + + assert_eq!(http1.routes.len(), 1, "must have routes"); // Delete the `HttpRoute` and ensure that the update reverts to the // default. diff --git a/policy-test/tests/e2e_authorization_policy.rs b/policy-test/tests/e2e_authorization_policy.rs index 15b9530d3d855..644769ba9321c 100644 --- a/policy-test/tests/e2e_authorization_policy.rs +++ b/policy-test/tests/e2e_authorization_policy.rs @@ -410,6 +410,37 @@ async fn either() { .await; } +#[tokio::test(flavor = "current_thread")] +async fn empty_authentications() { + with_temp_ns(|client, ns| async move { + // Create a policy that does not require any authentications. + let srv = create(&client, nginx::server(&ns)).await; + create( + &client, + authz_policy(&ns, "nginx", LocalTargetRef::from_resource(&srv), None), + ) + .await; + + // Create the nginx pod and wait for it to be ready. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)) + ); + + // All requests should work. + let curl = curl::Runner::init(&client, &ns).await; + let (injected, uninjected) = tokio::join!( + curl.run("curl-injected", "http://nginx", LinkerdInject::Enabled), + curl.run("curl-uninjected", "http://nginx", LinkerdInject::Disabled), + ); + let (injected_status, uninjected_status) = + tokio::join!(injected.exit_code(), uninjected.exit_code()); + assert_eq!(injected_status, 0, "injected curl must contact nginx"); + assert_eq!(uninjected_status, 0, "uninjected curl must contact nginx"); + }) + .await; +} + // === helpers === fn authz_policy( diff --git a/viz/charts/linkerd-viz/README.md b/viz/charts/linkerd-viz/README.md index c2ca8c6e6ba28..3cf7651f32de5 100644 --- a/viz/charts/linkerd-viz/README.md +++ b/viz/charts/linkerd-viz/README.md @@ -101,7 +101,7 @@ Kubernetes: `>=1.21.0-0` | grafana.url | string | `nil` | url of an in-cluster Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information | | identityTrustDomain | string | clusterDomain | Trust domain used for identity | | imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed. Registry secrets are applied to the respective service accounts | -| jaegerUrl | string | `""` | url of external jaeger instance Set this to `jaeger.linkerd-jaeger.svc.` if you plan to use jaeger extension | +| jaegerUrl | string | `""` | url of external jaeger instance Set this to `jaeger.linkerd-jaeger.svc.:16686` if you plan to use jaeger extension | | linkerdNamespace | string | `"linkerd"` | Namespace of the Linkerd core control-plane install | | linkerdVersion | string | `"linkerdVersionValue"` | control plane version. See Proxy section for proxy version | | metricsAPI.UID | string | `nil` | UID for the metrics-api resource | diff --git a/viz/charts/linkerd-viz/values.yaml b/viz/charts/linkerd-viz/values.yaml index 3c57f5d2054e7..793d6132672fa 100644 --- a/viz/charts/linkerd-viz/values.yaml +++ b/viz/charts/linkerd-viz/values.yaml @@ -61,7 +61,7 @@ enablePSP: false prometheusUrl: "" # -- url of external jaeger instance -# Set this to `jaeger.linkerd-jaeger.svc.` if you plan to use jaeger extension +# Set this to `jaeger.linkerd-jaeger.svc.:16686` if you plan to use jaeger extension jaegerUrl: "" # metrics API configuration diff --git a/web/app/package.json b/web/app/package.json index cbe34c93ab3c5..b343220e03f76 100644 --- a/web/app/package.json +++ b/web/app/package.json @@ -85,7 +85,7 @@ }, "resolutions": { "@lingui/**/**/minimist": ">=1.2.5", - "moment": "2.29.3", + "moment": "2.29.4", "multicast-dns": "7.2.3", "webpack-dev-server/selfsigned/node-forge": ">=0.10.0" }, diff --git a/web/app/yarn.lock b/web/app/yarn.lock index 20677eea888fd..efe4b83dd9403 100644 --- a/web/app/yarn.lock +++ b/web/app/yarn.lock @@ -5851,10 +5851,10 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moment@2.29.3, moment@^2.29.4: - version "2.29.3" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3" - integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw== +moment@2.29.4, moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== moo@^0.5.0: version "0.5.1"