use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::net::IpAddr;
use std::num::NonZeroU16;
use crate::SnapshotBuildSpec;
pub const WORKSPACE_MOULIN_BUILD_CONFIG_PATH: &str = "/internal/v1/workspace/moulin-build-config";
pub const ADMIN_WORKSPACE_MOULIN_BUILD_CONFIG_PATH: &str =
"/admin/api/workspace-moulin-build-config";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct WorkspaceMoulinBuildAdminConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub http_proxy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub https_proxy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub no_proxy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry_mirror: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub build_args: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub build_env: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "mode", rename_all = "snake_case", deny_unknown_fields)]
pub enum MoulinBuildNetworkPolicy {
Direct,
ManagedProxy {
#[serde(default, skip_serializing_if = "Option::is_none")]
http_proxy: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
https_proxy: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
no_proxy: Vec<String>,
},
}
impl WorkspaceMoulinBuildAdminConfig {
pub fn effective_build_env(&self) -> BTreeMap<String, String> {
self.build_env.clone()
}
pub fn effective_network_policy(&self) -> Option<MoulinBuildNetworkPolicy> {
if self.http_proxy.is_none() && self.https_proxy.is_none() {
return None;
}
let mut seen = BTreeSet::new();
let no_proxy = self
.no_proxy
.as_deref()
.into_iter()
.flat_map(|value| value.split(','))
.map(str::trim)
.filter(|value| !value.is_empty())
.filter(|value| seen.insert((*value).to_string()))
.map(str::to_string)
.collect();
Some(MoulinBuildNetworkPolicy::ManagedProxy {
http_proxy: self.http_proxy.clone(),
https_proxy: self.https_proxy.clone(),
no_proxy,
})
}
}
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct MoulinEgressRule {
host: String,
port: NonZeroU16,
}
impl MoulinEgressRule {
pub fn new(host: impl Into<String>, port: u16) -> Result<Self, String> {
let host = validate_exact_hostname(host.into())?;
let port =
NonZeroU16::new(port).ok_or_else(|| "egress port must be non-zero".to_string())?;
Ok(Self { host, port })
}
pub fn host(&self) -> &str {
&self.host
}
pub fn port(&self) -> u16 {
self.port.get()
}
}
impl<'de> Deserialize<'de> for MoulinEgressRule {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct WireRule {
host: String,
port: u16,
}
let rule = WireRule::deserialize(deserializer)?;
Self::new(rule.host, rule.port).map_err(serde::de::Error::custom)
}
}
pub fn validate_moulin_egress_rules(rules: &[MoulinEgressRule]) -> Result<(), String> {
if rules.len() > 20 {
return Err("egressRules supports at most 20 entries".into());
}
let unique = rules
.iter()
.map(|rule| (rule.host(), rule.port()))
.collect::<BTreeSet<_>>();
if unique.len() != rules.len() {
return Err("egressRules contains duplicates".into());
}
Ok(())
}
fn validate_exact_hostname(host: String) -> Result<String, String> {
let host = host.trim().trim_end_matches('.').to_ascii_lowercase();
if !host.is_ascii() || host.len() > 253 || host.parse::<IpAddr>().is_ok() || host.contains('*')
{
return Err("egress host must be an exact ASCII hostname".into());
}
let labels = host.split('.').collect::<Vec<_>>();
if labels.len() < 2
|| labels.iter().any(|label| {
label.is_empty()
|| label.len() > 63
|| label.starts_with('-')
|| label.ends_with('-')
|| !label
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
})
{
return Err("egress host must be an exact ASCII hostname".into());
}
Ok(host)
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvironmentProviderSlotView {
pub base_url: String,
#[serde(default)]
pub build_url: String,
pub credential_configured: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_build: Option<SnapshotBuildSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_template_id: Option<String>,
pub active: bool,
#[serde(default)]
pub egress_rules: Vec<MoulinEgressRule>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnvironmentProviderAdminConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_snapshot_id: Option<String>,
pub moulin: EnvironmentProviderSlotView,
pub e2b: EnvironmentProviderSlotView,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_provider: Option<String>,
#[serde(default)]
pub default_resources: crate::SandboxResources,
pub managed: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateEnvironmentProviderSlotRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub credential: Option<String>,
#[serde(default)]
pub clear_credential: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_build: Option<SnapshotBuildSpec>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_template_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub egress_rules: Option<Vec<MoulinEgressRule>>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct UpdateEnvironmentProviderAdminConfigRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_snapshot_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub moulin: Option<UpdateEnvironmentProviderSlotRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub e2b: Option<UpdateEnvironmentProviderSlotRequest>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_resources: Option<crate::SandboxResources>,
}