-
Notifications
You must be signed in to change notification settings - Fork 232
feat(switch): add --target-imgref to decouple pull source from upgrade origin #2467
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
46b6177
a898af4
bff448d
22d8079
8a7941b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -165,6 +165,22 @@ pub(crate) struct SwitchOpts { | |
| #[clap(long, default_value = "registry")] | ||
| pub(crate) transport: String, | ||
|
|
||
| /// Specify the image to record as the origin for subsequent updates. | ||
| /// | ||
| /// By default the image is fetched from `--transport`/`<TARGET>` and that same | ||
| /// reference is also persisted as the origin used for future `bootc upgrade`s. | ||
| /// Use this to decouple the two: the image is still fetched now from | ||
| /// `--transport`/`<TARGET>` (e.g. a local `containers-storage` copy loaded via | ||
| /// `podman load`), but the origin recorded for subsequent updates is this | ||
| /// reference instead (e.g. the normal registry pull spec). | ||
| #[clap(long, conflicts_with_all = ["from_downloaded", "mutate_in_place"])] | ||
| pub(crate) target_imgref: Option<String>, | ||
|
|
||
| /// The transport for `--target-imgref`; e.g. registry, oci, oci-archive, | ||
| /// docker-daemon, containers-storage. Defaults to `registry`. | ||
| #[clap(long, default_value = "registry")] | ||
| pub(crate) target_transport: String, | ||
|
|
||
| #[clap(flatten)] | ||
| pub(crate) download_opts: DownloadOnlyOpts, | ||
|
|
||
|
|
@@ -1495,6 +1511,27 @@ pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> { | |
| return Ok(target); | ||
| } | ||
|
|
||
| /// Build the optional `--target-imgref` for `switch`; this is the reference that will | ||
| /// be recorded as the origin for subsequent updates, decoupled from the source the | ||
| /// image is fetched from now. Returns `None` when `--target-imgref` was not provided. | ||
| pub(crate) fn target_imgref_for_switch( | ||
| opts: &SwitchOpts, | ||
| ) -> Result<Option<ostree_container::OstreeImageReference>> { | ||
| let Some(name) = opts.target_imgref.as_deref() else { | ||
| return Ok(None); | ||
| }; | ||
| let transport = ostree_container::Transport::try_from(opts.target_transport.as_str())?; | ||
| let imgref = ostree_container::ImageReference { | ||
| transport, | ||
| name: name.to_string(), | ||
| }; | ||
| let sigverify = sigpolicy_from_opt(opts.enforce_container_sigpolicy); | ||
| Ok(Some(ostree_container::OstreeImageReference { | ||
| sigverify, | ||
| imgref, | ||
| })) | ||
| } | ||
|
|
||
| /// Implementation of the `bootc switch` CLI command for ostree backend. | ||
| #[context("Switching (ostree)")] | ||
| async fn switch_ostree( | ||
|
|
@@ -1517,19 +1554,32 @@ async fn switch_ostree( | |
| .await; | ||
| } | ||
|
|
||
| let target = imgref_for_switch(&opts)?; | ||
| // The source we fetch the image from right now. | ||
| let source = imgref_for_switch(&opts)?; | ||
| // Optional decoupled reference to persist as the origin for subsequent updates. | ||
| let target_imgref = target_imgref_for_switch(&opts)?; | ||
| // What we record in the spec/origin for future upgrades: the `--target-imgref` | ||
| // if given, otherwise the same reference we're fetching from. | ||
| let origin_ref = match target_imgref.as_ref() { | ||
| Some(t) => ImageReference::from(t.clone()), | ||
| None => source.clone(), | ||
|
Comment on lines
+1563
to
+1565
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, this is a very valid issue and a little bit more complicated
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For composefs backend we can add another field in the JSON we store in |
||
| }; | ||
| let prog: ProgressWriter = opts.progress.try_into()?; | ||
| let cancellable = gio::Cancellable::NONE; | ||
|
|
||
| let repo = &booted_ostree.repo(); | ||
|
|
||
| let new_spec = { | ||
| let mut new_spec = host.spec.clone(); | ||
| new_spec.image = Some(target.clone()); | ||
| new_spec.image = Some(origin_ref.clone()); | ||
| new_spec | ||
| }; | ||
|
|
||
| if new_spec == host.spec { | ||
| // Only take the unchanged fast path when the pull source is also the origin. | ||
| // With `--target-imgref` the source is decoupled from the persisted origin, so a | ||
| // switch from a different source keeping the same origin (issue #2464) must still | ||
| // run the pull even though `new_spec == host.spec`. | ||
| if target_imgref.is_none() && new_spec == host.spec { | ||
| println!("Image specification is unchanged."); | ||
| if opts.apply && host.status.staged.is_some() { | ||
| crate::reboot::reboot()?; | ||
|
|
@@ -1549,11 +1599,13 @@ async fn switch_ostree( | |
| tracing::info!( | ||
| message_id = SWITCH_JOURNAL_ID, | ||
| bootc.old_image_reference = old_image, | ||
| bootc.new_image_reference = &target.image, | ||
| bootc.new_image_transport = &target.transport, | ||
| bootc.new_image_reference = &origin_ref.image, | ||
| bootc.new_image_transport = &origin_ref.transport, | ||
| bootc.source_image_reference = &source.image, | ||
| bootc.source_image_transport = &source.transport, | ||
| "Switching from image {} to {}", | ||
| old_image, | ||
| target.image | ||
| origin_ref.image | ||
| ); | ||
|
|
||
| let new_spec = RequiredHostSpec::from_spec(&new_spec)?; | ||
|
|
@@ -1564,14 +1616,14 @@ async fn switch_ostree( | |
| let use_unified = if opts.unified_storage_exp { | ||
| true | ||
| } else { | ||
| crate::deploy::image_exists_in_unified_storage(storage, &target).await? | ||
| crate::deploy::image_exists_in_unified_storage(storage, &source).await? | ||
| }; | ||
|
|
||
| let fetched = if use_unified { | ||
| crate::deploy::pull_unified( | ||
| repo, | ||
| &target, | ||
| None, | ||
| &source, | ||
| target_imgref.as_ref(), | ||
| opts.quiet, | ||
| prog.clone(), | ||
| storage, | ||
|
|
@@ -1581,8 +1633,8 @@ async fn switch_ostree( | |
| } else { | ||
| crate::deploy::pull( | ||
| repo, | ||
| &target, | ||
| None, | ||
| &source, | ||
| target_imgref.as_ref(), | ||
| opts.quiet, | ||
| prog.clone(), | ||
| Some(&booted_ostree.deployment), | ||
|
|
@@ -2722,6 +2774,74 @@ mod tests { | |
| )); | ||
| } | ||
|
|
||
| #[test] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'd rather have tmt integration tests. Please take a look at the |
||
| fn test_parse_switch_target_imgref() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test isn't wrong exactly, but it's only testing syntax. Our real test suite uses tmt. |
||
| // Without --target-imgref, source and origin are the same reference. | ||
| let o = Opt::try_parse_from([ | ||
| "bootc", | ||
| "switch", | ||
| "--transport", | ||
| "containers-storage", | ||
| "localhost/someimage", | ||
| ]) | ||
| .unwrap(); | ||
| let opts = match o { | ||
| Opt::Switch(opts) => opts, | ||
| o => panic!("Expected switch opts, not {o:?}"), | ||
| }; | ||
| assert_eq!(opts.transport, "containers-storage"); | ||
| assert!(opts.target_imgref.is_none()); | ||
| assert!(target_imgref_for_switch(&opts).unwrap().is_none()); | ||
| let source = imgref_for_switch(&opts).unwrap(); | ||
| assert_eq!(source.transport, "containers-storage"); | ||
| assert_eq!(source.image, "localhost/someimage"); | ||
|
|
||
| // With --target-imgref, the image is fetched from --transport/<TARGET> but the | ||
| // origin recorded for upgrades is the (registry, by default) target imgref. | ||
| let o = Opt::try_parse_from([ | ||
| "bootc", | ||
| "switch", | ||
| "--transport", | ||
| "containers-storage", | ||
| "--target-imgref", | ||
| "quay.io/example/os:latest", | ||
| "localhost/someimage", | ||
| ]) | ||
| .unwrap(); | ||
| let opts = match o { | ||
| Opt::Switch(opts) => opts, | ||
| o => panic!("Expected switch opts, not {o:?}"), | ||
| }; | ||
| assert_eq!( | ||
| opts.target_imgref.as_deref(), | ||
| Some("quay.io/example/os:latest") | ||
| ); | ||
| assert_eq!(opts.target_transport, "registry"); | ||
| let source = imgref_for_switch(&opts).unwrap(); | ||
| assert_eq!(source.transport, "containers-storage"); | ||
| assert_eq!(source.image, "localhost/someimage"); | ||
| let target = target_imgref_for_switch(&opts).unwrap().unwrap(); | ||
| assert_eq!( | ||
| target.imgref.transport, | ||
| ostree_container::Transport::Registry | ||
| ); | ||
| assert_eq!(target.imgref.name, "quay.io/example/os:latest"); | ||
|
|
||
| // --target-imgref performs a real pull; --mutate-in-place performs none, so the | ||
| // combination is rejected rather than silently ignoring --target-imgref. | ||
| assert!( | ||
| Opt::try_parse_from([ | ||
| "bootc", | ||
| "switch", | ||
| "--mutate-in-place", | ||
| "--target-imgref", | ||
| "quay.io/example/os:latest", | ||
| "localhost/someimage", | ||
| ]) | ||
| .is_err() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_parse_selinux_is_unlabeled() { | ||
| let opt = Opt::try_parse_from([ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| # number: 49 | ||
| # tmt: | ||
| # summary: switch --target-imgref records a decoupled origin (composefs) | ||
| # duration: 30m | ||
| # extra: | ||
| # skip_if_ostree: true | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why skip for ostree? This should work for both backends |
||
| # fixme_skip_if_uki: true | ||
| # | ||
| # Verify that `bootc switch --target-imgref` fetches the image from the given | ||
| # source (here a local containers-storage copy) but persists the *decoupled* | ||
| # reference as the origin for subsequent upgrades (issue #2464), on the | ||
| # composefs backend. | ||
| # | ||
| # Skipped on UKI: the test switches to a freshly derived image, whose composefs | ||
| # digest won't match the one sealed into the booted UKI, so the switch is | ||
| # rejected before the origin is ever recorded (same limitation as | ||
| # test-composefs-gc). The decoupling logic is boot-type agnostic and is covered | ||
|
Comment on lines
+14
to
+17
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is true. We can and should be able to do this with UKI as well, all we're doing is changing the origin so I don't get why this wouldn't work. See |
||
| # on the BLS composefs matrix. | ||
| use std assert | ||
| use tap.nu | ||
|
|
||
| tap begin "bootc switch --target-imgref decouples pull source from origin" | ||
|
|
||
| # Make the booted image available in podman storage as our pull-source base. | ||
| bootc image copy-to-storage | ||
|
|
||
| # Derive a new image so its content — and therefore its composefs fs-verity | ||
| # digest — differs from the booted deployment; otherwise the same-digest guard | ||
| # (see test-43) would refuse the switch. This derived image is our pull source. | ||
| ("FROM localhost/bootc\n" | ||
| + "RUN touch /usr/share/testing-target-imgref\n") | podman build -t localhost/bootc-source -f - . | ||
|
|
||
| # The reference we want recorded as the origin for future upgrades. It is never | ||
| # pulled (so it needs no network / need not exist): switch fetches from the | ||
| # local source above and only *records* this as the origin. | ||
| let origin = "quay.io/example/os:latest" | ||
|
|
||
| # Fetch from the local containers-storage copy, but decouple the persisted origin. | ||
| bootc switch --transport containers-storage --target-imgref $origin localhost/bootc-source | ||
|
|
||
| # The staged deployment's origin must be the --target-imgref value, not the | ||
| # containers-storage source we actually pulled from. | ||
| let st = bootc status --json | from json | ||
| assert ($st.status.staged != null) "Expected a staged deployment after switch" | ||
| let staged = $st.status.staged.image | ||
| assert equal $staged.image.transport "registry" | ||
| assert equal $staged.image.image $origin | ||
|
Comment on lines
+38
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After this we should reboot and test upgrade to make sure it picks the right image to upgrade from |
||
|
|
||
| tap ok | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could also be an
impl SwitchOpts