Skip to content

Commit

Permalink
[antlir2][clone] add user/group option to clone
Browse files Browse the repository at this point in the history
Summary:
Yet another case of antlir2 being more correct than antlir1 causing us pain.
Some features do need a way to clone files/directories and just force their
ownership to a new username.

Also updated `debuginfo.bzl` as it is one useful callsite where the user:group
really should be `root:root` as antlir1 would do.

Test Plan:
```
❯ buck2 test fbcode//antlir/antlir2/test_images/clone:
Buck UI: https://www.internalfb.com/buck2/30c0cbb5-4ba9-4cb0-95f0-f9d6b1c08ba5
Test UI: https://www.internalfb.com/intern/testinfra/testrun/7318349583156114
Network: Up: 37KiB  Down: 20KiB  (reSessionID-049c7d96-0072-4763-b2d6-91fa092aca53)
Jobs completed: 156682. Time elapsed: 21.1s.
Cache hits: 0%. Commands: 24 (cached: 0, remote: 0, local: 24)
Tests finished: Pass 28. Fail 0. Fatal 0. Skip 0. Build failure 0
```

Reviewed By: TangoRoxy

Differential Revision: D49958653

fbshipit-source-id: 99d6dbf0902fa8a005666f109c84662f432e6164
  • Loading branch information
vmagro authored and facebook-github-bot committed Oct 5, 2023
1 parent 4863a72 commit 28cca37
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 55 deletions.
24 changes: 23 additions & 1 deletion antlir/antlir2/bzl/feature/clone.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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("/")
Expand All @@ -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(
Expand All @@ -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,
Expand Down
155 changes: 101 additions & 54 deletions antlir/antlir2/features/clone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<CloneUserGroup>,
}

#[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 {
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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()))?;
Expand Down
21 changes: 21 additions & 0 deletions antlir/antlir2/test_images/clone/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
11 changes: 11 additions & 0 deletions antlir/antlir2/test_images/clone/clone-file-root.toml
Original file line number Diff line number Diff line change
@@ -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!
'''
2 changes: 2 additions & 0 deletions antlir/bzl/debuginfo.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 28cca37

Please sign in to comment.