Skip to content

Commit feac6b4

Browse files
committed
feat(gateway): expose workspace service route discovery
Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent 600d3d3 commit feac6b4

20 files changed

Lines changed: 1784 additions & 429 deletions

‎crates/openshell-cli/tests/ensure_providers_integration.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ impl TestOpenShell {
8080

8181
#[tonic::async_trait]
8282
impl OpenShell for TestOpenShell {
83+
async fn get_service_route_snapshot(
84+
&self,
85+
_: tonic::Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
86+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
87+
Err(Status::unimplemented("unused"))
88+
}
89+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<
90+
Result<openshell_core::proto::WatchServiceRoutesResponse, Status>,
91+
>;
92+
async fn watch_service_routes(
93+
&self,
94+
_: tonic::Request<openshell_core::proto::WatchServiceRoutesRequest>,
95+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
96+
Err(Status::unimplemented("unused"))
97+
}
8398
async fn peer_notify_config_update(
8499
&self,
85100
_request: tonic::Request<openshell_core::proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-cli/tests/mtls_integration.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ struct TestOpenShell;
3232

3333
#[tonic::async_trait]
3434
impl OpenShell for TestOpenShell {
35+
async fn get_service_route_snapshot(
36+
&self,
37+
_: tonic::Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
38+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
39+
Err(Status::unimplemented("unused"))
40+
}
41+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<
42+
Result<openshell_core::proto::WatchServiceRoutesResponse, Status>,
43+
>;
44+
async fn watch_service_routes(
45+
&self,
46+
_: tonic::Request<openshell_core::proto::WatchServiceRoutesRequest>,
47+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
48+
Err(Status::unimplemented("unused"))
49+
}
3550
async fn peer_notify_config_update(
3651
&self,
3752
_request: tonic::Request<openshell_core::proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-cli/tests/provider_commands_integration.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ impl TestOpenShell {
235235

236236
#[tonic::async_trait]
237237
impl OpenShell for TestOpenShell {
238+
async fn get_service_route_snapshot(
239+
&self,
240+
_: tonic::Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
241+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
242+
Err(Status::unimplemented("unused"))
243+
}
244+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<
245+
Result<openshell_core::proto::WatchServiceRoutesResponse, Status>,
246+
>;
247+
async fn watch_service_routes(
248+
&self,
249+
_: tonic::Request<openshell_core::proto::WatchServiceRoutesRequest>,
250+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
251+
Err(Status::unimplemented("unused"))
252+
}
238253
async fn peer_notify_config_update(
239254
&self,
240255
_request: tonic::Request<openshell_core::proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-cli/tests/sandbox_create_lifecycle_integration.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,21 @@ struct TestOpenShell {
101101

102102
#[tonic::async_trait]
103103
impl OpenShell for TestOpenShell {
104+
async fn get_service_route_snapshot(
105+
&self,
106+
_: tonic::Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
107+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
108+
Err(Status::unimplemented("unused"))
109+
}
110+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<
111+
Result<openshell_core::proto::WatchServiceRoutesResponse, Status>,
112+
>;
113+
async fn watch_service_routes(
114+
&self,
115+
_: tonic::Request<openshell_core::proto::WatchServiceRoutesRequest>,
116+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
117+
Err(Status::unimplemented("unused"))
118+
}
104119
async fn peer_notify_config_update(
105120
&self,
106121
_request: tonic::Request<openshell_core::proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-cli/tests/sandbox_exec_streaming_integration.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ macro_rules! mock_gateway {
163163
) => {
164164
#[tonic::async_trait]
165165
impl OpenShell for MockGateway {
166+
async fn get_service_route_snapshot(&self, _: Request<openshell_core::proto::GetServiceRouteSnapshotRequest>) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
167+
Err(Status::unimplemented("unused"))
168+
}
169+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<Result<openshell_core::proto::WatchServiceRoutesResponse, Status>>;
170+
async fn watch_service_routes(&self, _: Request<openshell_core::proto::WatchServiceRoutesRequest>) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
171+
Err(Status::unimplemented("unused"))
172+
}
166173
$(async fn $method(&self, _: Request<$request>) -> Result<Response<$response>, Status> {
167174
Err(Status::unimplemented("unused test RPC"))
168175
})*

‎crates/openshell-cli/tests/sandbox_name_fallback_integration.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,21 @@ struct TestOpenShell {
4747

4848
#[tonic::async_trait]
4949
impl OpenShell for TestOpenShell {
50+
async fn get_service_route_snapshot(
51+
&self,
52+
_: tonic::Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
53+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
54+
Err(Status::unimplemented("unused"))
55+
}
56+
type WatchServiceRoutesStream = tokio_stream::wrappers::ReceiverStream<
57+
Result<openshell_core::proto::WatchServiceRoutesResponse, Status>,
58+
>;
59+
async fn watch_service_routes(
60+
&self,
61+
_: tonic::Request<openshell_core::proto::WatchServiceRoutesRequest>,
62+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
63+
Err(Status::unimplemented("unused"))
64+
}
5065
async fn peer_notify_config_update(
5166
&self,
5267
_request: tonic::Request<openshell_core::proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-sdk/tests/client_mock.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,20 @@ fn workload_template_proto(name: &str, workspace: &str) -> proto::SandboxWorkloa
236236

237237
#[tonic::async_trait]
238238
impl OpenShell for TestOpenShell {
239+
async fn get_service_route_snapshot(
240+
&self,
241+
_: tonic::Request<proto::GetServiceRouteSnapshotRequest>,
242+
) -> Result<Response<proto::GetServiceRouteSnapshotResponse>, Status> {
243+
Err(Status::unimplemented("unused"))
244+
}
245+
type WatchServiceRoutesStream =
246+
tokio_stream::wrappers::ReceiverStream<Result<proto::WatchServiceRoutesResponse, Status>>;
247+
async fn watch_service_routes(
248+
&self,
249+
_: tonic::Request<proto::WatchServiceRoutesRequest>,
250+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
251+
Err(Status::unimplemented("unused"))
252+
}
239253
async fn peer_notify_config_update(
240254
&self,
241255
_request: tonic::Request<proto::PeerNotifyConfigUpdateRequest>,

‎crates/openshell-server/README.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,27 @@ change durable sandbox protobufs. Reads revalidate owner, configuration,
206206
generation, and expiry. Cleanup scans bounded pages and uses conditional deletes
207207
so concurrent renewals are preserved. Discovery consumers must not treat these
208208
records as application health or external ingress readiness.
209+
210+
## Workspace service discovery
211+
212+
`GetServiceRouteSnapshot` and `WatchServiceRoutes` expose accepted installations
213+
for one explicit active workspace. They require `sandbox:read` and workspace
214+
user access; omitted/all-workspaces selectors and sandbox principals are rejected.
215+
Dedicated discovery machine grants are separate work.
216+
217+
Each successful response replaces the complete view. Consumers preserve the
218+
returned hostname as TLS SNI and HTTP authority when dialing the supervisor
219+
endpoint, remove absent routes, and expire cached entries at `expires_at` even
220+
when discovery is unavailable. An error is not an empty replacement. The opaque
221+
snapshot revision includes lease renewals; `installation_version` remains stable
222+
across those renewals and changes when installation identity changes. These
223+
records establish neither application health nor ingress readiness.
224+
225+
Collection checks installation resource versions across two scans, retrying once
226+
if changed. This is a verified view, not a transaction across configuration and
227+
owner resources. Reads are bounded to 1,000 installation records, 4,096 routes,
228+
4 MiB, and ten seconds. Overflow, corruption, or storage failure returns an error.
229+
Watches refresh and reauthorize each second, use a one-message buffer, and end
230+
after one minute or a five-second blocked send. Each gateway process accepts at
231+
most 32 watches. Reconnect and relist on stream termination; revisions are opaque
232+
equality tokens, not resumable cursors or an ordering across gateway replicas.

‎crates/openshell-server/src/grpc/mod.rs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub mod supervisor_host;
1414
pub use sandbox::mint_persisted_authentication;
1515
mod service;
1616
pub mod service_advertisement;
17+
mod service_discovery;
1718
mod validation;
1819
pub mod workspace;
1920

@@ -511,6 +512,22 @@ impl OpenShell for OpenShellService {
511512
service::handle_list_services(&self.state, request).await
512513
}
513514

515+
async fn get_service_route_snapshot(
516+
&self,
517+
request: Request<openshell_core::proto::GetServiceRouteSnapshotRequest>,
518+
) -> Result<Response<openshell_core::proto::GetServiceRouteSnapshotResponse>, Status> {
519+
service_discovery::get_snapshot(&self.state, request).await
520+
}
521+
522+
type WatchServiceRoutesStream = service_discovery::RouteWatchStream;
523+
524+
async fn watch_service_routes(
525+
&self,
526+
request: Request<openshell_core::proto::WatchServiceRoutesRequest>,
527+
) -> Result<Response<Self::WatchServiceRoutesStream>, Status> {
528+
service_discovery::watch(&self.state, request).await
529+
}
530+
514531
async fn delete_service(
515532
&self,
516533
request: Request<DeleteServiceRequest>,

‎crates/openshell-server/src/grpc/service_advertisement.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ pub fn spawn_cleanup(state: Arc<ServerState>, mut shutdown: tokio::sync::watch::
495495
}
496496

497497
#[cfg(test)]
498-
mod tests {
498+
pub mod tests {
499499
use super::*;
500500

501501
fn route() -> SandboxServiceRoute {
@@ -635,6 +635,7 @@ mod tests {
635635
port: 9443,
636636
domain: "services.example.com".into(),
637637
};
638+
Arc::get_mut(&mut state).unwrap().admin_role = "openshell-admin".into();
638639
let sandbox = Sandbox {
639640
metadata: Some(ObjectMeta {
640641
id: "sandbox".into(),

0 commit comments

Comments
 (0)