diff --git a/env/nginx_bind_le/Cargo.lock b/env/nginx_bind_le/Cargo.lock index 7fc32e6..639164e 100644 --- a/env/nginx_bind_le/Cargo.lock +++ b/env/nginx_bind_le/Cargo.lock @@ -417,7 +417,10 @@ name = "nginx_bind_le" version = "0.1.0" dependencies = [ "async-trait", + "lazy_static", "libconductor", + "libconfig", + "rust-embed", "serde", "serde_json", "tera", @@ -634,6 +637,40 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "rust-embed" +version = "6.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +dependencies = [ + "sha2", + "walkdir", +] + [[package]] name = "ryu" version = "1.0.11" @@ -697,6 +734,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -801,6 +849,7 @@ dependencies = [ "autocfg", "bytes", "libc", + "memchr", "mio", "num_cpus", "pin-project-lite", diff --git a/env/nginx_bind_le/Cargo.toml b/env/nginx_bind_le/Cargo.toml index f76f95c..a41b1fb 100644 --- a/env/nginx_bind_le/Cargo.toml +++ b/env/nginx_bind_le/Cargo.toml @@ -2,6 +2,7 @@ name = "nginx_bind_le" version = "0.1.0" edition = "2021" +include = ["/templates"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,8 +10,11 @@ edition = "2021" serde = { version = "1", features=["derive"]} serde_json = { version ="1", features = ["raw_value"]} async-trait = "0.1.57" -tokio = { version = "1.23.0", features = ["process"] } +tokio = { version = "1.23.0", features = ["process", "fs", "io-util"] } tera = "1.17.1" +rust-embed = "6.4.2" +lazy_static = "1.4.0" +libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" } [dependencies.libconductor] path = "../libconductor" diff --git a/env/nginx_bind_le/src/lib.rs b/env/nginx_bind_le/src/lib.rs index 0a4201f..3547538 100644 --- a/env/nginx_bind_le/src/lib.rs +++ b/env/nginx_bind_le/src/lib.rs @@ -20,6 +20,9 @@ use tokio::process::Command; use libconductor::*; mod nginx; +mod templates; + +use nginx::Nginx; #[derive(Clone, Debug, PartialEq, Eq)] pub struct NginxBindLEConductor; @@ -33,10 +36,17 @@ impl Conductor for NginxBindLEConductor { EventType::NewSite { hostname, path, - branch, - } => unimplemented!(), - EventType::Config { data } => unimplemented!(), - EventType::DeleteSite { hostname } => unimplemented!(), + branch: _branch, + } => { + Nginx::new_site(&hostname, &path, None).await.unwrap(); + } + EventType::Config { hostname, data } => { + unimplemented!(); + // Nginx::new_site(&hostname, &path, Some(data)).await.unwrap(); + } + EventType::DeleteSite { hostname } => { + Nginx::rm_site(&hostname).await.unwrap(); + } }; } fn name(&self) -> &'static str { @@ -44,7 +54,7 @@ impl Conductor for NginxBindLEConductor { } async fn health(&self) -> bool { - nginx::Nginx::status().await + nginx::Nginx::env_exists() && nginx::Nginx::status().await } } #[cfg(test)] @@ -54,8 +64,26 @@ mod tests { #[tokio::test] async fn all_good() { + const HOSTNAME: &str = "lab.batsense.net"; let c = NginxBindLEConductor {}; assert_eq!(c.name(), CONDUCTOR_NAME); assert!(c.health().await); + if Nginx::site_exists(HOSTNAME) { + c.process(EventType::DeleteSite { + hostname: HOSTNAME.into(), + }) + .await; + } + + c.process(EventType::NewSite { + hostname: HOSTNAME.into(), + branch: "librepages".into(), + path: "/var/www/website/".into(), + }) + .await; + c.process(EventType::DeleteSite { + hostname: HOSTNAME.into(), + }) + .await; } } diff --git a/env/nginx_bind_le/src/nginx.rs b/env/nginx_bind_le/src/nginx.rs index 4682722..2ba062b 100644 --- a/env/nginx_bind_le/src/nginx.rs +++ b/env/nginx_bind_le/src/nginx.rs @@ -14,11 +14,30 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +use std::cell::RefCell; +use std::error::Error; +use std::path::{Path, PathBuf}; + +use tera::*; +use tokio::fs; +use tokio::io::AsyncWriteExt; use tokio::process::Command; +use crate::templates::*; + pub struct Nginx; impl Nginx { + pub async fn reload() -> MyResult<()> { + Command::new("sudo") + .arg("nginx") + .arg("-s") + .arg("reload") + .spawn()? + .wait() + .await?; + Ok(()) + } pub async fn status() -> bool { async fn run_async_cmd(cmd: &mut Command) -> bool { if let Ok(mut child) = cmd.spawn() { @@ -30,4 +49,84 @@ impl Nginx { } run_async_cmd(Command::new("sudo").arg("nginx").arg("-t")).await } + + pub async fn new_site( + hostname: &str, + path: &str, + config: Option, + ) -> MyResult<()> { + let config = CreateSite::new(hostname, path, config); + let contents = config.render(); + let staging = Self::get_staging(hostname); + let prod = Self::get_prod(hostname); + + let mut file = fs::File::create(&staging).await?; + file.write_all(contents.as_bytes()).await?; + file.sync_all().await?; + fs::symlink(&staging, &prod).await?; + Self::reload().await + } + + fn get_staging(hostname: &str) -> PathBuf { + Path::new(NGINX_STAGING_CONFIG_PATH).join(hostname) + } + fn get_prod(hostname: &str) -> PathBuf { + Path::new(NGINX_PRODUCTION_CONFIG_PATH).join(hostname) + } + + pub fn site_exists(hostname: &str) -> bool { + Self::get_prod(hostname).exists() + } + + pub async fn rm_site(hostname: &str) -> MyResult<()> { + let staging = Self::get_staging(hostname); + let prod = Self::get_prod(hostname); + + fs::remove_file(&prod).await?; + fs::remove_file(&staging).await?; + Self::reload().await + } + + pub fn env_exists() -> bool { + let prod = Path::new(NGINX_PRODUCTION_CONFIG_PATH); + let staging = Path::new(NGINX_STAGING_CONFIG_PATH); + prod.exists() && prod.is_dir() && staging.exists() && staging.is_dir() + } +} + +pub struct CreateSite { + ctx: RefCell, +} + +pub const CREATE_SITE: TemplateFile = TemplateFile::new("create_site", "nginx/create-site.j2"); +pub const CREATE_SITE_FRAGMENT: TemplateFile = + TemplateFile::new("new_site_frag", "nginx/_new_site.fragement.j2"); + +pub const HOSTNAME_KEY: &str = "hostname"; +pub const DOMAINS_KEY: &str = "domains"; +pub const PATH_KEY: &str = "path"; +pub const REDIRECTS_KEY: &str = "redirects"; + +pub const NGINX_STAGING_CONFIG_PATH: &str = "/etc/librepages/nginx/sites-available/"; +pub const NGINX_PRODUCTION_CONFIG_PATH: &str = "/etc/librepages/nginx/sites-enabled/"; + +type MyResult = std::result::Result>; + +impl CreateSite { + fn new(hostname: &str, path: &str, config: Option) -> Self { + let ctx = RefCell::new(context()); + ctx.borrow_mut().insert(HOSTNAME_KEY, hostname); + ctx.borrow_mut().insert(PATH_KEY, path); + if let Some(config) = config { + ctx.borrow_mut().insert(REDIRECTS_KEY, &config.redirects); + ctx.borrow_mut().insert(DOMAINS_KEY, &config.domains); + } + Self { ctx } + } + + fn render(&self) -> String { + TEMPLATES + .render(CREATE_SITE.name, &self.ctx.borrow()) + .unwrap() + } }