@@ -64,7 +64,7 @@ struct PullSpec {
6464 gpu : bool ,
6565}
6666
67- pub ( crate ) async fn cmd_image ( cmd : ImageCmd ) -> Result < ( ) > {
67+ pub ( crate ) async fn cmd_image ( cmd : ImageCmd , release_api_base_url : & str ) -> Result < ( ) > {
6868 match cmd {
6969 ImageCmd :: Pull {
7070 version,
@@ -75,7 +75,15 @@ pub(crate) async fn cmd_image(cmd: ImageCmd) -> Result<()> {
7575 } => {
7676 let image_dir = loc. dir ( ) ;
7777 validate_image_dir ( & image_dir) ?;
78- pull ( version. as_deref ( ) , gpu, & image_dir, force, insecure) . await ?;
78+ pull (
79+ version. as_deref ( ) ,
80+ gpu,
81+ & image_dir,
82+ force,
83+ insecure,
84+ release_api_base_url,
85+ )
86+ . await ?;
7987 Ok ( ( ) )
8088 }
8189 ImageCmd :: List { loc } => {
@@ -98,6 +106,7 @@ pub(crate) async fn pull(
98106 image_dir : & str ,
99107 force : bool ,
100108 insecure : bool ,
109+ release_api_base_url : & str ,
101110) -> Result < String > {
102111 println ! (
103112 "dstackup image pull — {} image" ,
@@ -107,7 +116,7 @@ pub(crate) async fn pull(
107116 "unified"
108117 }
109118 ) ;
110- let release = fetch_release ( version) . await ?;
119+ let release = fetch_release ( version, release_api_base_url ) . await ?;
111120
112121 let asset = pick_asset ( & release. assets , gpu) . with_context ( || {
113122 format ! (
@@ -376,6 +385,7 @@ pub(crate) async fn resolve_or_pull_image(
376385 requested : Option < & str > ,
377386 require : bool ,
378387 required_files : & [ & str ] ,
388+ release_api_base_url : & str ,
379389) -> Result < Option < String > > {
380390 if let Some ( name) = requested {
381391 if !valid_image_name ( name) {
@@ -391,7 +401,15 @@ pub(crate) async fn resolve_or_pull_image(
391401 }
392402 if let Some ( spec) = pull_spec ( name) {
393403 println ! ( " [..] image {name} not found locally; downloading it" ) ;
394- let pulled = pull ( Some ( & spec. version ) , spec. gpu , image_dir, false , false ) . await ?;
404+ let pulled = pull (
405+ Some ( & spec. version ) ,
406+ spec. gpu ,
407+ image_dir,
408+ false ,
409+ false ,
410+ release_api_base_url,
411+ )
412+ . await ?;
395413 ensure_image_has_required_files ( image_dir, & pulled, required_files) ?;
396414 return Ok ( Some ( pulled) ) ;
397415 }
@@ -459,7 +477,7 @@ pub(crate) async fn resolve_or_pull_image(
459477 } else {
460478 println ! ( " [..] no local guest image found; downloading the latest cpu image" ) ;
461479 }
462- let pulled = pull ( None , false , image_dir, false , false ) . await ?;
480+ let pulled = pull ( None , false , image_dir, false , false , release_api_base_url ) . await ?;
463481
464482 if Path :: new ( image_dir)
465483 . join ( & pulled)
@@ -599,18 +617,19 @@ fn missing_named_image_message(image_dir: &str, name: &str) -> String {
599617/// released from this monorepo. Do not probe the new repository first for old
600618/// versions: the version boundary is authoritative and avoids redundant or
601619/// misleading requests.
602- async fn fetch_release ( version : Option < & str > ) -> Result < Release > {
620+ async fn fetch_release ( version : Option < & str > , release_api_base_url : & str ) -> Result < Release > {
603621 let client = reqwest:: Client :: new ( ) ;
604622 if let Some ( version) = version {
605- let ( version, url, releases_url) = tagged_release_location ( version) ?;
623+ let ( version, url, releases_url) = tagged_release_location ( version, release_api_base_url ) ?;
606624 return fetch_tagged_release ( & client, & url, releases_url)
607625 . await ?
608626 . with_context ( || {
609627 format ! ( "guest-OS version {version} was not found; check {releases_url}" )
610628 } ) ;
611629 }
612630
613- let list_url = format ! ( "https://api.github.com/repos/{REPO}/releases?per_page=100" ) ;
631+ let api_base = release_api_base_url. trim_end_matches ( '/' ) ;
632+ let list_url = format ! ( "{api_base}/{REPO}/releases?per_page=100" ) ;
614633 let releases: Vec < Release > = client
615634 . get ( & list_url)
616635 . header ( "user-agent" , "dstackup" )
@@ -630,13 +649,16 @@ async fn fetch_release(version: Option<&str>) -> Result<Release> {
630649 return Ok ( release) ;
631650 }
632651
633- let legacy_url = format ! ( "https://api.github.com/repos /{LEGACY_REPO}/releases/latest" ) ;
652+ let legacy_url = format ! ( "{api_base} /{LEGACY_REPO}/releases/latest" ) ;
634653 fetch_tagged_release ( & client, & legacy_url, LEGACY_RELEASES_URL )
635654 . await ?
636655 . with_context ( || format ! ( "no guest-OS release found; check {RELEASES_URL}" ) )
637656}
638657
639- fn tagged_release_location ( version : & str ) -> Result < ( String , String , & ' static str ) > {
658+ fn tagged_release_location (
659+ version : & str ,
660+ release_api_base_url : & str ,
661+ ) -> Result < ( String , String , & ' static str ) > {
640662 let version = version
641663 . trim_start_matches ( RELEASE_TAG_PREFIX )
642664 . trim_start_matches ( 'v' ) ;
@@ -648,7 +670,10 @@ fn tagged_release_location(version: &str) -> Result<(String, String, &'static st
648670 } ;
649671 Ok ( (
650672 version. to_string ( ) ,
651- format ! ( "https://api.github.com/repos/{repo}/releases/tags/{tag_prefix}{version}" ) ,
673+ format ! (
674+ "{}/{repo}/releases/tags/{tag_prefix}{version}" ,
675+ release_api_base_url. trim_end_matches( '/' )
676+ ) ,
652677 releases_url,
653678 ) )
654679}
@@ -830,7 +855,8 @@ mod tests {
830855 #[ test]
831856 fn routes_pinned_releases_at_the_monorepo_boundary ( ) {
832857 for version in [ "0.5.11" , "v0.5.11" , "guest-os-v0.5.11" ] {
833- let ( normalized, url, releases_url) = tagged_release_location ( version) . unwrap ( ) ;
858+ let ( normalized, url, releases_url) =
859+ tagged_release_location ( version, crate :: cli:: DEFAULT_RELEASE_API_BASE_URL ) . unwrap ( ) ;
834860 assert_eq ! ( normalized, "0.5.11" ) ;
835861 assert_eq ! (
836862 url,
@@ -840,7 +866,8 @@ mod tests {
840866 }
841867
842868 for version in [ "0.6.0" , "0.6.0.a2" , "1.0.0" ] {
843- let ( normalized, url, releases_url) = tagged_release_location ( version) . unwrap ( ) ;
869+ let ( normalized, url, releases_url) =
870+ tagged_release_location ( version, crate :: cli:: DEFAULT_RELEASE_API_BASE_URL ) . unwrap ( ) ;
844871 assert_eq ! ( normalized, version) ;
845872 assert_eq ! (
846873 url,
@@ -853,10 +880,22 @@ mod tests {
853880 #[ test]
854881 fn rejects_versions_without_a_numeric_core ( ) {
855882 for version in [ "0.6" , "latest" , "0.x.0" , "0.6.x" ] {
856- assert ! ( tagged_release_location( version) . is_err( ) , "{version}" ) ;
883+ assert ! (
884+ tagged_release_location( version, crate :: cli:: DEFAULT_RELEASE_API_BASE_URL ) . is_err( ) ,
885+ "{version}"
886+ ) ;
857887 }
858888 }
859889
890+ #[ test]
891+ fn release_api_base_url_is_configurable_and_trailing_slash_safe ( ) {
892+ let ( _, url, _) = tagged_release_location ( "0.6.0" , "http://127.0.0.1:1234/api/" ) . unwrap ( ) ;
893+ assert_eq ! (
894+ url,
895+ "http://127.0.0.1:1234/api/Dstack-TEE/dstack/releases/tags/guest-os-v0.6.0"
896+ ) ;
897+ }
898+
860899 #[ test]
861900 fn messages_mention_the_pull_command ( ) {
862901 assert ! ( no_image_message( "/d" ) . contains( "dstackup image pull" ) ) ;
0 commit comments