diff --git a/antlir/antlir2/bzl/feature/clone.bzl b/antlir/antlir2/bzl/feature/clone.bzl index d3f20e0543..8bb4dd21c2 100644 --- a/antlir/antlir2/bzl/feature/clone.bzl +++ b/antlir/antlir2/bzl/feature/clone.bzl @@ -13,7 +13,9 @@ def clone( *, src_layer: str | Select, src_path: str | Select, - dst_path: str | Select) -> ParseTimeFeature: + dst_path: str | Select, + user: str | None = None, + group: str | None = None) -> ParseTimeFeature: """ Copies a subtree of an existing layer into the one under construction. To the extent possible, filesystem metadata are preserved. @@ -68,21 +70,31 @@ def clone( }, kwargs = { "dst_path": dst_path, + "group": group, "src_path": src_path, + "user": user, }, ) +clone_usergroup = record( + user = str, + group = str, +) + clone_record = record( src_layer = layer_dep, src_path = str, dst_path = str, omit_outer_dir = bool, pre_existing_dest = bool, + usergroup = clone_usergroup | None, ) def clone_analyze( src_path: str, dst_path: str, + user: str | None, + group: str | None, deps: dict[str, Dependency], plugin: FeaturePluginInfo) -> FeatureAnalysis: omit_outer_dir = src_path.endswith("/") @@ -97,6 +109,15 @@ def clone_analyze( src_layer = deps["src_layer"] + usergroup = None + if user and group: + usergroup = clone_usergroup( + user = user, + group = group, + ) + elif user or group: + fail("either none or both of {user, group} must be set") + return FeatureAnalysis( feature_type = "clone", data = clone_record( @@ -105,6 +126,7 @@ def clone_analyze( dst_path = dst_path, omit_outer_dir = omit_outer_dir, pre_existing_dest = pre_existing_dest, + usergroup = usergroup, ), required_layers = [src_layer[LayerInfo]], plugin = plugin, diff --git a/antlir/antlir2/features/clone.rs b/antlir/antlir2/features/clone.rs index 99660527ab..2daa11ac53 100644 --- a/antlir/antlir2/features/clone.rs +++ b/antlir/antlir2/features/clone.rs @@ -20,8 +20,10 @@ use antlir2_depgraph::item::Path as PathItem; use antlir2_depgraph::requires_provides::Requirement; use antlir2_depgraph::requires_provides::Validator; use antlir2_depgraph::Graph; +use antlir2_features::types::GroupName; use antlir2_features::types::LayerInfo; use antlir2_features::types::PathInLayer; +use antlir2_features::types::UserName; use antlir2_users::group::EtcGroup; use antlir2_users::passwd::EtcPasswd; use anyhow::Context; @@ -39,6 +41,14 @@ pub struct Clone { pub pre_existing_dest: bool, pub src_path: PathInLayer, pub dst_path: PathInLayer, + #[serde(default)] + pub usergroup: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)] +pub struct CloneUserGroup { + pub user: UserName, + pub group: GroupName, } impl<'f> antlir2_feature_impl::Feature<'f> for Clone { @@ -73,54 +83,65 @@ impl<'f> antlir2_feature_impl::Feature<'f> for Clone { Validator::FileType(FileType::Directory), )); } - // Files we clone will usually be owned by root:root, but not always! To - // be safe we have to make sure that all discovered users and groups - // exist in this destination layer - let mut uids = HashSet::new(); - let mut gids = HashSet::new(); - for entry in WalkDir::new( - self.src_layer - .subvol_symlink - .join(self.src_path.strip_prefix("/").unwrap_or(&self.src_path)), - ) { - // ignore any errors, they'll surface again later at a more - // appropriate place than this user/group id collection process - if let Ok(metadata) = entry.and_then(|e| e.metadata()) { - uids.insert(metadata.uid()); - gids.insert(metadata.gid()); - } - } - let users: EtcPasswd = - std::fs::read_to_string(self.src_layer.subvol_symlink.join("etc/passwd")) - .and_then(|s| s.parse().map_err(std::io::Error::other)) - .unwrap_or_else(|_| Default::default()); - let groups: EtcGroup = - std::fs::read_to_string(self.src_layer.subvol_symlink.join("etc/group")) - .and_then(|s| s.parse().map_err(std::io::Error::other)) - .unwrap_or_else(|_| Default::default()); - for uid in uids { + if let Some(usergroup) = &self.usergroup { v.push(Requirement::ordered( - ItemKey::User( - users - .get_user_by_id(uid.into()) - .expect("this layer could not have been built if this uid is missing") - .name - .clone(), - ), + ItemKey::User(usergroup.user.clone().into()), Validator::Exists, )); - } - for gid in gids { v.push(Requirement::ordered( - ItemKey::Group( - groups - .get_group_by_id(gid.into()) - .expect("this layer could not have been built if this gid is missing") - .name - .clone(), - ), + ItemKey::Group(usergroup.group.clone().into()), Validator::Exists, )); + } else { + // Files we clone will usually be owned by root:root, but not always! To + // be safe we have to make sure that all discovered users and groups + // exist in this destination layer + let mut uids = HashSet::new(); + let mut gids = HashSet::new(); + for entry in WalkDir::new( + self.src_layer + .subvol_symlink + .join(self.src_path.strip_prefix("/").unwrap_or(&self.src_path)), + ) { + // ignore any errors, they'll surface again later at a more + // appropriate place than this user/group id collection process + if let Ok(metadata) = entry.and_then(|e| e.metadata()) { + uids.insert(metadata.uid()); + gids.insert(metadata.gid()); + } + } + let users: EtcPasswd = + std::fs::read_to_string(self.src_layer.subvol_symlink.join("etc/passwd")) + .and_then(|s| s.parse().map_err(std::io::Error::other)) + .unwrap_or_else(|_| Default::default()); + let groups: EtcGroup = + std::fs::read_to_string(self.src_layer.subvol_symlink.join("etc/group")) + .and_then(|s| s.parse().map_err(std::io::Error::other)) + .unwrap_or_else(|_| Default::default()); + for uid in uids { + v.push(Requirement::ordered( + ItemKey::User( + users + .get_user_by_id(uid.into()) + .expect("this layer could not have been built if this uid is missing") + .name + .clone(), + ), + Validator::Exists, + )); + } + for gid in gids { + v.push(Requirement::ordered( + ItemKey::Group( + groups + .get_group_by_id(gid.into()) + .expect("this layer could not have been built if this gid is missing") + .name + .clone(), + ), + Validator::Exists, + )); + } } Ok(v) } @@ -241,18 +262,44 @@ impl<'f> antlir2_feature_impl::Feature<'f> for Clone { let meta = entry.metadata().map_err(std::io::Error::from)?; - let new_uid = ctx.uid( - &src_userdb - .get_user_by_id(meta.uid().into()) - .with_context(|| format!("src_layer missing passwd entry for {}", meta.uid()))? - .name, - )?; - let new_gid = ctx.gid( - &src_groupdb - .get_group_by_id(meta.gid().into()) - .with_context(|| format!("src_layer missing group entry for {}", meta.gid()))? - .name, - )?; + let (new_uid, new_gid) = match &self.usergroup { + Some(usergroup) => ( + ctx.uid( + &src_userdb + .get_user_by_name(&usergroup.user) + .with_context(|| { + format!("src_layer missing passwd entry for {}", usergroup.user) + })? + .name, + )?, + ctx.gid( + &src_groupdb + .get_group_by_name(&usergroup.group) + .with_context(|| { + format!("src_layer missing group entry for {}", usergroup.group) + })? + .name, + )?, + ), + None => ( + ctx.uid( + &src_userdb + .get_user_by_id(meta.uid().into()) + .with_context(|| { + format!("src_layer missing passwd entry for {}", meta.uid()) + })? + .name, + )?, + ctx.gid( + &src_groupdb + .get_group_by_id(meta.gid().into()) + .with_context(|| { + format!("src_layer missing group entry for {}", meta.gid()) + })? + .name, + )?, + ), + }; tracing::trace!("lchown {}:{} {}", new_uid, new_gid, dst_path.display()); std::os::unix::fs::lchown(&dst_path, Some(new_uid.into()), Some(new_gid.into()))?; diff --git a/antlir/antlir2/test_images/clone/BUCK b/antlir/antlir2/test_images/clone/BUCK index cad3ac75f0..6fd5f59577 100644 --- a/antlir/antlir2/test_images/clone/BUCK +++ b/antlir/antlir2/test_images/clone/BUCK @@ -228,3 +228,24 @@ image_diff_test( diff_type = "file", layer = ":clone-file-remap-ids", ) + +image.layer( + name = "clone-file-chown-root", + features = [ + feature.clone( + dst_path = "/cloned-file", + group = "root", + src_layer = ":clone-src", + src_path = "/f", + user = "root", + ), + ], + parent_layer = ":base-ids-remap", +) + +image_diff_test( + name = "clone-file-chown-root-test", + diff = "clone-file-root.toml", + diff_type = "file", + layer = ":clone-file-chown-root", +) diff --git a/antlir/antlir2/test_images/clone/clone-file-root.toml b/antlir/antlir2/test_images/clone/clone-file-root.toml new file mode 100644 index 0000000000..a81ac60646 --- /dev/null +++ b/antlir/antlir2/test_images/clone/clone-file-root.toml @@ -0,0 +1,11 @@ +[file.cloned-file] +op = 'added' + +[file.cloned-file.diff] +mode = 'u+r,g+r,o+r' +file-type = 'regular-file' +user = "root" +group = "root" +text = ''' +This file will be cloned! +''' diff --git a/antlir/bzl/debuginfo.bzl b/antlir/bzl/debuginfo.bzl index 741d4df9bf..4fdf77c6d0 100644 --- a/antlir/bzl/debuginfo.bzl +++ b/antlir/bzl/debuginfo.bzl @@ -48,6 +48,8 @@ def _split( src_layer = layer, src_path = "/usr/lib/debug", dst_path = "/", + user = "root", + group = "root", ), ], flavor = "//antlir/antlir2/flavor:none",