@@ -23,7 +23,9 @@ use anyhow::{anyhow, Context, Result};
2323use camino:: Utf8Path ;
2424use camino:: Utf8PathBuf ;
2525use cap_std:: fs:: { Dir , MetadataExt } ;
26+ use cap_std_ext:: cap_primitives;
2627use cap_std_ext:: cap_std;
28+ use cap_std_ext:: cap_std:: io_lifetimes:: AsFilelike ;
2729use cap_std_ext:: prelude:: CapStdExtDirExt ;
2830use chrono:: prelude:: * ;
2931use clap:: ValueEnum ;
@@ -138,6 +140,27 @@ pub(crate) struct InstallConfigOpts {
138140 #[ clap( long) ]
139141 karg : Option < Vec < String > > ,
140142
143+ /// Inject arbitrary files into the target deployment `/etc`. One can use
144+ /// this for example to inject systemd units, or `tmpfiles.d` snippets
145+ /// which set up SSH keys.
146+ ///
147+ /// Files injected this way become "unmanaged state"; they will be carried
148+ /// forward across upgrades, but will not otherwise be updated unless
149+ /// a secondary mechanism takes ownership thereafter.
150+ ///
151+ /// This option can be specified multiple times; the files will be copied
152+ /// in order.
153+ ///
154+ /// Any missing parent directories will be implicitly created with root ownership
155+ /// and mode 0755.
156+ ///
157+ /// This option pairs well with additional bind mount
158+ /// volumes set up via the container orchestrator, e.g.:
159+ /// `podman run ... -v /path/to/config:/tmp/etc <image> bootc install to-disk --copy-etc /tmp/etc`
160+ #[ clap( long) ]
161+ #[ serde( default ) ]
162+ pub ( crate ) copy_etc : Option < Vec < Utf8PathBuf > > ,
163+
141164 /// The path to an `authorized_keys` that will be injected into the `root` account.
142165 ///
143166 /// The implementation of this uses systemd `tmpfiles.d`, writing to a file named
@@ -672,6 +695,24 @@ async fn initialize_ostree_root_from_self(
672695 osconfig:: inject_root_ssh_authorized_keys ( & root, sepolicy, contents) ?;
673696 }
674697
698+ // Copy unmanaged configuration
699+ let target_etc = root. open_dir ( "etc" ) . context ( "Opening deployment /etc" ) ?;
700+ let copy_etc = state
701+ . config_opts
702+ . copy_etc
703+ . iter ( )
704+ . flatten ( )
705+ . cloned ( )
706+ . collect :: < Vec < _ > > ( ) ;
707+ for src in copy_etc {
708+ println ! ( "Injecting configuration from {src}" ) ;
709+ let src = Dir :: open_ambient_dir ( & src, cap_std:: ambient_authority ( ) )
710+ . with_context ( || format ! ( "Opening {src}" ) ) ?;
711+ let mut pb = "." . into ( ) ;
712+ let n = copy_unmanaged_etc ( sepolicy, & src, & target_etc, & mut pb) ?;
713+ tracing:: debug!( "Copied config files: {n}" ) ;
714+ }
715+
675716 let uname = rustix:: system:: uname ( ) ;
676717
677718 let labels = crate :: status:: labels_of_config ( & imgstate. configuration ) ;
@@ -1077,6 +1118,70 @@ async fn prepare_install(
10771118 Ok ( state)
10781119}
10791120
1121+ // Backing implementation of --copy-etc; just your basic
1122+ // recursive copy algorithm. Parent directories are
1123+ // created as necessary
1124+ fn copy_unmanaged_etc (
1125+ sepolicy : Option < & ostree:: SePolicy > ,
1126+ src : & Dir ,
1127+ dest : & Dir ,
1128+ path : & mut Utf8PathBuf ,
1129+ ) -> Result < u64 > {
1130+ let mut r = 0u64 ;
1131+ for ent in src. read_dir ( & path) ? {
1132+ let ent = ent?;
1133+ let name = ent. file_name ( ) ;
1134+ let name = if let Some ( name) = name. to_str ( ) {
1135+ name
1136+ } else {
1137+ anyhow:: bail!( "Non-UTF8 name: {name:?}" ) ;
1138+ } ;
1139+ let meta = ent. metadata ( ) ?;
1140+ // Build the relative path
1141+ path. push ( Utf8Path :: new ( name) ) ;
1142+ // And the absolute path for looking up SELinux labels
1143+ let as_path = {
1144+ let mut p = Utf8PathBuf :: from ( "/etc" ) ;
1145+ p. push ( & path) ;
1146+ p
1147+ } ;
1148+ r += 1 ;
1149+ if meta. is_dir ( ) {
1150+ if let Some ( parent) = path. parent ( ) {
1151+ dest. create_dir_all ( parent)
1152+ . with_context ( || format ! ( "Creating {parent}" ) ) ?;
1153+ }
1154+ crate :: lsm:: ensure_dir_labeled (
1155+ dest,
1156+ & path,
1157+ Some ( & as_path) ,
1158+ meta. mode ( ) . into ( ) ,
1159+ sepolicy,
1160+ ) ?;
1161+ r += copy_unmanaged_etc ( sepolicy, src, dest, path) ?;
1162+ } else {
1163+ dest. remove_file_optional ( & path) ?;
1164+ if meta. is_symlink ( ) {
1165+ let link_target = cap_primitives:: fs:: read_link_contents (
1166+ & src. as_filelike_view ( ) ,
1167+ path. as_std_path ( ) ,
1168+ )
1169+ . context ( "Reading symlink" ) ?;
1170+ cap_primitives:: fs:: symlink_contents ( link_target, & dest. as_filelike_view ( ) , & path)
1171+ . with_context ( || format ! ( "Writing symlink {path:?}" ) ) ?;
1172+ } else {
1173+ src. copy ( & path, dest, & path)
1174+ . with_context ( || format ! ( "Copying {path:?}" ) ) ?;
1175+ }
1176+ if let Some ( sepolicy) = sepolicy {
1177+ crate :: lsm:: ensure_labeled ( dest, path, Some ( & as_path) , & meta, sepolicy) ?;
1178+ }
1179+ }
1180+ assert ! ( path. pop( ) ) ;
1181+ }
1182+ Ok ( r)
1183+ }
1184+
10801185async fn install_to_filesystem_impl ( state : & State , rootfs : & mut RootSetup ) -> Result < ( ) > {
10811186 if state. override_disable_selinux {
10821187 rootfs. kargs . push ( "selinux=0" . to_string ( ) ) ;
@@ -1469,13 +1574,79 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
14691574 install_to_filesystem ( opts, true ) . await
14701575}
14711576
1472- #[ test]
1473- fn install_opts_serializable ( ) {
1474- let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1475- "device" : "/dev/vda"
1476- } ) )
1477- . unwrap ( ) ;
1478- assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1577+ #[ cfg( test) ]
1578+ mod tests {
1579+ use super :: * ;
1580+
1581+ #[ test]
1582+ fn install_opts_serializable ( ) {
1583+ let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1584+ "device" : "/dev/vda"
1585+ } ) )
1586+ . unwrap ( ) ;
1587+ assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1588+ }
1589+
1590+ #[ test]
1591+ fn test_copy_etc ( ) -> Result < ( ) > {
1592+ use std:: path:: PathBuf ;
1593+ fn impl_count ( d : & Dir , path : & mut PathBuf ) -> Result < u64 > {
1594+ let mut c = 0u64 ;
1595+ for ent in d. read_dir ( & path) ? {
1596+ let ent = ent?;
1597+ path. push ( ent. file_name ( ) ) ;
1598+ c += 1 ;
1599+ if ent. file_type ( ) ?. is_dir ( ) {
1600+ c += impl_count ( d, path) ?;
1601+ }
1602+ path. pop ( ) ;
1603+ }
1604+ return Ok ( c) ;
1605+ }
1606+ fn count ( d : & Dir ) -> Result < u64 > {
1607+ let mut p = PathBuf :: from ( "." ) ;
1608+ impl_count ( d, & mut p)
1609+ }
1610+
1611+ use cap_std_ext:: cap_tempfile:: TempDir ;
1612+ let tmproot = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1613+ let src_etc = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1614+
1615+ let init_tmproot = || -> Result < ( ) > {
1616+ tmproot. write ( "foo.conf" , "somefoo" ) ?;
1617+ tmproot. symlink ( "foo.conf" , "foo-link.conf" ) ?;
1618+ tmproot. create_dir_all ( "systemd/system" ) ?;
1619+ tmproot. write ( "systemd/system/foo.service" , "[fooservice]" ) ?;
1620+ tmproot. write ( "systemd/system/other.service" , "[otherservice]" ) ?;
1621+ Ok ( ( ) )
1622+ } ;
1623+
1624+ let mut pb = "." . into ( ) ;
1625+ // First, a no-op
1626+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1627+ assert_eq ! ( count( & tmproot) . unwrap( ) , 0 ) ;
1628+
1629+ init_tmproot ( ) ?;
1630+
1631+ // Another no-op but with data in dest already
1632+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1633+ assert_eq ! ( count( & tmproot) . unwrap( ) , 6 ) ;
1634+
1635+ src_etc. write ( "injected.conf" , "injected" ) ?;
1636+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1637+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1638+
1639+ src_etc. create_dir_all ( "systemd/system" ) ?;
1640+ src_etc. write ( "systemd/system/foo.service" , "[overwrittenfoo]" ) ?;
1641+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1642+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1643+ assert_eq ! (
1644+ tmproot. read_to_string( "systemd/system/foo.service" ) ?,
1645+ "[overwrittenfoo]"
1646+ ) ;
1647+
1648+ Ok ( ( ) )
1649+ }
14791650}
14801651
14811652#[ test]
0 commit comments