@@ -11,6 +11,7 @@ mod baseline;
1111use std:: io:: BufWriter ;
1212use std:: io:: Write ;
1313use std:: os:: fd:: AsFd ;
14+ use std:: os:: unix:: fs:: DirBuilderExt ;
1415use std:: os:: unix:: process:: CommandExt ;
1516use std:: process:: Command ;
1617use std:: str:: FromStr ;
@@ -21,11 +22,15 @@ use anyhow::{anyhow, Context, Result};
2122use camino:: Utf8Path ;
2223use camino:: Utf8PathBuf ;
2324use cap_std:: fs:: Dir ;
25+ use cap_std_ext:: cap_primitives;
2426use cap_std_ext:: cap_std;
27+ use cap_std_ext:: cap_std:: fs:: DirBuilder ;
28+ use cap_std_ext:: cap_std:: io_lifetimes:: AsFilelike ;
2529use cap_std_ext:: prelude:: CapStdExtDirExt ;
2630use chrono:: prelude:: * ;
2731use clap:: ValueEnum ;
2832use ostree_ext:: oci_spec;
33+ use rustix:: fd:: AsRawFd ;
2934use rustix:: fs:: FileTypeExt ;
3035use rustix:: fs:: MetadataExt ;
3136
@@ -38,6 +43,7 @@ use serde::{Deserialize, Serialize};
3843
3944use self :: baseline:: InstallBlockDeviceOpts ;
4045use crate :: containerenv:: ContainerExecutionInfo ;
46+ use crate :: lsm:: Labeler ;
4147use crate :: task:: Task ;
4248use crate :: utils:: sigpolicy_from_opts;
4349
@@ -124,6 +130,27 @@ pub(crate) struct InstallConfigOpts {
124130 #[ serde( default ) ]
125131 pub ( crate ) disable_selinux : bool ,
126132
133+ /// Inject arbitrary files into the target deployment `/etc`. One can use
134+ /// this for example to inject systemd units, or `tmpfiles.d` snippets
135+ /// which set up SSH keys.
136+ ///
137+ /// Files injected this way become "unmanaged state"; they will be carried
138+ /// forward across upgrades, but will not otherwise be updated unless
139+ /// a secondary mechanism takes ownership thereafter.
140+ ///
141+ /// This option can be specified multiple times; the files will be copied
142+ /// in order.
143+ ///
144+ /// Any missing parent directories will be implicitly created with root ownership
145+ /// and mode 0755.
146+ ///
147+ /// This option pairs well with additional bind mount
148+ /// volumes set up via the container orchestrator, e.g.:
149+ /// `podman run ... -v /path/to/config:/tmp/etc <image> bootc install to-disk --copy-etc /tmp/etc`
150+ #[ clap( long) ]
151+ #[ serde( default ) ]
152+ pub ( crate ) copy_etc : Option < Vec < Utf8PathBuf > > ,
153+
127154 // Only occupy at most this much space (if no units are provided, GB is assumed).
128155 // Using this option reserves space for partitions created dynamically on the
129156 // next boot, or by subsequent tools.
@@ -564,11 +591,16 @@ kargs = ["console=ttyS0", "foo=bar"]
564591 }
565592}
566593
594+ struct DeploymentComplete {
595+ aleph : InstallAleph ,
596+ deployment : Dir ,
597+ }
598+
567599#[ context( "Creating ostree deployment" ) ]
568600async fn initialize_ostree_root_from_self (
569601 state : & State ,
570602 root_setup : & RootSetup ,
571- ) -> Result < InstallAleph > {
603+ ) -> Result < DeploymentComplete > {
572604 let rootfs_dir = & root_setup. rootfs_fd ;
573605 let rootfs = root_setup. rootfs . as_path ( ) ;
574606 let cancellable = gio:: Cancellable :: NONE ;
@@ -714,7 +746,10 @@ async fn initialize_ostree_root_from_self(
714746 kernel : uname. release ( ) . to_str ( ) ?. to_string ( ) ,
715747 } ;
716748
717- Ok ( aleph)
749+ Ok ( DeploymentComplete {
750+ aleph,
751+ deployment : root,
752+ } )
718753}
719754
720755#[ context( "Copying to oci" ) ]
@@ -1058,6 +1093,63 @@ async fn prepare_install(
10581093 Ok ( state)
10591094}
10601095
1096+ // Backing implementation of --copy-etc; just your basic
1097+ // recursive copy algorithm. Parent directories are
1098+ // created as necessary
1099+ fn copy_unmanaged_etc (
1100+ sepolicy : & ostree:: SePolicy ,
1101+ src : & Dir ,
1102+ dest : & Dir ,
1103+ path : & mut Utf8PathBuf ,
1104+ ) -> Result < u64 > {
1105+ let mut r = 0u64 ;
1106+ for ent in src. read_dir ( & path) ? {
1107+ let ent = ent?;
1108+ let name = ent. file_name ( ) ;
1109+ let name = if let Some ( name) = name. to_str ( ) {
1110+ name
1111+ } else {
1112+ anyhow:: bail!( "Non-UTF8 name: {name:?}" ) ;
1113+ } ;
1114+ let meta = ent. metadata ( ) ?;
1115+ path. push ( Utf8Path :: new ( name) ) ;
1116+ r += 1 ;
1117+ if meta. is_dir ( ) {
1118+ if let Some ( parent) = path. parent ( ) {
1119+ dest. create_dir_all ( parent)
1120+ . with_context ( || format ! ( "Creating {parent}" ) ) ?;
1121+ }
1122+ if !dest. try_exists ( & path) ? {
1123+ let mut db = DirBuilder :: new ( ) ;
1124+ db. mode ( meta. mode ( ) ) ;
1125+ let label = Labeler :: new ( sepolicy, path, meta. mode ( ) ) ?;
1126+ dest. create_dir_with ( & path, & db)
1127+ . with_context ( || format ! ( "Creating {path:?}" ) ) ?;
1128+ drop ( label) ;
1129+ }
1130+ r += copy_unmanaged_etc ( sepolicy, src, dest, path) ?;
1131+ } else {
1132+ dest. remove_file_optional ( & path) ?;
1133+ let label = Labeler :: new ( sepolicy, path, meta. mode ( ) ) ?;
1134+ if meta. is_symlink ( ) {
1135+ let link_target = cap_primitives:: fs:: read_link_contents (
1136+ & src. as_filelike_view ( ) ,
1137+ path. as_std_path ( ) ,
1138+ )
1139+ . context ( "Reading symlink" ) ?;
1140+ cap_primitives:: fs:: symlink_contents ( link_target, & dest. as_filelike_view ( ) , & path)
1141+ . with_context ( || format ! ( "Writing symlink {path:?}" ) ) ?;
1142+ } else {
1143+ src. copy ( & path, dest, & path)
1144+ . with_context ( || format ! ( "Copying {path:?}" ) ) ?;
1145+ }
1146+ drop ( label) ;
1147+ }
1148+ assert ! ( path. pop( ) ) ;
1149+ }
1150+ Ok ( r)
1151+ }
1152+
10611153async fn install_to_filesystem_impl ( state : & State , rootfs : & mut RootSetup ) -> Result < ( ) > {
10621154 if state. override_disable_selinux {
10631155 rootfs. kargs . push ( "selinux=0" . to_string ( ) ) ;
@@ -1071,16 +1163,41 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
10711163 tracing:: debug!( "boot uuid={boot_uuid}" ) ;
10721164
10731165 // Write the aleph data that captures the system state at the time of provisioning for aid in future debugging.
1074- {
1075- let aleph = initialize_ostree_root_from_self ( state, rootfs) . await ?;
1076- rootfs
1077- . rootfs_fd
1078- . atomic_replace_with ( BOOTC_ALEPH_PATH , |f| {
1079- serde_json:: to_writer ( f, & aleph) ?;
1080- anyhow:: Ok ( ( ) )
1081- } )
1082- . context ( "Writing aleph version" ) ?;
1083- }
1166+ let deployresult = initialize_ostree_root_from_self ( state, rootfs) . await ?;
1167+ rootfs
1168+ . rootfs_fd
1169+ . atomic_replace_with ( BOOTC_ALEPH_PATH , |f| {
1170+ serde_json:: to_writer ( f, & deployresult. aleph ) ?;
1171+ anyhow:: Ok ( ( ) )
1172+ } )
1173+ . context ( "Writing aleph version" ) ?;
1174+ let sepolicy =
1175+ ostree:: SePolicy :: new_at ( deployresult. deployment . as_raw_fd ( ) , gio:: Cancellable :: NONE ) ?;
1176+
1177+ // Copy unmanaged configuration
1178+ let target_etc = deployresult
1179+ . deployment
1180+ . open_dir ( "etc" )
1181+ . context ( "Opening deployment /etc" ) ?;
1182+ let copy_etc = state
1183+ . config_opts
1184+ . copy_etc
1185+ . iter ( )
1186+ . flatten ( )
1187+ . cloned ( )
1188+ . collect :: < Vec < _ > > ( ) ;
1189+ tokio:: task:: spawn_blocking ( move || {
1190+ for src in copy_etc {
1191+ println ! ( "Injecting configuration from {src}" ) ;
1192+ let src = Dir :: open_ambient_dir ( & src, cap_std:: ambient_authority ( ) )
1193+ . with_context ( || format ! ( "Opening {src}" ) ) ?;
1194+ let mut pb = "." . into ( ) ;
1195+ let n = copy_unmanaged_etc ( & sepolicy, & src, & target_etc, & mut pb) ?;
1196+ tracing:: debug!( "Copied config files: {n}" ) ;
1197+ }
1198+ anyhow:: Ok ( ( ) )
1199+ } )
1200+ . await ??;
10841201
10851202 crate :: bootloader:: install_via_bootupd ( & rootfs. device , & rootfs. rootfs , & state. config_opts ) ?;
10861203 tracing:: debug!( "Installed bootloader" ) ;
@@ -1092,6 +1209,8 @@ async fn install_to_filesystem_impl(state: &State, rootfs: &mut RootSetup) -> Re
10921209 . args ( [ "+i" , "." ] )
10931210 . run ( ) ?;
10941211
1212+ drop ( deployresult) ;
1213+
10951214 // Finalize mounted filesystems
10961215 if !rootfs. is_alongside {
10971216 let bootfs = rootfs. rootfs . join ( "boot" ) ;
@@ -1369,11 +1488,78 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
13691488 Ok ( ( ) )
13701489}
13711490
1372- #[ test]
1373- fn install_opts_serializable ( ) {
1374- let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1375- "device" : "/dev/vda"
1376- } ) )
1377- . unwrap ( ) ;
1378- assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1491+ #[ cfg( test) ]
1492+ mod tests {
1493+ use super :: * ;
1494+
1495+ #[ test]
1496+ fn install_opts_serializable ( ) {
1497+ let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1498+ "device" : "/dev/vda"
1499+ } ) )
1500+ . unwrap ( ) ;
1501+ assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1502+ }
1503+
1504+ #[ test]
1505+ fn test_copy_etc ( ) -> Result < ( ) > {
1506+ use std:: path:: PathBuf ;
1507+ fn impl_count ( d : & Dir , path : & mut PathBuf ) -> Result < u64 > {
1508+ let mut c = 0u64 ;
1509+ for ent in d. read_dir ( & path) ? {
1510+ let ent = ent?;
1511+ path. push ( ent. file_name ( ) ) ;
1512+ c += 1 ;
1513+ if ent. file_type ( ) ?. is_dir ( ) {
1514+ c += impl_count ( d, path) ?;
1515+ }
1516+ path. pop ( ) ;
1517+ }
1518+ return Ok ( c) ;
1519+ }
1520+ fn count ( d : & Dir ) -> Result < u64 > {
1521+ let mut p = PathBuf :: from ( "." ) ;
1522+ impl_count ( d, & mut p)
1523+ }
1524+
1525+ use cap_std_ext:: cap_tempfile:: TempDir ;
1526+ let tmproot = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1527+ let src_etc = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1528+
1529+ let init_tmproot = || -> Result < ( ) > {
1530+ tmproot. write ( "foo.conf" , "somefoo" ) ?;
1531+ tmproot. symlink ( "foo.conf" , "foo-link.conf" ) ?;
1532+ tmproot. create_dir_all ( "systemd/system" ) ?;
1533+ tmproot. write ( "systemd/system/foo.service" , "[fooservice]" ) ?;
1534+ tmproot. write ( "systemd/system/other.service" , "[otherservice]" ) ?;
1535+ Ok ( ( ) )
1536+ } ;
1537+
1538+ let mut pb = "." . into ( ) ;
1539+ let sepolicy = & ostree:: SePolicy :: new_at ( tmproot. as_raw_fd ( ) , gio:: Cancellable :: NONE ) ?;
1540+ // First, a no-op
1541+ copy_unmanaged_etc ( sepolicy, & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1542+ assert_eq ! ( count( & tmproot) . unwrap( ) , 0 ) ;
1543+
1544+ init_tmproot ( ) ?;
1545+
1546+ // Another no-op but with data in dest already
1547+ copy_unmanaged_etc ( sepolicy, & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1548+ assert_eq ! ( count( & tmproot) . unwrap( ) , 6 ) ;
1549+
1550+ src_etc. write ( "injected.conf" , "injected" ) ?;
1551+ copy_unmanaged_etc ( sepolicy, & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1552+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1553+
1554+ src_etc. create_dir_all ( "systemd/system" ) ?;
1555+ src_etc. write ( "systemd/system/foo.service" , "[overwrittenfoo]" ) ?;
1556+ copy_unmanaged_etc ( sepolicy, & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1557+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1558+ assert_eq ! (
1559+ tmproot. read_to_string( "systemd/system/foo.service" ) ?,
1560+ "[overwrittenfoo]"
1561+ ) ;
1562+
1563+ Ok ( ( ) )
1564+ }
13791565}
0 commit comments