@@ -102,6 +102,18 @@ pub(crate) struct InstallTargetOpts {
102
102
pub ( crate ) skip_fetch_check : bool ,
103
103
}
104
104
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
+
105
117
#[ derive( clap:: Args , Debug , Clone , Serialize , Deserialize ) ]
106
118
pub ( crate ) struct InstallConfigOpts {
107
119
/// Disable SELinux in the target (installed) system.
@@ -137,6 +149,10 @@ pub(crate) struct InstallToDiskOpts {
137
149
#[ serde( flatten) ]
138
150
pub ( crate ) block_opts : InstallBlockDeviceOpts ,
139
151
152
+ #[ clap( flatten) ]
153
+ #[ serde( flatten) ]
154
+ pub ( crate ) source_opts : InstallSourceOpts ,
155
+
140
156
#[ clap( flatten) ]
141
157
#[ serde( flatten) ]
142
158
pub ( crate ) target_opts : InstallTargetOpts ,
@@ -209,6 +225,9 @@ pub(crate) struct InstallToFilesystemOpts {
209
225
#[ clap( flatten) ]
210
226
pub ( crate ) filesystem_opts : InstallTargetFilesystemOpts ,
211
227
228
+ #[ clap( flatten) ]
229
+ pub ( crate ) source_opts : InstallSourceOpts ,
230
+
212
231
#[ clap( flatten) ]
213
232
pub ( crate ) target_opts : InstallTargetOpts ,
214
233
@@ -222,18 +241,25 @@ pub(crate) struct SourceInfo {
222
241
/// Image reference we'll pull from (today always containers-storage: type)
223
242
pub ( crate ) imageref : ostree_container:: ImageReference ,
224
243
/// The digest to use for pulls
225
- pub ( crate ) digest : String ,
244
+ pub ( crate ) digest : Option < String > ,
226
245
/// Whether or not SELinux appears to be enabled in the source commit
227
246
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 ,
228
256
}
229
257
230
258
// Shared read-only global state
231
259
pub ( crate ) struct State {
232
260
pub ( crate ) source : SourceInfo ,
233
261
/// Force SELinux off in target system
234
262
pub ( crate ) override_disable_selinux : bool ,
235
- /// True if the skoepo on host supports containers-storage:
236
- pub ( crate ) skopeo_supports_containers_storage : bool ,
237
263
#[ allow( dead_code) ]
238
264
pub ( crate ) setenforce_guard : Option < crate :: lsm:: SetEnforceGuard > ,
239
265
#[ allow( dead_code) ]
@@ -368,6 +394,29 @@ impl SourceInfo {
368
394
name : container_info. image . clone ( ) ,
369
395
} ;
370
396
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 > {
371
420
let cancellable = ostree:: gio:: Cancellable :: NONE ;
372
421
let commit = Task :: new ( "Reading ostree commit" , "ostree" )
373
422
. args ( [ "--repo=/ostree/repo" , "rev-parse" , "--single" ] )
@@ -386,6 +435,7 @@ impl SourceInfo {
386
435
imageref,
387
436
digest,
388
437
selinux,
438
+ in_host_mountns,
389
439
} )
390
440
}
391
441
}
@@ -562,28 +612,39 @@ async fn initialize_ostree_root_from_self(
562
612
let sysroot = ostree:: Sysroot :: new ( Some ( & gio:: File :: for_path ( rootfs) ) ) ;
563
613
sysroot. load ( cancellable) ?;
564
614
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
-
572
615
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) )
580
647
}
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
587
648
} ;
588
649
let src_imageref = ostree_container:: OstreeImageReference {
589
650
// There are no signatures to verify since we're fetching the already
@@ -601,7 +662,7 @@ async fn initialize_ostree_root_from_self(
601
662
let mut options = ostree_container:: deploy:: DeployOpts :: default ( ) ;
602
663
options. kargs = Some ( kargs. as_slice ( ) ) ;
603
664
options. target_imgref = Some ( & state. target_imgref ) ;
604
- options. proxy_cfg = Some ( proxy_cfg) ;
665
+ options. proxy_cfg = proxy_cfg;
605
666
println ! ( "Creating initial deployment" ) ;
606
667
let target_image = state. target_imgref . to_string ( ) ;
607
668
let state =
@@ -906,6 +967,7 @@ async fn verify_target_fetch(imgref: &ostree_container::OstreeImageReference) ->
906
967
/// Preparation for an install; validates and prepares some (thereafter immutable) global state.
907
968
async fn prepare_install (
908
969
config_opts : InstallConfigOpts ,
970
+ source_opts : InstallSourceOpts ,
909
971
target_opts : InstallTargetOpts ,
910
972
) -> Result < Arc < State > > {
911
973
// We need full root privileges, i.e. --privileged in podman
@@ -919,16 +981,20 @@ async fn prepare_install(
919
981
let rootfs = cap_std:: fs:: Dir :: open_ambient_dir ( "/" , cap_std:: ambient_authority ( ) )
920
982
. context ( "Opening /" ) ?;
921
983
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
+ }
930
993
931
- let source = SourceInfo :: from_container ( & container_info) ?;
994
+ SourceInfo :: from_container ( & container_info) ?
995
+ }
996
+ Some ( source) => SourceInfo :: from_imageref ( & source) ?,
997
+ } ;
932
998
933
999
// Parse the target CLI image reference options and create the *target* image
934
1000
// reference, which defaults to pulling from a registry.
@@ -982,7 +1048,6 @@ async fn prepare_install(
982
1048
// combines our command line options along with some bind mounts from the host.
983
1049
let state = Arc :: new ( State {
984
1050
override_disable_selinux,
985
- skopeo_supports_containers_storage,
986
1051
setenforce_guard,
987
1052
source,
988
1053
config_opts,
@@ -1065,7 +1130,7 @@ pub(crate) async fn install_to_disk(opts: InstallToDiskOpts) -> Result<()> {
1065
1130
anyhow:: bail!( "Not a block device: {}" , block_opts. device) ;
1066
1131
}
1067
1132
}
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 ?;
1069
1134
1070
1135
// This is all blocking stuff
1071
1136
let mut rootfs = {
@@ -1174,7 +1239,7 @@ pub(crate) async fn install_to_filesystem(opts: InstallToFilesystemOpts) -> Resu
1174
1239
}
1175
1240
1176
1241
// 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 ?;
1178
1243
1179
1244
match fsopts. replace {
1180
1245
Some ( ReplaceMode :: Wipe ) => {
0 commit comments