use git2::build::{CheckoutBuilder, RepoBuilder}; use git2::{Cred, FetchOptions, PushOptions, RemoteCallbacks, Signature}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; use tempdir::TempDir; #[tokio::main] async fn main() -> eyre::Result<()> { let _ = dotenv::dotenv(); color_eyre::install().unwrap(); tracing_subscriber::fmt().pretty().init(); let matches = clap::Command::new("update-deployment") .arg(clap::Arg::new("repo").long("repo").required(true)) .arg(clap::Arg::new("service").long("service").required(true)) .arg(clap::Arg::new("image").long("image").required(true)) .arg(clap::Arg::new("path").long("path")) .get_matches(); let repo = matches.get_one::("repo").unwrap(); let service = matches.get_one::("service").unwrap(); let image = matches.get_one::("image").unwrap(); let docker_compose_path = matches.get_one::("path"); tracing::info!( repo = repo, service = service, image = image, path = docker_compose_path, "updating deployment" ); let tmpdir = TempDir::new("update-deployment")?; let tmpdir = tmpdir.path(); tracing::info!( repo = repo, dest_dir = tmpdir.display().to_string(), "clone repo" ); let mut cb = RemoteCallbacks::new(); cb.credentials(|_, username, _| { if let Some(sock) = std::env::var("SSH_AUTH_SOCK").ok() { return Cred::ssh_key_from_agent(username.unwrap_or("git")); } let username = std::env::var("GIT_USERNAME").expect("GIT_USERNAME to be set"); let password = std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"); Cred::userpass_plaintext(&username, &password) }); let co = CheckoutBuilder::new(); let mut fo = FetchOptions::new(); fo.remote_callbacks(cb); RepoBuilder::new() .fetch_options(fo) .with_checkout(co) .clone(repo, tmpdir)?; let mut repo_dir = tmpdir.to_path_buf(); repo_dir.push("repo"); let repo = git2::Repository::clone(repo, &repo_dir)?; let dir = std::fs::read_dir(&repo_dir)?; for file in dir { println!("file: {}", file?.file_name().to_str().unwrap()); } let (docker_compose_raw, docker_compose_path) = if let Some(docker_compose_path) = docker_compose_path { let mut path = repo_dir.clone(); path.push(docker_compose_path); (tokio::fs::read(&path).await?, path) } else { let mut path = repo_dir.clone(); path.push("docker-compose.yml"); (tokio::fs::read(&path).await?, path) }; let mut docker_compose: DockerCompose = serde_yaml::from_slice(docker_compose_raw.as_slice())?; tracing::info!( docker_compose = serde_json::to_string(&docker_compose)?, "found docker-compose.yml" ); let service = docker_compose.services.get_mut(service); if let Some(service) = service { if let Some(img) = service.image.as_mut() { *img = image.clone(); } } let dest_docker_compose = serde_yaml::to_string(&docker_compose)?; tokio::fs::write(docker_compose_path, dest_docker_compose).await?; let tree_id = { let mut index = repo.index()?; index.add_all(&["."], git2::IndexAddOption::DEFAULT, None)?; index.write()?; index.write_tree()? }; let sig = Signature::now("kjuulh", "contact@kjuulh.io")?; let tree = repo.find_tree(tree_id)?; let head = repo.head()?; let commit = head.peel_to_commit()?; let _ = repo.commit( Some("HEAD"), &sig, &sig, "update: docker-compose", &tree, &[&commit], )?; let mut remote = repo.find_remote("origin")?; let mut cb = RemoteCallbacks::new(); cb.credentials(|_, username, _| { if let Some(sock) = std::env::var("SSH_AUTH_SOCK").ok() { return Cred::ssh_key_from_agent(username.unwrap_or("git")); } let username = std::env::var("GIT_USERNAME").expect("GIT_USERNAME to be set"); let password = std::env::var("GIT_PASSWORD").expect("GIT_PASSWORD to be set"); Cred::userpass_plaintext(&username, &password) }); remote.push( &["HEAD:refs/heads/main"], Some(PushOptions::new().remote_callbacks(cb)), )?; Ok(()) } use std::collections::BTreeMap as Map; #[derive(Clone, Debug, Serialize, Deserialize)] struct DockerCompose { #[serde(flatten)] pub other: Map, pub services: Map, } #[derive(Clone, Debug, Serialize, Deserialize)] struct Service { pub image: Option, #[serde(flatten)] pub other: Map, }