diff --git a/src/ensure.rs b/src/ensure.rs index b204c9c..d4dcbce 100644 --- a/src/ensure.rs +++ b/src/ensure.rs @@ -2,15 +2,15 @@ * Copyright 2021 Oxide Computer Company */ -use std::path::{Path, PathBuf}; +use anyhow::{anyhow, bail, Result}; +use digest::Digest; +use jmclib::log::prelude::*; +use std::ffi::CString; use std::fs::{DirBuilder, File}; +use std::io::{BufRead, BufReader, Read, Write}; use std::os::unix::fs::DirBuilderExt; -use std::ffi::CString; -use std::io::{Read, BufRead, BufReader, Write}; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use jmclib::log::prelude::*; -use anyhow::{Result, bail, anyhow}; -use digest::Digest; #[allow(dead_code)] #[derive(Debug, PartialEq)] @@ -52,14 +52,16 @@ impl FileInfo { pub fn is_file(&self) -> bool { matches!(&self.filetype, FileType::File) } + + pub fn is_dir(&self) -> bool { + matches!(&self.filetype, FileType::Directory) + } } pub fn check>(p: P) -> Result> { let name: &str = p.as_ref().to_str().unwrap(); let cname = CString::new(name.to_string())?; - let st = Box::into_raw(Box::new(unsafe { - std::mem::zeroed::() - })); + let st = Box::into_raw(Box::new(unsafe { std::mem::zeroed::() })); let (r, e, st) = unsafe { let r = libc::lstat(cname.as_ptr(), st); let e = *libc::___errno(); @@ -113,17 +115,25 @@ pub fn chown>(path: P, owner: u32, group: u32) -> Result<()> { (r, e) }; if r != 0 { - bail!("lchown({}, {}, {}): errno {}", path.as_ref().display(), - owner, group, e); + bail!( + "lchown({}, {}, {}): errno {}", + path.as_ref().display(), + owner, + group, + e + ); } Ok(()) } -pub fn perms>(log: &Logger, p: P, owner: u32, group: u32, - perms: u32) - -> Result -{ +pub fn perms>( + log: &Logger, + p: P, + owner: u32, + group: u32, + perms: u32, +) -> Result { let p = p.as_ref(); let log = log.new(o!("path" => p.display().to_string())); let mut did_work = false; @@ -161,8 +171,10 @@ pub fn perms>(log: &Logger, p: P, owner: u32, group: u32, } (o, g) => { did_work = true; - info!(log, "ownership wrong ({:?}:{:?}, not {}:{})", o, g, - owner, group); + info!( + log, + "ownership wrong ({:?}:{:?}, not {}:{})", o, g, owner, group + ); chown(p, owner, group)?; info!(log, "chown ok"); @@ -172,10 +184,13 @@ pub fn perms>(log: &Logger, p: P, owner: u32, group: u32, Ok(did_work) } -pub fn directory>(log: &Logger, dir: P, owner: u32, - group: u32, mode: u32) - -> Result -{ +pub fn directory>( + log: &Logger, + dir: P, + owner: u32, + group: u32, + mode: u32, +) -> Result { let dir = dir.as_ref(); let mut did_work = false; @@ -192,10 +207,7 @@ pub fn directory>(log: &Logger, dir: P, owner: u32, */ did_work = true; info!(log, "creating directory: {}", dir.display()); - DirBuilder::new() - .recursive(true) - .mode(mode) - .create(dir)?; + DirBuilder::new().recursive(true).mode(mode).create(dir)?; /* * Check the path again, to make sure we have up-to-date information: @@ -282,27 +294,39 @@ pub fn removed>(log: &Logger, dst: P) -> Result<()> { if let Some(fi) = check(dst)? { match fi.filetype { FileType::File | FileType::Link => { - info!(log, "file {} exists (as {:?}), removing", - dst.display(), fi.filetype); + info!( + log, + "file {} exists (as {:?}), removing", + dst.display(), + fi.filetype + ); std::fs::remove_file(dst)?; } t => { - bail!("file {} exists as {:?}, unexpected type", - dst.display(), t); + bail!("file {} exists as {:?}, unexpected type", dst.display(), t); } } } else { - info!(log, "file {} does not already exist, skipping removal", - dst.display()); + info!( + log, + "file {} does not already exist, skipping removal", + dst.display() + ); } Ok(()) } -pub fn filestr>(log: &Logger, contents: &str, dst: P, - owner: u32, group: u32, mode: u32, create: Create) -> Result -{ +pub fn filestr>( + log: &Logger, + contents: &str, + dst: P, + owner: u32, + group: u32, + mode: u32, + create: Create, +) -> Result { let dst = dst.as_ref(); let mut did_work = false; @@ -312,13 +336,11 @@ pub fn filestr>(log: &Logger, contents: &str, dst: P, */ match create { Create::IfMissing if fi.filetype == FileType::File => { - info!(log, "file {} exists, skipping population", - dst.display()); + info!(log, "file {} exists, skipping population", dst.display()); false } Create::IfMissing if fi.filetype == FileType::Link => { - warn!(log, "symlink {} exists, skipping population", - dst.display()); + warn!(log, "symlink {} exists, skipping population", dst.display()); false } Create::IfMissing => { @@ -326,8 +348,11 @@ pub fn filestr>(log: &Logger, contents: &str, dst: P, * Avoid clobbering an unexpected entry when we have been asked * to preserve in the face of modifications. */ - bail!("{} should be a file, but is a {:?}", - dst.display(), fi.filetype); + bail!( + "{} should be a file, but is a {:?}", + dst.display(), + fi.filetype + ); } Create::Always if fi.filetype == FileType::File => { /* @@ -335,12 +360,14 @@ pub fn filestr>(log: &Logger, contents: &str, dst: P, * what we expect. */ if comparestr(contents, dst)? { - info!(log, "file {} exists, with correct contents", - dst.display()); + info!(log, "file {} exists, with correct contents", dst.display()); false } else { - warn!(log, "file {} exists, with wrong contents, unlinking", - dst.display()); + warn!( + log, + "file {} exists, with wrong contents, unlinking", + dst.display() + ); std::fs::remove_file(dst)?; true } @@ -350,8 +377,12 @@ pub fn filestr>(log: &Logger, contents: &str, dst: P, * We found a file type we don't expect. Try to unlink it * anyway. */ - warn!(log, "file {} exists, of type {:?}, unlinking", - dst.display(), fi.filetype); + warn!( + log, + "file {} exists, of type {:?}, unlinking", + dst.display(), + fi.filetype + ); std::fs::remove_file(dst)?; true } @@ -381,9 +412,15 @@ pub fn filestr>(log: &Logger, contents: &str, dst: P, Ok(did_work) } -pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, - owner: u32, group: u32, mode: u32, create: Create) -> Result -{ +pub fn file, P2: AsRef>( + log: &Logger, + src: P1, + dst: P2, + owner: u32, + group: u32, + mode: u32, + create: Create, +) -> Result { let src = src.as_ref(); let dst = dst.as_ref(); let mut did_work = false; @@ -394,13 +431,11 @@ pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, */ match create { Create::IfMissing if fi.filetype == FileType::File => { - info!(log, "file {} exists, skipping population", - dst.display()); + info!(log, "file {} exists, skipping population", dst.display()); false } Create::IfMissing if fi.filetype == FileType::Link => { - warn!(log, "symlink {} exists, skipping population", - dst.display()); + warn!(log, "symlink {} exists, skipping population", dst.display()); false } Create::IfMissing => { @@ -408,8 +443,11 @@ pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, * Avoid clobbering an unexpected entry when we have been asked * to preserve in the face of modifications. */ - bail!("{} should be a file, but is a {:?}", - dst.display(), fi.filetype); + bail!( + "{} should be a file, but is a {:?}", + dst.display(), + fi.filetype + ); } Create::Always if fi.filetype == FileType::File => { /* @@ -417,12 +455,14 @@ pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, * what we expect. */ if compare(src, dst)? { - info!(log, "file {} exists, with correct contents", - dst.display()); + info!(log, "file {} exists, with correct contents", dst.display()); false } else { - warn!(log, "file {} exists, with wrong contents, unlinking", - dst.display()); + warn!( + log, + "file {} exists, with wrong contents, unlinking", + dst.display() + ); std::fs::remove_file(dst)?; true } @@ -432,8 +472,12 @@ pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, * We found a file type we don't expect. Try to unlink it * anyway. */ - warn!(log, "file {} exists, of type {:?}, unlinking", - dst.display(), fi.filetype); + warn!( + log, + "file {} exists, of type {:?}, unlinking", + dst.display(), + fi.filetype + ); std::fs::remove_file(dst)?; true } @@ -457,10 +501,13 @@ pub fn file, P2: AsRef>(log: &Logger, src: P1, dst: P2, Ok(did_work) } -pub fn symlink, P2: AsRef>(log: &Logger, dst: P1, - target: P2, owner: u32, group: u32) - -> Result -{ +pub fn symlink, P2: AsRef>( + log: &Logger, + dst: P1, + target: P2, + owner: u32, + group: u32, +) -> Result { let dst = dst.as_ref(); let target = target.as_ref(); let mut did_work = false; @@ -472,8 +519,12 @@ pub fn symlink, P2: AsRef>(log: &Logger, dst: P1, info!(log, "link target ok ({})", target.display()); false } else { - warn!(log, "link target wrong: want {}, got {}; unlinking", - target.display(), fitarget.display()); + warn!( + log, + "link target wrong: want {}, got {}; unlinking", + target.display(), + fitarget.display() + ); std::fs::remove_file(dst)?; true } @@ -481,8 +532,12 @@ pub fn symlink, P2: AsRef>(log: &Logger, dst: P1, /* * File type not correct. Unlink. */ - warn!(log, "file {} exists, of type {:?}, unlinking", - dst.display(), fi.filetype); + warn!( + log, + "file {} exists, of type {:?}, unlinking", + dst.display(), + fi.filetype + ); std::fs::remove_file(dst)?; true } @@ -540,8 +595,11 @@ pub fn hash_file>(p: P, hashtype: &HashType) -> Result { Ok(out) } -fn spawn_reader(log: &Logger, name: &str, stream: Option) - -> Option> +fn spawn_reader( + log: &Logger, + name: &str, + stream: Option, +) -> Option> where T: Read + Send + 'static, { @@ -587,8 +645,7 @@ pub fn run>(log: &Logger, args: &[S]) -> Result<()> { run_envs(log, args, envs) } -pub fn run_envs(log: &Logger, args: SI, env: Option) - -> Result<()> +pub fn run_envs(log: &Logger, args: SI, env: Option) -> Result<()> where S: AsRef, SI: IntoIterator, @@ -596,10 +653,7 @@ where K: AsRef + Eq + std::hash::Hash + std::fmt::Debug, V: AsRef + std::fmt::Debug, { - let args: Vec = args - .into_iter() - .map(|s| s.as_ref().to_string()) - .collect(); + let args: Vec = args.into_iter().map(|s| s.as_ref().to_string()).collect(); let env = if let Some(env) = env { let env: std::collections::HashMap<_, _> = env.into_iter().collect(); diff --git a/src/main.rs b/src/main.rs index ee727f4..aef19aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1759,6 +1759,26 @@ impl ImageBuilder { Ok(p) } + // Searches for a directory named "dirname" which exists in one + // of the "external_src" paths. + // + // Returns the first directory in which "$(external_src)/dirname" + // exists. + fn external_src_dir(&self, dirname: &str) -> Result { + for dir in self.external_src.iter() { + let mut s = dir.clone(); + s.push(dirname); + if let Some(fi) = ensure::check(&s)? { + if !fi.is_dir() { + bail!("template file {} is wrong type: {:?}", s.display(), + fi.filetype); + } + return Ok(s); + } + } + bail!("could not find external source directory \"{}\"", dirname); + } + fn external_src_file(&self, filename: &str) -> Result { for dir in self.external_src.iter() { let mut s = dir.clone(); @@ -2542,24 +2562,55 @@ fn run_steps(ib: &mut ImageBuilder) -> Result<()> { #[derive(Deserialize)] struct DirArgs { + // The name and metadata of the directrory to be created. dir: String, owner: String, group: String, mode: String, + + // An optional source file of the directory, if contents are + // to be copied, rather than simply creating the directory. + extsrc: Option } let a: DirArgs = step.args()?; let owner = translate_uid(&a.owner)?; let group = translate_gid(&a.group)?; + let extsrc = ib.expando(a.extsrc.as_deref())?; if !a.dir.starts_with('/') { bail!("dir must be fully qualified path"); } - let dir = format!("{}{}", targmp, a.dir); + let dst = format!("{}{}", targmp, a.dir); let mode = u32::from_str_radix(&a.mode, 8)?; - ensure::directory(log, &dir, owner, group, mode)?; + ensure::directory(log, &dst, owner, group, mode)?; + + if let Some(extsrc) = &extsrc { + if extsrc.starts_with('/') { + bail!("external source directory must be a relative path"); + } + let dst = PathBuf::from(dst); + + // "src" is the absolute path to a directory to be copied. + let src = ib.external_src_dir(extsrc)?; + + for entry in walkdir::WalkDir::new(&src) { + let entry = entry?; + let from = entry.path(); + let to = dst.join(from.strip_prefix(&src)?); + if entry.file_type().is_dir() { + if let Err(e) = std::fs::create_dir(to) { + if !matches!(e.kind(), std::io::ErrorKind::AlreadyExists) { + return Err(e.into()); + } + } + } else if entry.file_type().is_file() { + std::fs::copy(from, to)?; + } + } + } } "ensure_file" => { let mp = ib.root()?;