use crate::internal::{Ingress, IngressType};
use k8s_crds_gateway::{
BackendTlsPolicy, BackendTlsPolicySpec, BackendTlsPolicyTargetRefs, BackendTlsPolicyValidation,
HttpRoute, HttpRouteRules, HttpRouteRulesBackendRefs, HttpRouteRulesFilters,
HttpRouteRulesFiltersExtensionRef, HttpRouteRulesFiltersRequestRedirectScheme,
HttpRouteRulesFiltersType, HttpRouteRulesFiltersUrlRewrite,
HttpRouteRulesFiltersUrlRewritePath, HttpRouteRulesFiltersUrlRewritePathType,
HttpRouteRulesMatches, HttpRouteRulesMatchesPath, HttpRouteRulesMatchesPathType, HttpRouteSpec,
};
use k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta;
use slugify::slugify;
use std::hash::{DefaultHasher, Hash, Hasher};
fn middleware_filter(mw_name: String) -> HttpRouteRulesFilters {
HttpRouteRulesFilters {
cors: None,
r#type: HttpRouteRulesFiltersType::ExtensionRef,
extension_ref: Some(HttpRouteRulesFiltersExtensionRef {
group: "traefik.io".to_string(),
kind: "Middleware".to_string(),
name: mw_name,
}),
request_header_modifier: None,
request_mirror: None,
request_redirect: None,
response_header_modifier: None,
url_rewrite: None,
}
}
const fn strip_prefix_filter(prefix: String) -> HttpRouteRulesFilters {
HttpRouteRulesFilters {
cors: None,
extension_ref: None,
request_header_modifier: None,
request_mirror: None,
request_redirect: None,
response_header_modifier: None,
r#type: HttpRouteRulesFiltersType::UrlRewrite,
url_rewrite: Some(HttpRouteRulesFiltersUrlRewrite {
hostname: None,
path: Some(HttpRouteRulesFiltersUrlRewritePath {
replace_full_path: None,
replace_prefix_match: Some(prefix),
r#type: HttpRouteRulesFiltersUrlRewritePathType::ReplacePrefixMatch,
}),
}),
}
}
fn join_path_prefixes(prefix_a: &str, prefix_b: &str) -> String {
let mut joined = String::new();
if !prefix_a.starts_with('/') {
joined.push('/');
}
joined.push_str(prefix_a);
if !prefix_a.ends_with('/') && !prefix_b.starts_with('/') {
joined.push('/');
}
joined.push_str(prefix_b);
joined
}
trait IngressHash {
fn get_hash(&self) -> String;
}
impl IngressHash for Ingress {
fn get_hash(&self) -> String {
let mut hasher = DefaultHasher::new();
self.hash(&mut hasher);
hasher.finish().to_string()
}
}
#[must_use]
pub fn generate_tls_policies(route_name: &str, sources: &[&Ingress]) -> Vec<BackendTlsPolicy> {
sources
.iter()
.filter(|s| s.use_https || s.use_https_with_hostname.is_some())
.map(|s| BackendTlsPolicy {
metadata: ObjectMeta {
name: Some(route_name.to_string()),
..Default::default()
},
spec: BackendTlsPolicySpec {
options: None,
target_refs: vec![BackendTlsPolicyTargetRefs {
group: "gateway.networking.k8s.io".to_string(),
kind: "HTTPRoute".to_string(),
name: route_name.to_string(),
section_name: Some(s.get_hash()),
}],
validation: BackendTlsPolicyValidation {
ca_certificate_refs: None,
hostname: if let Some(ref hostname) = s.use_https_with_hostname {
hostname.clone()
} else if let Some(ref target_service) = s.target_service {
target_service.clone()
} else {
"unknownSvc.local".to_string()
},
subject_alt_names: None,
well_known_ca_certificates: None,
},
},
status: None,
})
.collect()
}
#[must_use]
pub fn get_app_ingress(
domain: Option<&String>,
sources: &[Ingress],
is_tls: bool,
user: &str,
protect: bool,
custom_prefix: Option<&String>,
component_id: Option<&String>,
) -> (HttpRoute, Vec<BackendTlsPolicy>) {
let sources: Vec<&Ingress> = sources
.iter()
.filter(|source| source.component.as_ref() == component_id)
.collect();
let use_http_fallback_routes = !is_tls
&& sources
.iter()
.any(|source| source.r#type == IngressType::HttpFallback);
let mapper = |source: &Ingress| {
let mut middlewares = Vec::new();
if source.enable_compression {
middlewares.push(middleware_filter("compress-v2".to_string()));
}
if protect && !source.auth_exclude {
middlewares.push(middleware_filter(format!("auth-{user}")));
}
let prefix;
if source.strip_prefix
&& let Some(ref path_prefix) = source.path_prefix
&& path_prefix != "/"
{
if let Some(custom_prefix) = custom_prefix {
prefix = Some(join_path_prefixes(custom_prefix, path_prefix));
} else {
prefix = Some(path_prefix.clone());
}
} else {
prefix = custom_prefix.cloned();
}
if let Some(ref prefix) = prefix {
middlewares.push(strip_prefix_filter(prefix.clone()));
}
HttpRouteRules {
backend_refs: Some(
if let (Some(svc), Some(port)) =
(source.target_service.as_ref(), source.target_port)
{
vec![HttpRouteRulesBackendRefs {
filters: None,
group: None,
kind: Some("Service".to_string()),
name: svc.clone(),
port: Some(i32::from(port)),
namespace: source.target_ns.clone().or_else(|| {
source
.target_app
.as_ref()
.map(|app| format!("{user}-{app}"))
}),
weight: None,
}]
} else {
vec![]
},
),
filters: if middlewares.is_empty() {
None
} else {
Some(middlewares)
},
matches: if let Some(prefix) = prefix {
Some(vec![HttpRouteRulesMatches {
headers: None,
method: None,
path: Some(HttpRouteRulesMatchesPath {
r#type: Some(HttpRouteRulesMatchesPathType::PathPrefix),
value: Some(prefix),
}),
query_params: None,
}])
} else {
None
},
name: Some(source.get_hash()),
timeouts: None,
}
};
let route_name = if let Some(domain) = domain.as_ref() {
slugify!(domain)
} else {
"fallback-ingress".to_string()
};
let sources: Vec<_> = sources
.into_iter()
.filter(|source| {
if use_http_fallback_routes {
source.r#type == IngressType::HttpFallback
} else {
source.r#type == IngressType::Https
}
})
.collect();
let policies = generate_tls_policies(&route_name, &sources);
(
HttpRoute {
metadata: ObjectMeta {
name: Some(route_name),
..Default::default()
},
spec: HttpRouteSpec {
hostnames: domain.map(|d| vec![d.clone()]),
parent_refs: Some(vec![k8s_crds_gateway::HttpRouteParentRefs {
group: Some("gateway.networking.k8s.io".to_string()),
kind: Some("Gateway".to_string()),
name: "n5i".to_string(),
namespace: Some(n5i::distro::DISTRO_ID.to_string()),
port: Some(
std::env::var("GATEWAY_TLS_LISTENER_PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(443),
),
section_name: None,
}]),
rules: Some(sources.into_iter().map(mapper).collect()),
},
status: None,
},
policies,
)
}
#[must_use]
pub fn get_ingress_routes(
domain: &String,
sources: &[Ingress],
user: &str,
protect: bool,
custom_prefix: Option<&String>,
component_id: Option<&String>,
) -> ([HttpRoute; 2], Vec<BackendTlsPolicy>) {
let sources: Vec<Ingress> = sources
.iter()
.filter_map(|source| {
if source.component.as_ref() == component_id {
Some(source.clone())
} else {
None
}
})
.collect();
let has_fallback = sources
.iter()
.any(|source| source.r#type == IngressType::HttpFallback);
let (main_ingress, mut policies) = get_app_ingress(
Some(domain),
&sources,
true,
user,
protect,
custom_prefix,
component_id,
);
let http_route = if has_fallback {
let (mut http_route, mut additional_policies) = get_app_ingress(
Some(domain),
&sources,
false,
user,
protect,
custom_prefix,
component_id,
);
http_route.metadata.name = Some(format!("{}-http", slugify!(&domain)));
policies.append(&mut additional_policies);
http_route
} else {
HttpRoute {
metadata: ObjectMeta {
name: Some(format!("{}-http", slugify!(&domain))),
..Default::default()
},
spec: HttpRouteSpec {
hostnames: Some(vec![domain.clone()]),
parent_refs: Some(vec![k8s_crds_gateway::HttpRouteParentRefs {
group: Some("gateway.networking.k8s.io".to_string()),
kind: Some("Gateway".to_string()),
name: "n5i".to_string(),
namespace: Some(n5i::distro::DISTRO_ID.to_string()),
port: Some(80),
section_name: None,
}]),
rules: Some(vec![HttpRouteRules {
name: Some("http-redirect".to_string()),
matches: None,
filters: Some(vec![HttpRouteRulesFilters {
r#type: HttpRouteRulesFiltersType::RequestRedirect,
request_redirect: Some(
k8s_crds_gateway::HttpRouteRulesFiltersRequestRedirect {
hostname: None,
scheme: Some(HttpRouteRulesFiltersRequestRedirectScheme::Https),
port: None,
status_code: None,
path: None,
},
),
cors: None,
extension_ref: None,
request_header_modifier: None,
request_mirror: None,
response_header_modifier: None,
url_rewrite: None,
}]),
backend_refs: None,
timeouts: None,
}]),
},
status: None,
}
};
([main_ingress, http_route], policies)
}