use crate::app::app_resources;
use crate::application_types::ApplicationValues;
use crate::dsh_api_client::DshApiClient;
use crate::error::DshApiResult;
use crate::parse::parse_function;
use crate::types::{AppCatalogApp, AppCatalogAppResourcesValue, Application, PortMapping, Vhost};
use crate::{Dependant, DependantApp, DependantApplication};
use futures::try_join;
use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use std::sync::LazyLock;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum VhostInjection {
#[serde(rename = "env")]
EnvVar { env_var_name: String },
#[serde(rename = "variable")]
Variable { variable_name: String },
#[serde(rename = "vhost")]
Vhost { exposed_port: String, zone: Option<String> },
}
impl VhostInjection {
pub(crate) fn vhost<S, T>(exposed_port: S, zone: Option<T>) -> Self
where
S: Into<String>,
T: Into<String>,
{
Self::Vhost { exposed_port: exposed_port.into(), zone: zone.map(|zone| zone.into()) }
}
}
impl Display for VhostInjection {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
VhostInjection::EnvVar { env_var_name } => write!(f, "{}", env_var_name),
VhostInjection::Variable { variable_name } => write!(f, "{{ vhost('{}') }}", variable_name),
VhostInjection::Vhost { exposed_port, zone } => match zone {
Some(a_zone) => write!(f, "vhost({}:{})", exposed_port, a_zone),
None => write!(f, "{}", exposed_port),
},
}
}
}
impl DshApiClient {
pub async fn vhosts_with_dependant_applications(&self) -> DshApiResult<Vec<(String, Vec<DependantApplication<VhostInjection>>)>> {
let applications = self.get_application_configuration_map().await?;
let mut vhosts_map = HashMap::<String, Vec<DependantApplication<VhostInjection>>>::new();
for ApplicationValues { id, application, values } in vhosts_from_applications(&applications) {
for (vhost, port, _) in values {
let dependant_applications = vhosts_map.entry(vhost.clone()).or_default();
dependant_applications.push(DependantApplication::new(
id.to_string(),
application.instances,
vec![VhostInjection::Vhost { exposed_port: port.to_string(), zone: None }],
));
}
}
let mut vhosts: Vec<(String, Vec<DependantApplication<VhostInjection>>)> = Vec::from_iter(vhosts_map.into_iter());
vhosts.sort_by(|(vhost_id_a, _), (vhost_id_b, _)| vhost_id_a.cmp(vhost_id_b));
Ok(vhosts)
}
pub async fn vhosts_with_dependant_apps(&self) -> DshApiResult<Vec<(String, Vec<DependantApp>)>> {
let apps = self.get_appcatalogapp_configuration_map().await?;
let mut vhosts_map = HashMap::<String, Vec<DependantApp>>::new();
let mut app_ids = apps.keys().collect_vec();
app_ids.sort();
for app_id in app_ids {
let app = apps.get(app_id).unwrap();
for (_, vhost_string) in vhost_strings_from_app(app) {
let dependant_apps = vhosts_map.entry(vhost_string.vhost_name.clone()).or_default();
dependant_apps.push(DependantApp::new(app_id.clone(), vec![vhost_string.to_string()]));
}
}
let mut vhosts: Vec<(String, Vec<DependantApp>)> = Vec::from_iter(vhosts_map);
vhosts.sort_by(|(vhost_id_a, _), (vhost_id_b, _)| vhost_id_a.cmp(vhost_id_b));
Ok(vhosts)
}
pub async fn vhosts_with_dependants(&self) -> DshApiResult<Vec<(String, Vec<Dependant<VhostInjection>>)>> {
let (application_configuration_map, appcatalogapp_configuration_map) = try_join!(self.get_application_configuration_map(), self.get_appcatalogapp_configuration_map())?;
let mut vhosts_with_dependants_map = HashMap::<String, Vec<Dependant<VhostInjection>>>::new();
for ApplicationValues { id, application, values } in vhosts_from_applications(&application_configuration_map) {
for (vhost, port, _) in values {
let dependants = vhosts_with_dependants_map.entry(vhost.clone()).or_default();
dependants.push(Dependant::service(
id,
application.instances,
vec![VhostInjection::Vhost { exposed_port: port.to_string(), zone: None }],
));
}
}
let mut app_ids = appcatalogapp_configuration_map.keys().collect_vec();
app_ids.sort();
for app_id in app_ids {
let app = appcatalogapp_configuration_map.get(app_id).unwrap();
for (_, vhost_string) in vhost_strings_from_app(app) {
let dependants = vhosts_with_dependants_map.entry(vhost_string.vhost_name.clone()).or_default();
dependants.push(Dependant::app(app_id.clone(), vec![vhost_string.to_string()]));
}
}
let mut vhosts: Vec<(String, Vec<Dependant<VhostInjection>>)> = Vec::from_iter(vhosts_with_dependants_map.into_iter());
vhosts.sort_by(|(vhost_id_a, _), (vhost_id_b, _)| vhost_id_a.cmp(vhost_id_b));
Ok(vhosts)
}
}
pub fn vhost_port_mappings_from_application<'a>(vhost_id: &str, application: &'a Application) -> Vec<(&'a str, &'a PortMapping)> {
let mut port_mappings: Vec<(&'a str, &'a PortMapping)> = application
.exposed_ports
.iter()
.filter_map(|(port, port_mapping)| {
port_mapping.vhost.clone().and_then(|vhost_string| {
VhostString::from_str(vhost_string.as_str())
.ok()
.and_then(|vhost| if vhost.vhost_name == vhost_id { Some((port.as_str(), port_mapping)) } else { None })
})
})
.collect_vec();
port_mappings.sort_by(|(port_a, _), (port_b, _)| port_a.cmp(port_b));
port_mappings
}
pub fn vhost_port_mappings_from_applications<'a>(vhost_id: &str, applications: &'a HashMap<String, Application>) -> Vec<ApplicationValues<'a, (&'a str, &'a PortMapping)>> {
let mut application_tuples: Vec<ApplicationValues<(&str, &PortMapping)>> = applications
.iter()
.filter_map(|(application_id, application)| {
let port_mappings: Vec<(&str, &PortMapping)> = vhost_port_mappings_from_application(vhost_id, application);
if port_mappings.is_empty() {
None
} else {
Some(ApplicationValues::new(application_id, application, port_mappings))
}
})
.collect_vec();
application_tuples.sort();
application_tuples
}
pub fn vhost_resources_from_app(app: &AppCatalogApp) -> Vec<(&str, &Vhost)> {
app_resources(app, &|resource_value| match resource_value {
AppCatalogAppResourcesValue::Vhost(vhost) => Some(vhost),
_ => None,
})
}
pub(crate) fn vhost_strings_from_app(app: &AppCatalogApp) -> Vec<(&str, VhostString)> {
let mut resources: Vec<(&str, VhostString)> = vec![];
for (resource_id, resource) in &app.resources {
if let AppCatalogAppResourcesValue::Vhost(vhost) = resource {
if let Ok(vhost_string) = VhostString::from_resource_str(&vhost.value) {
resources.push((resource_id, vhost_string))
}
}
}
resources.sort_by(|(resource_id_a, _), (resource_id_b, _)| resource_id_a.cmp(resource_id_b));
resources
}
pub fn vhosts_from_application(application: &Application) -> Vec<(String, &str, &PortMapping)> {
let mut vhosts: Vec<(String, &str, &PortMapping)> = application
.exposed_ports
.iter()
.filter_map(|(port, port_mapping)| {
port_mapping.vhost.clone().and_then(|vhost_string| {
VhostString::from_str(vhost_string.as_str())
.ok()
.map(|vhost| (vhost.vhost_name, port.as_str(), port_mapping))
})
})
.collect_vec();
vhosts.sort_by(|(vhost_name_a, _, _), (vhost_name_b, _, _)| vhost_name_a.cmp(vhost_name_b));
vhosts
}
pub fn vhosts_from_applications(applications: &HashMap<String, Application>) -> Vec<ApplicationValues<(String, &str, &PortMapping)>> {
let mut vhosts: Vec<ApplicationValues<(String, &str, &PortMapping)>> = vec![];
for (application_id, application) in applications {
for (port, port_mapping) in &application.exposed_ports {
if let Some(vhost_string) = port_mapping.vhost.clone() {
if let Ok(vhost) = VhostString::from_str(vhost_string.as_str()) {
vhosts.push(ApplicationValues::new(application_id, application, vec![(vhost.vhost_name, port, port_mapping)]));
}
}
}
}
vhosts.sort();
vhosts
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct VhostString {
pub vhost_name: String,
pub kafka: bool,
pub tenant_name: Option<String>,
pub zone: Option<String>,
}
impl VhostString {
pub fn new<T, U, V>(vhost_name: T, kafka: bool, tenant_name: Option<U>, zone: Option<V>) -> Self
where
T: Into<String>,
U: Into<String>,
V: Into<String>,
{
Self { vhost_name: vhost_name.into(), kafka, tenant_name: tenant_name.map(Into::<String>::into), zone: zone.map(Into::<String>::into) }
}
pub fn from_resource_str(vhost_resource_string: &str) -> Result<Self, String> {
static VHOST_RESOURCE_STRING_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)@([a-zA-Z0-9_-]+)$").unwrap());
VHOST_RESOURCE_STRING_REGEX
.captures(vhost_resource_string)
.map(|captures| {
VhostString::new(
captures.get(1).map(|vhost_match| vhost_match.as_str()).unwrap_or_default(),
false,
captures.get(2).map(|tenant_match| Some(tenant_match.as_str())).unwrap_or_default(),
captures.get(3).map(|zone_match| zone_match.as_str()),
)
})
.ok_or(format!("invalid value in vhost string (\"{}\")", vhost_resource_string))
}
}
impl FromStr for VhostString {
type Err = String;
fn from_str(vhost_string: &str) -> Result<Self, Self::Err> {
static VALUE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"([a-zA-Z0-9_-]+)(\.kafka)?(?:\.([a-zA-Z0-9_-]+))?").unwrap());
let (value_string, zone) = parse_function(vhost_string, "vhost")?;
VALUE_REGEX
.captures(value_string)
.map(|captures| {
VhostString::new(
captures.get(1).map(|vhost_match| vhost_match.as_str()).unwrap_or_default(),
captures.get(2).is_some(),
captures.get(3).map(|tenant_match| tenant_match.as_str()),
zone,
)
})
.ok_or(format!("invalid value in vhost string (\"{}\")", vhost_string))
}
}
impl TryFrom<&PortMapping> for VhostString {
type Error = String;
fn try_from(port_mapping: &PortMapping) -> Result<Self, Self::Error> {
match &port_mapping.vhost {
Some(vhost) => VhostString::from_str(vhost),
None => Err("port mapping has no vhost".to_string()),
}
}
}
impl Display for VhostString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.vhost_name)?;
if self.kafka {
write!(f, ".kafka")?;
}
if let Some(tenant_name) = &self.tenant_name {
write!(f, ".{}", tenant_name)?;
}
if let Some(zone) = &self.zone {
write!(f, ".{}", zone)?;
}
Ok(())
}
}