Skip to content

Commit 893c24a

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 893c24a

2 files changed

Lines changed: 201 additions & 1 deletion

File tree

crates/lib/src/cli.rs

Lines changed: 170 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,27 @@ 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+
let current_image = host.spec.image.as_ref();
1064+
1065+
// Handle --list-tags: show available tags and exit
1066+
if opts.list_tags {
1067+
let image = current_image.ok_or_else(|| {
1068+
anyhow::anyhow!("--list-tags requires a booted image with a specified source")
1069+
})?;
1070+
return list_tags_for_current_image(image).await;
1071+
}
1072+
1073+
// Handle --tag: derive target from current image + new tag
1074+
let derived_image = if let Some(ref tag) = opts.tag {
1075+
let image = current_image.ok_or_else(|| {
1076+
anyhow::anyhow!("--tag requires a booted image with a specified source")
1077+
})?;
1078+
Some(derive_image_with_tag(image, tag)?)
1079+
} else {
1080+
None
1081+
};
1082+
1083+
let imgref = derived_image.as_ref().or(current_image);
10511084
let prog: ProgressWriter = opts.progress.try_into()?;
10521085

10531086
// If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
@@ -1220,6 +1253,70 @@ async fn upgrade(
12201253
Ok(())
12211254
}
12221255

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

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