diff --git a/crates/cuddle-ci/src/lib.rs b/crates/cuddle-ci/src/lib.rs index c0b451a..e0777d1 100644 --- a/crates/cuddle-ci/src/lib.rs +++ b/crates/cuddle-ci/src/lib.rs @@ -2,4 +2,5 @@ pub mod cli; pub use cli::*; pub mod dagger_middleware; +pub mod node_service; pub mod rust_service; diff --git a/crates/cuddle-ci/src/node_service.rs b/crates/cuddle-ci/src/node_service.rs new file mode 100644 index 0000000..378da25 --- /dev/null +++ b/crates/cuddle-ci/src/node_service.rs @@ -0,0 +1,280 @@ +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}, + 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 { + 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(self.get_src().to_string_lossy()); + 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" + .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"]), + } + .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_exec(vec!["yarn", "start"]); + + Ok(final_image) + } +} + +#[async_trait] +impl PullRequestAction for NodeService { + async fn execute_pull_request(&self) -> eyre::Result<()> { + self.build_release().await?; + + Ok(()) + } +} + +#[async_trait] +impl MainAction for NodeService { + async fn execute_main(&self) -> 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(()) + } +}