diff options
Diffstat (limited to 'src/project.rs')
| -rw-r--r-- | src/project.rs | 138 |
1 files changed, 63 insertions, 75 deletions
diff --git a/src/project.rs b/src/project.rs index 0dac2ef..57185b1 100644 --- a/src/project.rs +++ b/src/project.rs @@ -16,7 +16,7 @@ use std::collections::BTreeMap; use std::env; use std::fs::{self, File, OpenOptions, Permissions}; -use std::io::Read; +use std::io::{Read, Write}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::{Component, Path, PathBuf}; use std::process::Command; @@ -25,6 +25,7 @@ use std::time::Duration; use anyhow::{Context, Result, bail, ensure}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use stack_config::Config; use crate::{files, process}; @@ -64,6 +65,8 @@ pub(crate) struct Service { environment: Vec<String>, #[serde(default)] drop_capabilities: bool, + #[serde(default)] + read_only: bool, health: Vec<String>, mounts: Vec<Mount>, #[serde(default)] @@ -116,11 +119,16 @@ impl Project { pub(crate) fn settings(&self) -> Result<Settings> { let path = self.root.join("var/state.json"); if path.exists() { - return Ok(serde_json::from_reader(File::open(path)?)?); + let mut settings: Settings = serde_json::from_reader(File::open(path)?)?; + let config = Config::load(&self.root)?; + if config.is_explicit("LOCAL_HTTP_PORT") { + settings.port = config.port("LOCAL_HTTP_PORT")?; + } + return Ok(settings); } Ok(Settings { mode: Mode::Local, - port: 8080, + port: Config::load(&self.root)?.port("LOCAL_HTTP_PORT")?, project: format!( "stack-{}", files::hash_bytes(self.root.as_os_str().as_encoded_bytes()) @@ -230,17 +238,23 @@ impl Project { pub(crate) fn initialize(&self, settings: &Settings) -> Result<()> { files::private_dir(&self.root.join("var/data"))?; let env_file = self.root.join(".env"); - if !env_file.exists() { - let mut random = [0; 32]; - File::open("/dev/urandom")?.read_exact(&mut random)?; - let secret = hex::encode(random); - files::write( - &env_file, - format!("SEARXNG_SECRET={secret}\n").as_bytes(), - 0o600, - )?; + for key in ["SEARXNG_SECRET", "CHAT_SETUP_TOKEN"] { + if !Config::load(&self.root)?.is_explicit(key) { + let mut random = [0; 32]; + File::open("/dev/urandom")?.read_exact(&mut random)?; + let mut file = OpenOptions::new() + .create(true) + .append(true) + .mode(0o600) + .open(&env_file)?; + writeln!(file, "\n{key}={}", hex::encode(random))?; + } } + stack_config::initialize_public(&self.root)?; fs::set_permissions(&env_file, Permissions::from_mode(0o600))?; + for name in ["chat", "chat-local"] { + files::private_dir(&self.root.join("var/data").join(name))?; + } for name in ["repos", "caddy/data", "caddy/config", "searxng"] { fs::create_dir_all(self.root.join("var/data").join(name))?; } @@ -268,42 +282,7 @@ impl Project { &self.root.join("infra/searxng"), &self.root.join("var/data/searxng"), )?; - if settings.mode == Mode::Local { - self.local_site(settings.port)?; - } - Ok(()) - } - - fn local_site(&self, port: u16) -> Result<()> { - let generated = self.root.join("var/local/web"); - if generated.exists() { - files::regular_tree(&generated)?; - fs::remove_dir_all(&generated)?; - } - files::copy_tree(&self.root.join("web"), &generated)?; - let index = generated.join("index.html"); - let html = fs::read_to_string(&index)? - .replace("https://git.doan.luuduct.ai", "/git") - .replace("https://search.doan.luuduct.ai", "/search") - .replace("https://openpgpkey.luuduct.ai", ""); - files::write(&index, html.as_bytes(), 0o644)?; - let template = self.root.join("infra/containers/cgit/cgitrc.local"); - let content = fs::read_to_string(&template)? - .lines() - .map(|line| { - if line.starts_with("clone-prefix=") { - format!("clone-prefix=http://localhost:{port}/git") - } else { - line.to_owned() - } - }) - .collect::<Vec<_>>() - .join("\n"); - files::write( - &self.root.join("var/local/cgitrc"), - format!("{content}\n").as_bytes(), - 0o644, - ) + crate::render::prepare(self, settings) } pub(crate) fn container_name(settings: &Settings, service: &str) -> String { @@ -343,9 +322,7 @@ impl Project { .args(["network", "create", &network]), )?; } - let environment: BTreeMap<String, String> = - dotenvy::from_path_iter(self.root.join(".env"))? - .collect::<std::result::Result<_, _>>()?; + let environment = Config::load(&self.root)?; for (name, service) in config .services .iter() @@ -364,7 +341,11 @@ impl Project { process::line(format_args!( "Running {}", if settings.mode == Mode::Production { - "https://doan.luuduct.ai".to_owned() + environment + .public_values(None)? + .get("SITE_ORIGIN") + .context("Missing public site origin")? + .to_owned() } else { format!("http://localhost:{}", settings.port) } @@ -376,6 +357,9 @@ impl Project { && (settings.online || !self.root.join("var/image-lock.json").exists()) && let Some(context) = &service.build { + if context == "bin/chat" && !self.root.join("bin/chat/server").is_file() { + crate::chat::build(self)?; + } return process::run( self.command("podman")? .args(["build", "--tag", &service.image]) @@ -401,7 +385,7 @@ impl Project { network: &str, name: &str, service: &Service, - environment: &BTreeMap<String, String>, + environment: &Config, ) -> Result<()> { let container = Self::container_name(settings, name); let mut command = self.command("podman")?; @@ -426,6 +410,9 @@ impl Project { "--security-opt", "no-new-privileges", ]); + if service.read_only { + command.args(["--read-only", "--read-only-tmpfs=false"]); + } if service.drop_capabilities { command.args(["--cap-drop", "ALL"]); } @@ -439,38 +426,39 @@ impl Project { ] .to_vec() } else { - ["[::]:80:80", "[::]:443:443", "[::]:443:443/udp"] - .map(str::to_owned) - .to_vec() + let http = environment.port("HTTP_PORT")?; + let https = environment.port("HTTPS_PORT")?; + vec![ + format!("{}:{http}:{http}", crate::chat::web_bind(environment)?), + format!("{}:{https}:{https}", crate::chat::web_bind(environment)?), + format!( + "{}:{https}:{https}/udp", + crate::chat::web_bind(environment)? + ), + ] }; for port in ports { command.args(["--publish", &port]); } } for key in &service.environment { - command.arg("--env").arg(key).env( - key, - environment - .get(key) - .with_context(|| format!("Missing secret {key} in .env"))?, - ); + command + .arg("--env") + .arg(key) + .env(key, environment.value(key)?); + } + if name == "chat" { + crate::chat::environment(&mut command, environment, settings)?; } for mount in &service.mounts { let source = match mount.source.as_str() { - "@caddy" => self.root.join(if settings.mode == Mode::Local { - "infra/Caddyfile.local" - } else { - "infra/Caddyfile" - }), - "@web" => self.root.join(if settings.mode == Mode::Local { - "var/local/web" - } else { - "web" - }), - "@cgit" => self.root.join(if settings.mode == Mode::Local { - "var/local/cgitrc" + "@caddy" => crate::render::directory(self, settings).join("Caddyfile"), + "@web" => crate::render::directory(self, settings).join("web"), + "@cgit" => crate::render::directory(self, settings).join("cgitrc"), + "@chat" => self.root.join(if settings.mode == Mode::Local { + "var/data/chat-local" } else { - "infra/containers/cgit/cgitrc" + "var/data/chat" }), other => self.path(other)?, }; |
