use std::path::PathBuf; use async_trait::async_trait; use dagger_sdk::{Container, ContainerWithDirectoryOptsBuilder, HostDirectoryOptsBuilder}; use crate::{ dagger_middleware::DynMiddleware, rust_service::architecture::{Architecture, Os}, Context, MainAction, PullRequestAction, }; #[derive(Clone)] pub enum NodeServiceStage { BeforeDeps(DynMiddleware), AfterDeps(DynMiddleware), BeforeBase(DynMiddleware), AfterBase(DynMiddleware), BeforeBuild(DynMiddleware), AfterBuild(DynMiddleware), BeforePackage(DynMiddleware), AfterPackage(DynMiddleware), BeforeRelease(DynMiddleware), AfterRelease(DynMiddleware), } #[derive(Clone)] pub struct NodeService { client: dagger_sdk::Query, service: String, base_image: Option, final_image: Option, stages: Vec, source: Option, arch: Option, os: Option, } impl NodeService { pub fn new(value: dagger_sdk::Query, service: impl Into) -> Self { Self { client: value, service: service.into(), base_image: None, final_image: None, stages: Vec::new(), source: None, arch: None, os: None, } } pub fn with_base_image(&mut self, base: dagger_sdk::Container) -> &mut Self { self.base_image = Some(base); self } pub fn with_service(&mut self, service: impl Into) -> &mut Self { self.service = service.into(); self } pub fn with_stage(&mut self, stage: NodeServiceStage) -> &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_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 } fn get_src(&self) -> PathBuf { self.source .clone() .unwrap_or(std::env::current_dir().unwrap()) } 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}"), }) } 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}"), }) } pub async fn build_base(&self) -> eyre::Result { let src = self.client.host().directory_opts( self.get_src().to_string_lossy(), HostDirectoryOptsBuilder::default() .exclude(vec!["node_modules/", ".git/", ".cuddle/"]) .build()?, ); let pkg_files = self.client.host().directory_opts( self.get_src().to_string_lossy(), HostDirectoryOptsBuilder::default() .include(vec!["package.json", "yarn.lock"]) .build()?, ); let deps = "apk add --no-cache build-base gcc autoconf automake zlib-dev libpng-dev vips-dev git postgresql-dev" .split_whitespace(); let base_image = match self.base_image.clone() { Some(image) => image, None => self .client .container() .from("node:20-alpine") .with_exec(vec!["apk", "update"]) .with_exec(deps.collect()), } .with_env_variable("NODE_ENV", "production"); let base_yarn_image = base_image .with_workdir("/opt/") .with_directory(".", pkg_files) .with_exec(vec!["yarn", "global", "add", "node-gyp"]) .with_exec(vec![ "yarn", "config", "set", "network-timeout", "600000", "-g", ]) .with_exec(vec!["yarn", "install", "--production"]); let base_build = base_yarn_image .with_env_variable( "PATH", format!( "/opt/node_modules/.bin:{}", base_yarn_image.env_variable("PATH").await? ), ) .with_workdir("/opt/app") .with_directory(".", src) .with_exec(vec!["yarn", "build"]); Ok(base_build) } pub async fn build_release(&self) -> eyre::Result { let base = self.build_base().await?; let final_build_image = match self.final_image.clone() { Some(c) => c, None => self .client .container() .from("node:20-alpine") .with_exec(vec![ "apk", "add", "--no-cache", "vips-dev", "postgresql-dev", ]), } .with_env_variable("NODE_ENV", "production"); let final_image = final_build_image .with_workdir("/opt/") .with_directory("/opt/node_modules", base.directory("/opt/node_modules")) .with_workdir("/opt/app") .with_directory_opts( "/opt/app", base.directory("/opt/app"), ContainerWithDirectoryOptsBuilder::default() .owner("node:node") .build()?, ) .with_env_variable( "PATH", format!( "/opt/node_modules/.bin:{}", final_build_image.env_variable("PATH").await? ), ) .with_user("node") .with_exposed_port(1337) .with_entrypoint(vec!["yarn", "start"]); Ok(final_image) } } #[async_trait] impl PullRequestAction for NodeService { async fn execute_pull_request(&self, _ctx: &mut Context) -> eyre::Result<()> { let release = self.build_release().await?; release.sync().await?; Ok(()) } } #[async_trait] impl MainAction for NodeService { async fn execute_main(&self, _ctx: &mut Context) -> eyre::Result<()> { let container = self.build_release().await?; let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); container .publish(format!( "docker.io/kasperhermansen/{}:main-{}", self.service, timestamp, )) .await?; let update_deployments_docker_image = "docker.io/kasperhermansen/update-deployment:1701123940"; let dep = self .client .container() .from(update_deployments_docker_image); let dep = match std::env::var("SSH_AUTH_SOCK").ok() { Some(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.service ), "--service", &self.service, "--image", &format!("kasperhermansen/{}:main-{}", self.service, timestamp), ]), _ => 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.service ), "--service", &self.service, "--image", &format!("kasperhermansen/{}:main-{}", self.service, timestamp), ]), }; dep.sync().await?; Ok(()) } }