Skip to content

Commit 4e09e3a

Browse files
committed
cli: Add tag-aware upgrade operations
Implement bootc upgrade --tag and --list-tags to simplify tag-based image version management for customers who version images using tags. This adds: - bootc upgrade --tag <tag>: Upgrade to different tag of current image - bootc upgrade --list-tags: List available tags from registry - Automatic composition with --check for verification The --tag option derives the target image by replacing the tag portion of the current booted image reference. Only works when booted from registry transport images. Organizations version container images with tags (:latest, :dev, :test, :prod) and this allows them to upgrade between versions without retyping full registry paths or using switch (which is semantically about changing images, not versions). Assisted-by: Claude Sonnet 4.5 Signed-off-by: gursewak1997 <gursmangat@gmail.com>
1 parent adab93e commit 4e09e3a

2 files changed

Lines changed: 200 additions & 1 deletion

File tree

crates/lib/src/cli.rs

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ use indoc::indoc;
2929
use ostree::gio;
3030
use ostree_container::store::PrepareResult;
3131
use ostree_ext::container as ostree_container;
32+
use ostree_ext::container::Transport;
33+
use ostree_ext::oci_spec;
3234

3335
use ostree_ext::keyfileext::KeyFileExt;
3436
use ostree_ext::ostree;
@@ -123,6 +125,17 @@ pub(crate) struct UpgradeOpts {
123125
#[clap(long, conflicts_with_all = ["check", "download_only"])]
124126
pub(crate) from_downloaded: bool,
125127

128+
/// Upgrade to a different tag of the currently booted image.
129+
///
130+
/// This derives the target image by replacing the tag portion of the current
131+
/// booted image reference. Only works when booted from a registry image.
132+
#[clap(long)]
133+
pub(crate) tag: Option<String>,
134+
135+
/// List available tags for the currently booted image repository.
136+
#[clap(long, conflicts_with_all = ["apply", "download_only", "from_downloaded"])]
137+
pub(crate) list_tags: bool,
138+
126139
#[clap(flatten)]
127140
pub(crate) progress: ProgressOptions,
128141
}
@@ -1047,7 +1060,26 @@ async fn upgrade(
10471060
let repo = &booted_ostree.repo();
10481061

10491062
let host = crate::status::get_status(booted_ostree)?.1;
1050-
let imgref = host.spec.image.as_ref();
1063+
1064+
// Handle --list-tags: show available tags and exit
1065+
if opts.list_tags {
1066+
let current_image = host.spec.image
1067+
.as_ref()
1068+
.ok_or_else(|| anyhow::anyhow!("No image source specified in booted deployment"))?;
1069+
return list_tags_for_current_image(current_image).await;
1070+
}
1071+
1072+
// Handle --tag: derive target from current image + new tag
1073+
let derived_image = if let Some(ref tag) = opts.tag {
1074+
let current_image = host.spec.image
1075+
.as_ref()
1076+
.ok_or_else(|| anyhow::anyhow!("No image source specified in booted deployment"))?;
1077+
Some(derive_image_with_tag(current_image, tag)?)
1078+
} else {
1079+
None
1080+
};
1081+
1082+
let imgref = derived_image.as_ref().or_else(|| host.spec.image.as_ref());
10511083
let prog: ProgressWriter = opts.progress.try_into()?;
10521084

10531085
// If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
@@ -1220,6 +1252,70 @@ async fn upgrade(
12201252
Ok(())
12211253
}
12221254

1255+
/// Derive a new image reference by replacing the tag of the current image
1256+
fn derive_image_with_tag(current: &ImageReference, new_tag: &str) -> Result<ImageReference> {
1257+
// Only works for registry transport
1258+
if current.transport()? != Transport::Registry {
1259+
anyhow::bail!(
1260+
"The --tag option only works with registry images, current transport is '{}'",
1261+
current.transport
1262+
);
1263+
}
1264+
1265+
// Parse the current image reference
1266+
let reference: oci_spec::distribution::Reference = current.image.parse()?;
1267+
1268+
// Build new image reference with the new tag (stripping any digest)
1269+
let registry = reference.registry();
1270+
let repository = reference.repository();
1271+
let new_image = format!("{}/{}:{}", registry, repository, new_tag);
1272+
1273+
Ok(ImageReference {
1274+
image: new_image,
1275+
transport: current.transport.clone(),
1276+
signature: current.signature.clone(),
1277+
})
1278+
}
1279+
1280+
/// List available tags for the current image repository
1281+
async fn list_tags_for_current_image(current: &ImageReference) -> Result<()> {
1282+
// Only works for registry transport
1283+
if current.transport()? != Transport::Registry {
1284+
anyhow::bail!(
1285+
"The --list-tags option only works with registry images, current transport is '{}'",
1286+
current.transport
1287+
);
1288+
}
1289+
1290+
// Parse the image to extract repository (without tag/digest)
1291+
let reference: oci_spec::distribution::Reference = current.image.parse()?;
1292+
1293+
// Construct repository name: registry + repository
1294+
let registry = reference.registry();
1295+
let repository = reference.repository();
1296+
let repo_name = format!("{}/{}", registry, repository);
1297+
1298+
let repo = format!("docker://{}", repo_name);
1299+
1300+
// Use skopeo to list tags
1301+
let output = tokio::process::Command::new("skopeo")
1302+
.arg("list-tags")
1303+
.arg(&repo)
1304+
.output()
1305+
.await
1306+
.context("Failed to execute skopeo")?;
1307+
1308+
if !output.status.success() {
1309+
let stderr = String::from_utf8_lossy(&output.stderr);
1310+
anyhow::bail!("Failed to list tags: {}", stderr);
1311+
}
1312+
1313+
let stdout = String::from_utf8_lossy(&output.stdout);
1314+
println!("{}", stdout);
1315+
1316+
Ok(())
1317+
}
1318+
12231319
pub(crate) fn imgref_for_switch(opts: &SwitchOpts) -> Result<ImageReference> {
12241320
let transport = ostree_container::Transport::try_from(opts.transport.as_str())?;
12251321
let imgref = ostree_container::ImageReference {
@@ -2203,6 +2299,78 @@ mod tests {
22032299
assert_eq!(args.as_slice(), ["container", "image", "pull"]);
22042300
}
22052301

2302+
#[test]
2303+
fn test_parse_upgrade_options() {
2304+
// Test upgrade with --tag
2305+
let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1"]).unwrap();
2306+
match o {
2307+
Opt::Upgrade(opts) => {
2308+
assert_eq!(opts.tag, Some("v1.1".to_string()));
2309+
assert!(!opts.list_tags);
2310+
}
2311+
_ => panic!("Expected Upgrade variant"),
2312+
}
2313+
2314+
// Test upgrade with --list-tags
2315+
let o = Opt::try_parse_from(["bootc", "upgrade", "--list-tags"]).unwrap();
2316+
match o {
2317+
Opt::Upgrade(opts) => {
2318+
assert!(opts.tag.is_none());
2319+
assert!(opts.list_tags);
2320+
}
2321+
_ => panic!("Expected Upgrade variant"),
2322+
}
2323+
2324+
// Test that --list-tags and --apply are mutually exclusive
2325+
let result = Opt::try_parse_from(["bootc", "upgrade", "--list-tags", "--apply"]);
2326+
assert!(result.is_err());
2327+
2328+
// Test that --list-tags and --download-only are mutually exclusive
2329+
let result = Opt::try_parse_from(["bootc", "upgrade", "--list-tags", "--download-only"]);
2330+
assert!(result.is_err());
2331+
2332+
// Test that --tag works with --check (should compose naturally)
2333+
let o = Opt::try_parse_from(["bootc", "upgrade", "--tag", "v1.1", "--check"]).unwrap();
2334+
match o {
2335+
Opt::Upgrade(opts) => {
2336+
assert_eq!(opts.tag, Some("v1.1".to_string()));
2337+
assert!(opts.check);
2338+
}
2339+
_ => panic!("Expected Upgrade variant"),
2340+
}
2341+
}
2342+
2343+
#[test]
2344+
fn test_derive_image_with_tag() {
2345+
// Test basic tag replacement
2346+
let current = ImageReference {
2347+
image: "quay.io/example/myapp:v1.0".to_string(),
2348+
transport: "registry".to_string(),
2349+
signature: None,
2350+
};
2351+
let result = derive_image_with_tag(&current, "v1.1").unwrap();
2352+
assert_eq!(result.image, "quay.io/example/myapp:v1.1");
2353+
assert_eq!(result.transport, "registry");
2354+
2355+
// Test tag replacement with digest (digest should be stripped)
2356+
let current_with_digest = ImageReference {
2357+
image: "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890".to_string(),
2358+
transport: "registry".to_string(),
2359+
signature: None,
2360+
};
2361+
let result = derive_image_with_tag(&current_with_digest, "v2.0").unwrap();
2362+
assert_eq!(result.image, "quay.io/example/myapp:v2.0");
2363+
2364+
// Test that non-registry transport fails
2365+
let non_registry = ImageReference {
2366+
image: "/path/to/oci".to_string(),
2367+
transport: "oci".to_string(),
2368+
signature: None,
2369+
};
2370+
let result = derive_image_with_tag(&non_registry, "v1.1");
2371+
assert!(result.is_err());
2372+
}
2373+
22062374
#[test]
22072375
fn test_generate_completion_scripts_contain_commands() {
22082376
use clap_complete::aot::{Shell, generate};

docs/src/man/bootc-upgrade.8.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,21 @@ Soft reboot allows faster system restart by avoiding full hardware reboot when p
6969

7070
Apply a staged deployment that was previously downloaded with --download-only
7171

72+
**--tag**=*TAG*
73+
74+
Upgrade to a different tag of the currently booted image
75+
76+
This derives the target image by replacing the tag portion of the current booted image reference.
77+
Only works when booted from a registry image. Can be combined with --check to verify the tag
78+
without staging the deployment.
79+
80+
**--list-tags**
81+
82+
List available tags for the currently booted image repository
83+
84+
Queries the registry for all available tags. Only works when booted from a registry image.
85+
This option exits after displaying tags and does not perform an upgrade operation.
86+
7287
<!-- END GENERATED OPTIONS -->
7388

7489
# EXAMPLES
@@ -85,6 +100,22 @@ Upgrade with soft reboot if possible:
85100

86101
bootc upgrade --apply --soft-reboot=auto
87102

103+
Upgrade to a different tag:
104+
105+
bootc upgrade --tag v1.2
106+
107+
List available tags for the current image:
108+
109+
bootc upgrade --list-tags
110+
111+
Check if a specific tag has updates before applying:
112+
113+
bootc upgrade --tag prod --check
114+
115+
Upgrade to a tag and immediately apply:
116+
117+
bootc upgrade --tag v2.0 --apply
118+
88119
# SEE ALSO
89120

90121
**bootc**(8), **bootc-switch**(8), **bootc-status**(8), **bootc-rollback**(8)

0 commit comments

Comments
 (0)