diff --git a/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx b/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx index d4083edd7..38403196e 100644 --- a/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx +++ b/apps/fabro-web/app/routes/run-sandbox/services-panel.test.tsx @@ -25,6 +25,13 @@ function makeIdlePreview(): PreviewMutationShape { }; } +function makeServicesData(data: SandboxService[]) { + return { + data, + meta: { source: "ss" as const }, + }; +} + const mountedRenderers: TestRenderer.ReactTestRenderer[] = []; interface WindowLike { @@ -97,7 +104,7 @@ describe("ServicesPanelView", () => { test("shows empty state when the services list is empty", () => { const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([]) }, previewMutation: makeIdlePreview(), }); const titles = renderer.root.findAll( @@ -109,6 +116,46 @@ describe("ServicesPanelView", () => { expect(titles).toHaveLength(1); }); + test("shows an iproute2 tip when services were discovered from procfs", () => { + const service: SandboxService = { + port: 3000, + addresses: ["0.0.0.0:3000"], + processes: [], + preview_supported: true, + }; + const renderer = renderView({ + servicesQuery: { + ...makeIdleQuery(), + data: { + data: [service], + meta: { source: "procfs" }, + }, + }, + previewMutation: makeIdlePreview(), + }); + + const tipLabels = renderer.root.findAll( + (node) => + node.type === "span" && + Array.isArray(node.children) && + node.children.includes("Tip:"), + ); + expect(tipLabels).toHaveLength(1); + + const commands = renderer.root.findAll( + (node) => + node.type === "code" && + Array.isArray(node.children) && + node.children.includes("apt-get install iproute2"), + ); + expect(commands).toHaveLength(1); + + const tipText = JSON.stringify(renderer.toJSON()); + expect(tipText).toContain("Install "); + expect(tipText).toContain("ss"); + expect(tipText).toContain(" in the sandbox for improved services listing:"); + }); + test("shows API error state with the error message", () => { const renderer = renderView({ servicesQuery: { @@ -139,7 +186,7 @@ describe("ServicesPanelView", () => { preview_supported: false, }; const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: makeIdlePreview(), }); @@ -158,14 +205,6 @@ describe("ServicesPanelView", () => { node.children.includes("Preview"), ); expect(previewButtons).toHaveLength(0); - - const unavailable = renderer.root.findAll( - (node) => - node.type === "span" && - Array.isArray(node.children) && - node.children.includes("Unavailable"), - ); - expect(unavailable).toHaveLength(1); }); test("renders a Preview button for a previewable service and triggers with signed args", async () => { @@ -186,7 +225,7 @@ describe("ServicesPanelView", () => { installWindowOpen(windowOpenSpy); const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: { trigger }, }); const previewButton = renderer.root.find( @@ -226,7 +265,7 @@ describe("ServicesPanelView", () => { installWindowOpen(windowOpenSpy); const renderer = renderView({ - servicesQuery: { ...makeIdleQuery(), data: { data: [service] } }, + servicesQuery: { ...makeIdleQuery(), data: makeServicesData([service]) }, previewMutation: { trigger }, }); const previewButton = renderer.root.find( diff --git a/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx b/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx index 267e979b1..78a17a63a 100644 --- a/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx +++ b/apps/fabro-web/app/routes/run-sandbox/services-panel.tsx @@ -1,6 +1,9 @@ import { useState } from "react"; import { ArrowPathIcon } from "@heroicons/react/20/solid"; -import type { SandboxService } from "@qltysh/fabro-api-client"; +import type { + SandboxService, + SandboxServiceListResponse, +} from "@qltysh/fabro-api-client"; import { useSandboxServices } from "../../lib/queries"; import { usePreviewRun } from "../../lib/mutations"; @@ -10,7 +13,7 @@ import { ErrorState, LoadingState, } from "../../components/state"; -import { SECONDARY_BUTTON_CLASS, Tooltip } from "../../components/ui"; +import { Tooltip } from "../../components/ui"; export interface ServicesPanelProps { runId: string; @@ -20,6 +23,9 @@ export interface ServicesPanelProps { const REFRESH_BUTTON_CLASS = "inline-flex size-9 items-center justify-center rounded-lg text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-50"; +const PREVIEW_BUTTON_CLASS = + "inline-flex items-center justify-center rounded-md px-3 py-1 text-xs font-medium text-fg-2 outline-1 -outline-offset-1 outline-white/10 transition-colors hover:bg-overlay hover:text-fg focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-teal-500 disabled:cursor-not-allowed disabled:opacity-60"; + export default function ServicesPanel({ runId, leading }: ServicesPanelProps) { const servicesQuery = useSandboxServices(runId); const previewMutation = usePreviewRun(runId); @@ -34,7 +40,7 @@ export default function ServicesPanel({ runId, leading }: ServicesPanelProps) { } export interface ServicesQueryShape { - data?: { data: SandboxService[] } | undefined; + data?: SandboxServiceListResponse | undefined; error?: unknown; isLoading: boolean; isValidating: boolean; @@ -71,6 +77,7 @@ export function ServicesPanelView({ const [previewError, setPreviewError] = useState(null); const services = servicesQuery.data?.data ?? []; + const discoverySource = servicesQuery.data?.meta.source; const queryErrorMessage = describeQueryError(servicesQuery.error); const showLoading = servicesQuery.isLoading && !servicesQuery.data; const showError = queryErrorMessage !== null && !servicesQuery.data; @@ -142,11 +149,14 @@ export function ServicesPanelView({ ) : services.length === 0 ? ( ) : ( - + <> + {discoverySource === "procfs" ? : null} + + )} @@ -160,6 +170,17 @@ function describeQueryError(error: unknown): string | null { return "Could not load services."; } +function ProcfsDiscoveryTip() { + return ( +
+ Tip:{" "} + Install ss in the sandbox + for improved services listing:{" "} + apt-get install iproute2 +
+ ); +} + function ServicesTable({ services, pendingPort, @@ -174,10 +195,11 @@ function ServicesTable({ - - - - + + + + @@ -206,34 +228,32 @@ function ServiceRow({ }) { return ( - - + - - ); diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 4971f3f8b..300253a3f 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -8004,13 +8004,13 @@ components: example: 3000 addresses: type: array - description: Local bind addresses reported by `ss`. + description: Local bind addresses discovered from `ss` or `/proc/net/tcp*`. items: type: string example: ["127.0.0.1:3000", "[::]:3000"] processes: type: array - description: Visible process summaries reported by `ss`. + description: Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery. items: type: string example: ['users:(("node",pid=42,fd=23))'] @@ -8024,11 +8024,30 @@ components: type: object required: - data + - meta properties: data: type: array items: $ref: "#/components/schemas/SandboxService" + meta: + $ref: "#/components/schemas/SandboxServiceListMeta" + + SandboxServiceListMeta: + description: Metadata about sandbox service discovery. + type: object + required: + - source + properties: + source: + $ref: "#/components/schemas/SandboxServiceDiscoverySource" + + SandboxServiceDiscoverySource: + description: Tool or kernel interface used to discover sandbox services. + type: string + enum: + - ss + - procfs VncPreviewResponse: description: Response containing a signed noVNC preview URL for a Daytona sandbox. diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index f8896b9d6..6e0ce1ed4 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -386,6 +386,16 @@ fn main() { ("GitContext", "fabro_types::GitContext", &[]), ("SandboxDetails", "fabro_types::SandboxDetails", &[]), ("SandboxService", "fabro_types::SandboxService", &[]), + ( + "SandboxServiceDiscoverySource", + "fabro_types::SandboxServiceDiscoverySource", + &[], + ), + ( + "SandboxServiceListMeta", + "fabro_types::SandboxServiceListMeta", + &[], + ), ( "SandboxServiceListResponse", "fabro_types::SandboxServiceListResponse", diff --git a/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs b/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs index 32387fd2a..706ef59f2 100644 --- a/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs +++ b/lib/crates/fabro-api/tests/sandbox_services_round_trip.rs @@ -4,7 +4,10 @@ use fabro_api::types::{ SandboxService as ApiSandboxService, SandboxServiceListResponse as ApiSandboxServiceListResponse, }; -use fabro_types::{SandboxService, SandboxServiceListResponse}; +use fabro_types::{ + SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta, + SandboxServiceListResponse, +}; use serde_json::json; #[test] @@ -25,6 +28,9 @@ fn sandbox_services_json_matches_openapi_shape() { ], preview_supported: true, }], + meta: SandboxServiceListMeta { + source: SandboxServiceDiscoverySource::Ss, + }, }; assert_eq!( @@ -38,17 +44,22 @@ fn sandbox_services_json_matches_openapi_shape() { r#"users:(("vite",pid=84,fd=19))"#, ], "preview_supported": true - }] + }], + "meta": { + "source": "ss" + } }) ); } #[test] fn sandbox_services_deserializes_empty_response() { - let response: SandboxServiceListResponse = serde_json::from_value(json!({ "data": [] })) - .expect("empty service response should deserialize"); + let response: SandboxServiceListResponse = + serde_json::from_value(json!({ "data": [], "meta": { "source": "procfs" } })) + .expect("empty service response should deserialize"); assert!(response.data.is_empty()); + assert_eq!(response.meta.source, SandboxServiceDiscoverySource::Procfs); } fn assert_same_type() { diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 261302691..ce85d3807 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -23,6 +23,7 @@ use fabro_api::types::{ RunFilesMeta, RunFilesMetaScope, RunFilesMetaSource, SandboxService, SandboxServiceListResponse, }; +use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta}; use serde_json::json; use crate::error::ApiError; @@ -423,6 +424,9 @@ pub(crate) async fn list_sandbox_services_stub( preview_supported: false, }, ], + meta: SandboxServiceListMeta { + source: SandboxServiceDiscoverySource::Ss, + }, }), ) .into_response() diff --git a/lib/crates/fabro-server/src/server/handler/sandbox.rs b/lib/crates/fabro-server/src/server/handler/sandbox.rs index 1f447cde5..329ca1d18 100644 --- a/lib/crates/fabro-server/src/server/handler/sandbox.rs +++ b/lib/crates/fabro-server/src/server/handler/sandbox.rs @@ -1,9 +1,11 @@ use std::collections::BTreeMap; +use std::net::{Ipv4Addr, Ipv6Addr}; use std::num::NonZeroU64; use std::sync::Arc; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; use fabro_sandbox::{TerminalSize, open_terminal_for_run}; +use fabro_types::{SandboxServiceDiscoverySource, SandboxServiceListMeta}; use futures_util::FutureExt; use futures_util::future::BoxFuture; @@ -20,7 +22,19 @@ use super::super::{ const MAX_TERMINAL_CONTROL_BYTES: usize = 4096; const DEFAULT_VNC_NO_VNC_PORT: u16 = 6080; const DEFAULT_VNC_TTL_SECS: i32 = 3600; -const LIST_SANDBOX_SERVICES_COMMAND: &str = "ss -H -ltnp"; +const LIST_SANDBOX_SERVICES_COMMAND: &str = r#"if command -v ss >/dev/null 2>&1; then + ss -H -ltnp && exit 0 +fi +printf 'FABRO_PROC_NET_TCP procfs\n' +for file in /proc/net/tcp /proc/net/tcp6; do + if [ -r "$file" ]; then + printf 'FABRO_PROC_NET_TCP %s\n' "$file" + while IFS= read -r line; do + printf '%s\n' "$line" + done < "$file" + fi +done"#; +const LIST_SANDBOX_SERVICES_FAILURE_LABEL: &str = "sandbox service discovery command"; const LIST_SANDBOX_SERVICES_TIMEOUT_MS: u64 = 5_000; // Daytona's signed preview points at the noVNC service root, which serves a // directory listing. Force the iframe to the actual viewer page with @@ -572,8 +586,12 @@ async fn list_sandbox_services( .into_response(); } + let discovery = parse_sandbox_services(&result.stdout, &provider); Json(SandboxServiceListResponse { - data: parse_ss_listening_services(&result.stdout, &provider), + data: discovery.services, + meta: SandboxServiceListMeta { + source: discovery.source, + }, }) .into_response() } @@ -587,7 +605,29 @@ fn sandbox_service_command_failure_detail(result: &fabro_sandbox::ExecResult) -> if !stdout.is_empty() { return stdout.to_string(); } - format!("{LIST_SANDBOX_SERVICES_COMMAND} failed") + format!("{LIST_SANDBOX_SERVICES_FAILURE_LABEL} failed") +} + +struct SandboxServiceDiscovery { + services: Vec, + source: SandboxServiceDiscoverySource, +} + +fn parse_sandbox_services(output: &str, provider: &str) -> SandboxServiceDiscovery { + if output + .lines() + .any(|line| line.trim_start().starts_with("FABRO_PROC_NET_TCP ")) + { + SandboxServiceDiscovery { + services: parse_proc_net_listening_services(output, provider), + source: SandboxServiceDiscoverySource::Procfs, + } + } else { + SandboxServiceDiscovery { + services: parse_ss_listening_services(output, provider), + source: SandboxServiceDiscoverySource::Ss, + } + } } fn parse_ss_listening_services(output: &str, provider: &str) -> Vec { @@ -605,18 +645,9 @@ fn parse_ss_listening_services(output: &str, provider: &str) -> Vec 5).then(|| fields[5..].join(" ")); - let service = services.entry(port).or_insert_with(|| SandboxService { - port, - addresses: Vec::new(), - processes: Vec::new(), - preview_supported: preview_supported(provider, port), - }); - push_unique(&mut service.addresses, address.to_string()); - if let Some(process) = process { - push_unique(&mut service.processes, process); - } + push_service(&mut services, provider, port, address.to_string(), process); } - services.into_values().collect() + sorted_services(services) } fn parse_ss_local_port(address: &str) -> Option { @@ -624,6 +655,108 @@ fn parse_ss_local_port(address: &str) -> Option { (port > 0).then_some(port) } +#[derive(Clone, Copy)] +enum ProcNetFamily { + Ipv4, + Ipv6, +} + +fn parse_proc_net_listening_services(output: &str, provider: &str) -> Vec { + let mut services = BTreeMap::::new(); + let mut family = None; + for line in output + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + { + if let Some(path) = line.strip_prefix("FABRO_PROC_NET_TCP ") { + family = if path.ends_with("/tcp6") { + Some(ProcNetFamily::Ipv6) + } else { + Some(ProcNetFamily::Ipv4) + }; + continue; + } + if line.starts_with("sl") { + continue; + } + let Some(family) = family else { + continue; + }; + let fields = line.split_whitespace().collect::>(); + let (Some(local_address), Some(state)) = (fields.get(1), fields.get(3)) else { + continue; + }; + if *state != "0A" { + continue; + } + let Some((address, port)) = parse_proc_net_local_address(local_address, family) else { + continue; + }; + push_service(&mut services, provider, port, address, None); + } + sorted_services(services) +} + +fn sorted_services(services: BTreeMap) -> Vec { + let mut services = services.into_values().collect::>(); + services.sort_by_key(|service| (!service.preview_supported, service.port)); + services +} + +fn parse_proc_net_local_address(value: &str, family: ProcNetFamily) -> Option<(String, u16)> { + let (address_hex, port_hex) = value.split_once(':')?; + let port = u16::from_str_radix(port_hex, 16).ok()?; + if port == 0 { + return None; + } + let address = match family { + ProcNetFamily::Ipv4 => format!("{}:{port}", parse_proc_net_ipv4(address_hex)?), + ProcNetFamily::Ipv6 => format!("[{}]:{port}", parse_proc_net_ipv6(address_hex)?), + }; + Some((address, port)) +} + +fn parse_proc_net_ipv4(value: &str) -> Option { + if value.len() != 8 { + return None; + } + let raw = u32::from_str_radix(value, 16).ok()?; + Some(Ipv4Addr::from(raw.to_le_bytes())) +} + +fn parse_proc_net_ipv6(value: &str) -> Option { + if value.len() != 32 { + return None; + } + let mut bytes = [0_u8; 16]; + for (chunk_index, chunk) in value.as_bytes().chunks_exact(8).enumerate() { + let chunk = std::str::from_utf8(chunk).ok()?; + let raw = u32::from_str_radix(chunk, 16).ok()?; + bytes[chunk_index * 4..chunk_index * 4 + 4].copy_from_slice(&raw.to_le_bytes()); + } + Some(Ipv6Addr::from(bytes)) +} + +fn push_service( + services: &mut BTreeMap, + provider: &str, + port: u16, + address: String, + process: Option, +) { + let service = services.entry(port).or_insert_with(|| SandboxService { + port, + addresses: Vec::new(), + processes: Vec::new(), + preview_supported: preview_supported(provider, port), + }); + push_unique(&mut service.addresses, address); + if let Some(process) = process { + push_unique(&mut service.processes, process); + } +} + fn preview_supported(provider: &str, port: u16) -> bool { provider == SandboxProvider::Daytona.to_string() && (3000..=9999).contains(&port) } @@ -868,24 +1001,24 @@ LISTEN 0 4096 [::1]:2500 [::]:* users:(("debug",pid=168,fd=7)) ); assert_eq!(services.len(), 4); - assert_eq!(services[0].port, 2500); - assert_eq!(services[0].addresses, vec!["[::1]:2500"]); + assert_eq!(services[0].port, 3000); + assert_eq!(services[0].addresses, vec!["127.0.0.1:3000"]); assert_eq!(services[0].processes, vec![ - r#"users:(("debug",pid=168,fd=7))"# - ]); - assert!(!services[0].preview_supported); - assert_eq!(services[1].port, 3000); - assert_eq!(services[1].addresses, vec!["127.0.0.1:3000"]); - assert_eq!(services[1].processes, vec![ r#"users:(("node",pid=42,fd=23))"# ]); + assert!(services[0].preview_supported); + assert_eq!(services[1].port, 5173); + assert_eq!(services[1].addresses, vec!["0.0.0.0:5173"]); assert!(services[1].preview_supported); - assert_eq!(services[2].port, 5173); - assert_eq!(services[2].addresses, vec!["0.0.0.0:5173"]); + assert_eq!(services[2].port, 8080); + assert_eq!(services[2].addresses, vec!["[::]:8080"]); assert!(services[2].preview_supported); - assert_eq!(services[3].port, 8080); - assert_eq!(services[3].addresses, vec!["[::]:8080"]); - assert!(services[3].preview_supported); + assert_eq!(services[3].port, 2500); + assert_eq!(services[3].addresses, vec!["[::1]:2500"]); + assert_eq!(services[3].processes, vec![ + r#"users:(("debug",pid=168,fd=7))"# + ]); + assert!(!services[3].preview_supported); } #[test] @@ -931,6 +1064,52 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19)) }]); } + #[test] + fn proc_net_parser_extracts_listening_tcp_services_without_processes() { + let discovery = parse_sandbox_services( + r#" +FABRO_PROC_NET_TCP /proc/net/tcp + sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 0100007F:0BB8 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 11111 + 1: 00000000:1435 00000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 22222 + 2: 0100007F:2328 00000000:0000 01 00000000:00000000 00:00000000 00000000 501 0 33333 +FABRO_PROC_NET_TCP /proc/net/tcp6 + sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode + 0: 00000000000000000000000000000000:1F90 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 44444 + 1: 00000000000000000000000001000000:09C4 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 501 0 55555 +"#, + "daytona", + ); + + assert_eq!(discovery.source, SandboxServiceDiscoverySource::Procfs); + assert_eq!(discovery.services, vec![ + SandboxService { + port: 3000, + addresses: vec!["127.0.0.1:3000".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 5173, + addresses: vec!["0.0.0.0:5173".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 8080, + addresses: vec!["[::]:8080".to_string()], + processes: vec![], + preview_supported: true, + }, + SandboxService { + port: 2500, + addresses: vec!["[::1]:2500".to_string()], + processes: vec![], + preview_supported: false, + }, + ]); + } + #[test] fn preview_support_is_daytona_only_for_documented_range() { assert!(!preview_supported("daytona", 2500)); @@ -963,7 +1142,7 @@ LISTEN 0 4096 [::]:3000 [::]:* users:(("vite",pid=84,fd=19)) result.stdout.clear(); assert_eq!( sandbox_service_command_failure_detail(&result), - "ss -H -ltnp failed" + "sandbox service discovery command failed" ); } diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index 9e7e38c86..03271cb33 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -81,7 +81,10 @@ pub use run_summary::RunSummary; pub use run_title::{RunTitleError, infer_run_title, normalize_explicit_run_title}; pub use sandbox_details::{SandboxDetails, SandboxResources, SandboxState, SandboxTimestamps}; pub use sandbox_record::SandboxRecord; -pub use sandbox_services::{SandboxService, SandboxServiceListResponse}; +pub use sandbox_services::{ + SandboxService, SandboxServiceDiscoverySource, SandboxServiceListMeta, + SandboxServiceListResponse, +}; pub use secret::{SecretMetadata, SecretType}; pub use stage_completion::StageCompletion; pub use stage_handler::StageHandler; diff --git a/lib/crates/fabro-types/src/sandbox_services.rs b/lib/crates/fabro-types/src/sandbox_services.rs index 7212c174b..48f61be32 100644 --- a/lib/crates/fabro-types/src/sandbox_services.rs +++ b/lib/crates/fabro-types/src/sandbox_services.rs @@ -1,3 +1,15 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum SandboxServiceDiscoverySource { + Ss, + Procfs, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SandboxServiceListMeta { + pub source: SandboxServiceDiscoverySource, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SandboxService { pub port: u16, @@ -9,4 +21,5 @@ pub struct SandboxService { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SandboxServiceListResponse { pub data: Vec, + pub meta: SandboxServiceListMeta, } diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 5cf2ab09d..55d3fc6e5 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -290,6 +290,8 @@ models/sandbox-details.ts models/sandbox-file-entry.ts models/sandbox-file-list-response.ts models/sandbox-resources.ts +models/sandbox-service-discovery-source.ts +models/sandbox-service-list-meta.ts models/sandbox-service-list-response.ts models/sandbox-service.ts models/sandbox-state.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 083763307..95f805b5e 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -268,6 +268,8 @@ export * from './sandbox-file-entry'; export * from './sandbox-file-list-response'; export * from './sandbox-resources'; export * from './sandbox-service'; +export * from './sandbox-service-discovery-source'; +export * from './sandbox-service-list-meta'; export * from './sandbox-service-list-response'; export * from './sandbox-state'; export * from './sandbox-timestamps'; diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts new file mode 100644 index 000000000..78024461f --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-discovery-source.ts @@ -0,0 +1,29 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Tool or kernel interface used to discover sandbox services. + */ + +export const SandboxServiceDiscoverySource = { + SS: 'ss', + PROCFS: 'procfs' +} as const; + +export type SandboxServiceDiscoverySource = typeof SandboxServiceDiscoverySource[keyof typeof SandboxServiceDiscoverySource]; + + + diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts new file mode 100644 index 000000000..4d45b0489 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-meta.ts @@ -0,0 +1,28 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { SandboxServiceDiscoverySource } from './sandbox-service-discovery-source'; + +/** + * Metadata about sandbox service discovery. + */ +export interface SandboxServiceListMeta { + 'source': SandboxServiceDiscoverySource; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts index 15a6c634c..86071eba6 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service-list-response.ts @@ -16,11 +16,15 @@ // May contain unused imports in some cases // @ts-ignore import type { SandboxService } from './sandbox-service'; +// May contain unused imports in some cases +// @ts-ignore +import type { SandboxServiceListMeta } from './sandbox-service-list-meta'; /** * Non-paginated list of listening TCP services in a run sandbox. */ export interface SandboxServiceListResponse { 'data': Array; + 'meta': SandboxServiceListMeta; } diff --git a/lib/packages/fabro-api-client/src/models/sandbox-service.ts b/lib/packages/fabro-api-client/src/models/sandbox-service.ts index 0550d38ec..3b4c246ed 100644 --- a/lib/packages/fabro-api-client/src/models/sandbox-service.ts +++ b/lib/packages/fabro-api-client/src/models/sandbox-service.ts @@ -23,11 +23,11 @@ export interface SandboxService { */ 'port': number; /** - * Local bind addresses reported by `ss`. + * Local bind addresses discovered from `ss` or `/proc/net/tcp*`. */ 'addresses': Array; /** - * Visible process summaries reported by `ss`. + * Visible process summaries when available. Empty when the sandbox only supports `/proc/net/tcp*` discovery. */ 'processes': Array; /**
PortBindingsProcessActionPortBindingsProcess +
{service.port} + {service.port} {service.addresses.length > 0 ? ( service.addresses.join(", ") ) : ( - )} + {service.processes.length > 0 ? ( service.processes.join(", ") ) : ( - )} + {service.preview_supported ? ( - ) : ( - Unavailable - )} + ) : null}