1use axum::{
14 body::Body,
15 debug_handler,
16 extract::{Path, State},
17 http::{Request, StatusCode},
18 response::{IntoResponse, Response},
19 Json,
20};
21use microsandbox_core::management::{menv, orchestra};
22use microsandbox_utils::{DEFAULT_CONFIG, DEFAULT_PORTAL_GUEST_PORT, MICROSANDBOX_CONFIG_FILENAME};
23use reqwest;
24use serde_json::{self, json};
25use serde_yaml;
26use std::path::PathBuf;
27use tokio::{
28 fs as tokio_fs,
29 time::{sleep, timeout, Duration},
30};
31use tracing::{debug, trace, warn};
32
33use crate::{
34 error::ServerError,
35 mcp, middleware,
36 payload::{
37 JsonRpcError, JsonRpcRequest, JsonRpcResponse, JsonRpcResponseOrNotification,
38 RegularMessageResponse, SandboxMetricsGetParams, SandboxStartParams, SandboxStopParams,
39 JSONRPC_VERSION,
40 },
41 state::AppState,
42 SandboxStatus, SandboxStatusResponse, ServerResult,
43};
44
45pub async fn health() -> ServerResult<impl IntoResponse> {
51 Ok((
52 StatusCode::OK,
53 Json(RegularMessageResponse {
54 message: "Service is healthy".to_string(),
55 }),
56 ))
57}
58
59#[debug_handler]
65pub async fn mcp_handler(
66 State(state): State<AppState>,
67 Json(request): Json<JsonRpcRequest>,
68) -> ServerResult<impl IntoResponse> {
69 debug!(?request, "Received MCP request");
70 if request.jsonrpc != JSONRPC_VERSION {
72 let error = JsonRpcError {
73 code: -32600,
74 message: "Invalid or missing jsonrpc version field".to_string(),
75 data: None,
76 };
77 return Ok(JsonRpcResponseOrNotification::error(
78 error,
79 request.id.clone(),
80 ));
81 }
82
83 let request_id = request.id.clone();
85
86 match mcp::handle_mcp_method(state, request).await {
88 Ok(response) => {
89 Ok(response)
91 }
92 Err(e) => {
93 let error = JsonRpcError {
94 code: -32603,
95 message: format!("MCP method error: {}", e),
96 data: None,
97 };
98 Ok(JsonRpcResponseOrNotification::error(error, request_id))
99 }
100 }
101}
102
103#[debug_handler]
105pub async fn json_rpc_handler(
106 State(state): State<AppState>,
107 Json(request): Json<JsonRpcRequest>,
108) -> ServerResult<impl IntoResponse> {
109 debug!(?request, "Received JSON-RPC request");
110
111 if request.jsonrpc != JSONRPC_VERSION {
113 let error = JsonRpcError {
114 code: -32600,
115 message: "Invalid or missing jsonrpc version field".to_string(),
116 data: None,
117 };
118 return Ok((
119 StatusCode::BAD_REQUEST,
120 Json(JsonRpcResponse::error(error, request.id.clone())),
121 ));
122 }
123
124 let method = request.method.as_str();
125 let id = request.id.clone();
126
127 match method {
128 "sandbox.start" => {
130 let start_params: SandboxStartParams =
132 serde_json::from_value(request.params.clone()).map_err(|e| {
133 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
134 format!("Invalid params for sandbox.start: {}", e),
135 ))
136 })?;
137
138 let result = sandbox_start_impl(state, start_params).await?;
140
141 Ok((
143 StatusCode::OK,
144 Json(JsonRpcResponse::success(json!(result), id)),
145 ))
146 }
147 "sandbox.stop" => {
148 let stop_params: SandboxStopParams = serde_json::from_value(request.params.clone())
150 .map_err(|e| {
151 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
152 format!("Invalid params for sandbox.stop: {}", e),
153 ))
154 })?;
155
156 let result = sandbox_stop_impl(state, stop_params).await?;
158
159 Ok((
161 StatusCode::OK,
162 Json(JsonRpcResponse::success(json!(result), id)),
163 ))
164 }
165 "sandbox.metrics.get" => {
166 let metrics_params: SandboxMetricsGetParams =
168 serde_json::from_value(request.params.clone()).map_err(|e| {
169 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
170 format!("Invalid params for sandbox.metrics.get: {}", e),
171 ))
172 })?;
173
174 let result = sandbox_get_metrics_impl(state.clone(), metrics_params).await?;
176
177 Ok((
179 StatusCode::OK,
180 Json(JsonRpcResponse::success(json!(result), id)),
181 ))
182 }
183
184 "sandbox.repl.run" | "sandbox.command.run" => {
186 match forward_rpc_to_portal(state, request).await {
188 Ok((status, json_response)) => Ok((status, json_response)),
189 Err(e) => Err(e),
190 }
191 }
192
193 _ => {
194 let error = JsonRpcError {
195 code: -32601,
196 message: format!("Method not found: {}", method),
197 data: None,
198 };
199 Ok((
200 StatusCode::NOT_FOUND,
201 Json(JsonRpcResponse::error(error, id)),
202 ))
203 }
204 }
205}
206
207pub async fn forward_rpc_to_portal(
209 state: AppState,
210 request: JsonRpcRequest,
211) -> ServerResult<(StatusCode, Json<JsonRpcResponse>)> {
212 let (sandbox_name, namespace) = if let Some(params) = request.params.as_object() {
218 let sandbox = params
220 .get("sandbox")
221 .and_then(|v| v.as_str())
222 .ok_or_else(|| {
223 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
224 "Missing required 'sandbox' parameter for portal request".to_string(),
225 ))
226 })?;
227
228 let namespace = params
230 .get("namespace")
231 .and_then(|v| v.as_str())
232 .ok_or_else(|| {
233 ServerError::ValidationError(crate::error::ValidationError::InvalidInput(
234 "Missing required 'namespace' parameter for portal request".to_string(),
235 ))
236 })?;
237
238 (sandbox, namespace)
239 } else {
240 return Err(ServerError::ValidationError(
241 crate::error::ValidationError::InvalidInput(
242 "Request parameters must be an object containing 'sandbox' and 'namespace'"
243 .to_string(),
244 ),
245 ));
246 };
247
248 let portal_url = state
250 .get_portal_url_for_sandbox(namespace, sandbox_name)
251 .await?;
252
253 let portal_rpc_url = format!("{}/api/v1/rpc", portal_url);
255
256 debug!("Forwarding RPC to portal: {}", portal_rpc_url);
257
258 let client = reqwest::Client::new();
260
261 const MAX_RETRIES: u32 = 10_000;
263 const TIMEOUT_MS: u64 = 50;
264
265 let mut retry_count = 0;
267 let mut last_error = None;
268
269 while retry_count < MAX_RETRIES {
271 match client
273 .head(&portal_url)
274 .timeout(Duration::from_millis(TIMEOUT_MS))
275 .send()
276 .await
277 {
278 Ok(response) => {
279 debug!(
281 "Successfully connected to portal after {} retries (status: {})",
282 retry_count,
283 response.status()
284 );
285 break;
286 }
287 Err(e) => {
288 last_error = Some(e);
290 trace!("Connection attempt {} failed, retrying...", retry_count + 1);
291 }
292 }
293
294 retry_count += 1;
296 }
297
298 if retry_count >= MAX_RETRIES {
300 let error_msg = if let Some(e) = last_error {
301 format!(
302 "Failed to connect to portal after {} retries: {}",
303 MAX_RETRIES, e
304 )
305 } else {
306 format!("Failed to connect to portal after {} retries", MAX_RETRIES)
307 };
308 return Err(ServerError::InternalError(error_msg));
309 }
310
311 let response = client
313 .post(&portal_rpc_url)
314 .json(&request)
315 .send()
316 .await
317 .map_err(|e| {
318 ServerError::InternalError(format!("Failed to forward RPC to portal: {}", e))
319 })?;
320
321 if !response.status().is_success() {
323 let status = response.status();
324 let error_text = response
325 .text()
326 .await
327 .unwrap_or_else(|_| "Unknown error".to_string());
328
329 return Err(ServerError::InternalError(format!(
330 "Portal returned error status {}: {}",
331 status, error_text
332 )));
333 }
334
335 let portal_response: JsonRpcResponse = response.json().await.map_err(|e| {
337 ServerError::InternalError(format!("Failed to parse portal response: {}", e))
338 })?;
339
340 Ok((StatusCode::OK, Json(portal_response)))
342}
343
344pub async fn sandbox_start_impl(
346 state: AppState,
347 params: SandboxStartParams,
348) -> ServerResult<String> {
349 validate_sandbox_name(¶ms.sandbox)?;
351 validate_namespace(¶ms.namespace)?;
352
353 let namespace_dir = state
354 .get_config()
355 .get_namespace_dir()
356 .join(¶ms.namespace);
357 let config_file = MICROSANDBOX_CONFIG_FILENAME;
358 let config_path = namespace_dir.join(config_file);
359 let sandbox = ¶ms.sandbox;
360
361 if !namespace_dir.exists() {
363 tokio_fs::create_dir_all(&namespace_dir)
364 .await
365 .map_err(|e| {
366 ServerError::InternalError(format!("Failed to create namespace directory: {}", e))
367 })?;
368
369 menv::initialize(Some(namespace_dir.clone()))
371 .await
372 .map_err(|e| {
373 ServerError::InternalError(format!(
374 "Failed to initialize microsandbox environment: {}",
375 e
376 ))
377 })?;
378 }
379
380 let has_config_in_request = params
382 .config
383 .as_ref()
384 .and_then(|c| c.image.as_ref())
385 .is_some();
386 let has_existing_config_file = config_path.exists();
387
388 if !has_config_in_request && !has_existing_config_file {
389 return Err(ServerError::ValidationError(
390 crate::error::ValidationError::InvalidInput(format!(
391 "No configuration provided and no existing configuration found for sandbox '{}'",
392 sandbox
393 )),
394 ));
395 }
396
397 let mut config_yaml: serde_yaml::Value;
399
400 if has_existing_config_file {
402 let config_content = tokio_fs::read_to_string(&config_path).await.map_err(|e| {
404 ServerError::InternalError(format!("Failed to read config file: {}", e))
405 })?;
406
407 config_yaml = serde_yaml::from_str(&config_content).map_err(|e| {
409 ServerError::InternalError(format!("Failed to parse config file: {}", e))
410 })?;
411
412 if !has_config_in_request {
414 let has_sandbox_config = config_yaml
415 .get("sandboxes")
416 .and_then(|sandboxes| sandboxes.get(sandbox))
417 .is_some();
418
419 if !has_sandbox_config {
420 return Err(ServerError::ValidationError(
421 crate::error::ValidationError::InvalidInput(format!(
422 "Sandbox '{}' not found in existing configuration",
423 sandbox
424 )),
425 ));
426 }
427 }
428 } else {
429 if !has_config_in_request {
431 return Err(ServerError::ValidationError(
432 crate::error::ValidationError::InvalidInput(
433 "No configuration provided and no existing configuration file".to_string(),
434 ),
435 ));
436 }
437
438 tokio_fs::write(&config_path, DEFAULT_CONFIG)
440 .await
441 .map_err(|e| {
442 ServerError::InternalError(format!("Failed to create config file: {}", e))
443 })?;
444
445 config_yaml = serde_yaml::from_str(DEFAULT_CONFIG).map_err(|e| {
447 ServerError::InternalError(format!("Failed to parse default config: {}", e))
448 })?;
449 }
450
451 if !config_yaml.is_mapping() {
453 config_yaml = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
454 }
455
456 let config_map = config_yaml.as_mapping_mut().unwrap();
457 if !config_map.contains_key(&serde_yaml::Value::String("sandboxes".to_string())) {
458 config_map.insert(
459 serde_yaml::Value::String("sandboxes".to_string()),
460 serde_yaml::Value::Mapping(serde_yaml::Mapping::new()),
461 );
462 }
463
464 let sandboxes_key = serde_yaml::Value::String("sandboxes".to_string());
466 let sandboxes_value = config_map.get_mut(&sandboxes_key).unwrap();
467
468 if !sandboxes_value.is_mapping() {
470 *sandboxes_value = serde_yaml::Value::Mapping(serde_yaml::Mapping::new());
471 }
472
473 let sandboxes_map = sandboxes_value.as_mapping_mut().unwrap();
474
475 if let Some(config) = ¶ms.config {
477 if config.image.is_some() {
478 let mut sandbox_map = serde_yaml::Mapping::new();
480
481 if let Some(image) = &config.image {
483 sandbox_map.insert(
484 serde_yaml::Value::String("image".to_string()),
485 serde_yaml::Value::String(image.clone()),
486 );
487 }
488
489 if let Some(memory) = config.memory {
491 sandbox_map.insert(
492 serde_yaml::Value::String("memory".to_string()),
493 serde_yaml::Value::Number(serde_yaml::Number::from(memory)),
494 );
495 }
496
497 if let Some(cpus) = config.cpus {
498 sandbox_map.insert(
499 serde_yaml::Value::String("cpus".to_string()),
500 serde_yaml::Value::Number(serde_yaml::Number::from(cpus)),
501 );
502 }
503
504 if !config.volumes.is_empty() {
505 let volumes_array = config
506 .volumes
507 .iter()
508 .map(|v| serde_yaml::Value::String(v.clone()))
509 .collect::<Vec<_>>();
510 sandbox_map.insert(
511 serde_yaml::Value::String("volumes".to_string()),
512 serde_yaml::Value::Sequence(volumes_array),
513 );
514 }
515
516 if !config.ports.is_empty() {
517 let ports_array = config
518 .ports
519 .iter()
520 .map(|p| serde_yaml::Value::String(p.clone()))
521 .collect::<Vec<_>>();
522 sandbox_map.insert(
523 serde_yaml::Value::String("ports".to_string()),
524 serde_yaml::Value::Sequence(ports_array),
525 );
526 }
527
528 if !config.envs.is_empty() {
529 let envs_array = config
530 .envs
531 .iter()
532 .map(|e| serde_yaml::Value::String(e.clone()))
533 .collect::<Vec<_>>();
534 sandbox_map.insert(
535 serde_yaml::Value::String("envs".to_string()),
536 serde_yaml::Value::Sequence(envs_array),
537 );
538 }
539
540 if !config.depends_on.is_empty() {
541 let depends_on_array = config
542 .depends_on
543 .iter()
544 .map(|d| serde_yaml::Value::String(d.clone()))
545 .collect::<Vec<_>>();
546 sandbox_map.insert(
547 serde_yaml::Value::String("depends_on".to_string()),
548 serde_yaml::Value::Sequence(depends_on_array),
549 );
550 }
551
552 if let Some(workdir) = &config.workdir {
553 sandbox_map.insert(
554 serde_yaml::Value::String("workdir".to_string()),
555 serde_yaml::Value::String(workdir.clone()),
556 );
557 }
558
559 if let Some(shell) = &config.shell {
560 sandbox_map.insert(
561 serde_yaml::Value::String("shell".to_string()),
562 serde_yaml::Value::String(shell.clone()),
563 );
564 }
565
566 if !config.scripts.is_empty() {
567 let mut scripts_map = serde_yaml::Mapping::new();
568 for (script_name, script) in &config.scripts {
569 scripts_map.insert(
570 serde_yaml::Value::String(script_name.clone()),
571 serde_yaml::Value::String(script.clone()),
572 );
573 }
574 sandbox_map.insert(
575 serde_yaml::Value::String("scripts".to_string()),
576 serde_yaml::Value::Mapping(scripts_map),
577 );
578 }
579
580 if let Some(exec) = &config.exec {
581 sandbox_map.insert(
582 serde_yaml::Value::String("exec".to_string()),
583 serde_yaml::Value::String(exec.clone()),
584 );
585 }
586
587 sandboxes_map.insert(
589 serde_yaml::Value::String(sandbox.clone()),
590 serde_yaml::Value::Mapping(sandbox_map),
591 );
592 }
593 }
594
595 let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
597 let port = {
598 let mut port_manager = state.get_port_manager().write().await;
599 port_manager.assign_port(&sandbox_key).await.map_err(|e| {
600 ServerError::InternalError(format!("Failed to assign portal port: {}", e))
601 })?
602 };
603
604 debug!("Assigned portal port {} to sandbox {}", port, sandbox_key);
605
606 let sandbox_config = sandboxes_map
608 .get_mut(&serde_yaml::Value::String(sandbox.clone()))
609 .ok_or_else(|| {
610 ServerError::InternalError(format!("Sandbox '{}' not found in configuration", sandbox))
611 })?
612 .as_mapping_mut()
613 .ok_or_else(|| {
614 ServerError::InternalError(format!(
615 "Sandbox '{}' configuration is not a mapping",
616 sandbox
617 ))
618 })?;
619
620 let guest_port = DEFAULT_PORTAL_GUEST_PORT;
622 let portal_port_mapping = format!("{}:{}", port, guest_port);
623
624 let ports_key = serde_yaml::Value::String("ports".to_string());
625
626 if let Some(ports) = sandbox_config.get_mut(&ports_key) {
627 if let Some(ports_seq) = ports.as_sequence_mut() {
628 ports_seq.retain(|p| {
630 p.as_str()
631 .map(|s| !s.ends_with(&format!(":{}", guest_port)))
632 .unwrap_or(true)
633 });
634
635 ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
637 }
638 } else {
639 let mut ports_seq = serde_yaml::Sequence::new();
641 ports_seq.push(serde_yaml::Value::String(portal_port_mapping));
642 sandbox_config.insert(ports_key, serde_yaml::Value::Sequence(ports_seq));
643 }
644
645 let updated_config = serde_yaml::to_string(&config_yaml)
647 .map_err(|e| ServerError::InternalError(format!("Failed to serialize config: {}", e)))?;
648
649 tokio_fs::write(&config_path, updated_config)
650 .await
651 .map_err(|e| ServerError::InternalError(format!("Failed to write config file: {}", e)))?;
652
653 orchestra::up(
655 vec![sandbox.clone()],
656 Some(&namespace_dir),
657 Some(config_file),
658 true,
659 )
660 .await
661 .map_err(|e| {
662 ServerError::InternalError(format!("Failed to start sandbox {}: {}", params.sandbox, e))
663 })?;
664
665 let potentially_first_time_pull = if let Some(config) = ¶ms.config {
667 config.image.is_some()
668 } else {
669 false
670 };
671
672 let poll_timeout = if potentially_first_time_pull {
675 Duration::from_secs(180) } else {
677 Duration::from_secs(60) };
679
680 debug!("Waiting for sandbox {} to start...", sandbox);
682 match timeout(
683 poll_timeout,
684 poll_sandbox_until_running(¶ms.sandbox, &namespace_dir, config_file),
685 )
686 .await
687 {
688 Ok(result) => match result {
689 Ok(_) => {
690 debug!("Sandbox {} is now running", sandbox);
691 Ok(format!("Sandbox {} started successfully", params.sandbox))
692 }
693 Err(e) => {
694 warn!("Failed to verify sandbox {} is running: {}", sandbox, e);
696 Ok(format!(
697 "Sandbox {} was started, but couldn't verify it's running: {}",
698 params.sandbox, e
699 ))
700 }
701 },
702 Err(_) => {
703 warn!("Timeout waiting for sandbox {} to start", sandbox);
705 Ok(format!(
706 "Sandbox {} was started, but timed out waiting for it to be fully running. It may still be initializing.",
707 params.sandbox
708 ))
709 }
710 }
711}
712
713async fn poll_sandbox_until_running(
715 sandbox_name: &str,
716 namespace_dir: &PathBuf,
717 config_file: &str,
718) -> ServerResult<()> {
719 const POLL_INTERVAL: Duration = Duration::from_millis(20);
720 const MAX_ATTEMPTS: usize = 2500; for attempt in 1..=MAX_ATTEMPTS {
723 let statuses = orchestra::status(
725 vec![sandbox_name.to_string()],
726 Some(namespace_dir),
727 Some(config_file),
728 )
729 .await
730 .map_err(|e| ServerError::InternalError(format!("Failed to get sandbox status: {}", e)))?;
731
732 if let Some(status) = statuses.iter().find(|s| s.name == sandbox_name) {
734 if status.running {
735 debug!(
737 "Sandbox {} is running (verified on attempt {})",
738 sandbox_name, attempt
739 );
740 return Ok(());
741 }
742 }
743
744 sleep(POLL_INTERVAL).await;
746 }
747
748 Err(ServerError::InternalError(format!(
750 "Exceeded maximum attempts to verify sandbox {} is running",
751 sandbox_name
752 )))
753}
754
755pub async fn sandbox_stop_impl(state: AppState, params: SandboxStopParams) -> ServerResult<String> {
757 validate_sandbox_name(¶ms.sandbox)?;
759 validate_namespace(¶ms.namespace)?;
760
761 let namespace_dir = state
762 .get_config()
763 .get_namespace_dir()
764 .join(¶ms.namespace);
765 let config_file = MICROSANDBOX_CONFIG_FILENAME;
766 let sandbox = ¶ms.sandbox;
767 let sandbox_key = format!("{}/{}", params.namespace, params.sandbox);
768
769 if !namespace_dir.exists() {
771 return Err(ServerError::ValidationError(
772 crate::error::ValidationError::InvalidInput(format!(
773 "Namespace directory '{}' does not exist",
774 params.namespace
775 )),
776 ));
777 }
778
779 let config_path = namespace_dir.join(config_file);
781 if !config_path.exists() {
782 return Err(ServerError::ValidationError(
783 crate::error::ValidationError::InvalidInput(format!(
784 "Configuration file not found for namespace '{}'",
785 params.namespace
786 )),
787 ));
788 }
789
790 orchestra::down(
792 vec![sandbox.clone()],
793 Some(&namespace_dir),
794 Some(config_file),
795 )
796 .await
797 .map_err(|e| {
798 ServerError::InternalError(format!("Failed to stop sandbox {}: {}", params.sandbox, e))
799 })?;
800
801 {
803 let mut port_manager = state.get_port_manager().write().await;
804 port_manager.release_port(&sandbox_key).await.map_err(|e| {
805 ServerError::InternalError(format!("Failed to release portal port: {}", e))
806 })?;
807 }
808
809 debug!("Released portal port for sandbox {}", sandbox_key);
810
811 Ok(format!("Sandbox {} stopped successfully", params.sandbox))
813}
814
815pub async fn sandbox_get_metrics_impl(
817 state: AppState,
818 params: SandboxMetricsGetParams,
819) -> ServerResult<SandboxStatusResponse> {
820 if params.namespace != "*" {
822 validate_namespace(¶ms.namespace)?;
823 }
824
825 if let Some(sandbox) = ¶ms.sandbox {
827 validate_sandbox_name(sandbox)?;
828 }
829
830 let namespaces_dir = state.get_config().get_namespace_dir();
831
832 if !namespaces_dir.exists() {
834 return Err(ServerError::InternalError(format!(
835 "Namespaces directory '{}' does not exist",
836 namespaces_dir.display()
837 )));
838 }
839
840 let mut all_statuses = Vec::new();
842
843 if params.namespace == "*" {
845 let mut entries = tokio::fs::read_dir(&namespaces_dir).await.map_err(|e| {
847 ServerError::InternalError(format!("Failed to read namespaces directory: {}", e))
848 })?;
849
850 while let Some(entry) = entries.next_entry().await.map_err(|e| {
852 ServerError::InternalError(format!("Failed to read namespace directory entry: {}", e))
853 })? {
854 let path = entry.path();
855 if !path.is_dir() {
856 continue;
857 }
858
859 let namespace = path
860 .file_name()
861 .and_then(|n| n.to_str())
862 .unwrap_or("unknown")
863 .to_string();
864
865 let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
867 vec![sandbox.clone()]
868 } else {
869 vec![]
870 };
871
872 match orchestra::status(sandbox_names, Some(&path), None).await {
873 Ok(statuses) => {
874 for status in statuses {
875 all_statuses.push(SandboxStatus {
877 namespace: namespace.clone(),
878 name: status.name,
879 running: status.running,
880 cpu_usage: status.cpu_usage,
881 memory_usage: status.memory_usage,
882 disk_usage: status.disk_usage,
883 });
884 }
885 }
886 Err(e) => {
887 tracing::warn!("Error getting metrics for namespace {}: {}", namespace, e);
889 }
890 }
891 }
892 } else {
893 let namespace_dir = namespaces_dir.join(¶ms.namespace);
895
896 if !namespace_dir.exists() {
898 return Err(ServerError::ValidationError(
899 crate::error::ValidationError::InvalidInput(format!(
900 "Namespace directory '{}' does not exist",
901 params.namespace
902 )),
903 ));
904 }
905
906 let sandbox_names = if let Some(sandbox) = ¶ms.sandbox {
908 vec![sandbox.clone()]
909 } else {
910 vec![]
911 };
912
913 match orchestra::status(sandbox_names, Some(&namespace_dir), None).await {
914 Ok(statuses) => {
915 for status in statuses {
916 all_statuses.push(SandboxStatus {
918 namespace: params.namespace.clone(),
919 name: status.name,
920 running: status.running,
921 cpu_usage: status.cpu_usage,
922 memory_usage: status.memory_usage,
923 disk_usage: status.disk_usage,
924 });
925 }
926 }
927 Err(e) => {
928 return Err(ServerError::InternalError(format!(
929 "Error getting metrics for namespace {}: {}",
930 params.namespace, e
931 )));
932 }
933 }
934 }
935
936 Ok(SandboxStatusResponse {
937 sandboxes: all_statuses,
938 })
939}
940
941pub async fn proxy_request(
947 State(_state): State<AppState>,
948 Path((namespace, sandbox, path)): Path<(String, String, PathBuf)>,
949 req: Request<Body>,
950) -> ServerResult<impl IntoResponse> {
951 let path_str = path.display().to_string();
955
956 let original_uri = req.uri().clone();
958 let _target_uri = middleware::proxy_uri(original_uri, &namespace, &sandbox);
959
960 let response = format!(
964 "Axum Proxy Request\n\nNamespace: {}\nSandbox: {}\nPath: {}\nMethod: {}\nHeaders: {:?}",
965 namespace,
966 sandbox,
967 path_str,
968 req.method(),
969 req.headers()
970 );
971
972 let result = Response::builder()
973 .status(StatusCode::OK)
974 .header("Content-Type", "text/plain")
975 .body(Body::from(response))
976 .unwrap();
977
978 Ok(result)
979}
980
981pub async fn proxy_fallback() -> ServerResult<impl IntoResponse> {
983 Ok((StatusCode::NOT_FOUND, "Resource not found"))
984}
985
986fn validate_sandbox_name(name: &str) -> ServerResult<()> {
992 if name.is_empty() {
994 return Err(ServerError::ValidationError(
995 crate::error::ValidationError::InvalidInput("Sandbox name cannot be empty".to_string()),
996 ));
997 }
998
999 if name.len() > 63 {
1000 return Err(ServerError::ValidationError(
1001 crate::error::ValidationError::InvalidInput(
1002 "Sandbox name cannot exceed 63 characters".to_string(),
1003 ),
1004 ));
1005 }
1006
1007 let valid_chars = name
1009 .chars()
1010 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
1011
1012 if !valid_chars {
1013 return Err(ServerError::ValidationError(
1014 crate::error::ValidationError::InvalidInput(
1015 "Sandbox name can only contain alphanumeric characters, hyphens, or underscores"
1016 .to_string(),
1017 ),
1018 ));
1019 }
1020
1021 if !name.chars().next().unwrap().is_ascii_alphanumeric() {
1023 return Err(ServerError::ValidationError(
1024 crate::error::ValidationError::InvalidInput(
1025 "Sandbox name must start with an alphanumeric character".to_string(),
1026 ),
1027 ));
1028 }
1029
1030 Ok(())
1031}
1032
1033fn validate_namespace(namespace: &str) -> ServerResult<()> {
1035 if namespace.is_empty() {
1037 return Err(ServerError::ValidationError(
1038 crate::error::ValidationError::InvalidInput("Namespace cannot be empty".to_string()),
1039 ));
1040 }
1041
1042 if namespace.len() > 63 {
1043 return Err(ServerError::ValidationError(
1044 crate::error::ValidationError::InvalidInput(
1045 "Namespace cannot exceed 63 characters".to_string(),
1046 ),
1047 ));
1048 }
1049
1050 if namespace == "*" {
1052 return Err(ServerError::ValidationError(
1053 crate::error::ValidationError::InvalidInput(
1054 "Wildcard namespace (*) is not valid for sandbox creation".to_string(),
1055 ),
1056 ));
1057 }
1058
1059 let valid_chars = namespace
1061 .chars()
1062 .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
1063
1064 if !valid_chars {
1065 return Err(ServerError::ValidationError(
1066 crate::error::ValidationError::InvalidInput(
1067 "Namespace can only contain alphanumeric characters, hyphens, or underscores"
1068 .to_string(),
1069 ),
1070 ));
1071 }
1072
1073 if !namespace.chars().next().unwrap().is_ascii_alphanumeric() {
1075 return Err(ServerError::ValidationError(
1076 crate::error::ValidationError::InvalidInput(
1077 "Namespace must start with an alphanumeric character".to_string(),
1078 ),
1079 ));
1080 }
1081
1082 Ok(())
1083}