diff --git a/.idea/data_source_mapping.xml b/.idea/data_source_mapping.xml
deleted file mode 100644
index 1c4d39c..0000000
--- a/.idea/data_source_mapping.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs
index f7fbd59..27bd15e 100644
--- a/crates/migration/src/lib.rs
+++ b/crates/migration/src/lib.rs
@@ -9,6 +9,7 @@ impl MigratorTrait for Migrator {
Box::new(m2025_10_25_000001_create_jobs::Migration),
Box::new(m2025_10_25_000002_create_vms::Migration),
Box::new(m2025_11_02_000003_create_job_logs::Migration),
+ Box::new(m2025_11_15_000004_create_job_ssh_keys::Migration),
]
}
}
@@ -206,3 +207,54 @@ mod m2025_11_02_000003_create_job_logs {
}
}
}
+
+
+mod m2025_11_15_000004_create_job_ssh_keys {
+ use super::*;
+
+ pub struct Migration;
+
+ impl sea_orm_migration::prelude::MigrationName for Migration {
+ fn name(&self) -> &str {
+ "m2025_11_15_000004_create_job_ssh_keys"
+ }
+ }
+
+ #[derive(Iden)]
+ enum JobSshKeys {
+ Table,
+ RequestId,
+ PublicKey,
+ PrivateKey,
+ CreatedAt,
+ }
+
+ #[async_trait::async_trait]
+ impl MigrationTrait for Migration {
+ async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .create_table(
+ Table::create()
+ .table(JobSshKeys::Table)
+ .if_not_exists()
+ .col(ColumnDef::new(JobSshKeys::RequestId).uuid().not_null().primary_key())
+ .col(ColumnDef::new(JobSshKeys::PublicKey).text().not_null())
+ .col(ColumnDef::new(JobSshKeys::PrivateKey).text().not_null())
+ .col(
+ ColumnDef::new(JobSshKeys::CreatedAt)
+ .timestamp_with_time_zone()
+ .not_null()
+ .default(Expr::current_timestamp()),
+ )
+ .to_owned(),
+ )
+ .await
+ }
+
+ async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+ manager
+ .drop_table(Table::drop().table(JobSshKeys::Table).to_owned())
+ .await
+ }
+ }
+}
diff --git a/crates/orchestrator/Cargo.toml b/crates/orchestrator/Cargo.toml
index 29ef72d..070ef0f 100644
--- a/crates/orchestrator/Cargo.toml
+++ b/crates/orchestrator/Cargo.toml
@@ -13,16 +13,18 @@ common = { path = "../common" }
clap = { version = "4", features = ["derive", "env"] }
miette = { version = "7", features = ["fancy"] }
tracing = "0.1"
-tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs", "io-util"] }
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs", "io-util", "process", "net"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"
config = { version = "0.15", default-features = false, features = ["yaml"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "gzip", "brotli", "zstd"] }
-# HTTP server for logs
+# HTTP server for logs (runner serving removed)
axum = { version = "0.8", features = ["macros"] }
-# gRPC server
-tonic = { version = "0.14", features = ["transport"] }
+# SSH client for upload/exec/logs
+ssh2 = "0.9"
+ssh-key = { version = "0.6", features = ["ed25519"] }
+rand_core = "0.6"
# Compression/decompression
zstd = "0.13"
# DB (optional basic persistence)
diff --git a/crates/orchestrator/src/config.rs b/crates/orchestrator/src/config.rs
index afa864f..56574e2 100644
--- a/crates/orchestrator/src/config.rs
+++ b/crates/orchestrator/src/config.rs
@@ -47,6 +47,8 @@ pub struct ImageDefaults {
pub cpu: Option,
pub ram_mb: Option,
pub disk_gb: Option,
+ /// Default SSH username for this image (e.g., ubuntu, root)
+ pub ssh_user: Option,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
diff --git a/crates/orchestrator/src/http.rs b/crates/orchestrator/src/http.rs
index 2d37dbe..16ffc02 100644
--- a/crates/orchestrator/src/http.rs
+++ b/crates/orchestrator/src/http.rs
@@ -1,9 +1,7 @@
use axum::{extract::Path, http::StatusCode, response::{IntoResponse, Response}, routing::get, Router};
use std::net::SocketAddr;
-use std::path::PathBuf;
use std::sync::Arc;
-use tokio::fs;
-use tracing::{debug, info, warn};
+use tracing::{info, warn};
use uuid::Uuid;
use crate::persist::Persist;
@@ -11,14 +9,12 @@ use crate::persist::Persist;
#[derive(Clone)]
pub struct HttpState {
persist: Arc,
- runner_dir: Option,
}
-pub fn build_router(persist: Arc, runner_dir: Option) -> Router {
- let state = HttpState { persist, runner_dir };
+pub fn build_router(persist: Arc) -> Router {
+ let state = HttpState { persist };
Router::new()
.route("/jobs/{request_id}/logs", get(get_logs))
- .route("/runners/{name}", get(get_runner))
.with_state(state)
}
@@ -47,43 +43,8 @@ async fn get_logs(
}
}
-async fn get_runner(
- Path(name): Path,
- axum::extract::State(state): axum::extract::State,
-) -> Response {
- let Some(dir) = state.runner_dir.as_ref() else {
- return (StatusCode::SERVICE_UNAVAILABLE, "runner serving disabled").into_response();
- };
- // Basic validation: prevent path traversal; allow only simple file names
- if name.contains('/') || name.contains('\\') || name.starts_with('.') {
- return StatusCode::BAD_REQUEST.into_response();
- }
- let path = dir.join(&name);
- if !path.starts_with(dir) {
- return StatusCode::BAD_REQUEST.into_response();
- }
- match fs::read(&path).await {
- Ok(bytes) => {
- debug!(path = %path.display(), size = bytes.len(), "serving runner binary");
- (
- StatusCode::OK,
- [
- (axum::http::header::CONTENT_TYPE, "application/octet-stream"),
- (axum::http::header::CONTENT_DISPOSITION, &format!("attachment; filename=\"{}\"", name)),
- ],
- bytes,
- )
- .into_response()
- }
- Err(e) => {
- debug!(error = %e, path = %path.display(), "runner file not found");
- StatusCode::NOT_FOUND.into_response()
- }
- }
-}
-
-pub async fn serve(addr: SocketAddr, persist: Arc, runner_dir: Option, shutdown: impl std::future::Future