(
state: &Arc<ServerState>,
request: Request<ExposeServiceRequest>,
)
| 22 | const MAX_SANDBOX_NAME_LEN: usize = 28; |
| 23 | |
| 24 | pub(super) async fn handle_expose_service( |
| 25 | state: &Arc<ServerState>, |
| 26 | request: Request<ExposeServiceRequest>, |
| 27 | ) -> Result<Response<ServiceEndpointResponse>, Status> { |
| 28 | let req = request.into_inner(); |
| 29 | validate_endpoint_name("sandbox", &req.sandbox, MAX_SANDBOX_NAME_LEN)?; |
| 30 | validate_optional_endpoint_name("service", &req.service, MAX_SERVICE_NAME_LEN)?; |
| 31 | if req.target_port == 0 || req.target_port > u32::from(u16::MAX) { |
| 32 | return Err(Status::invalid_argument("target_port must be in 1..=65535")); |
| 33 | } |
| 34 | |
| 35 | let sandbox = state |
| 36 | .store |
| 37 | .get_message_by_name::<Sandbox>(&req.sandbox) |
| 38 | .await |
| 39 | .map_err(|e| Status::internal(format!("fetch sandbox failed: {e}")))? |
| 40 | .ok_or_else(|| Status::not_found("sandbox not found"))?; |
| 41 | |
| 42 | let now = crate::persistence::current_time_ms(); |
| 43 | let key = service_routing::endpoint_key(&req.sandbox, &req.service); |
| 44 | |
| 45 | // Fetch existing endpoint to determine create vs. update path |
| 46 | let existing = state |
| 47 | .store |
| 48 | .get_message_by_name::<ServiceEndpoint>(&key) |
| 49 | .await |
| 50 | .map_err(|e| Status::internal(format!("fetch endpoint failed: {e}")))?; |
| 51 | |
| 52 | let (id, created_at_ms, condition, created) = if let Some(existing) = existing { |
| 53 | // Update path: preserve id and created_at, use CAS to prevent conflicts |
| 54 | let resource_version = existing |
| 55 | .metadata |
| 56 | .as_ref() |
| 57 | .map_or(0, |metadata| metadata.resource_version); |
| 58 | ( |
| 59 | existing.object_id().to_string(), |
| 60 | existing |
| 61 | .metadata |
| 62 | .as_ref() |
| 63 | .map_or(now, |metadata| metadata.created_at_ms), |
| 64 | WriteCondition::MatchResourceVersion(resource_version), |
| 65 | false, |
| 66 | ) |
| 67 | } else { |
| 68 | // Create path: new id and created_at, use MustCreate to prevent races |
| 69 | ( |
| 70 | Uuid::new_v4().to_string(), |
| 71 | now, |
| 72 | WriteCondition::MustCreate, |
| 73 | true, |
| 74 | ) |
| 75 | }; |
| 76 | |
| 77 | let labels_json = serde_json::to_string(&HashMap::from([( |
| 78 | "sandbox".to_string(), |
| 79 | req.sandbox.clone(), |
| 80 | )])) |
| 81 | .map_err(|e| Status::internal(format!("serialize labels failed: {e}")))?; |
no test coverage detected