Skip to content

Commit a4210a9

Browse files
committed
feat(gateway): separate service ingress from the control plane
Signed-off-by: Mrunal Patel <mrunalp@gmail.com>
1 parent be7af99 commit a4210a9

19 files changed

Lines changed: 806 additions & 105 deletions

File tree

‎crates/openshell-cli/src/run.rs‎

Lines changed: 131 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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\nhint: 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\nhint: 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

40634073
fn 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(
43094327
fn 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

Comments
 (0)