use anyhow::Result;
use outpost::PortMapping;
use serde::Serialize;
use tempfile::TempDir;
use tokio::process::{Child, Command};
use tracing::{debug, info, instrument};
#[derive(Debug, Serialize)]
pub struct CloudflareConfigIngress {
#[serde(skip_serializing_if = "Option::is_none")]
pub hostname: Option<String>,
pub service: String,
}
#[derive(Debug, Serialize)]
pub struct CloudflareConfig {
pub tunnel: String,
#[serde(rename = "credentials-file")]
pub credentials_file: String,
pub ingress: Vec<CloudflareConfigIngress>,
}
#[derive(Debug)]
pub struct CloudflareProxy {
_temp: TempDir,
pub process: Child,
}
impl Drop for CloudflareProxy {
fn drop(&mut self) {
debug!("Stopping cloudflare tunnel");
futures::executor::block_on(self.process.kill()).unwrap();
}
}
impl CloudflareProxy {
#[instrument(ret)]
pub async fn new(service: String, fqdn: String, ports: Vec<PortMapping>) -> Result<Self> {
let temp = TempDir::new()?;
std::fs::write(
temp.path().join("cert.pem"),
std::env::var("OUTPOST_CLOUDFLARE_ORIGIN_CERT")?,
)?;
if Command::new("cloudflared")
.arg("tunnel")
.arg("--origincert")
.arg(temp.path().join("cert.pem"))
.arg("delete")
.arg(&service)
.spawn()?
.wait()
.await?
.success()
{
debug!("Deleted existing tunnel successfully");
}
assert!(Command::new("cloudflared")
.arg("tunnel")
.arg("--origincert")
.arg(temp.path().join("cert.pem"))
.arg("create")
.arg(&service)
.spawn()?
.wait()
.await?
.success());
assert!(Command::new("cloudflared")
.arg("tunnel")
.arg("--origincert")
.arg(temp.path().join("cert.pem"))
.arg("route")
.arg("dns")
.arg("--overwrite-dns")
.arg(&service)
.arg(&fqdn)
.spawn()?
.wait()
.await?
.success());
let mut config = CloudflareConfig {
tunnel: "".to_string(),
credentials_file: "".to_string(),
ingress: vec![
CloudflareConfigIngress {
hostname: None,
service: "http_status:404".into(),
},
],
};
for entry in std::fs::read_dir(&temp)? {
let entry = entry?;
if entry
.file_name()
.to_string_lossy()
.to_owned()
.ends_with(".json")
{
config.tunnel = entry
.path()
.file_stem()
.unwrap()
.to_string_lossy()
.to_string();
config.credentials_file = entry.path().to_string_lossy().to_string();
}
}
for port in ports {
config.ingress.insert(
0,
CloudflareConfigIngress {
hostname: Some(fqdn.clone()),
service: format!("http://{}:{}", &service, port.local),
},
)
}
debug!(config = ?config, "Generated cloudflared config");
let config_path = temp.path().join("config.yml");
std::fs::write(&config_path, serde_yaml::to_string(&config)?)?;
info!("Starting cloudflare tunnel");
Ok(Self {
process: Command::new("cloudflared")
.arg("--no-autoupdate")
.arg("tunnel")
.arg("--origincert")
.arg(temp.path().join("cert.pem"))
.arg("--config")
.arg(config_path.to_string_lossy().to_string())
.arg("run")
.arg(&service)
.spawn()?,
_temp: temp,
})
}
}