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