@@ -23,7 +23,9 @@ use anyhow::{anyhow, Context, Result};
23
23
use camino:: Utf8Path ;
24
24
use camino:: Utf8PathBuf ;
25
25
use cap_std:: fs:: { Dir , MetadataExt } ;
26
+ use cap_std_ext:: cap_primitives;
26
27
use cap_std_ext:: cap_std;
28
+ use cap_std_ext:: cap_std:: io_lifetimes:: AsFilelike ;
27
29
use cap_std_ext:: prelude:: CapStdExtDirExt ;
28
30
use chrono:: prelude:: * ;
29
31
use clap:: ValueEnum ;
@@ -138,6 +140,27 @@ pub(crate) struct InstallConfigOpts {
138
140
#[ clap( long) ]
139
141
karg : Option < Vec < String > > ,
140
142
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
+
141
164
/// The path to an `authorized_keys` that will be injected into the `root` account.
142
165
///
143
166
/// The implementation of this uses systemd `tmpfiles.d`, writing to a file named
@@ -672,6 +695,24 @@ async fn initialize_ostree_root_from_self(
672
695
osconfig:: inject_root_ssh_authorized_keys ( & root, sepolicy, contents) ?;
673
696
}
674
697
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
+
675
716
let uname = rustix:: system:: uname ( ) ;
676
717
677
718
let labels = crate :: status:: labels_of_config ( & imgstate. configuration ) ;
@@ -1077,6 +1118,70 @@ async fn prepare_install(
1077
1118
Ok ( state)
1078
1119
}
1079
1120
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
+
1080
1185
async fn install_to_filesystem_impl ( state : & State , rootfs : & mut RootSetup ) -> Result < ( ) > {
1081
1186
if state. override_disable_selinux {
1082
1187
rootfs. kargs . push ( "selinux=0" . to_string ( ) ) ;
@@ -1469,13 +1574,79 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
1469
1574
install_to_filesystem ( opts, true ) . await
1470
1575
}
1471
1576
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
+ }
1479
1650
}
1480
1651
1481
1652
#[ test]
0 commit comments