From 3e9c4a20a3dfe32d0fa5d98d5159cd9cfa8bd318 Mon Sep 17 00:00:00 2001 From: Raphael Coeffic Date: Fri, 8 Nov 2024 12:20:07 +0100 Subject: [PATCH] feat: improve shell (#6) --- .gitignore | 1 + Cargo.lock | 72 +++++++++++++++ image_builder/Cargo.toml | 1 + image_builder/src/debug-shell/flake.nix | 2 + image_builder/src/etc/zshrc | 61 ++++++++++++ image_builder/src/lib.rs | 51 ++++++++-- src/base_image.rs | 118 ++++++++++++++++++++++++ src/embedded_image.rs | 13 +-- src/main.rs | 73 +++++++++------ src/pid_file.rs | 7 +- 10 files changed, 355 insertions(+), 44 deletions(-) create mode 100644 image_builder/src/etc/zshrc create mode 100644 src/base_image.rs diff --git a/.gitignore b/.gitignore index 9e12db0..3c6c120 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ nix image_builder/base.tar.xz base.tar.xz +base.sha256 diff --git a/Cargo.lock b/Cargo.lock index 3cd7302..4c83e73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,6 +105,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -215,6 +224,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -224,6 +242,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "5.0.1" @@ -367,6 +405,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -451,6 +499,7 @@ dependencies = [ "log", "regex", "rustix", + "sha2", "tar", "tempfile", "ureq", @@ -892,6 +941,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -986,6 +1046,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -1053,6 +1119,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/image_builder/Cargo.toml b/image_builder/Cargo.toml index 1a2c6f1..fc769fe 100644 --- a/image_builder/Cargo.toml +++ b/image_builder/Cargo.toml @@ -16,3 +16,4 @@ ureq = { version = "2.10.1", default-features = false, features = ["tls", "nativ rustix = { workspace = true, features = ["runtime"] } tar = { workspace = true } tempfile = { workspace = true } +sha2 = "0.10.8" diff --git a/image_builder/src/debug-shell/flake.nix b/image_builder/src/debug-shell/flake.nix index f928d9a..5305e62 100644 --- a/image_builder/src/debug-shell/flake.nix +++ b/image_builder/src/debug-shell/flake.nix @@ -37,12 +37,14 @@ netcat-openbsd procps sngrep + sqlite strace tcpdump util-linux vim xz zsh + zsh-prezto zsh-autosuggestions zsh-completions zsh-fast-syntax-highlighting diff --git a/image_builder/src/etc/zshrc b/image_builder/src/etc/zshrc new file mode 100644 index 0000000..79768d8 --- /dev/null +++ b/image_builder/src/etc/zshrc @@ -0,0 +1,61 @@ +# set usual editor +export VISUAL=vim +export EDITOR=vim +alias e=$EDITOR + +# -- zsh modules +autoload -Uz compinit + +ZSH_HIGHLIGHT_HIGHLIGHTERS=(main brackets pattern cursor line) +ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=40 +ZSH_AUTOSUGGEST_STRATEGY=(match_prev_cmd) + +# prevent prezto dependency loader +pmodload() {} + +zstyle ':prezto:*:*' color yes +zstyle ':prezto:module:editor' key-bindings emacs +zstyle ':prezto:module:editor' dot-expansion yes +zstyle ':prezto:module:utility' safe-ops no + +BASE_SHARE=/nix/.base/share +SITE_FUNCTIONS=$BASE_SHARE/zsh/site-functions +PRETZO_MODULES=$BASE_SHARE/zsh-prezto/modules + +# -- prezto modules +source $PRETZO_MODULES/helper/init.zsh +fpath+=( $PRETZO_MODULES/helper ) +source $PRETZO_MODULES/environment/init.zsh +fpath+=( $PRETZO_MODULES/environment ) +source $PRETZO_MODULES/terminal/init.zsh +fpath+=( $PRETZO_MODULES/terminal ) +source $PRETZO_MODULES/editor/init.zsh +fpath+=( $PRETZO_MODULES/editor ) +source $PRETZO_MODULES/utility/init.zsh +fpath+=( $PRETZO_MODULES/utility ) + +# -- some extras +source $SITE_FUNCTIONS/fast-syntax-highlighting.plugin.zsh +source $BASE_SHARE/zsh-autosuggestions/zsh-autosuggestions.zsh + +source $PRETZO_MODULES/completion/init.zsh +fpath+=( $PRETZO_MODULES/completion ) + +unsetopt EXTENDED_GLOB +unsetopt MENU_COMPLETE + +setopt AUTO_CD +setopt AUTO_MENU +setopt CDABLE_VARS +setopt CLOBBER +setopt MULTIOS +setopt PUSHDSILENT + +HISTFILE="${XDG_CACHE_HOME:-$HOME/.cache}/.zsh_history" +HISTSIZE=10000 # in memory +SAVEHIST=10000 # persistent +bindkey -e # emacs keymap + +# help stuff +# echo "Welcome to the dive shell!" + diff --git a/image_builder/src/lib.rs b/image_builder/src/lib.rs index 59bc92c..36cf0ec 100644 --- a/image_builder/src/lib.rs +++ b/image_builder/src/lib.rs @@ -19,6 +19,7 @@ use rustix::{ runtime::{fork, Fork}, thread::{unshare, Pid, UnshareFlags}, }; +use sha2::{Digest, Sha256}; use tar::Archive; use tempfile::tempdir; @@ -34,6 +35,9 @@ static FLAKE_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/src/debug-shell"); // Break compilation if 'flake.nix' does not exist static _FLAKE_NIX_GUARD_: &str = include_str!("debug-shell/flake.nix"); +static STATIC_FILES: &[(&str, &str)] = + &[("/nix/etc/.zshrc", include_str!("etc/zshrc"))]; + type PostProcessFn = Box Result<()>>; pub struct BaseImageBuilder { @@ -82,11 +86,7 @@ impl BaseImageBuilder { where P: AsRef, { - let output = output.as_ref(); - let archive_suffix = if compress { "tar.xz" } else { "tar" }; - let archive_name = format!("{}.{}", output.display(), archive_suffix); - - self.package_output.replace(PathBuf::from(archive_name)); + self.package_output.replace(PathBuf::from(output.as_ref())); self.compress = compress; self } @@ -112,8 +112,28 @@ impl BaseImageBuilder { if build_status.is_err() { return Self::BUILD_FAILED; } - let base_path = build_status.unwrap(); + + // copy static files (zshrc, etc) + let mut hasher = Sha256::new(); + hasher.update(base_path.as_os_str().as_encoded_bytes()); + + if let Err(err) = STATIC_FILES.iter().try_for_each(|(dest, content)| { + hasher.update(content); + fs::write(dest, content) + }) { + log::error!("failed to copy static files: {err}"); + return Self::POST_PROCESS_FAILED; + } + + let hash = hasher.finalize(); + if let Err(err) = + fs::write("/nix/.base.sha256", format!("{:x}\n", hash)) + { + log::error!("failed to write hash file: {err}"); + return Self::POST_PROCESS_FAILED; + } + if let Err(err) = self.do_post_process(&base_path) { log::error!("post process failed: {}", err); return Self::POST_PROCESS_FAILED; @@ -200,12 +220,15 @@ impl BaseImageBuilder { let nix_set = read_nix_paths("/nix")?; let mut tar_cmd = Command::new("tar"); + let archive_suffix = if self.compress { "tar.xz" } else { "tar" }; + let archive_name = format!("{}.{}", output.display(), archive_suffix); + tar_cmd.args([ "--directory=/nix", "--exclude=var/nix/*", "-c", "-f", - &format!("{}", output.display()), + &archive_name, ]); if self.compress { @@ -220,7 +243,7 @@ impl BaseImageBuilder { let path_env = format!("{current_path}/nix/.base/bin"); tar_cmd - .args([".bin", ".base", "etc", "var/nix"]) + .args([".bin", ".base", ".base.sha256", "etc", "var/nix"]) .args(nix_set.union(&base_set).map(|p| "store/".to_owned() + p)) .env("PATH", path_env); @@ -234,6 +257,10 @@ impl BaseImageBuilder { } } + let sha256_name = format!("{}.sha256", output.display()); + fs::copy("/nix/.base.sha256", sha256_name) + .context("could not copy hash file")?; + Ok(()) } } @@ -287,7 +314,13 @@ where fn chmod_apply(path: &Path, func: fn(u32) -> u32) -> Result<(), io::Error> { let metadata = fs::metadata(path)?; let mode = metadata.permissions().mode(); - fs::set_permissions(path, fs::Permissions::from_mode(func(mode))) + let new_mode = func(mode); + + if new_mode != mode { + fs::set_permissions(path, fs::Permissions::from_mode(new_mode)) + } else { + Ok(()) + } } // fix permissions recursively diff --git a/src/base_image.rs b/src/base_image.rs new file mode 100644 index 0000000..7b67237 --- /dev/null +++ b/src/base_image.rs @@ -0,0 +1,118 @@ +use std::{fs, io, path::Path}; + +use anyhow::{bail, Result}; +use image_builder::progress_bar; +use liblzma::read::XzDecoder; +use tar::Archive; + +#[cfg(feature = "embedded_image")] +use crate::embedded_image; + +#[cfg(not(feature = "embedded_image"))] +use image_builder::BaseImageBuilder; + +pub fn update_base_image

(dest: &Path, image: Option

) -> Result<()> +where + P: AsRef, + P: std::fmt::Debug, +{ + let current_sha256 = if fs::exists(dest.join(".base.sha256"))? { + Some(fs::read(dest.join(".base.sha256"))?.trim_ascii().to_owned()) + } else { + log::debug!("no current hash"); + None + }; + + log::debug!("image = {:?}", image); + let image_sha256 = if let Some(image) = &image { + Some( + fs::read(format!("{}.sha256", image.as_ref().display()))? + .trim_ascii() + .to_owned(), + ) + } else { + #[cfg(feature = "embedded_image")] + { + Some( + crate::embedded_image::base_image_sha256() + .as_bytes() + .to_owned(), + ) + } + + #[cfg(not(feature = "embedded_image"))] + { + log::debug!("no image"); + None + } + }; + + if image_sha256.is_none() { + if current_sha256.is_none() { + #[cfg(feature = "embedded_image")] + return embedded_image::install_base_image(dest); + + #[cfg(not(feature = "embedded_image"))] + return BaseImageBuilder::new(dest).build_base(); + } + + return Ok(()); + } + + if current_sha256.is_some_and(|h| h == image_sha256.unwrap()) { + log::debug!("SHA256 unchanged"); + return Ok(()); + } + + if let Some(image) = &image { + install_base_image_from_archive(dest, image.as_ref()) + } else { + #[cfg(feature = "embedded_image")] + return embedded_image::install_base_image(dest); + + #[cfg(not(feature = "embedded_image"))] + unreachable!() + } +} + +fn install_base_image_from_archive(dest: &Path, image: &Path) -> Result<()> { + if let Ok(f) = fs::File::open(image.with_extension("tar.xz")) { + let bar = progress_bar(f.metadata()?.len()); + install_base_image_from_reader(dest, XzDecoder::new(bar.wrap_read(f))) + } else if let Ok(f) = fs::File::open(image.with_extension("tar")) { + let bar = progress_bar(f.metadata()?.len()); + install_base_image_from_reader(dest, bar.wrap_read(f)) + } else { + bail!("could not find base image archive"); + } +} + +pub fn install_base_image_from_reader(dest: &Path, reader: R) -> Result<()> +where + R: io::Read, +{ + if dest.exists() { + // remove previous base image first + log::info!("removing current base image"); + for dir in dest.read_dir()? { + let path = dir?.path(); + if let Err(err) = image_builder::chmod(&path, |mode| mode | 0o700) { + log::error!( + "could not fix permissions for {}: {}", + path.display(), + err + ); + bail!(err); + } + if let Err(err) = fs::remove_dir_all(&path) { + log::error!("could not remove {}: {}", path.display(), err); + bail!(err); + } + } + } else { + fs::create_dir_all(dest).unwrap(); + } + + log::info!("unpacking base image into {}", dest.display()); + Ok(Archive::new(reader).unpack(dest)?) +} diff --git a/src/embedded_image.rs b/src/embedded_image.rs index 5f0f8e5..aa5b834 100644 --- a/src/embedded_image.rs +++ b/src/embedded_image.rs @@ -1,9 +1,10 @@ -use std::{fs, path::Path}; +use std::path::Path; use anyhow::Result; use image_builder::progress_bar; use liblzma::read::XzDecoder; -use tar::Archive; + +use crate::install_base_image_from_reader; pub fn install_base_image

(dest: P) -> Result<()> where @@ -13,9 +14,9 @@ where let bar = progress_bar(base_image.len() as u64); let decoder = XzDecoder::new(bar.wrap_read(base_image.as_slice())); - let dest = dest.as_ref(); - fs::create_dir_all(dest).unwrap(); + install_base_image_from_reader(dest.as_ref(), decoder) +} - log::info!("unpacking base image into {}", dest.display()); - Ok(Archive::new(decoder).unpack(dest)?) +pub fn base_image_sha256() -> &'static str { + include_str!("../base.sha256").trim() } diff --git a/src/main.rs b/src/main.rs index 92c7fa2..5e319a1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,31 +15,31 @@ use rustix::{ runtime::{fork, Fork}, }; +mod base_image; mod namespaces; mod overlay; mod pid_file; mod pid_lookup; mod shared_mount; +#[cfg(feature = "embedded_image")] +mod embedded_image; + +use base_image::*; use namespaces::*; use overlay::*; use pid_lookup::*; use shared_mount::*; -#[cfg(feature = "embedded_image")] -mod embedded_image; - -#[cfg(not(feature = "embedded_image"))] -use image_builder::BaseImageBuilder; - const APP_NAME: &str = "dive"; const IMG_DIR: &str = "base-img"; const OVL_DIR: &str = "overlay"; const DEFAULT_PATH: &str = "/usr/local/bin:/usr/bin:/bin"; -const ENV_IMG_DIR: &str = "_NSDGB_IMG_DIR"; -const ENV_OVL_DIR: &str = "_NSDGB_OVL_DIR"; +const ENV_IMG_DIR: &str = "_IMG_DIR"; +const ENV_OVL_DIR: &str = "_OVL_DIR"; +const ENV_LEAD_PID: &str = "_LEAD_PID"; /// Container debug CLI #[derive(Parser, Debug)] @@ -49,10 +49,21 @@ struct Args { #[arg(short, long, env)] img_dir: Option, + /// Base image name + #[arg(short, long, env)] + base_img: Option, + /// Container ID container_id: String, } +fn get_lead_pid(container_id: &str) -> Option { + std::env::var(ENV_LEAD_PID) + .ok() + .and_then(|pid| pid.parse().ok()) + .or_else(|| pid_lookup(container_id)) +} + fn get_img_dir(args: &Args) -> PathBuf { if let Ok(img_dir) = std::env::var(ENV_IMG_DIR) { return PathBuf::from(img_dir); @@ -60,11 +71,7 @@ fn get_img_dir(args: &Args) -> PathBuf { if args.img_dir.is_some() { return PathBuf::from(args.img_dir.clone().unwrap()); } - dirs::state_dir() - .unwrap() - .join(APP_NAME) - .join(IMG_DIR) - .to_owned() + dirs::state_dir().unwrap().join(APP_NAME).join(IMG_DIR) } fn get_overlay_dir() -> PathBuf { @@ -84,6 +91,7 @@ fn init_logging() { } fn reexec_with_sudo( + container_id: &str, lead_pid: i32, img_dir: &Path, overlay_dir: &Path, @@ -93,14 +101,19 @@ fn reexec_with_sudo( Err(Command::new("sudo") .args([ format!("LOGLEVEL={}", loglevel), + format!("{}={}", ENV_LEAD_PID, lead_pid), format!("{}={}", ENV_IMG_DIR, img_dir.display()), format!("{}={}", ENV_OVL_DIR, overlay_dir.display()), format!("{}", self_exe.display()), ]) - .arg(lead_pid.to_string()) + .arg(container_id) .exec()) } +fn runs_with_sudo() -> bool { + std::env::var("SUDO_UID").is_ok() +} + fn prepare_shell_environment( shared_mount: &SharedMount, lead_pid: i32, @@ -120,7 +133,7 @@ fn prepare_shell_environment( Ok(()) } -fn exec_shell() -> Result<()> { +fn exec_shell(container_id: &str) -> Result<()> { // // TODO: path HOME w/ user as defined by /etc/passwd // @@ -149,6 +162,7 @@ fn exec_shell() -> Result<()> { DEFAULT_PATH.to_string() }; + // TODO: these variable except for TERM should be initialized in zshenv let nix_bin_path = "/nix/.base/sbin:/nix/.base/bin:/nix/.bin"; cmd.env("PATH", format!("{nix_bin_path}:{proc_path}")); @@ -158,9 +172,21 @@ fn exec_shell() -> Result<()> { cmd.env("TERM", "xterm"); } + if let Ok(lang) = std::env::var("LANG") { + cmd.env("LANG", lang); + } else { + cmd.env("LANG", "C.UTF-8"); + } + + let prompt = format!( + "%F{{cyan}}({container_id}) %F{{blue}}%~ %(?.%F{{green}}.%F{{red}})%#%f " + ); + cmd.env("PROMPT", &prompt); + let nix_base = "/nix/.base"; let data_dir = format!("/usr/local/share:/usr/share:{nix_base}/share"); cmd.envs([ + ("ZDOTDIR", "/nix/etc"), ("NIX_CONF_DIR", "/nix/etc"), ("XDG_CACHE_HOME", "/nix/.cache"), ("XDG_CONFIG_HOME", "/nix/.config"), @@ -189,7 +215,7 @@ fn main() -> Result<()> { let args = Args::parse(); init_logging(); - let lead_pid = if let Some(pid) = pid_lookup(&args.container_id) { + let lead_pid = if let Some(pid) = get_lead_pid(&args.container_id) { pid } else { log::error!("could not find container PID"); @@ -198,15 +224,8 @@ fn main() -> Result<()> { log::debug!("container PID: {}", lead_pid); let img_dir = get_img_dir(&args); - if !img_dir.exists() || !img_dir.join("store").exists() { - #[cfg(feature = "embedded_image")] - embedded_image::install_base_image(&img_dir) - .context("could not unpack base image")?; - - #[cfg(not(feature = "embedded_image"))] - BaseImageBuilder::new(&img_dir) - .build_base() - .context("could not build base image")?; + if !runs_with_sudo() { + update_base_image(&img_dir, args.base_img)?; } let overlay_dir = get_overlay_dir(); @@ -214,7 +233,7 @@ fn main() -> Result<()> { if !geteuid().is_root() { log::debug!("re-executing with sudo..."); - reexec_with_sudo(lead_pid, &img_dir, &overlay_dir)? + reexec_with_sudo(&args.container_id, lead_pid, &img_dir, &overlay_dir)? } let shared_mount = SharedMount::new(&overlay_dir, overlay_builder) @@ -228,7 +247,7 @@ fn main() -> Result<()> { exit(1); } // in normal cases, there is no return from exec_shell() - if let Err(err) = exec_shell() { + if let Err(err) = exec_shell(&args.container_id) { log::error!("cannot execute shell: {err}"); exit(1); } diff --git a/src/pid_file.rs b/src/pid_file.rs index 74e2415..813201d 100644 --- a/src/pid_file.rs +++ b/src/pid_file.rs @@ -11,9 +11,12 @@ pub struct PidFile { } impl PidFile { - pub fn new(dir: &Path) -> Result { + pub fn new

(dir: P) -> Result + where + P: AsRef, + { let pid = process::id().to_string(); - let path = dir.join(&pid); + let path = dir.as_ref().join(&pid); std::fs::write(&path, pid)?; Ok(PidFile { path }) }