otoroshictl 0.0.12

a CLI to manage your otoroshi clusters with style ;)
use std::sync::Arc;

use moka::sync::Cache;
use moka::sync::CacheBuilder;
use rustls::Certificate;
use rustls::PrivateKey;
use serde::Deserialize;
use serde::Serialize;

use crate::cli::cliopts::CliOpts;
use crate::cli::config::OtoroshiCtlConfigSpecClusterClientCert;
use crate::sidecar::config::OtoroshiSidecarConfigSpecOtoroshiSettingsLocationKubernetesLocation;
use crate::utils::otoroshi::OtoroshiConnectionConfig;

use super::config::OtoroshiSidecarConfig;
use super::config::OtoroshiSidecarConfigSpecOtoroshiSettingsCredentials;
use super::config::OtoroshiSidecarConfigSpecOtoroshiSettingsLocation;
use crate::commands::entities::OtoroshExposedResource;
use crate::commands::entities::OtoroshExposedResourceVersion;

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiChallengePluginAlgo {
    pub secret: String,
    pub size: i32,
    pub base64: bool,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiChallengePlugin {
    pub version: String,
    pub ttl: i32,
    pub request_header_name: Option<String>,
    pub response_header_name: Option<String>,
    pub state_resp_leeway: i32,
    pub algo_to_backend: OtoroshiChallengePluginAlgo,
    pub algo_from_backend: OtoroshiChallengePluginAlgo,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiPlugins {
    pub plugin: String,
    pub config: serde_json::Value,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiRoute {
    pub id: String,
    pub name: String,
    pub plugins: Vec<OtoroshiPlugins>,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiCertificate {
    pub id: String,
    pub name: String,
    pub chain: String,
    pub privateKey: String,
    pub subject: String,
}

impl OtoroshiCertificate {
    pub fn certs(&self) -> Vec<Certificate> {
        let streader = stringreader::StringReader::new(self.chain.as_str());
        let mut bufreader = std::io::BufReader::new(streader);
        let certs = rustls_pemfile::certs(&mut bufreader).unwrap();
        certs.into_iter().map(Certificate).collect()
    }
    pub fn key(&self) -> PrivateKey {
        let streader = stringreader::StringReader::new(self.privateKey.as_str());
        let mut bufreader = std::io::BufReader::new(streader);
        let keys_raw = rustls_pemfile::pkcs8_private_keys(&mut bufreader).unwrap();
        let keys: Vec<PrivateKey> = keys_raw.into_iter().map(PrivateKey).collect();
        keys.first().unwrap().to_owned()
    }
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[allow(non_snake_case)]
pub struct OtoroshiApikey {
    pub clientId: String,
    pub clientSecret: String,
    pub clientName: Option<String>,
}

pub struct SidecarCache {
    cli_opts: CliOpts,
    sidecar_config: OtoroshiSidecarConfig,
    routes_cache: Cache<String, Arc<OtoroshiRoute>>,
    certificates_cache: Cache<String, Arc<OtoroshiCertificate>>,
    apikeys_cache: Cache<String, Arc<OtoroshiApikey>>,
}

impl SidecarCache {
    pub fn new(sidecar_config: OtoroshiSidecarConfig, cli_opts: CliOpts) -> SidecarCache {
        SidecarCache {
            cli_opts,
            sidecar_config,
            routes_cache: CacheBuilder::new(50)
                .time_to_live(std::time::Duration::from_secs(120))
                .build(),
            certificates_cache: CacheBuilder::new(500)
                .time_to_live(std::time::Duration::from_secs(120))
                .build(),
            apikeys_cache: CacheBuilder::new(2000)
                .time_to_live(std::time::Duration::from_secs(120))
                .build(),
        }
    }

    fn otoroshi_config(&self) -> OtoroshiConnectionConfig {
        let otoroshi_settings = self
            .sidecar_config
            .spec
            .compute_otoroshi(self.cli_opts.clone());
        let otoroshi_location = otoroshi_settings
            .location
            .unwrap_or(OtoroshiSidecarConfigSpecOtoroshiSettingsLocation::default());
        let otoroshi_routing_location = otoroshi_settings.routing_location;
        let otoroshi_credentials = otoroshi_settings
            .credentials
            .unwrap_or(OtoroshiSidecarConfigSpecOtoroshiSettingsCredentials::default());
        let hostname_with_port: String = if self.sidecar_config.clone().spec.kubernetes {
            let kube = otoroshi_location.clone().kubernetes.unwrap_or(
                OtoroshiSidecarConfigSpecOtoroshiSettingsLocationKubernetesLocation::default(),
            );
            format!(
                "{}.{}.svc.cluster.local:{}",
                kube.service, kube.namespace, otoroshi_location.port
            )
        } else {
            format!(
                "{}:{}",
                otoroshi_location.clone().hostname.unwrap(),
                otoroshi_location.clone().port
            )
        };
        let hostname: String = if self.sidecar_config.clone().spec.kubernetes {
            let kube = otoroshi_location.clone().kubernetes.unwrap_or(
                OtoroshiSidecarConfigSpecOtoroshiSettingsLocationKubernetesLocation::default(),
            );
            format!("{}.{}.svc.cluster.local", kube.service, kube.namespace)
        } else {
            otoroshi_location.clone().hostname.unwrap().to_string()
        };

        OtoroshiConnectionConfig {
            host: hostname_with_port,
            hostname,
            port: otoroshi_location.port,
            ip_addresses: otoroshi_location.ip_addresses,
            cid: otoroshi_credentials.client_id,
            csec: otoroshi_credentials.client_secret,
            chealth: None,
            tls: otoroshi_location.tls,
            routing_hostname: otoroshi_routing_location.clone().and_then(|i| i.hostname),
            routing_port: otoroshi_routing_location.clone().map(|i| i.port),
            routing_tls: otoroshi_routing_location.clone().map(|i| i.tls),
            routing_ip_addresses: otoroshi_routing_location
                .clone()
                .and_then(|i| i.ip_addresses),
            mtls: otoroshi_settings.client_cert.map(|cert| {
                OtoroshiCtlConfigSpecClusterClientCert {
                    cert_location: cert.cert_location,
                    cert_value: cert.cert_value,
                    key_location: cert.key_location,
                    key_value: cert.key_value,
                    ca_location: cert.ca_location,
                    ca_value: cert.ca_value,
                }
            }),
        }
    }

    pub async fn update(&self) {
        let inbounds = &self.sidecar_config.spec.inbound;
        let outbounds = &self.sidecar_config.spec.outbounds.outbounds;
        for tls in inbounds.tls.clone().into_iter() {
            let _ = self.async_get_cert_by_id(tls.cert_id).await;
        }
        for mtls in inbounds.mtls.clone().into_iter() {
            let _ = self.async_get_cert_by_id(mtls.ca_cert_id).await;
        }
        for protocol in inbounds.otoroshi_protocol.clone().into_iter() {
            for route_id in protocol.route_id.clone().into_iter() {
                let _ = self.async_get_route_by_id(route_id).await;
            }
        }
        for outbound in outbounds.iter() {
            for apikey in outbound.1.apikey.clone().into_iter() {
                let _ = self.async_get_apikey_by_id(apikey.apikey_id).await;
            }
            for mtls in outbound.1.mtls.clone().into_iter() {
                let _ = self.async_get_cert_by_id(mtls.client_cert_id).await;
            }
        }
    }

    pub async fn async_get_cert_by_id(&self, id: String) -> Option<OtoroshiCertificate> {
        let key = id.clone();
        match self.certificates_cache.get(&key) {
            Some(cert) => Some(cert.as_ref().clone()),
            None => {
                let config = self.otoroshi_config();
                let res = OtoroshExposedResource {
                    kind: "Certificate".to_string(),
                    plural_name: "certificates".to_string(),
                    singular_name: "certificate".to_string(),
                    group: "pki.otoroshi.io".to_string(),
                    version: OtoroshExposedResourceVersion {
                        name: "v1".to_string(),
                        served: true,
                        deprecated: false,
                        storage: true,
                    },
                };
                match crate::utils::otoroshi::Otoroshi::get_one_resource_with_config(
                    res, id, config,
                )
                .await
                {
                    None => None,
                    Some(resp) => {
                        let cert: OtoroshiCertificate = serde_json::from_value(resp.body).unwrap();
                        self.certificates_cache.insert(key, Arc::new(cert.clone()));
                        Some(cert)
                    }
                }
            }
        }
    }

    pub fn wait_and_get_cert_by_id(&self, id: String, max: i32) -> OtoroshiCertificate {
        let cert: OtoroshiCertificate;
        let mut count = 0;
        loop {
            if count > max {
                panic!("too much retries ({}) to certificate with id '{}'", max, id);
            }
            match self.get_cert_by_id(id.clone()) {
                None => std::thread::sleep(std::time::Duration::from_secs(1)),
                Some(c) => {
                    cert = c;
                    break;
                }
            }
            count += 1;
        }
        cert
    }

    pub fn get_cert_by_id(&self, id: String) -> Option<OtoroshiCertificate> {
        let key = id.clone();
        self.certificates_cache
            .get(&key)
            .map(|cert| cert.as_ref().clone())
    }

    pub async fn async_get_route_by_id(&self, id: String) -> Option<OtoroshiRoute> {
        let key = id.clone();
        match self.routes_cache.get(&key) {
            Some(route) => Some(route.as_ref().clone()),
            None => {
                let config = self.otoroshi_config();
                let res = OtoroshExposedResource {
                    kind: "Route".to_string(),
                    plural_name: "routes".to_string(),
                    singular_name: "route".to_string(),
                    group: "proxy.otoroshi.io".to_string(),
                    version: OtoroshExposedResourceVersion {
                        name: "v1".to_string(),
                        served: true,
                        deprecated: false,
                        storage: true,
                    },
                };
                match crate::utils::otoroshi::Otoroshi::get_one_resource_with_config(
                    res, id, config,
                )
                .await
                {
                    None => None,
                    Some(resp) => {
                        let route: OtoroshiRoute = serde_json::from_value(resp.body).unwrap();
                        self.routes_cache.insert(key, Arc::new(route.clone()));
                        Some(route)
                    }
                }
            }
        }
    }

    pub fn get_route_by_id(&self, id: String) -> Option<OtoroshiRoute> {
        let key = id.clone();
        self.routes_cache
            .get(&key)
            .map(|route| route.as_ref().clone())
    }

    pub fn wait_and_get_route_by_id(&self, id: String, max: i32) -> OtoroshiRoute {
        let route: OtoroshiRoute;
        let mut count = 0;
        loop {
            if count > max {
                panic!("too much retries ({}) to route with id '{}'", max, id);
            }
            match self.get_route_by_id(id.clone()) {
                None => std::thread::sleep(std::time::Duration::from_secs(1)),
                Some(c) => {
                    route = c;
                    break;
                }
            }
            count += 1;
        }
        route
    }

    pub async fn async_get_apikey_by_id(&self, id: String) -> Option<OtoroshiApikey> {
        let key = id.clone();
        match self.apikeys_cache.get(&key) {
            Some(apikey) => Some(apikey.as_ref().clone()),
            None => {
                let config = self.otoroshi_config();
                let res = OtoroshExposedResource {
                    kind: "Apikey".to_string(),
                    plural_name: "apikeys".to_string(),
                    singular_name: "apikey".to_string(),
                    group: "apim.otoroshi.io".to_string(),
                    version: OtoroshExposedResourceVersion {
                        name: "v1".to_string(),
                        served: true,
                        deprecated: false,
                        storage: true,
                    },
                };
                match crate::utils::otoroshi::Otoroshi::get_one_resource_with_config(
                    res, id, config,
                )
                .await
                {
                    None => None,
                    Some(resp) => {
                        let apikey: OtoroshiApikey = serde_json::from_value(resp.body).unwrap();
                        self.apikeys_cache.insert(key, Arc::new(apikey.clone()));
                        Some(apikey)
                    }
                }
            }
        }
    }

    pub fn get_apikey_by_id(&self, id: String) -> Option<OtoroshiApikey> {
        let key = id.clone();
        self.apikeys_cache
            .get(&key)
            .map(|apikey| apikey.as_ref().clone())
    }

    pub fn wait_and_get_apikey_by_id(&self, id: String, max: i32) -> OtoroshiApikey {
        let apikey: OtoroshiApikey;
        let mut count = 0;
        loop {
            if count > max {
                panic!("too much retries ({}) to apikey with id '{}'", max, id);
            }
            match self.get_apikey_by_id(id.clone()) {
                None => std::thread::sleep(std::time::Duration::from_secs(1)),
                Some(c) => {
                    apikey = c;
                    break;
                }
            }
            count += 1;
        }
        apikey
    }
}