use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverrideMode {
DockerNetwork,
HostNetwork,
}
pub struct HttpsOverride<'a> {
pub domain: &'a str,
pub port: u16,
pub slug: Option<String>,
pub use_port_443: bool,
}
pub fn is_deprecated_railway_var(key: &str) -> bool {
if key == "RAILWAY_STATIC_URL" {
return true;
}
if key.starts_with("RAILWAY_SERVICE_") && key.ends_with("_URL") {
return true;
}
false
}
pub type PublicDomainMapping = HashMap<String, String>;
#[allow(clippy::too_many_arguments)]
pub fn override_railway_vars(
vars: BTreeMap<String, String>,
service_slug: &str,
port_mapping: &HashMap<i64, u16>,
service_slugs: &HashMap<String, String>,
slug_port_mappings: &HashMap<String, HashMap<i64, u16>>,
public_domain_mapping: &PublicDomainMapping,
mode: OverrideMode,
https: Option<HttpsOverride>,
) -> BTreeMap<String, String> {
vars.into_iter()
.filter(|(key, _)| !is_deprecated_railway_var(key))
.map(|(key, value)| {
let new_value = match key.as_str() {
"RAILWAY_PRIVATE_DOMAIN" => match mode {
OverrideMode::DockerNetwork => service_slug.to_string(),
OverrideMode::HostNetwork => "localhost".to_string(),
},
"RAILWAY_PUBLIC_DOMAIN" => match &https {
Some(h) if h.use_port_443 => {
match &h.slug {
Some(slug) => format!("{}.{}", slug, h.domain),
None => format!("{}.{}", service_slug, h.domain),
}
}
Some(h) => format!("{}:{}", h.domain, h.port),
None => "localhost".to_string(),
},
"RAILWAY_TCP_PROXY_DOMAIN" => "localhost".to_string(),
"RAILWAY_TCP_PROXY_PORT" => port_mapping
.values()
.next()
.map(|p| p.to_string())
.unwrap_or(value),
_ => replace_domain_refs(
&value,
service_slugs,
slug_port_mappings,
public_domain_mapping,
mode,
),
};
(key, new_value)
})
.collect()
}
fn replace_domain_refs(
value: &str,
service_slugs: &HashMap<String, String>,
slug_port_mappings: &HashMap<String, HashMap<i64, u16>>,
public_domain_mapping: &PublicDomainMapping,
mode: OverrideMode,
) -> String {
let mut result = value.to_string();
for slug in service_slugs.values() {
let port_mapping = slug_port_mappings.get(slug);
let railway_domain = format!("{}.railway.internal", slug);
if result.contains(&railway_domain) {
match mode {
OverrideMode::DockerNetwork => {
result = result.replace(&railway_domain, slug);
}
OverrideMode::HostNetwork => {
if let Some(ports) = port_mapping {
result = replace_domain_with_port_mapping(&result, &railway_domain, ports);
} else {
result = result.replace(&railway_domain, "localhost");
}
}
}
}
if mode == OverrideMode::HostNetwork {
if let Some(ports) = port_mapping {
for (internal, external) in ports {
let old_pattern = format!("{}:{}", slug, internal);
let new_pattern = format!("localhost:{}", external);
result = result.replace(&old_pattern, &new_pattern);
}
}
}
}
for (prod_domain, local_domain) in public_domain_mapping {
result = result.replace(prod_domain, local_domain);
}
result
}
fn replace_domain_with_port_mapping(
value: &str,
domain: &str,
port_mapping: &HashMap<i64, u16>,
) -> String {
let mut result = value.to_string();
for (internal, external) in port_mapping {
let old_pattern = format!("{}:{}", domain, internal);
let new_pattern = format!("localhost:{}", external);
result = result.replace(&old_pattern, &new_pattern);
}
result = result.replace(domain, "localhost");
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_deprecated_railway_var() {
assert!(is_deprecated_railway_var("RAILWAY_STATIC_URL"));
assert!(is_deprecated_railway_var("RAILWAY_SERVICE_API_URL"));
assert!(!is_deprecated_railway_var("RAILWAY_SERVICE_ID"));
assert!(!is_deprecated_railway_var("RAILWAY_SERVICE_NAME"));
assert!(!is_deprecated_railway_var("DATABASE_URL"));
}
#[test]
fn test_override_private_domain_docker() {
let mut vars = BTreeMap::new();
vars.insert(
"RAILWAY_PRIVATE_DOMAIN".to_string(),
"old.value".to_string(),
);
let result = override_railway_vars(
vars,
"my-service",
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
OverrideMode::DockerNetwork,
None,
);
assert_eq!(
result.get("RAILWAY_PRIVATE_DOMAIN"),
Some(&"my-service".to_string())
);
}
#[test]
fn test_override_private_domain_host() {
let mut vars = BTreeMap::new();
vars.insert(
"RAILWAY_PRIVATE_DOMAIN".to_string(),
"old.value".to_string(),
);
let result = override_railway_vars(
vars,
"my-service",
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
OverrideMode::HostNetwork,
None,
);
assert_eq!(
result.get("RAILWAY_PRIVATE_DOMAIN"),
Some(&"localhost".to_string())
);
}
#[test]
fn test_override_public_domain_with_https_port_443() {
let mut vars = BTreeMap::new();
vars.insert("RAILWAY_PUBLIC_DOMAIN".to_string(), "old.value".to_string());
let https = HttpsOverride {
domain: "myproject.localhost",
port: 443,
slug: Some("api".to_string()),
use_port_443: true,
};
let result = override_railway_vars(
vars,
"api",
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
OverrideMode::HostNetwork,
Some(https),
);
assert_eq!(
result.get("RAILWAY_PUBLIC_DOMAIN"),
Some(&"api.myproject.localhost".to_string())
);
}
#[test]
fn test_filter_deprecated_vars() {
let mut vars = BTreeMap::new();
vars.insert("RAILWAY_STATIC_URL".to_string(), "value".to_string());
vars.insert("RAILWAY_SERVICE_API_URL".to_string(), "value".to_string());
vars.insert("DATABASE_URL".to_string(), "postgres://...".to_string());
let result = override_railway_vars(
vars,
"service",
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
OverrideMode::HostNetwork,
None,
);
assert!(!result.contains_key("RAILWAY_STATIC_URL"));
assert!(!result.contains_key("RAILWAY_SERVICE_API_URL"));
assert!(result.contains_key("DATABASE_URL"));
}
#[test]
fn test_replace_cross_service_domains() {
let mut vars = BTreeMap::new();
vars.insert(
"REDIS_URL".to_string(),
"redis://redis.railway.internal:6379".to_string(),
);
vars.insert(
"API_URL".to_string(),
"https://api-prod.up.railway.app/v1".to_string(),
);
vars.insert(
"CUSTOM_URL".to_string(),
"https://api.mycompany.io/graphql".to_string(),
);
vars.insert(
"COMBINED".to_string(),
"api=https://api-prod.up.railway.app,custom=https://api.mycompany.io".to_string(),
);
let mut service_slugs = HashMap::new();
service_slugs.insert("svc-redis".to_string(), "redis".to_string());
let mut slug_port_mappings = HashMap::new();
let mut redis_ports = HashMap::new();
redis_ports.insert(6379i64, 16379u16);
slug_port_mappings.insert("redis".to_string(), redis_ports);
let mut public_domain_mapping = HashMap::new();
public_domain_mapping.insert(
"api-prod.up.railway.app".to_string(),
"api.local.railway.localhost".to_string(),
);
public_domain_mapping.insert(
"api.mycompany.io".to_string(),
"custom.local.railway.localhost".to_string(),
);
let result = override_railway_vars(
vars,
"my-service",
&HashMap::new(),
&service_slugs,
&slug_port_mappings,
&public_domain_mapping,
OverrideMode::HostNetwork,
None,
);
assert_eq!(
result.get("REDIS_URL"),
Some(&"redis://localhost:16379".to_string())
);
assert_eq!(
result.get("API_URL"),
Some(&"https://api.local.railway.localhost/v1".to_string())
);
assert_eq!(
result.get("CUSTOM_URL"),
Some(&"https://custom.local.railway.localhost/graphql".to_string())
);
assert_eq!(
result.get("COMBINED"),
Some(
&"api=https://api.local.railway.localhost,custom=https://custom.local.railway.localhost"
.to_string()
)
);
}
#[test]
fn test_private_domain_docker_mode() {
let mut vars = BTreeMap::new();
vars.insert(
"REDIS_URL".to_string(),
"redis://redis.railway.internal:6379".to_string(),
);
let mut service_slugs = HashMap::new();
service_slugs.insert("svc-redis".to_string(), "redis".to_string());
let result = override_railway_vars(
vars,
"my-service",
&HashMap::new(),
&service_slugs,
&HashMap::new(),
&HashMap::new(),
OverrideMode::DockerNetwork,
None,
);
assert_eq!(
result.get("REDIS_URL"),
Some(&"redis://redis:6379".to_string())
);
}
}