@@ -123,6 +123,13 @@ pub(crate) struct UpgradeOpts {
123123 #[ clap( long, conflicts_with_all = [ "check" , "download_only" ] ) ]
124124 pub ( crate ) from_downloaded : bool ,
125125
126+ /// Upgrade to a different tag of the currently booted image.
127+ ///
128+ /// This derives the target image by replacing the tag portion of the current
129+ /// booted image reference.
130+ #[ clap( long) ]
131+ pub ( crate ) tag : Option < String > ,
132+
126133 #[ clap( flatten) ]
127134 pub ( crate ) progress : ProgressOptions ,
128135}
@@ -1047,7 +1054,19 @@ async fn upgrade(
10471054 let repo = & booted_ostree. repo ( ) ;
10481055
10491056 let host = crate :: status:: get_status ( booted_ostree) ?. 1 ;
1050- let imgref = host. spec . image . as_ref ( ) ;
1057+ let current_image = host. spec . image . as_ref ( ) ;
1058+
1059+ // Handle --tag: derive target from current image + new tag
1060+ let derived_image = if let Some ( ref tag) = opts. tag {
1061+ let image = current_image. ok_or_else ( || {
1062+ anyhow:: anyhow!( "--tag requires a booted image with a specified source" )
1063+ } ) ?;
1064+ Some ( image. with_tag ( tag) ?)
1065+ } else {
1066+ None
1067+ } ;
1068+
1069+ let imgref = derived_image. as_ref ( ) . or ( current_image) ;
10511070 let prog: ProgressWriter = opts. progress . try_into ( ) ?;
10521071
10531072 // If there's no specified image, let's be nice and check if the booted system is using rpm-ostree
@@ -1063,15 +1082,16 @@ async fn upgrade(
10631082 }
10641083 }
10651084
1066- let spec = RequiredHostSpec :: from_spec ( & host. spec ) ?;
1085+ let imgref = imgref. ok_or_else ( || anyhow:: anyhow!( "No image source specified" ) ) ?;
1086+ // Use the derived image reference (if --tag was specified) instead of the spec's image
1087+ let spec = RequiredHostSpec { image : imgref } ;
10671088 let booted_image = host
10681089 . status
10691090 . booted
10701091 . as_ref ( )
10711092 . map ( |b| b. query_image ( repo) )
10721093 . transpose ( ) ?
10731094 . flatten ( ) ;
1074- let imgref = imgref. ok_or_else ( || anyhow:: anyhow!( "No image source specified" ) ) ?;
10751095 // Find the currently queued digest, if any before we pull
10761096 let staged = host. status . staged . as_ref ( ) ;
10771097 let staged_image = staged. as_ref ( ) . and_then ( |s| s. image . as_ref ( ) ) ;
@@ -1099,16 +1119,17 @@ async fn upgrade(
10991119 }
11001120
11011121 if opts. check {
1102- let imgref = imgref. clone ( ) . into ( ) ;
1122+ let ostree_imgref = imgref. clone ( ) . into ( ) ;
11031123 let mut imp =
1104- crate :: deploy:: new_importer ( repo, & imgref, Some ( & booted_ostree. deployment ) ) . await ?;
1124+ crate :: deploy:: new_importer ( repo, & ostree_imgref, Some ( & booted_ostree. deployment ) )
1125+ . await ?;
11051126 match imp. prepare ( ) . await ? {
11061127 PrepareResult :: AlreadyPresent ( _) => {
1107- println ! ( "No changes in: {imgref :#}" ) ;
1128+ println ! ( "No changes in: {ostree_imgref :#}" ) ;
11081129 }
11091130 PrepareResult :: Ready ( r) => {
11101131 crate :: deploy:: check_bootc_label ( & r. config ) ;
1111- println ! ( "Update available for: {imgref :#}" ) ;
1132+ println ! ( "Update available for: {ostree_imgref :#}" ) ;
11121133 if let Some ( version) = r. version ( ) {
11131134 println ! ( " Version: {version}" ) ;
11141135 }
@@ -1236,7 +1257,6 @@ async fn upgrade(
12361257
12371258 Ok ( ( ) )
12381259}
1239-
12401260pub ( crate ) fn imgref_for_switch ( opts : & SwitchOpts ) -> Result < ImageReference > {
12411261 let transport = ostree_container:: Transport :: try_from ( opts. transport . as_str ( ) ) ?;
12421262 let imgref = ostree_container:: ImageReference {
@@ -2245,6 +2265,82 @@ mod tests {
22452265 assert_eq ! ( args. as_slice( ) , [ "container" , "image" , "pull" ] ) ;
22462266 }
22472267
2268+ #[ test]
2269+ fn test_parse_upgrade_options ( ) {
2270+ // Test upgrade with --tag
2271+ let o = Opt :: try_parse_from ( [ "bootc" , "upgrade" , "--tag" , "v1.1" ] ) . unwrap ( ) ;
2272+ match o {
2273+ Opt :: Upgrade ( opts) => {
2274+ assert_eq ! ( opts. tag, Some ( "v1.1" . to_string( ) ) ) ;
2275+ }
2276+ _ => panic ! ( "Expected Upgrade variant" ) ,
2277+ }
2278+
2279+ // Test that --tag works with --check (should compose naturally)
2280+ let o = Opt :: try_parse_from ( [ "bootc" , "upgrade" , "--tag" , "v1.1" , "--check" ] ) . unwrap ( ) ;
2281+ match o {
2282+ Opt :: Upgrade ( opts) => {
2283+ assert_eq ! ( opts. tag, Some ( "v1.1" . to_string( ) ) ) ;
2284+ assert ! ( opts. check) ;
2285+ }
2286+ _ => panic ! ( "Expected Upgrade variant" ) ,
2287+ }
2288+ }
2289+
2290+ #[ test]
2291+ fn test_image_reference_with_tag ( ) {
2292+ // Test basic tag replacement for registry transport
2293+ let current = ImageReference {
2294+ image : "quay.io/example/myapp:v1.0" . to_string ( ) ,
2295+ transport : "registry" . to_string ( ) ,
2296+ signature : None ,
2297+ } ;
2298+ let result = current. with_tag ( "v1.1" ) . unwrap ( ) ;
2299+ assert_eq ! ( result. image, "quay.io/example/myapp:v1.1" ) ;
2300+ assert_eq ! ( result. transport, "registry" ) ;
2301+
2302+ // Test tag replacement with digest (digest should be stripped for registry)
2303+ let current_with_digest = ImageReference {
2304+ image : "quay.io/example/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890" . to_string ( ) ,
2305+ transport : "registry" . to_string ( ) ,
2306+ signature : None ,
2307+ } ;
2308+ let result = current_with_digest. with_tag ( "v2.0" ) . unwrap ( ) ;
2309+ assert_eq ! ( result. image, "quay.io/example/myapp:v2.0" ) ;
2310+
2311+ // Test that non-registry transport works (containers-storage)
2312+ let containers_storage = ImageReference {
2313+ image : "localhost/myapp:v1.0" . to_string ( ) ,
2314+ transport : "containers-storage" . to_string ( ) ,
2315+ signature : None ,
2316+ } ;
2317+ let result = containers_storage. with_tag ( "v1.1" ) . unwrap ( ) ;
2318+ assert_eq ! ( result. image, "localhost/myapp:v1.1" ) ;
2319+ assert_eq ! ( result. transport, "containers-storage" ) ;
2320+
2321+ // Test digest stripping for non-registry transport
2322+ let containers_storage_with_digest = ImageReference {
2323+ image :
2324+ "localhost/myapp:v1.0@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
2325+ . to_string ( ) ,
2326+ transport : "containers-storage" . to_string ( ) ,
2327+ signature : None ,
2328+ } ;
2329+ let result = containers_storage_with_digest. with_tag ( "v2.0" ) . unwrap ( ) ;
2330+ assert_eq ! ( result. image, "localhost/myapp:v2.0" ) ;
2331+ assert_eq ! ( result. transport, "containers-storage" ) ;
2332+
2333+ // Test image without tag (edge case)
2334+ let no_tag = ImageReference {
2335+ image : "localhost/myapp" . to_string ( ) ,
2336+ transport : "containers-storage" . to_string ( ) ,
2337+ signature : None ,
2338+ } ;
2339+ let result = no_tag. with_tag ( "v1.0" ) . unwrap ( ) ;
2340+ assert_eq ! ( result. image, "localhost/myapp:v1.0" ) ;
2341+ assert_eq ! ( result. transport, "containers-storage" ) ;
2342+ }
2343+
22482344 #[ test]
22492345 fn test_generate_completion_scripts_contain_commands ( ) {
22502346 use clap_complete:: aot:: { Shell , generate} ;
0 commit comments