@@ -24,7 +24,9 @@ use anyhow::{anyhow, Context, Result};
24
24
use camino:: Utf8Path ;
25
25
use camino:: Utf8PathBuf ;
26
26
use cap_std:: fs:: { Dir , MetadataExt } ;
27
+ use cap_std_ext:: cap_primitives;
27
28
use cap_std_ext:: cap_std;
29
+ use cap_std_ext:: cap_std:: io_lifetimes:: AsFilelike ;
28
30
use cap_std_ext:: prelude:: CapStdExtDirExt ;
29
31
use chrono:: prelude:: * ;
30
32
use clap:: ValueEnum ;
@@ -139,6 +141,27 @@ pub(crate) struct InstallConfigOpts {
139
141
#[ clap( long) ]
140
142
karg : Option < Vec < String > > ,
141
143
144
+ /// Inject arbitrary files into the target deployment `/etc`. One can use
145
+ /// this for example to inject systemd units, or `tmpfiles.d` snippets
146
+ /// which set up SSH keys.
147
+ ///
148
+ /// Files injected this way become "unmanaged state"; they will be carried
149
+ /// forward across upgrades, but will not otherwise be updated unless
150
+ /// a secondary mechanism takes ownership thereafter.
151
+ ///
152
+ /// This option can be specified multiple times; the files will be copied
153
+ /// in order.
154
+ ///
155
+ /// Any missing parent directories will be implicitly created with root ownership
156
+ /// and mode 0755.
157
+ ///
158
+ /// This option pairs well with additional bind mount
159
+ /// volumes set up via the container orchestrator, e.g.:
160
+ /// `podman run ... -v /path/to/config:/config <image> bootc install to-disk --copy-etc /config`
161
+ #[ clap( long) ]
162
+ #[ serde( default ) ]
163
+ pub ( crate ) copy_etc : Option < Vec < Utf8PathBuf > > ,
164
+
142
165
/// The path to an `authorized_keys` that will be injected into the `root` account.
143
166
///
144
167
/// The implementation of this uses systemd `tmpfiles.d`, writing to a file named
@@ -697,6 +720,24 @@ async fn initialize_ostree_root_from_self(
697
720
osconfig:: inject_root_ssh_authorized_keys ( & root, sepolicy, contents) ?;
698
721
}
699
722
723
+ // Copy unmanaged configuration
724
+ let target_etc = root. open_dir ( "etc" ) . context ( "Opening deployment /etc" ) ?;
725
+ let copy_etc = state
726
+ . config_opts
727
+ . copy_etc
728
+ . iter ( )
729
+ . flatten ( )
730
+ . cloned ( )
731
+ . collect :: < Vec < _ > > ( ) ;
732
+ for src in copy_etc {
733
+ println ! ( "Injecting configuration from {src}" ) ;
734
+ let src = Dir :: open_ambient_dir ( & src, cap_std:: ambient_authority ( ) )
735
+ . with_context ( || format ! ( "Opening {src}" ) ) ?;
736
+ let mut pb = "." . into ( ) ;
737
+ let n = copy_unmanaged_etc ( sepolicy, & src, & target_etc, & mut pb) ?;
738
+ tracing:: debug!( "Copied config files: {n}" ) ;
739
+ }
740
+
700
741
let uname = rustix:: system:: uname ( ) ;
701
742
702
743
let labels = crate :: status:: labels_of_config ( & imgstate. configuration ) ;
@@ -1166,6 +1207,70 @@ async fn prepare_install(
1166
1207
Ok ( state)
1167
1208
}
1168
1209
1210
+ // Backing implementation of --copy-etc; just your basic
1211
+ // recursive copy algorithm. Parent directories are
1212
+ // created as necessary
1213
+ fn copy_unmanaged_etc (
1214
+ sepolicy : Option < & ostree:: SePolicy > ,
1215
+ src : & Dir ,
1216
+ dest : & Dir ,
1217
+ path : & mut Utf8PathBuf ,
1218
+ ) -> Result < u64 > {
1219
+ let mut r = 0u64 ;
1220
+ for ent in src. read_dir ( & path) ? {
1221
+ let ent = ent?;
1222
+ let name = ent. file_name ( ) ;
1223
+ let name = if let Some ( name) = name. to_str ( ) {
1224
+ name
1225
+ } else {
1226
+ anyhow:: bail!( "Non-UTF8 name: {name:?}" ) ;
1227
+ } ;
1228
+ let meta = ent. metadata ( ) ?;
1229
+ // Build the relative path
1230
+ path. push ( Utf8Path :: new ( name) ) ;
1231
+ // And the absolute path for looking up SELinux labels
1232
+ let as_path = {
1233
+ let mut p = Utf8PathBuf :: from ( "/etc" ) ;
1234
+ p. push ( & path) ;
1235
+ p
1236
+ } ;
1237
+ r += 1 ;
1238
+ if meta. is_dir ( ) {
1239
+ if let Some ( parent) = path. parent ( ) {
1240
+ dest. create_dir_all ( parent)
1241
+ . with_context ( || format ! ( "Creating {parent}" ) ) ?;
1242
+ }
1243
+ crate :: lsm:: ensure_dir_labeled (
1244
+ dest,
1245
+ & path,
1246
+ Some ( & as_path) ,
1247
+ meta. mode ( ) . into ( ) ,
1248
+ sepolicy,
1249
+ ) ?;
1250
+ r += copy_unmanaged_etc ( sepolicy, src, dest, path) ?;
1251
+ } else {
1252
+ dest. remove_file_optional ( & path) ?;
1253
+ if meta. is_symlink ( ) {
1254
+ let link_target = cap_primitives:: fs:: read_link_contents (
1255
+ & src. as_filelike_view ( ) ,
1256
+ path. as_std_path ( ) ,
1257
+ )
1258
+ . context ( "Reading symlink" ) ?;
1259
+ cap_primitives:: fs:: symlink_contents ( link_target, & dest. as_filelike_view ( ) , & path)
1260
+ . with_context ( || format ! ( "Writing symlink {path:?}" ) ) ?;
1261
+ } else {
1262
+ src. copy ( & path, dest, & path)
1263
+ . with_context ( || format ! ( "Copying {path:?}" ) ) ?;
1264
+ }
1265
+ if let Some ( sepolicy) = sepolicy {
1266
+ crate :: lsm:: ensure_labeled ( dest, path, Some ( & as_path) , & meta, sepolicy) ?;
1267
+ }
1268
+ }
1269
+ assert ! ( path. pop( ) ) ;
1270
+ }
1271
+ Ok ( r)
1272
+ }
1273
+
1169
1274
async fn install_to_filesystem_impl ( state : & State , rootfs : & mut RootSetup ) -> Result < ( ) > {
1170
1275
if matches ! ( state. selinux_state, SELinuxFinalState :: ForceTargetDisabled ) {
1171
1276
rootfs. kargs . push ( "selinux=0" . to_string ( ) ) ;
@@ -1606,13 +1711,79 @@ pub(crate) async fn install_to_existing_root(opts: InstallToExistingRootOpts) ->
1606
1711
install_to_filesystem ( opts, true ) . await
1607
1712
}
1608
1713
1609
- #[ test]
1610
- fn install_opts_serializable ( ) {
1611
- let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1612
- "device" : "/dev/vda"
1613
- } ) )
1614
- . unwrap ( ) ;
1615
- assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1714
+ #[ cfg( test) ]
1715
+ mod tests {
1716
+ use super :: * ;
1717
+
1718
+ #[ test]
1719
+ fn install_opts_serializable ( ) {
1720
+ let c: InstallToDiskOpts = serde_json:: from_value ( serde_json:: json!( {
1721
+ "device" : "/dev/vda"
1722
+ } ) )
1723
+ . unwrap ( ) ;
1724
+ assert_eq ! ( c. block_opts. device, "/dev/vda" ) ;
1725
+ }
1726
+
1727
+ #[ test]
1728
+ fn test_copy_etc ( ) -> Result < ( ) > {
1729
+ use std:: path:: PathBuf ;
1730
+ fn impl_count ( d : & Dir , path : & mut PathBuf ) -> Result < u64 > {
1731
+ let mut c = 0u64 ;
1732
+ for ent in d. read_dir ( & path) ? {
1733
+ let ent = ent?;
1734
+ path. push ( ent. file_name ( ) ) ;
1735
+ c += 1 ;
1736
+ if ent. file_type ( ) ?. is_dir ( ) {
1737
+ c += impl_count ( d, path) ?;
1738
+ }
1739
+ path. pop ( ) ;
1740
+ }
1741
+ return Ok ( c) ;
1742
+ }
1743
+ fn count ( d : & Dir ) -> Result < u64 > {
1744
+ let mut p = PathBuf :: from ( "." ) ;
1745
+ impl_count ( d, & mut p)
1746
+ }
1747
+
1748
+ use cap_std_ext:: cap_tempfile:: TempDir ;
1749
+ let tmproot = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1750
+ let src_etc = TempDir :: new ( cap_std:: ambient_authority ( ) ) ?;
1751
+
1752
+ let init_tmproot = || -> Result < ( ) > {
1753
+ tmproot. write ( "foo.conf" , "somefoo" ) ?;
1754
+ tmproot. symlink ( "foo.conf" , "foo-link.conf" ) ?;
1755
+ tmproot. create_dir_all ( "systemd/system" ) ?;
1756
+ tmproot. write ( "systemd/system/foo.service" , "[fooservice]" ) ?;
1757
+ tmproot. write ( "systemd/system/other.service" , "[otherservice]" ) ?;
1758
+ Ok ( ( ) )
1759
+ } ;
1760
+
1761
+ let mut pb = "." . into ( ) ;
1762
+ // First, a no-op
1763
+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1764
+ assert_eq ! ( count( & tmproot) . unwrap( ) , 0 ) ;
1765
+
1766
+ init_tmproot ( ) ?;
1767
+
1768
+ // Another no-op but with data in dest already
1769
+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1770
+ assert_eq ! ( count( & tmproot) . unwrap( ) , 6 ) ;
1771
+
1772
+ src_etc. write ( "injected.conf" , "injected" ) ?;
1773
+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1774
+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1775
+
1776
+ src_etc. create_dir_all ( "systemd/system" ) ?;
1777
+ src_etc. write ( "systemd/system/foo.service" , "[overwrittenfoo]" ) ?;
1778
+ copy_unmanaged_etc ( None , & src_etc, & tmproot, & mut pb) . unwrap ( ) ;
1779
+ assert_eq ! ( count( & tmproot) . unwrap( ) , 7 ) ;
1780
+ assert_eq ! (
1781
+ tmproot. read_to_string( "systemd/system/foo.service" ) ?,
1782
+ "[overwrittenfoo]"
1783
+ ) ;
1784
+
1785
+ Ok ( ( ) )
1786
+ }
1616
1787
}
1617
1788
1618
1789
#[ test]
0 commit comments