n5i-apps 0.7.0

Utils for working with n5i apps
// SPDX-FileCopyrightText: 2024-2026 The n5i Project
//
// SPDX-License-Identifier: AGPL-3.0-or-later

use crate::internal::{Ingress, IngressType};
use k8s_crds_traefik::*;
use k8s_openapi::apimachinery::pkg::apis::meta::v1 as k8s_meta;
use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;
use slugify::slugify;

pub fn get_app_ingress(
    domain: Option<String>,
    sources: &[Ingress],
    entrypoints: Vec<String>,
    is_tls: bool,
    user: &str,
    protect: bool,
    custom_prefix: Option<String>,
    component_id: Option<String>,
    has_wildcard: bool,
) -> IngressRoute {
    let sources: Vec<&Ingress> = sources
        .iter()
        .filter(|source| source.component == component_id)
        .collect();
    let mapper = |source: &&Ingress| {
        let mut middlewares = Vec::new();
        if source.enable_compression {
            middlewares.push(IngressRouteRoutesMiddlewares {
                name: "compress-v2".to_owned(),
                namespace: Some(n5i::distro::DISTRO_ID.to_string()),
            })
        }
        if protect && !source.auth_exclude {
            middlewares.push(IngressRouteRoutesMiddlewares {
                name: format!("auth-{user}"),
                namespace: Some(n5i::distro::DISTRO_ID.to_string()),
            })
        }
        if let Some(custom_prefix) = custom_prefix.as_ref() {
            middlewares.push(IngressRouteRoutesMiddlewares {
                name: format!("strip-custom-prefix-{custom_prefix}"),
                namespace: None,
            })
        };
        if source.strip_prefix
            && source
                .path_prefix
                .as_ref()
                .is_some_and(|prefix| prefix != "/")
        {
            middlewares.push(IngressRouteRoutesMiddlewares {
                name: format!(
                    "strip-{}-prefix",
                    slugify!(source.path_prefix.as_ref().unwrap())
                ),
                namespace: None,
            })
        }
        IngressRouteRoutes {
            kind: Some(IngressRouteRoutesKind::Rule),
            r#match: if let Some(domain) = domain.as_ref() {
                format!(
                    "PathPrefix(`{}`) && Host(`{}`)",
                    source.path_prefix.clone().unwrap_or("/".to_string()),
                    domain
                )
            } else {
                format!(
                    "PathPrefix(`{}`)",
                    source.path_prefix.clone().unwrap_or("/".to_string())
                )
            },
            middlewares: if middlewares.is_empty() {
                None
            } else {
                Some(middlewares)
            },
            services: Some(
                if let (Some(svc), Some(port)) =
                    (source.target_service.as_ref(), source.target_port)
                {
                    vec![IngressRouteRoutesServices {
                        name: svc.clone(),
                        port: Some(IntOrString::Int(port as i32)),
                        namespace: source.target_ns.clone().or_else(|| {
                            source
                                .target_app
                                .as_ref()
                                .map(|app| format!("{user}-{app}"))
                        }),
                        scheme: if source.use_https {
                            Some("https".to_string())
                        } else {
                            None
                        },
                        servers_transport: if source.use_https && source.tls_insecure {
                            Some("insecure-https-transport".to_string())
                        } else {
                            None
                        },
                        ..Default::default()
                    }]
                } else {
                    vec![]
                },
            ),
            priority: None,
            syntax: None,
            observability: None,
        }
    };
    let use_http_fallback_routes = !is_tls
        && sources
            .iter()
            .any(|source| source.r#type == IngressType::HttpFallback);

    IngressRoute {
        metadata: k8s_meta::ObjectMeta {
            name: if let Some(domain) = domain.as_ref() {
                Some(slugify!(domain))
            } else {
                Some("ingress".to_string())
            },
            ..Default::default()
        },
        spec: IngressRouteSpec {
            entry_points: Some(entrypoints),
            parent_refs: None,
            routes: sources
                .iter()
                .filter(|source| {
                    if use_http_fallback_routes {
                        source.r#type == IngressType::HttpFallback
                    } else {
                        source.r#type == IngressType::Https
                    }
                })
                .map(mapper)
                .collect(),

            tls: if is_tls && domain.is_some() && !has_wildcard {
                Some(IngressRouteTls {
                    secret_name: Some(format!("{}-tls", slugify!(&domain.as_ref().unwrap()))),
                    ..Default::default()
                })
            } else {
                None
            },
        },
    }
}

// Returns an ingress route for the given app, as well as one that implements a redirect from http to https.
pub fn get_ingress_routes(
    domain: String,
    sources: &[Ingress],
    user: &str,
    protect: bool,
    custom_prefix: Option<String>,
    component_id: Option<String>,
    has_wildcard: bool,
) -> [IngressRoute; 2] {
    let sources: Vec<Ingress> = sources
        .iter()
        .filter_map(|source| {
            if source.component == component_id {
                Some(source.clone())
            } else {
                None
            }
        })
        .collect();
    let has_fallback = sources
        .iter()
        .any(|source| source.r#type == IngressType::HttpFallback);
    let main_ingress = get_app_ingress(
        Some(domain.clone()),
        &sources,
        vec!["websecure".to_string()],
        true,
        user,
        protect,
        custom_prefix.clone(),
        component_id.clone(),
        has_wildcard,
    );
    let http_route = if has_fallback {
        let mut http_route = get_app_ingress(
            Some(domain.clone()),
            &sources,
            vec!["web".to_string()],
            false,
            user,
            protect,
            custom_prefix,
            component_id,
            false,
        );
        http_route.metadata.name = Some(format!("{}-http", slugify!(&domain)));
        http_route
    } else {
        let main_svc = main_ingress
            .spec
            .routes
            .first()
            .and_then(|r| r.services.as_ref())
            .and_then(|s| s.first())
            .map(|s| vec![s.clone()]);
        IngressRoute {
            metadata: k8s_meta::ObjectMeta {
                name: Some(format!("{}-http", slugify!(&domain))),
                ..Default::default()
            },
            spec: IngressRouteSpec {
                entry_points: Some(vec!["web".to_owned()]),
                parent_refs: None,
                routes: vec![IngressRouteRoutes {
                    kind: Some(IngressRouteRoutesKind::Rule),
                    r#match: format!("Host(`{domain}`)"),
                    priority: None,
                    middlewares: Some(vec![IngressRouteRoutesMiddlewares {
                        name: "https-redirect-v2".to_owned(),
                        namespace: Some(n5i::distro::DISTRO_ID.to_string()),
                    }]),
                    services: main_svc,
                    syntax: None,
                    observability: None,
                }],

                tls: None,
            },
        }
    };
    [main_ingress, http_route]
}