use std::{path::PathBuf, sync::Arc}; use async_trait::async_trait; use dagger_rust::source::RustSource; use dagger_sdk::Container; use futures::{stream, StreamExt}; use crate::{ dagger_middleware::DaggerMiddleware, rust_service::{ architecture::{Architecture, Os}, extensions::CargoBInstallExt, RustServiceContext, RustServiceStage, }, Context, MainAction, PullRequestAction, }; #[derive(Clone)] pub struct LeptosService { pub(crate) client: dagger_sdk::Query, base_image: Option, final_image: Option, stages: Vec, source: Option, crates: Vec, bin_name: String, arch: Option, os: Option, deploy_target_name: Option, deploy: bool, } impl LeptosService { pub fn new(client: dagger_sdk::Query, bin_name: impl Into) -> Self { Self { client, base_image: None, final_image: None, stages: Vec::new(), source: None, crates: Vec::new(), bin_name: bin_name.into(), arch: None, os: None, deploy_target_name: None, deploy: true, } } pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self { self.base_image = Some(base); self } pub fn with_stage(&mut self, stage: RustServiceStage) -> &mut Self { self.stages.push(stage); self } pub fn with_source(&mut self, path: impl Into) -> &mut Self { self.source = Some(path.into()); self } pub fn with_deploy_target(&mut self, deploy_target: impl Into) -> &mut Self { self.deploy_target_name = Some(deploy_target.into()); self } pub fn with_crates( &mut self, crates: impl IntoIterator>, ) -> &mut Self { self.crates = crates.into_iter().map(|c| c.into()).collect(); self } pub fn with_arch(&mut self, arch: Architecture) -> &mut Self { self.arch = Some(arch); self } pub fn with_os(&mut self, os: Os) -> &mut Self { self.os = Some(os); self } pub fn with_deploy(&mut self, deploy: bool) -> &mut Self { self.deploy = deploy; self } fn get_src(&self) -> PathBuf { self.source .clone() .unwrap_or(std::env::current_dir().unwrap()) } pub fn get_arch(&self) -> Architecture { self.arch .clone() .unwrap_or_else(|| match std::env::consts::ARCH { "x86" | "x86_64" | "amd64" => Architecture::Amd64, "arm" => Architecture::Arm64, arch => panic!("unsupported architecture: {arch}"), }) } pub fn get_os(&self) -> Os { self.os .clone() .unwrap_or_else(|| match std::env::consts::OS { "linux" => Os::Linux, "macos" => Os::MacOS, os => panic!("unsupported os: {os}"), }) } async fn run_stage( &self, stages: impl IntoIterator>, container: Container, ) -> eyre::Result { let before_deps_stream = stream::iter(stages.into_iter().map(Ok)); let res = StreamExt::fold(before_deps_stream, Ok(container), |base, m| async move { match (base, m) { (Ok(base), Ok(m)) => m.handle(base).await, (_, Err(e)) | (Err(e), _) => eyre::bail!("failed with {e}"), } }) .await?; Ok(res) } pub async fn build_base(&self) -> eyre::Result { let rust_src = RustSource::new(self.client.clone()); let (src, dep_src) = rust_src .get_rust_src(Some(&self.get_src()), self.crates.clone()) .await?; let base_image = self .base_image .clone() .unwrap_or(self.client.container().from("rustlang/rust:nightly")); let before_deps = self .stages .iter() .filter_map(|s| match s { RustServiceStage::BeforeDeps(middleware) => Some(middleware), _ => None, }) .collect::>(); let image = self.run_stage(before_deps, base_image).await?; let image = image.with_exec(vec!["rustup", "target", "add", "wasm32-unknown-unknown"]); let after_deps = self .stages .iter() .filter_map(|s| match s { RustServiceStage::AfterDeps(m) => Some(m), _ => None, }) .collect::>(); let image = self.run_stage(after_deps, image).await?; let before_base = self .stages .iter() .filter_map(|s| match s { RustServiceStage::BeforeBase(m) => Some(m), _ => None, }) .collect::>(); let image = self.run_stage(before_base, image).await?; let cache = self.client.cache_volume("rust_target_cache"); let rust_prebuild = image .with_workdir("/mnt/src") .with_directory("/mnt/src", dep_src) .with_exec(vec![ "cargo", "leptos", "build", "--release", "-vv", "--project", &self.bin_name, ]) .with_mounted_cache("/mnt/src/target/", cache); let incremental_dir = rust_src .get_rust_target_src(&self.get_src(), rust_prebuild.clone(), self.crates.clone()) .await?; let rust_with_src = image .with_workdir("/mnt/src") .with_directory( "/usr/local/cargo", rust_prebuild.directory("/usr/local/cargo"), ) .with_directory("/mnt/src/target", incremental_dir) .with_directory("/mnt/src/", src); let after_base = self .stages .iter() .filter_map(|s| match s { RustServiceStage::AfterBase(m) => Some(m), _ => None, }) .collect::>(); let image = self.run_stage(after_base, rust_with_src).await?; Ok(image) } pub async fn build_release(&self) -> eyre::Result { let base = self.build_base().await?; let before_build = self .stages .iter() .filter_map(|s| match s { RustServiceStage::BeforeBuild(m) => Some(m), _ => None, }) .collect::>(); let base = self.run_stage(before_build, base).await?; let binary_build = base.with_exec(vec![ "cargo", "leptos", "build", "--release", "--project", &self.bin_name, ]); let after_build = self .stages .iter() .filter_map(|s| match s { RustServiceStage::AfterBuild(m) => Some(m), _ => None, }) .collect::>(); let binary_build = self.run_stage(after_build, binary_build).await?; let dest = self .final_image .clone() .unwrap_or(self.client.container().from("debian:bullseye")); let before_package = self .stages .iter() .filter_map(|s| match s { RustServiceStage::BeforePackage(m) => Some(m), _ => None, }) .collect::>(); let dest = self.run_stage(before_package, dest).await?; let final_image = dest .with_workdir("/mnt/app") .with_file( format!("/usr/local/bin/{}", self.bin_name), binary_build.file(format!("/mnt/src/target/release/{}", self.bin_name)), ) .with_directory( "/mnt/app/target/site", binary_build.directory("/mnt/src/target/site"), ) .with_file( "/mnt/app/Cargo.toml", binary_build.file(format!("/mnt/src/crates/{}/Cargo.toml", self.bin_name)), ) .with_env_variable("APP_ENVIRONMENT", "production") .with_env_variable("LEPTOS_OUTPUT_NAME", &self.bin_name) .with_env_variable("LEPTOS_SITE_ADDR", "0.0.0.0:3000") .with_env_variable("LEPTOS_SITE_PKG_DIR", "pkg") .with_exposed_port(3000); let after_package = self .stages .iter() .filter_map(|s| match s { RustServiceStage::AfterPackage(m) => Some(m), _ => None, }) .collect::>(); let final_image = self.run_stage(after_package, final_image).await?; Ok(final_image) } pub async fn build_test(&self) -> eyre::Result<()> { let base = self.build_base().await?; let before_build = self .stages .iter() .filter_map(|s| match s { RustServiceStage::BeforeBuild(m) => Some(m), _ => None, }) .collect::>(); let base = self.run_stage(before_build, base).await?; base.with_exec(vec!["cargo", "leptos", "test", "--release"]) .sync() .await?; Ok(()) } fn get_deploy_target(&self) -> String { self.deploy_target_name .clone() .unwrap_or(self.bin_name.clone()) } } #[async_trait] impl PullRequestAction for LeptosService { async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> { let mut s = self.clone(); s.with_cargo_binstall("latest", ["cargo-leptos"]) .build_test() .await?; Ok(()) } } #[async_trait] impl MainAction for LeptosService { async fn execute_main(&self, ctx: &mut Context) -> eyre::Result<()> { let mut s = self.clone(); let container = s .with_cargo_binstall("latest", ["cargo-leptos"]) .build_release() .await?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); let tag = format!( "docker.io/kasperhermansen/{}:main-{}", self.bin_name, timestamp, ); container.publish(tag.clone()).await?; tracing::info!("published: {}", tag); ctx.set_image_tag(format!("main-{}", ×tamp.to_string()))?; if self.deploy { let update_deployments_docker_image = "docker.io/kasperhermansen/update-deployment:1701123940"; let dep = self .client .container() .from(update_deployments_docker_image); let dep = if let Ok(sock) = std::env::var("SSH_AUTH_SOCK") { dep.with_unix_socket("/tmp/ssh_sock", self.client.host().unix_socket(sock)) .with_env_variable("SSH_AUTH_SOCK", "/tmp/ssh_sock") .with_exec(vec![ "update-deployment", "--repo", &format!( "git@git.front.kjuulh.io:kjuulh/{}-deployment.git", self.get_deploy_target() ), "--service", &self.bin_name, "--image", &format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp), ]) } else { dep.with_env_variable("GIT_USERNAME", "kjuulh") .with_env_variable( "GIT_PASSWORD", std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"), ) .with_exec(vec![ "update-deployment", "--repo", &format!( "https://git.front.kjuulh.io/kjuulh/{}-deployment.git", self.get_deploy_target() ), "--service", &self.bin_name, "--image", &format!("kasperhermansen/{}:main-{}", self.bin_name, timestamp), ]) }; dep.sync().await?; } Ok(()) } }