@@ -701,25 +701,35 @@ pub async fn sandbox_create(
701701 . collect ( ) ,
702702 } ;
703703
704- let response = match client. create_sandbox ( request) . await {
705- Ok ( resp) => resp,
706- Err ( status) if status. code ( ) == Code :: AlreadyExists => {
707- return Err ( miette:: miette!(
708- "{}\n \n hint: delete it first with: openshell sandbox delete <name>\n or use a different name" ,
709- status. message( )
710- ) ) ;
711- }
712- Err ( status) => return Err ( miette:: miette!( status. to_string( ) ) ) ,
704+ // Drop response metadata before waiting for sandbox lifecycle events.
705+ let ( sandbox, service_urls) = {
706+ let response = match client. create_sandbox ( request) . await {
707+ Ok ( resp) => resp,
708+ Err ( status) if status. code ( ) == Code :: AlreadyExists => {
709+ return Err ( miette:: miette!(
710+ "{}\n \n hint: delete it first with: openshell sandbox delete <name>\n or use a different name" ,
711+ status. message( )
712+ ) ) ;
713+ }
714+ Err ( status) => return Err ( miette:: miette!( status. to_string( ) ) ) ,
715+ } ;
716+ let dedicated_service_port = service_ingress_port ( & response) ;
717+ let response = response. into_inner ( ) ;
718+ let service_urls = response
719+ . service_urls
720+ . into_iter ( )
721+ . map ( |( service, url) | {
722+ (
723+ service,
724+ service_url_for_gateway ( & url, & effective_server, dedicated_service_port) ,
725+ )
726+ } )
727+ . collect :: < HashMap < _ , _ > > ( ) ;
728+ let sandbox = response
729+ . sandbox
730+ . ok_or_else ( || miette:: miette!( "sandbox missing from response" ) ) ?;
731+ ( sandbox, service_urls)
713732 } ;
714- let response = response. into_inner ( ) ;
715- let service_urls = response
716- . service_urls
717- . into_iter ( )
718- . map ( |( service, url) | ( service, service_url_for_gateway ( & url, & effective_server) ) )
719- . collect :: < HashMap < _ , _ > > ( ) ;
720- let sandbox = response
721- . sandbox
722- . ok_or_else ( || miette:: miette!( "sandbox missing from response" ) ) ?;
723733
724734 let interactive = std:: io:: stdout ( ) . is_terminal ( ) ;
725735 let sandbox_name = if sandbox. object_name ( ) . is_empty ( ) {
@@ -1117,10 +1127,7 @@ pub async fn sandbox_create(
11171127 "\u{2713} " . green( ) . bold( ) ,
11181128 ) ;
11191129 if let Some ( url) = service_urls. get ( "" ) . filter ( |url| !url. is_empty ( ) ) {
1120- eprintln ! (
1121- " Access at: {}" ,
1122- service_url_for_gateway( url, & effective_server)
1123- ) ;
1130+ eprintln ! ( " Access at: {url}" ) ;
11241131 }
11251132 }
11261133
@@ -4027,7 +4034,7 @@ pub async fn service_expose(
40274034 ) ;
40284035 }
40294036 if !response. url . is_empty ( ) {
4030- let url = service_url_for_gateway ( & response. url , server ) ;
4037+ let url = & response. url ;
40314038 println ! ( " URL: {}" , url. cyan( ) ) ;
40324039 }
40334040 Ok ( ( ) )
@@ -4043,7 +4050,7 @@ async fn expose_service_endpoint(
40434050 tls : & TlsOptions ,
40444051) -> Result < ServiceEndpointResponse > {
40454052 let mut client = grpc_client ( server, tls) . await ?;
4046- client
4053+ let response = client
40474054 . expose_service ( ExposeServiceRequest {
40484055 request_id : String :: new ( ) ,
40494056 sandbox : sandbox. to_string ( ) ,
@@ -4056,8 +4063,11 @@ async fn expose_service_endpoint(
40564063 ) ) ,
40574064 } )
40584065 . await
4059- . map_err ( service_expose_status_error)
4060- . map ( tonic:: Response :: into_inner)
4066+ . map_err ( service_expose_status_error) ?;
4067+ let dedicated_service_port = service_ingress_port ( & response) ;
4068+ let mut response = response. into_inner ( ) ;
4069+ response. url = service_url_for_gateway ( & response. url , server, dedicated_service_port) ;
4070+ Ok ( response)
40614071}
40624072
40634073fn service_expose_status_error ( status : Status ) -> miette:: Report {
@@ -4088,14 +4098,15 @@ pub async fn service_list(
40884098 } ) ,
40894099 } )
40904100 . await
4091- . map_err ( |status| service_status_error ( "list services" , "sandbox:read" , status) ) ?
4092- . into_inner ( ) ;
4101+ . map_err ( |status| service_status_error ( "list services" , "sandbox:read" , status) ) ?;
4102+ let dedicated_service_port = service_ingress_port ( & response) ;
4103+ let response = response. into_inner ( ) ;
40934104
40944105 let next_page_token = response. next_page_token . clone ( ) ;
40954106 let services = response
40964107 . services
40974108 . iter ( )
4098- . filter_map ( |response| service_endpoint_to_json ( response, server) )
4109+ . filter_map ( |response| service_endpoint_to_json ( response, server, dedicated_service_port ) )
40994110 . collect :: < Vec < _ > > ( ) ;
41004111 if crate :: output:: print_paginated_output_collection (
41014112 output,
@@ -4117,7 +4128,12 @@ pub async fn service_list(
41174128 return Ok ( ( ) ) ;
41184129 }
41194130
4120- print_service_endpoint_table ( & response. services , server, all_workspaces) ;
4131+ print_service_endpoint_table (
4132+ & response. services ,
4133+ server,
4134+ all_workspaces,
4135+ dedicated_service_port,
4136+ ) ;
41214137 Ok ( ( ) )
41224138}
41234139
@@ -4138,10 +4154,11 @@ pub async fn service_get(
41384154 name : service. to_string ( ) ,
41394155 } )
41404156 . await
4141- . map_err ( |status| service_status_error ( "get service" , "sandbox:read" , status) ) ?
4142- . into_inner ( ) ;
4157+ . map_err ( |status| service_status_error ( "get service" , "sandbox:read" , status) ) ?;
4158+ let dedicated_service_port = service_ingress_port ( & response) ;
4159+ let response = response. into_inner ( ) ;
41434160
4144- print_service_endpoint_table ( & [ response] , server, false ) ;
4161+ print_service_endpoint_table ( & [ response] , server, false , dedicated_service_port ) ;
41454162 Ok ( ( ) )
41464163}
41474164
@@ -4212,6 +4229,7 @@ fn print_service_endpoint_table(
42124229 services : & [ ServiceEndpointResponse ] ,
42134230 gateway_endpoint : & str ,
42144231 all_workspaces : bool ,
4232+ dedicated_service_port : Option < u16 > ,
42154233) {
42164234 let rows = services
42174235 . iter ( )
@@ -4227,7 +4245,7 @@ fn print_service_endpoint_table(
42274245 let url = if response. url . is_empty ( ) {
42284246 String :: new ( )
42294247 } else {
4230- service_url_for_gateway ( & response. url , gateway_endpoint)
4248+ service_url_for_gateway ( & response. url , gateway_endpoint, dedicated_service_port )
42314249 } ;
42324250 Some ( (
42334251 workspace. to_string ( ) ,
@@ -4309,6 +4327,7 @@ fn print_service_endpoint_table(
43094327fn service_endpoint_to_json (
43104328 response : & ServiceEndpointResponse ,
43114329 gateway_endpoint : & str ,
4330+ dedicated_service_port : Option < u16 > ,
43124331) -> Option < serde_json:: Value > {
43134332 let endpoint = response. endpoint . as_ref ( ) ?;
43144333 let workspace = endpoint
@@ -4318,7 +4337,7 @@ fn service_endpoint_to_json(
43184337 let url = if response. url . is_empty ( ) {
43194338 String :: new ( )
43204339 } else {
4321- service_url_for_gateway ( & response. url , gateway_endpoint)
4340+ service_url_for_gateway ( & response. url , gateway_endpoint, dedicated_service_port )
43224341 } ;
43234342
43244343 Some ( serde_json:: json!( {
@@ -4342,25 +4361,32 @@ fn service_authorization_mode_name(mode: i32) -> &'static str {
43424361 }
43434362}
43444363
4345- /// Read gcloud Application Default Credentials from disk.
4346- ///
4347- /// Returns `(client_id, client_secret, refresh_token)`.
4348- ///
4349- /// Checks `GOOGLE_APPLICATION_CREDENTIALS` first; falls back to
4350- /// `$CLOUDSDK_CONFIG/application_default_credentials.json` when set, then to
4351- /// `~/.config/gcloud/application_default_credentials.json`.
4352- fn service_url_for_gateway ( service_url : & str , gateway_endpoint : & str ) -> String {
4353- let ( Ok ( mut service_url) , Ok ( gateway_endpoint) ) = (
4354- url:: Url :: parse ( service_url) ,
4355- url:: Url :: parse ( gateway_endpoint) ,
4356- ) else {
4364+ /// Dedicated ingress is independent of externally forwarded control-plane ports.
4365+ fn service_ingress_port < T > ( response : & tonic:: Response < T > ) -> Option < u16 > {
4366+ response
4367+ . metadata ( )
4368+ . get ( openshell_core:: config:: SERVICE_PORT_METADATA_KEY )
4369+ . and_then ( |value| value. to_str ( ) . ok ( ) )
4370+ . and_then ( |value| value. parse :: < u16 > ( ) . ok ( ) )
4371+ . filter ( |port| * port != 0 )
4372+ }
4373+
4374+ fn service_url_for_gateway (
4375+ service_url : & str ,
4376+ gateway_endpoint : & str ,
4377+ dedicated_service_port : Option < u16 > ,
4378+ ) -> String {
4379+ let Ok ( mut service_url) = url:: Url :: parse ( service_url) else {
43574380 return service_url. to_string ( ) ;
43584381 } ;
4359-
4360- if service_url
4361- . set_port ( gateway_endpoint. port_or_known_default ( ) )
4362- . is_err ( )
4363- {
4382+ let port = if let Some ( port) = dedicated_service_port {
4383+ Some ( port)
4384+ } else if let Ok ( gateway) = url:: Url :: parse ( gateway_endpoint) {
4385+ gateway. port_or_known_default ( )
4386+ } else {
4387+ return service_url. to_string ( ) ;
4388+ } ;
4389+ if service_url. set_port ( port) . is_err ( ) {
43644390 return service_url. to_string ( ) ;
43654391 }
43664392
@@ -6927,7 +6953,7 @@ mod tests {
69276953 url : "https://api.openshell.localhost:3000/" . to_string ( ) ,
69286954 } ;
69296955
6930- let value = service_endpoint_to_json ( & response, "https://gateway.example:17670" )
6956+ let value = service_endpoint_to_json ( & response, "https://gateway.example:17670" , None )
69316957 . expect ( "service endpoint JSON" ) ;
69326958 assert_eq ! (
69336959 value,
@@ -6940,7 +6966,9 @@ mod tests {
69406966 "url" : "https://api.openshell.localhost:17670/" ,
69416967 } )
69426968 ) ;
6943- assert ! ( service_endpoint_to_json( & ServiceEndpointResponse :: default ( ) , "unused" ) . is_none( ) ) ;
6969+ assert ! (
6970+ service_endpoint_to_json( & ServiceEndpointResponse :: default ( ) , "unused" , None ) . is_none( )
6971+ ) ;
69446972 }
69456973
69466974 #[ test]
@@ -7633,18 +7661,61 @@ mod tests {
76337661 assert_eq ! (
76347662 service_url_for_gateway(
76357663 "https://quiet-flamingo--notebook.navigator.openshell.localhost:8080/" ,
7636- "https://127.0.0.1:31886"
7664+ "https://127.0.0.1:31886" ,
7665+ None
76377666 ) ,
76387667 "https://quiet-flamingo--notebook.navigator.openshell.localhost:31886/"
76397668 ) ;
76407669 }
76417670
7671+ #[ test]
7672+ fn service_url_for_gateway_preserves_dedicated_service_port ( ) {
7673+ // A replay can contain a URL from an earlier listener configuration.
7674+ assert_eq ! (
7675+ service_url_for_gateway(
7676+ "https://default--app--web.example.com:8080/" ,
7677+ "https://127.0.0.1:31886" ,
7678+ Some ( 8082 )
7679+ ) ,
7680+ "https://default--app--web.example.com:8082/"
7681+ ) ;
7682+ assert_eq ! (
7683+ service_url_for_gateway(
7684+ "https://default--app--web.example.com:8082/" ,
7685+ "https://127.0.0.1:31886" ,
7686+ Some ( 8082 )
7687+ ) ,
7688+ "https://default--app--web.example.com:8082/"
7689+ ) ;
7690+ assert_eq ! (
7691+ service_url_for_gateway(
7692+ "https://default--app--web.example.com/" ,
7693+ "https://127.0.0.1:31886" ,
7694+ Some ( 443 )
7695+ ) ,
7696+ "https://default--app--web.example.com/"
7697+ ) ;
7698+ let mut response = tonic:: Response :: new ( ( ) ) ;
7699+ assert_eq ! ( super :: service_ingress_port( & response) , None ) ;
7700+ response. metadata_mut ( ) . insert (
7701+ openshell_core:: config:: SERVICE_PORT_METADATA_KEY ,
7702+ "8082" . parse ( ) . unwrap ( ) ,
7703+ ) ;
7704+ assert_eq ! ( super :: service_ingress_port( & response) , Some ( 8082 ) ) ;
7705+ response. metadata_mut ( ) . insert (
7706+ openshell_core:: config:: SERVICE_PORT_METADATA_KEY ,
7707+ "0" . parse ( ) . unwrap ( ) ,
7708+ ) ;
7709+ assert_eq ! ( super :: service_ingress_port( & response) , None ) ;
7710+ }
7711+
76427712 #[ test]
76437713 fn service_url_for_gateway_omits_default_external_port ( ) {
76447714 assert_eq ! (
76457715 service_url_for_gateway(
76467716 "https://quiet-flamingo--notebook.navigator.openshell.localhost:8080/" ,
7647- "https://gateway.example.com"
7717+ "https://gateway.example.com" ,
7718+ None
76487719 ) ,
76497720 "https://quiet-flamingo--notebook.navigator.openshell.localhost/"
76507721 ) ;
@@ -7655,7 +7726,8 @@ mod tests {
76557726 assert_eq ! (
76567727 service_url_for_gateway(
76577728 "http://quiet-flamingo--notebook.navigator.openshell.localhost:8080/" ,
7658- "https://127.0.0.1:31886"
7729+ "https://127.0.0.1:31886" ,
7730+ None
76597731 ) ,
76607732 "http://quiet-flamingo--notebook.navigator.openshell.localhost:31886/"
76617733 ) ;
@@ -7666,7 +7738,8 @@ mod tests {
76667738 assert_eq ! (
76677739 service_url_for_gateway(
76687740 "http://quiet-flamingo--notebook.navigator.openshell.localhost:8080/" ,
7669- "https://gateway.example.com"
7741+ "https://gateway.example.com" ,
7742+ None
76707743 ) ,
76717744 "http://quiet-flamingo--notebook.navigator.openshell.localhost:443/"
76727745 ) ;
0 commit comments