@@ -102,6 +102,18 @@ pub(crate) struct InstallTargetOpts {
102102 pub ( crate ) skip_fetch_check : bool ,
103103}
104104
105+ #[ derive( clap:: Args , Debug , Clone , Serialize , Deserialize ) ]
106+ pub ( crate ) struct InstallSourceOpts {
107+ /// Install the system from an explicitly given source.
108+ ///
109+ /// By default, bootc install and install-to-filesystem assumes that it runs in a podman container, and
110+ /// it takes the container image to install from the podman's container registry.
111+ /// If --source-imgref is given, bootc uses it as the installation source, instead of the behaviour explained
112+ /// in the previous paragraph. See skopeo(1) for accepted formats.
113+ #[ clap( long) ]
114+ pub ( crate ) source_imgref : Option < String > ,
115+ }
116+
105117#[ derive( clap:: Args , Debug , Clone , Serialize , Deserialize ) ]
106118pub ( crate ) struct InstallConfigOpts {
107119 /// Disable SELinux in the target (installed) system.
@@ -137,6 +149,10 @@ pub(crate) struct InstallToDiskOpts {
137149 #[ serde( flatten) ]
138150 pub ( crate ) block_opts : InstallBlockDeviceOpts ,
139151
152+ #[ clap( flatten) ]
153+ #[ serde( flatten) ]
154+ pub ( crate ) source_opts : InstallSourceOpts ,
155+
140156 #[ clap( flatten) ]
141157 #[ serde( flatten) ]
142158 pub ( crate ) target_opts : InstallTargetOpts ,
@@ -209,6 +225,9 @@ pub(crate) struct InstallToFilesystemOpts {
209225 #[ clap( flatten) ]
210226 pub ( crate ) filesystem_opts : InstallTargetFilesystemOpts ,
211227
228+ #[ clap( flatten) ]
229+ pub ( crate ) source_opts : InstallSourceOpts ,
230+
212231 #[ clap( flatten) ]
213232 pub ( crate ) target_opts : InstallTargetOpts ,
214233
@@ -222,18 +241,25 @@ pub(crate) struct SourceInfo {
222241 /// Image reference we'll pull from (today always containers-storage: type)
223242 pub ( crate ) imageref : ostree_container:: ImageReference ,
224243 /// The digest to use for pulls
225- pub ( crate ) digest : String ,
244+ pub ( crate ) digest : Option < String > ,
226245 /// Whether or not SELinux appears to be enabled in the source commit
227246 pub ( crate ) selinux : bool ,
247+ /// Whether the source is available in the host mount namespace
248+ pub ( crate ) in_host_mountns : Option < HostMountnsInfo > ,
249+ }
250+
251+ /// Information about the host mount namespace
252+ #[ derive( Debug , Clone ) ]
253+ pub ( crate ) struct HostMountnsInfo {
254+ /// True if the skoepo on host supports containers-storage:
255+ pub ( crate ) skopeo_supports_containers_storage : bool ,
228256}
229257
230258// Shared read-only global state
231259pub ( crate ) struct State {
232260 pub ( crate ) source : SourceInfo ,
233261 /// Force SELinux off in target system
234262 pub ( crate ) override_disable_selinux : bool ,
235- /// True if the skoepo on host supports containers-storage:
236- pub ( crate ) skopeo_supports_containers_storage : bool ,
237263 #[ allow( dead_code) ]
238264 pub ( crate ) setenforce_guard : Option < crate :: lsm:: SetEnforceGuard > ,
239265 #[ allow( dead_code) ]
@@ -368,6 +394,29 @@ impl SourceInfo {
368394 name : container_info. image . clone ( ) ,
369395 } ;
370396 let digest = crate :: podman:: imageid_to_digest ( & container_info. imageid ) ?;
397+
398+ let skopeo_supports_containers_storage = skopeo_supports_containers_storage ( )
399+ . context ( "Failed to run skopeo (it currently must be installed in the host root)" ) ?;
400+ Self :: from (
401+ imageref,
402+ Some ( digest) ,
403+ Some ( HostMountnsInfo {
404+ skopeo_supports_containers_storage,
405+ } ) ,
406+ )
407+ }
408+
409+ #[ context( "Creating source info from a given imageref" ) ]
410+ pub ( crate ) fn from_imageref ( imageref : & str ) -> Result < Self > {
411+ let imageref = ostree_container:: ImageReference :: try_from ( imageref) ?;
412+ Self :: from ( imageref, None , None )
413+ }
414+
415+ fn from (
416+ imageref : ostree_container:: ImageReference ,
417+ digest : Option < String > ,
418+ in_host_mountns : Option < HostMountnsInfo > ,
419+ ) -> Result < Self > {
371420 let cancellable = ostree:: gio:: Cancellable :: NONE ;
372421 let commit = Task :: new ( "Reading ostree commit" , "ostree" )
373422 . args ( [ "--repo=/ostree/repo" , "rev-parse" , "--single" ] )
@@ -386,6 +435,7 @@ impl SourceInfo {
386435 imageref,
387436 digest,
388437 selinux,
438+ in_host_mountns,
389439 } )
390440 }
391441}
@@ -562,28 +612,39 @@ async fn initialize_ostree_root_from_self(
562612 let sysroot = ostree:: Sysroot :: new ( Some ( & gio:: File :: for_path ( rootfs) ) ) ;
563613 sysroot. load ( cancellable) ?;
564614
565- // We need to fetch the container image from the root mount namespace
566- let skopeo_cmd = run_in_host_mountns ( "skopeo" ) ;
567- let proxy_cfg = ostree_container:: store:: ImageProxyConfig {
568- skopeo_cmd : Some ( skopeo_cmd) ,
569- ..Default :: default ( )
570- } ;
571-
572615 let mut temporary_dir = None ;
573- let src_imageref = if state. skopeo_supports_containers_storage {
574- // We always use exactly the digest of the running image to ensure predictability.
575- let spec =
576- crate :: utils:: digested_pullspec ( & state. source . imageref . name , & state. source . digest ) ;
577- ostree_container:: ImageReference {
578- transport : ostree_container:: Transport :: ContainerStorage ,
579- name : spec,
616+ let ( src_imageref, proxy_cfg) = match & state. source . in_host_mountns {
617+ None => ( state. source . imageref . clone ( ) , None ) ,
618+ Some ( host_mountns_info) => {
619+ let src_imageref = if host_mountns_info. skopeo_supports_containers_storage {
620+ // We always use exactly the digest of the running image to ensure predictability.
621+ let digest = state
622+ . source
623+ . digest
624+ . as_ref ( )
625+ . ok_or_else ( || anyhow:: anyhow!( "Missing container image digest" ) ) ?;
626+ let spec = crate :: utils:: digested_pullspec ( & state. source . imageref . name , digest) ;
627+ ostree_container:: ImageReference {
628+ transport : ostree_container:: Transport :: ContainerStorage ,
629+ name : spec,
630+ }
631+ } else {
632+ let td = tempfile:: tempdir_in ( "/var/tmp" ) ?;
633+ let path: & Utf8Path = td. path ( ) . try_into ( ) . unwrap ( ) ;
634+ let r = copy_to_oci ( & state. source . imageref , path) ?;
635+ temporary_dir = Some ( td) ;
636+ r
637+ } ;
638+
639+ // We need to fetch the container image from the root mount namespace
640+ let skopeo_cmd = run_in_host_mountns ( "skopeo" ) ;
641+ let proxy_cfg = ostree_container:: store:: ImageProxyConfig {
642+ skopeo_cmd : Some ( skopeo_cmd) ,
643+ ..Default :: default ( )
644+ } ;
645+
646+ ( src_imageref, Some ( proxy_cfg) )
580647 }
581- } else {
582- let td = tempfile:: tempdir_in ( "/var/tmp" ) ?;
583- let path: & Utf8Path = td. path ( ) . try_into ( ) . unwrap ( ) ;
584- let r = copy_to_oci ( & state. source . imageref , path) ?;
585- temporary_dir = Some ( td) ;
586- r
587648 } ;
588649 let src_imageref = ostree_container:: OstreeImageReference {
589650 // There are no signatures to verify since we're fetching the already
@@ -601,7 +662,7 @@ async fn initialize_ostree_root_from_self(
601662 let mut options = ostree_container:: deploy:: DeployOpts :: default ( ) ;
602663 options. kargs = Some ( kargs. as_slice ( ) ) ;
603664 options. target_imgref = Some ( & state. target_imgref ) ;
604- options. proxy_cfg = Some ( proxy_cfg) ;
665+ options. proxy_cfg = proxy_cfg;
605666 println ! ( "Creating initial deployment" ) ;
606667 let target_image = state. target_imgref . to_string ( ) ;
607668 let state =
@@ -906,6 +967,7 @@ async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) ->
906967/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
907968async fn prepare_install (
908969 config_opts : InstallConfigOpts ,
970+ source_opts : InstallSourceOpts ,
909971 target_opts : InstallTargetOpts ,
910972) -> Result < Arc < State > > {
911973 // We need full root privileges, i.e. --privileged in podman
@@ -919,16 +981,20 @@ async fn prepare_install(
919981 let rootfs = cap_std:: fs:: Dir :: open_ambient_dir ( "/" , cap_std:: ambient_authority ( ) )
920982 . context ( "Opening /" ) ?;
921983
922- // This command currently *must* be run inside a privileged container.
923- let container_info = crate :: containerenv:: get_container_execution_info ( & rootfs) ?;
924- if let Some ( "1" ) = container_info. rootless . as_deref ( ) {
925- anyhow:: bail!( "Cannot install from rootless podman; this command must be run as root" ) ;
926- }
927-
928- let skopeo_supports_containers_storage = skopeo_supports_containers_storage ( )
929- . context ( "Failed to run skopeo (it currently must be installed in the host root)" ) ?;
984+ let source = match source_opts. source_imgref {
985+ None => {
986+ let container_info = crate :: containerenv:: get_container_execution_info ( & rootfs) ?;
987+ // This command currently *must* be run inside a privileged container.
988+ if let Some ( "1" ) = container_info. rootless . as_deref ( ) {
989+ anyhow:: bail!(
990+ "Cannot install from rootless podman; this command must be run as root"
991+ ) ;
992+ }
930993
931- let source = SourceInfo :: from_container ( & container_info) ?;
994+ SourceInfo :: from_container ( & container_info) ?
995+ }
996+ Some ( source) => SourceInfo :: from_imageref ( & source) ?,
997+ } ;
932998
933999 // Parse the target CLI image reference options and create the *target* image
9341000 // reference, which defaults to pulling from a registry.
@@ -982,7 +1048,6 @@ async fn prepare_install(
9821048 // combines our command line options along with some bind mounts from the host.
9831049 let state = Arc :: new ( State {
9841050 override_disable_selinux,
985- skopeo_supports_containers_storage,
9861051 setenforce_guard,
9871052 source,
9881053 config_opts,
@@ -1065,7 +1130,7 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> {
10651130 anyhow:: bail!( "Not a block device: {}" , block_opts. device) ;
10661131 }
10671132 }
1068- let state = prepare_install ( opts. config_opts , opts. target_opts ) . await ?;
1133+ let state = prepare_install ( opts. config_opts , opts. source_opts , opts . target_opts ) . await ?;
10691134
10701135 // This is all blocking stuff
10711136 let mut rootfs = {
@@ -1174,7 +1239,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
11741239 }
11751240
11761241 // Gather global state, destructuring the provided options
1177- let state = prepare_install ( opts. config_opts , opts. target_opts ) . await ?;
1242+ let state = prepare_install ( opts. config_opts , opts. source_opts , opts . target_opts ) . await ?;
11781243
11791244 match fsopts. replace {
11801245 Some ( ReplaceMode :: Wipe ) => {
0 commit comments