Skip to content

feat(switch): add --target-imgref to decouple pull source from upgrade origin - #2467

Open
Jorge-Polanco-Roque wants to merge 4 commits into
bootc-dev:mainfrom
Jorge-Polanco-Roque:feat/switch-target-imgref
Open

Jorge-Polanco-Roque wants to merge 4 commits into
bootc-dev:mainfrom
Jorge-Polanco-Roque:feat/switch-target-imgref

Conversation

@Jorge-Polanco-Roque

Copy link
Copy Markdown

Problem

Following a customer case: after losing connectivity to their registry, they side-loaded an OCI image (scp + podman load) and applied it with bootc switch --transport containers-storage <img>. That worked for the immediate switch, but subsequent upgrades then tried to fetch from containers-storage — because the source used to pull is also persisted as the origin for upgrades. There was no way to say "pull from here now, but track this other imgref for upgrades."

Closes: #2464

Approach

Add a --target-imgref option (plus --target-transport, defaulting to registry) to bootc switch, mirroring what bootc install already exposes via InstallTargetOpts. This decouples two concepts that switch previously conflated:

  • source — where the image is pulled from now (--transport + the positional target).
  • origin — the imgref persisted for future upgrades (--target-imgref if given, otherwise the source).

The underlying mechanism already existed: deploy::pull/pull_unified accept a target_imgref that install uses and switch was passing as None. This change wires switch to build that target ref the same way install does, so no new pull machinery is introduced.

The experimental composefs backend currently dereferences a single ref through do_upgrade; rather than silently ignore the new option there, it returns a clear error that --target-imgref is not yet supported on that backend.

Testing

  • Added test_parse_switch_target_imgref (clap parsing + propagation through both helpers): confirms source stays on containers-storage while the target-imgref resolves to the registry transport.
  • Built and tested bootc-lib on Fedora (matching CI deps): cargo test -p bootc-lib passes, cargo fmt --check clean, and cargo clippy introduces no new warnings in the touched files.

Notes

Open questions for maintainers, happy to adjust: whether to keep --target-transport (install parity) or require the transport inside the imgref; the conflicts_with = from_downloaded guard; and the composefs-backend follow-up.

Disclosure: this change was prepared with AI assistance for code review and drafting; a human authored and reviewed it and signed off (DCO).

…e origin

When an image is side-loaded (e.g. scp + podman load into containers-storage
after losing registry connectivity) and applied with
`bootc switch --transport containers-storage <img>`, bootc would then keep
fetching upgrades from containers-storage.

Add `--target-imgref` (and `--target-transport`, mirroring `bootc install`)
so the image can be pulled from a local source now while a different imgref is
persisted as the origin for future upgrades. Reuses the existing target_imgref
mechanism that deploy::pull already accepted and switch passed as None.

Closes: bootc-dev#2464

Assisted-by: Claude (AI)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jorge Polanco <55784702+Jorge-Polanco-Roque@users.noreply.github.com>
@bootc-bot
bootc-bot Bot requested a review from cgwalters September 16, 2026 14:15
Ran `cargo xtask update-generated` after adding the --target-imgref and
--target-transport options so the generated man page matches the CLI and
the validate check passes.

Assisted-by: Claude (AI)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jorge Polanco <55784702+Jorge-Polanco-Roque@users.noreply.github.com>
@github-actions github-actions Bot added the area/documentation Updates to the documentation label Sep 16, 2026
@cgwalters
cgwalters requested a balanced review from Copilot September 16, 2026 22:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The unchanged-spec fast path defeats the primary recovery scenario, and some option combinations are silently ignored.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds separate pull-source and future-upgrade-origin handling to bootc switch.

Changes:

  • Adds --target-imgref and --target-transport.
  • Passes the target origin through OSTree pull paths.
  • Rejects --target-imgref on composefs.
File summaries
File Description
docs/src/man/bootc-switch.8.md Documents the new options.
crates/lib/src/cli.rs Implements parsing and OSTree switching behavior.
crates/lib/src/bootc_composefs/switch.rs Reports unsupported composefs usage.
Review details

Suppressed comments (1)

crates/lib/src/cli.rs:182

  • An explicitly supplied --target-transport is silently ignored when --target-imgref is absent because target_imgref_for_switch returns before parsing it. This accepts meaningless input (including invalid transport names) and can leave users believing a different upgrade origin was recorded. Make this option require --target-imgref (or define and implement standalone semantics).
    #[clap(long, default_value = "registry")]
    pub(crate) target_transport: String,
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/lib/src/cli.rs Outdated
Comment on lines +176 to +177
#[clap(long, conflicts_with = "from_downloaded")]
pub(crate) target_imgref: Option<String>,
Comment thread crates/lib/src/cli.rs
Comment on lines +1563 to +1565
let origin_ref = match target_imgref.as_ref() {
Some(t) => ImageReference::from(t.clone()),
None => source.clone(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 /run/composefs/staged-deployment

…-source changes

Address review: reject --target-imgref with the no-pull --mutate-in-place
mode (conflicts_with), and don't short-circuit switch as a no-op when only
the origin is unchanged but the pull source differs — that is exactly the
containers-storage recovery case from bootc-dev#2464.

Assisted-by: Claude (AI)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jorge Polanco <55784702+Jorge-Polanco-Roque@users.noreply.github.com>

@Johan-Liebert1 Johan-Liebert1 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for starting this

}

if opts.target_imgref.is_some() {
anyhow::bail!("--target-imgref is not yet supported with the composefs backend");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep both backends in sync

Comment thread crates/lib/src/cli.rs
));
}

#[test]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 tmt directory in repository root

Comment thread crates/lib/src/cli.rs
Comment on lines +1563 to +1565
let origin_ref = match target_imgref.as_ref() {
Some(t) => ImageReference::from(t.clone()),
None => source.clone(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Comment thread crates/lib/src/cli.rs
Comment on lines +1563 to +1565
let origin_ref = match target_imgref.as_ref() {
Some(t) => ImageReference::from(t.clone()),
None => source.clone(),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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 /run/composefs/staged-deployment

Comment thread crates/lib/src/cli.rs
}

#[test]
fn test_parse_switch_target_imgref() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Comment thread crates/lib/src/cli.rs
/// 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(

Copy link
Copy Markdown
Collaborator

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

@Jorge-Polanco-Roque

Copy link
Copy Markdown
Author

Thanks for the review — pushed a follow-up commit.

  1. switch.rs "keep both backends in sync": done. The composefs backend now supports --target-imgref instead of bailing. It pulls from the source and records the decoupled reference as the origin, same as ostree.

  2. tmt integration test: added test-switch-target-imgref.nu (test-49, skip_if_ostree). It derives a source image, switches with --target-imgref, and asserts the staged origin is the target ref, not the containers-storage source we pulled from. Kept the parse unit test too.

  3. On persisting the origin: I found the decoupled ref already lands in the persistent .origin (ORIGIN_CONTAINER) via write_composefs_state, so plain --target-imgref switches don't need an extra field in /run/composefs/staged-deployment — the origin survives to the next upgrade through the existing state write. Let me know if you were picturing the field for a case I'm missing.

One thing I deliberately left out: the identical-content recovery case (source is a byte-identical copy of the booted image, re-pointing only the origin) still hits the existing fs-verity same-digest guard in do_upgrade/validate_update and errors. That guard is duplicated in two places, and relaxing it safely feels like your call on the design — happy to follow up with the "rewrite origin in place" semantics (this may be where the staged-deployment field earns its keep). Wanted to check the direction before touching a safety guard.

The composefs backend previously bailed with "not yet supported" for
`switch --target-imgref`. Thread the decoupled origin through do_upgrade
so the image is fetched from the source (e.g. a local containers-storage
copy) while the reference persisted as the origin for future upgrades is
the `--target-imgref` value, matching the ostree backend (issue bootc-dev#2464).

- DoUpgradeOpts gains `origin_override`; write_composefs_state records it
  as ORIGIN_CONTAINER, falling back to the pull source when absent.
- The unchanged fast path is skipped when `--target-imgref` is set, so a
  switch from a different source keeping the same origin still pulls.
- The switch journal entry now records both source and target images.

Adds tmt/tests/booted/test-switch-target-imgref.nu covering composefs.

Generated-by: AI
I'm familiar with this area and reviewed the change; build/tests run in CI.

Signed-off-by: Jorge Polanco <55784702+Jorge-Polanco-Roque@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/documentation Updates to the documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --target-imgref option to bootc switch

4 participants