runtime: own working directories
This commit is contained in:
parent
afc43780a7
commit
fa233ba025
|
|
@ -1,11 +1,11 @@
|
|||
use crate::catalog::{CreateWorkerRequest, WorkingDirectoryStatus};
|
||||
use crate::catalog::{CreateWorkerRequest, WorkingDirectoryRequest, WorkingDirectoryStatus};
|
||||
use crate::config_bundle::ConfigBundle;
|
||||
use crate::error::RuntimeError;
|
||||
use crate::identity::WorkerRef;
|
||||
use crate::interaction::WorkerInput;
|
||||
#[cfg(feature = "ws-server")]
|
||||
use crate::observation::WorkerObservationEvent;
|
||||
use crate::working_directory::WorkingDirectoryBinding;
|
||||
use crate::working_directory::{WorkingDirectoryBinding, WorkingDirectoryDiagnostic};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
|
@ -313,6 +313,42 @@ pub trait WorkerExecutionBackend: Send + Sync + 'static {
|
|||
|
||||
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult;
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
_request: &WorkingDirectoryRequest,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_unsupported",
|
||||
"Worker execution backend does not support working directory materialization",
|
||||
))
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_unknown",
|
||||
format!("working directory `{working_directory_id}` is not known to this Runtime"),
|
||||
))
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_cleanup_unsupported",
|
||||
format!(
|
||||
"working directory `{working_directory_id}` cannot be cleaned up by this backend"
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
fn dispatch_input(
|
||||
&self,
|
||||
handle: &WorkerExecutionHandle,
|
||||
|
|
@ -358,6 +394,31 @@ impl WorkerExecutionBackendRef {
|
|||
self.backend.spawn_worker(request)
|
||||
}
|
||||
|
||||
pub(crate) fn create_working_directory(
|
||||
&self,
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
self.backend.create_working_directory(request)
|
||||
}
|
||||
|
||||
pub(crate) fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
|
||||
self.backend.list_working_directories()
|
||||
}
|
||||
|
||||
pub(crate) fn working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
self.backend.working_directory(working_directory_id)
|
||||
}
|
||||
|
||||
pub(crate) fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
self.backend.cleanup_working_directory(working_directory_id)
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_input(
|
||||
&self,
|
||||
handle: &WorkerExecutionHandle,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
use crate::Runtime;
|
||||
use crate::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, WorkerDetail, WorkerLifecycleAck, WorkerSummary,
|
||||
WorkingDirectoryRequest, WorkingDirectoryStatus,
|
||||
};
|
||||
use crate::config_bundle::{ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary};
|
||||
use crate::error::RuntimeError;
|
||||
|
|
@ -138,6 +139,14 @@ pub fn runtime_http_router(runtime: Runtime, local_token: Option<String>) -> Rou
|
|||
"/v1/config-bundles/{bundle_id}/availability",
|
||||
get(check_config_bundle),
|
||||
)
|
||||
.route(
|
||||
"/v1/working-directories",
|
||||
get(list_working_directories).post(create_working_directory),
|
||||
)
|
||||
.route(
|
||||
"/v1/working-directories/{working_directory_id}",
|
||||
get(get_working_directory).delete(cleanup_working_directory),
|
||||
)
|
||||
.route("/v1/workers", get(list_workers).post(create_worker))
|
||||
.route("/v1/workers/{worker_id}", get(get_worker))
|
||||
.route("/v1/workers/{worker_id}/input", post(send_worker_input))
|
||||
|
|
@ -197,6 +206,18 @@ pub struct RuntimeHttpWorkersResponse {
|
|||
pub workers: Vec<WorkerSummary>,
|
||||
}
|
||||
|
||||
/// `GET /v1/working-directories` response.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkingDirectoriesResponse {
|
||||
pub working_directories: Vec<WorkingDirectoryStatus>,
|
||||
}
|
||||
|
||||
/// Working directory response used by create/detail/delete endpoints.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkingDirectoryResponse {
|
||||
pub working_directory: WorkingDirectoryStatus,
|
||||
}
|
||||
|
||||
/// Worker detail response used by create/detail endpoints.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RuntimeHttpWorkerResponse {
|
||||
|
|
@ -399,6 +420,58 @@ async fn list_workers(
|
|||
Ok(Json(RuntimeHttpWorkersResponse { workers }))
|
||||
}
|
||||
|
||||
async fn list_working_directories(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
) -> RestResult<RuntimeHttpWorkingDirectoriesResponse> {
|
||||
let working_directories = state
|
||||
.runtime
|
||||
.list_working_directories()
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpWorkingDirectoriesResponse {
|
||||
working_directories,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn create_working_directory(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
body: Result<Json<WorkingDirectoryRequest>, JsonRejection>,
|
||||
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
|
||||
let Json(request) = body.map_err(RuntimeHttpRestError::json_rejection)?;
|
||||
let working_directory = state
|
||||
.runtime
|
||||
.create_working_directory(request)
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
|
||||
working_directory,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_working_directory(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(working_directory_id): Path<String>,
|
||||
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
|
||||
let working_directory = state
|
||||
.runtime
|
||||
.working_directory(&working_directory_id)
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
|
||||
working_directory,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn cleanup_working_directory(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(working_directory_id): Path<String>,
|
||||
) -> RestResult<RuntimeHttpWorkingDirectoryResponse> {
|
||||
let working_directory = state
|
||||
.runtime
|
||||
.cleanup_working_directory(&working_directory_id)
|
||||
.map_err(RuntimeHttpRestError::runtime)?;
|
||||
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
|
||||
working_directory,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_worker(
|
||||
State(state): State<RuntimeHttpState>,
|
||||
Path(worker_id): Path<String>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use crate::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail, WorkerLifecycleAck,
|
||||
WorkerStatus, WorkerSummary, WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
|
||||
WorkerStatus, WorkerSummary, WorkingDirectoryRequest,
|
||||
WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
|
||||
};
|
||||
use crate::config_bundle::{
|
||||
ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle,
|
||||
|
|
@ -234,6 +235,79 @@ impl Runtime {
|
|||
Ok(event_id)
|
||||
}
|
||||
|
||||
/// Create a Runtime-owned working directory through the attached execution backend.
|
||||
pub fn create_working_directory(
|
||||
&self,
|
||||
request: WorkingDirectoryRequest,
|
||||
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
||||
let backend = {
|
||||
let state = self.lock()?;
|
||||
state.ensure_running()?;
|
||||
state.execution_backend.clone().ok_or_else(|| {
|
||||
RuntimeError::ExecutionBackendUnavailable {
|
||||
message: "working directory creation requires an execution backend".to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
backend
|
||||
.create_working_directory(&request)
|
||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
||||
}
|
||||
|
||||
/// List Runtime-owned working directories through the attached execution backend.
|
||||
pub fn list_working_directories(
|
||||
&self,
|
||||
) -> Result<Vec<CatalogWorkingDirectoryStatus>, RuntimeError> {
|
||||
let backend = {
|
||||
let state = self.lock()?;
|
||||
state.ensure_running()?;
|
||||
state.execution_backend.clone().ok_or_else(|| {
|
||||
RuntimeError::ExecutionBackendUnavailable {
|
||||
message: "working directory listing requires an execution backend".to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
Ok(backend.list_working_directories())
|
||||
}
|
||||
|
||||
/// Get a Runtime-owned working directory status.
|
||||
pub fn working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
||||
let backend = {
|
||||
let state = self.lock()?;
|
||||
state.ensure_running()?;
|
||||
state.execution_backend.clone().ok_or_else(|| {
|
||||
RuntimeError::ExecutionBackendUnavailable {
|
||||
message: "working directory lookup requires an execution backend".to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
backend
|
||||
.working_directory(working_directory_id)
|
||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
||||
}
|
||||
|
||||
/// Cleanup a Runtime-owned working directory.
|
||||
pub fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
||||
let backend = {
|
||||
let state = self.lock()?;
|
||||
state.ensure_running()?;
|
||||
state.execution_backend.clone().ok_or_else(|| {
|
||||
RuntimeError::ExecutionBackendUnavailable {
|
||||
message: "working directory cleanup requires an execution backend".to_string(),
|
||||
}
|
||||
})?
|
||||
};
|
||||
backend
|
||||
.cleanup_working_directory(working_directory_id)
|
||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
||||
}
|
||||
|
||||
/// Create a Worker through the canonical ConfigBundle + execution backend path.
|
||||
pub fn create_worker(
|
||||
&self,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::catalog::{WorkingDirectoryRequest, WorkingDirectoryStatus};
|
||||
use crate::config_bundle::verified_profile_source_archive;
|
||||
use crate::execution::{
|
||||
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
|
||||
|
|
@ -24,7 +25,9 @@ use crate::resource::{
|
|||
BackendResourceClient, BackendResourceError, ProfileSourceArchiveCache,
|
||||
build_profile_source_archive_fetch_request, profile_source_archive_from_response,
|
||||
};
|
||||
use crate::working_directory::{WorkingDirectoryBinding, WorkingDirectoryMaterializer};
|
||||
use crate::working_directory::{
|
||||
WorkingDirectoryBinding, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use manifest::paths;
|
||||
use protocol::{Method, Segment, WorkerStatus};
|
||||
|
|
@ -441,6 +444,52 @@ where
|
|||
&self.backend_id
|
||||
}
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
request: &WorkingDirectoryRequest,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
let Some(materializer) = self.working_directory_materializer.as_ref() else {
|
||||
return Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_materializer_unavailable",
|
||||
"working directory materialization requested, but no materializer is configured for this runtime backend",
|
||||
));
|
||||
};
|
||||
Ok(materializer.create(request)?.status())
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> Vec<WorkingDirectoryStatus> {
|
||||
self.working_directory_materializer
|
||||
.as_ref()
|
||||
.and_then(|materializer| materializer.list_working_directories().ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
let Some(materializer) = self.working_directory_materializer.as_ref() else {
|
||||
return Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_materializer_unavailable",
|
||||
"working directory lookup requested, but no materializer is configured for this runtime backend",
|
||||
));
|
||||
};
|
||||
materializer.working_directory_status(working_directory_id)
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> Result<WorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||
let Some(materializer) = self.working_directory_materializer.as_ref() else {
|
||||
return Err(WorkingDirectoryDiagnostic::rejected(
|
||||
"working_directory_materializer_unavailable",
|
||||
"working directory cleanup requested, but no materializer is configured for this runtime backend",
|
||||
));
|
||||
};
|
||||
materializer.cleanup_working_directory(working_directory_id)
|
||||
}
|
||||
|
||||
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
|
||||
if self
|
||||
.workers
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ pub struct WorkingDirectoryDiagnostic {
|
|||
}
|
||||
|
||||
impl WorkingDirectoryDiagnostic {
|
||||
pub fn rejected(code: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self::new(code, message)
|
||||
}
|
||||
|
||||
fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code: code.into(),
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use std::{
|
|||
use worker_runtime::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail as EmbeddedWorkerDetail,
|
||||
WorkerStatus as EmbeddedWorkerStatus, WorkingDirectoryClaim, WorkingDirectoryRequest,
|
||||
WorkingDirectorySummary,
|
||||
WorkingDirectoryStatus, WorkingDirectorySummary,
|
||||
};
|
||||
use worker_runtime::config_bundle::{
|
||||
ConfigBundle, ConfigBundleAvailability, ConfigBundleMetadata, ConfigBundleProvenance,
|
||||
|
|
@ -32,6 +32,7 @@ use worker_runtime::http_server::{
|
|||
RuntimeHttpErrorResponse, RuntimeHttpSummaryResponse, RuntimeHttpTranscriptResponse,
|
||||
RuntimeHttpWorkerInputResponse, RuntimeHttpWorkerLifecycleRequest,
|
||||
RuntimeHttpWorkerLifecycleResponse, RuntimeHttpWorkerResponse, RuntimeHttpWorkersResponse,
|
||||
RuntimeHttpWorkingDirectoriesResponse, RuntimeHttpWorkingDirectoryResponse,
|
||||
};
|
||||
use worker_runtime::identity::{
|
||||
RuntimeId as EmbeddedRuntimeId, WorkerId as EmbeddedWorkerId, WorkerRef as EmbeddedWorkerRef,
|
||||
|
|
@ -266,6 +267,14 @@ pub struct WorkerLookupResult {
|
|||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct RuntimeWorkingDirectoryResult {
|
||||
pub state: WorkerOperationState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub working_directory: Option<WorkingDirectoryStatus>,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
/// Browser-safe worker spawn request shape.
|
||||
///
|
||||
/// The request carries Browser-facing launch semantics only: workspace intent,
|
||||
|
|
@ -537,6 +546,56 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
|
|||
|
||||
fn worker(&self, worker_id: &str) -> WorkerLookupResult;
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
_request: WorkingDirectoryRequest,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Unsupported,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic(
|
||||
"runtime_working_directory_create_unsupported",
|
||||
DiagnosticSeverity::Info,
|
||||
"runtime does not implement working directory creation".to_string(),
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
|
||||
RuntimeList::new(Vec::new(), Vec::new())
|
||||
}
|
||||
|
||||
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
|
||||
RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Unsupported,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic(
|
||||
"runtime_working_directory_lookup_unsupported",
|
||||
DiagnosticSeverity::Info,
|
||||
format!(
|
||||
"runtime does not implement working directory lookup for `{working_directory_id}`"
|
||||
),
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Unsupported,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic(
|
||||
"runtime_working_directory_cleanup_unsupported",
|
||||
DiagnosticSeverity::Info,
|
||||
format!(
|
||||
"runtime does not implement working directory cleanup for `{working_directory_id}`"
|
||||
),
|
||||
)],
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
|
||||
WorkerSpawnResult {
|
||||
state: WorkerOperationState::Unsupported,
|
||||
|
|
@ -882,6 +941,47 @@ impl RuntimeRegistry {
|
|||
Ok(runtime.spawn_worker(request))
|
||||
}
|
||||
|
||||
pub fn create_working_directory(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
request: WorkingDirectoryRequest,
|
||||
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
Ok(runtime.create_working_directory(request))
|
||||
}
|
||||
|
||||
pub fn list_working_directories(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
) -> Result<RuntimeList<WorkingDirectoryStatus>, RuntimeRegistryError> {
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
Ok(runtime.list_working_directories())
|
||||
}
|
||||
|
||||
pub fn working_directory(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
working_directory_id: &str,
|
||||
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
validate_backend_identifier("working_directory_id", working_directory_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
Ok(runtime.working_directory(working_directory_id))
|
||||
}
|
||||
|
||||
pub fn cleanup_working_directory(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
working_directory_id: &str,
|
||||
) -> Result<RuntimeWorkingDirectoryResult, RuntimeRegistryError> {
|
||||
validate_backend_identifier("runtime_id", runtime_id)?;
|
||||
validate_backend_identifier("working_directory_id", working_directory_id)?;
|
||||
let runtime = self.runtime(runtime_id)?;
|
||||
Ok(runtime.cleanup_working_directory(working_directory_id))
|
||||
}
|
||||
|
||||
pub fn sync_config_bundle(
|
||||
&self,
|
||||
runtime_id: &str,
|
||||
|
|
@ -1311,6 +1411,64 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
request: WorkingDirectoryRequest,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
match self.runtime.create_working_directory(request) {
|
||||
Ok(working_directory) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(err) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![embedded_runtime_diagnostic(&err)],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
|
||||
match self.runtime.list_working_directories() {
|
||||
Ok(items) => RuntimeList::new(items, Vec::new()),
|
||||
Err(err) => RuntimeList::new(Vec::new(), vec![embedded_runtime_diagnostic(&err)]),
|
||||
}
|
||||
}
|
||||
|
||||
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
|
||||
match self.runtime.working_directory(working_directory_id) {
|
||||
Ok(working_directory) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(err) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![embedded_runtime_diagnostic(&err)],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
match self.runtime.cleanup_working_directory(working_directory_id) {
|
||||
Ok(working_directory) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(err) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![embedded_runtime_diagnostic(&err)],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
|
||||
let mut diagnostics = Vec::new();
|
||||
if matches!(
|
||||
|
|
@ -1811,6 +1969,13 @@ impl RemoteWorkerRuntime {
|
|||
self.send_json(self.http.post(self.endpoint(path)).json(body))
|
||||
}
|
||||
|
||||
fn delete_json<T>(&self, path: &str) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
{
|
||||
self.send_json(self.http.delete(self.endpoint(path)))
|
||||
}
|
||||
|
||||
fn send_json<T>(&self, request: RequestBuilder) -> Result<T, RuntimeDiagnostic>
|
||||
where
|
||||
T: DeserializeOwned + Send + 'static,
|
||||
|
|
@ -2035,6 +2200,71 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
request: WorkingDirectoryRequest,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
match self.post_json::<_, RuntimeHttpWorkingDirectoryResponse>(
|
||||
"/v1/working-directories",
|
||||
&request,
|
||||
) {
|
||||
Ok(response) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(response.working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(diagnostic) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> RuntimeList<WorkingDirectoryStatus> {
|
||||
match self.get_json::<RuntimeHttpWorkingDirectoriesResponse>("/v1/working-directories") {
|
||||
Ok(response) => RuntimeList::new(response.working_directories, Vec::new()),
|
||||
Err(diagnostic) => RuntimeList::new(Vec::new(), vec![diagnostic]),
|
||||
}
|
||||
}
|
||||
|
||||
fn working_directory(&self, working_directory_id: &str) -> RuntimeWorkingDirectoryResult {
|
||||
match self.get_json::<RuntimeHttpWorkingDirectoryResponse>(&format!(
|
||||
"/v1/working-directories/{working_directory_id}"
|
||||
)) {
|
||||
Ok(response) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(response.working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(diagnostic) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> RuntimeWorkingDirectoryResult {
|
||||
match self.delete_json::<RuntimeHttpWorkingDirectoryResponse>(&format!(
|
||||
"/v1/working-directories/{working_directory_id}"
|
||||
)) {
|
||||
Ok(response) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Accepted,
|
||||
working_directory: Some(response.working_directory),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
Err(diagnostic) => RuntimeWorkingDirectoryResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
working_directory: None,
|
||||
diagnostics: vec![diagnostic],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_worker(&self, request: WorkerSpawnRequest) -> WorkerSpawnResult {
|
||||
if matches!(
|
||||
request.acceptance,
|
||||
|
|
@ -2801,6 +3031,14 @@ fn remote_reqwest_diagnostic(runtime_id: &str, err: reqwest::Error) -> RuntimeDi
|
|||
}
|
||||
}
|
||||
|
||||
fn sanitize_remote_runtime_message(code: &str, message: &str) -> String {
|
||||
if message.contains('/') || message.contains('\\') {
|
||||
format!("remote Runtime returned {code}; backend-private details were omitted")
|
||||
} else {
|
||||
message.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_http_status_diagnostic(
|
||||
runtime_id: &str,
|
||||
status: StatusCode,
|
||||
|
|
@ -2811,6 +3049,10 @@ fn remote_http_status_diagnostic(
|
|||
.as_ref()
|
||||
.map(|error| error.error.code.as_str())
|
||||
.unwrap_or("remote_http_error");
|
||||
let remote_message = error
|
||||
.as_ref()
|
||||
.map(|error| sanitize_remote_runtime_message(remote_code, &error.error.message))
|
||||
.unwrap_or_else(|| "remote Runtime did not return a typed error body".to_string());
|
||||
let (code, severity) = match status {
|
||||
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
||||
("remote_runtime_auth_failed", DiagnosticSeverity::Error)
|
||||
|
|
@ -2820,14 +3062,12 @@ fn remote_http_status_diagnostic(
|
|||
("remote_runtime_unsupported", DiagnosticSeverity::Warning)
|
||||
}
|
||||
_ if status.is_server_error() => ("remote_runtime_http_error", DiagnosticSeverity::Error),
|
||||
_ => ("remote_runtime_http_rejected", DiagnosticSeverity::Warning),
|
||||
_ => (remote_code, DiagnosticSeverity::Warning),
|
||||
};
|
||||
diagnostic(
|
||||
code,
|
||||
severity,
|
||||
format!(
|
||||
"Remote Runtime '{runtime_id}' rejected request ({remote_code}, HTTP {status}); internal details were sanitized"
|
||||
),
|
||||
format!("Remote Runtime '{runtime_id}' rejected request (HTTP {status}): {remote_message}"),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ use serde::{Deserialize, Serialize};
|
|||
use tokio::net::TcpListener;
|
||||
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
|
||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||
use worker_runtime::working_directory::{
|
||||
LocalGitWorktreeMaterializer, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
|
||||
};
|
||||
use worker_runtime::working_directory::LocalGitWorktreeMaterializer;
|
||||
|
||||
use crate::companion::{
|
||||
CompanionCancelRequest, CompanionConsole, CompanionMessageRequest, CompanionMessageResponse,
|
||||
|
|
@ -67,6 +65,10 @@ use worker_runtime::interaction::{
|
|||
|
||||
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
|
||||
|
||||
fn embedded_runtime_id() -> String {
|
||||
EMBEDDED_WORKER_RUNTIME_ID.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AuthConfig {
|
||||
/// Local/dev-only mode. If a token is configured by a future entrypoint, it
|
||||
|
|
@ -162,14 +164,10 @@ pub struct WorkspaceApi {
|
|||
companion: Arc<CompanionConsole>,
|
||||
observation_proxy: BackendObservationProxy,
|
||||
resource_broker: BackendResourceBroker,
|
||||
working_directory_materializer: Arc<LocalGitWorktreeMaterializer>,
|
||||
}
|
||||
|
||||
impl WorkspaceApi {
|
||||
pub async fn new(config: ServerConfig, store: Arc<dyn ControlPlaneStore>) -> Result<Self> {
|
||||
let materializer = Arc::new(LocalGitWorktreeMaterializer::new(
|
||||
config.embedded_runtime_store_root.clone(),
|
||||
));
|
||||
let resource_broker = BackendResourceBroker::default();
|
||||
let execution_backend = WorkerRuntimeExecutionBackend::new(
|
||||
ProfileRuntimeWorkerFactory::new(config.workspace_root.clone())
|
||||
|
|
@ -180,7 +178,9 @@ impl WorkspaceApi {
|
|||
"failed to initialize embedded Worker backend: {err}"
|
||||
))
|
||||
})?
|
||||
.with_working_directory_materializer((*materializer).clone());
|
||||
.with_working_directory_materializer(LocalGitWorktreeMaterializer::new(
|
||||
config.embedded_runtime_store_root.clone(),
|
||||
));
|
||||
Self::new_with_execution_backend_and_broker(
|
||||
config,
|
||||
store,
|
||||
|
|
@ -241,9 +241,6 @@ impl WorkspaceApi {
|
|||
let runtime = Arc::new(runtime);
|
||||
let companion = Arc::new(CompanionConsole::new(runtime.clone()));
|
||||
let observation_proxy = BackendObservationProxy::new(config.runtime_event_sources.clone());
|
||||
let working_directory_materializer = Arc::new(LocalGitWorktreeMaterializer::new(
|
||||
config.embedded_runtime_store_root.clone(),
|
||||
));
|
||||
Ok(Self {
|
||||
records: LocalProjectRecordReader::new(config.workspace_root.clone()),
|
||||
config,
|
||||
|
|
@ -252,7 +249,6 @@ impl WorkspaceApi {
|
|||
companion,
|
||||
observation_proxy,
|
||||
resource_broker,
|
||||
working_directory_materializer,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -326,6 +322,14 @@ pub fn build_router(api: WorkspaceApi) -> Router {
|
|||
)
|
||||
.route("/api/hosts", get(list_hosts))
|
||||
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories",
|
||||
get(scoped_list_runtime_working_directories).post(scoped_create_runtime_working_directory),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories/{working_directory_id}",
|
||||
get(scoped_runtime_working_directory_detail).delete(scoped_cleanup_runtime_working_directory),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/working-directories",
|
||||
get(scoped_list_working_directories).post(scoped_create_working_directory),
|
||||
|
|
@ -662,6 +666,8 @@ pub enum BrowserWorkingDirectoryCleanupPolicy {
|
|||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BrowserWorkingDirectoryCreateRequest {
|
||||
#[serde(default = "embedded_runtime_id")]
|
||||
pub runtime_id: String,
|
||||
pub repository_id: String,
|
||||
#[serde(default)]
|
||||
pub selector: Option<String>,
|
||||
|
|
@ -807,6 +813,13 @@ struct ScopedWorkingDirectoryPath {
|
|||
working_directory_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedRuntimeWorkingDirectoryPath {
|
||||
workspace_id: String,
|
||||
runtime_id: String,
|
||||
working_directory_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedConfigBundlePath {
|
||||
workspace_id: String,
|
||||
|
|
@ -1054,6 +1067,52 @@ async fn scoped_get_worker_launch_options(
|
|||
get_worker_launch_options(State(api)).await
|
||||
}
|
||||
|
||||
async fn scoped_list_runtime_working_directories(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let list = api
|
||||
.runtime
|
||||
.list_working_directories(&path.runtime_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
items: list
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|status| status.summary)
|
||||
.collect(),
|
||||
diagnostics: list.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn scoped_create_runtime_working_directory(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||
Json(mut request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
request.runtime_id = path.runtime_id;
|
||||
create_working_directory_for_runtime(api, request)
|
||||
}
|
||||
|
||||
async fn scoped_runtime_working_directory_detail(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
working_directory_detail_for_runtime(api, &path.runtime_id, &path.working_directory_id)
|
||||
}
|
||||
|
||||
async fn scoped_cleanup_runtime_working_directory(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkingDirectoryPath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
cleanup_working_directory_for_runtime(api, &path.runtime_id, &path.working_directory_id)
|
||||
}
|
||||
|
||||
async fn scoped_list_working_directories(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
|
|
@ -1073,22 +1132,7 @@ async fn scoped_create_working_directory(
|
|||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let working_directory_request = working_directory_request_for_browser(&api, request)?;
|
||||
let binding = api
|
||||
.working_directory_materializer
|
||||
.create(&working_directory_request)
|
||||
.map_err(|diagnostic| {
|
||||
ApiError::from(Error::RuntimeOperationFailed {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
})?;
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
item: binding.status().summary,
|
||||
diagnostics: Vec::new(),
|
||||
}))
|
||||
create_working_directory_for_runtime(api, request)
|
||||
}
|
||||
|
||||
async fn scoped_working_directory_detail(
|
||||
|
|
@ -1096,21 +1140,11 @@ async fn scoped_working_directory_detail(
|
|||
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let status = api
|
||||
.working_directory_materializer
|
||||
.working_directory_status(&path.working_directory_id)
|
||||
.map_err(|diagnostic| {
|
||||
ApiError::from(Error::RuntimeOperationFailed {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
})?;
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
item: status.summary,
|
||||
diagnostics: Vec::new(),
|
||||
}))
|
||||
working_directory_detail_for_runtime(
|
||||
api,
|
||||
EMBEDDED_WORKER_RUNTIME_ID,
|
||||
&path.working_directory_id,
|
||||
)
|
||||
}
|
||||
|
||||
async fn scoped_cleanup_working_directory(
|
||||
|
|
@ -1118,20 +1152,89 @@ async fn scoped_cleanup_working_directory(
|
|||
AxumPath(path): AxumPath<ScopedWorkingDirectoryPath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let status = api
|
||||
.working_directory_materializer
|
||||
.cleanup_working_directory(&path.working_directory_id)
|
||||
.map_err(|diagnostic| {
|
||||
ApiError::from(Error::RuntimeOperationFailed {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
})?;
|
||||
cleanup_working_directory_for_runtime(
|
||||
api,
|
||||
EMBEDDED_WORKER_RUNTIME_ID,
|
||||
&path.working_directory_id,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_working_directory_for_runtime(
|
||||
api: WorkspaceApi,
|
||||
request: BrowserWorkingDirectoryCreateRequest,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
let runtime_id = request.runtime_id.clone();
|
||||
let working_directory_request = working_directory_request_for_browser(&api, request)?;
|
||||
let result = api
|
||||
.runtime
|
||||
.create_working_directory(&runtime_id, working_directory_request)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let Some(working_directory) = result.working_directory else {
|
||||
return Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id,
|
||||
code: "workspace_working_directory_create_failed".to_string(),
|
||||
message: "Runtime did not create working directory".to_string(),
|
||||
},
|
||||
result.diagnostics,
|
||||
));
|
||||
};
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
item: status.summary,
|
||||
diagnostics: Vec::new(),
|
||||
item: working_directory.summary,
|
||||
diagnostics: result.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
fn working_directory_detail_for_runtime(
|
||||
api: WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
working_directory_id: &str,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
let result = api
|
||||
.runtime
|
||||
.working_directory(runtime_id, working_directory_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let Some(working_directory) = result.working_directory else {
|
||||
return Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: runtime_id.to_string(),
|
||||
code: "workspace_working_directory_lookup_failed".to_string(),
|
||||
message: "Runtime did not return working directory".to_string(),
|
||||
},
|
||||
result.diagnostics,
|
||||
));
|
||||
};
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
item: working_directory.summary,
|
||||
diagnostics: result.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
fn cleanup_working_directory_for_runtime(
|
||||
api: WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
working_directory_id: &str,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryDetailResponse>> {
|
||||
let result = api
|
||||
.runtime
|
||||
.cleanup_working_directory(runtime_id, working_directory_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let Some(working_directory) = result.working_directory else {
|
||||
return Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: runtime_id.to_string(),
|
||||
code: "workspace_working_directory_cleanup_failed".to_string(),
|
||||
message: "Runtime did not cleanup working directory".to_string(),
|
||||
},
|
||||
result.diagnostics,
|
||||
));
|
||||
};
|
||||
Ok(Json(BrowserWorkingDirectoryDetailResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
item: working_directory.summary,
|
||||
diagnostics: result.diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -1812,26 +1915,7 @@ async fn create_workspace_worker(
|
|||
working_directory_id: selection.working_directory_id,
|
||||
relative_cwd: selection.relative_cwd,
|
||||
});
|
||||
if resolved_working_directory.is_some() && request.runtime_id != EMBEDDED_WORKER_RUNTIME_ID {
|
||||
return Err(Error::RuntimeOperationFailed {
|
||||
runtime_id: request.runtime_id.clone(),
|
||||
code: "working_directory_runtime_unsupported".to_string(),
|
||||
message:
|
||||
"created working directories are only supported by the embedded local runtime in v0"
|
||||
.to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
if let Some(working_directory) = resolved_working_directory.as_ref() {
|
||||
api.working_directory_materializer
|
||||
.bind_working_directory(
|
||||
&working_directory.working_directory_id,
|
||||
working_directory.relative_cwd.as_deref(),
|
||||
)
|
||||
.map_err(|diagnostic| {
|
||||
working_directory_api_error(request.runtime_id.clone(), diagnostic)
|
||||
})?;
|
||||
}
|
||||
validate_working_directory_claim_for_browser(resolved_working_directory.as_ref())?;
|
||||
let result = api
|
||||
.runtime
|
||||
.spawn_worker(
|
||||
|
|
@ -2924,16 +3008,55 @@ fn working_directory_repository_options(
|
|||
}
|
||||
|
||||
fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult<Vec<WorkingDirectorySummary>> {
|
||||
api.working_directory_materializer
|
||||
.list_working_directories()
|
||||
.map(|items| items.into_iter().map(|status| status.summary).collect())
|
||||
.map_err(|diagnostic| {
|
||||
ApiError::from(Error::RuntimeOperationFailed {
|
||||
let list = api
|
||||
.runtime
|
||||
.list_working_directories(EMBEDDED_WORKER_RUNTIME_ID)
|
||||
.map_err(|err| err.into_error())?;
|
||||
if !list.diagnostics.is_empty() {
|
||||
return Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||
code: diagnostic.code,
|
||||
message: diagnostic.message,
|
||||
})
|
||||
})
|
||||
code: "workspace_working_directory_list_failed".to_string(),
|
||||
message: "Runtime did not list working directories".to_string(),
|
||||
},
|
||||
list.diagnostics,
|
||||
));
|
||||
}
|
||||
Ok(list
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|status| status.summary)
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn validate_working_directory_claim_for_browser(
|
||||
claim: Option<&WorkingDirectoryClaim>,
|
||||
) -> ApiResult<()> {
|
||||
let Some(claim) = claim else {
|
||||
return Ok(());
|
||||
};
|
||||
if let Some(relative_cwd) = claim.relative_cwd.as_deref() {
|
||||
let path = Path::new(relative_cwd);
|
||||
if path.is_absolute()
|
||||
|| path
|
||||
.components()
|
||||
.any(|component| !matches!(component, Component::CurDir | Component::Normal(_)))
|
||||
{
|
||||
return Err(ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.to_string(),
|
||||
code: "working_directory_relative_cwd_invalid".to_string(),
|
||||
message: "working directory relative_cwd must stay inside the Runtime-owned working directory".to_string(),
|
||||
},
|
||||
vec![RuntimeDiagnostic {
|
||||
code: "working_directory_relative_cwd_invalid".to_string(),
|
||||
severity: DiagnosticSeverity::Error,
|
||||
message: "relative_cwd must be a relative path without parent traversal".to_string(),
|
||||
}],
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn working_directory_request_for_browser(
|
||||
|
|
@ -3065,24 +3188,6 @@ fn worker_create_not_accepted_error(
|
|||
)
|
||||
}
|
||||
|
||||
fn working_directory_api_error(
|
||||
runtime_id: String,
|
||||
diagnostic: WorkingDirectoryDiagnostic,
|
||||
) -> ApiError {
|
||||
ApiError::with_diagnostics(
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id,
|
||||
code: diagnostic.code.clone(),
|
||||
message: diagnostic.message.clone(),
|
||||
},
|
||||
vec![RuntimeDiagnostic {
|
||||
code: diagnostic.code,
|
||||
severity: DiagnosticSeverity::Error,
|
||||
message: diagnostic.message,
|
||||
}],
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_bad_request(code: &'static str, message: &'static str) -> ApiError {
|
||||
Error::RuntimeOperationFailed {
|
||||
runtime_id: "workspace-backend".to_string(),
|
||||
|
|
@ -3475,6 +3580,7 @@ mod tests {
|
|||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tower::ServiceExt;
|
||||
use worker_runtime::resource::BackendResourceClient;
|
||||
use worker_runtime::working_directory::WorkingDirectoryMaterializer;
|
||||
|
||||
use crate::hosts::{
|
||||
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
|
||||
|
|
@ -3731,7 +3837,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DeterministicExecutionBackend {
|
||||
contexts: std::sync::Mutex<
|
||||
std::collections::HashMap<
|
||||
|
|
@ -3739,6 +3844,26 @@ mod tests {
|
|||
worker_runtime::execution::WorkerExecutionContext,
|
||||
>,
|
||||
>,
|
||||
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer,
|
||||
}
|
||||
|
||||
impl Default for DeterministicExecutionBackend {
|
||||
fn default() -> Self {
|
||||
let unique = format!(
|
||||
"yoi-deterministic-wd-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
);
|
||||
Self {
|
||||
contexts: std::sync::Mutex::new(std::collections::HashMap::new()),
|
||||
materializer: worker_runtime::working_directory::LocalGitWorktreeMaterializer::new(
|
||||
std::env::temp_dir().join(unique),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl worker_runtime::execution::WorkerExecutionBackend for DeterministicExecutionBackend {
|
||||
|
|
@ -3746,10 +3871,65 @@ mod tests {
|
|||
"deterministic-workspace-server-test"
|
||||
}
|
||||
|
||||
fn create_working_directory(
|
||||
&self,
|
||||
request: &worker_runtime::catalog::WorkingDirectoryRequest,
|
||||
) -> std::result::Result<
|
||||
worker_runtime::catalog::WorkingDirectoryStatus,
|
||||
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
|
||||
> {
|
||||
Ok(self.materializer.create(request)?.status())
|
||||
}
|
||||
|
||||
fn list_working_directories(&self) -> Vec<worker_runtime::catalog::WorkingDirectoryStatus> {
|
||||
self.materializer
|
||||
.list_working_directories()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> std::result::Result<
|
||||
worker_runtime::catalog::WorkingDirectoryStatus,
|
||||
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
|
||||
> {
|
||||
self.materializer
|
||||
.working_directory_status(working_directory_id)
|
||||
}
|
||||
|
||||
fn cleanup_working_directory(
|
||||
&self,
|
||||
working_directory_id: &str,
|
||||
) -> std::result::Result<
|
||||
worker_runtime::catalog::WorkingDirectoryStatus,
|
||||
worker_runtime::working_directory::WorkingDirectoryDiagnostic,
|
||||
> {
|
||||
self.materializer
|
||||
.cleanup_working_directory(working_directory_id)
|
||||
}
|
||||
|
||||
fn spawn_worker(
|
||||
&self,
|
||||
request: worker_runtime::execution::WorkerExecutionSpawnRequest,
|
||||
) -> worker_runtime::execution::WorkerExecutionSpawnResult {
|
||||
let working_directory = match request.request.working_directory.as_ref() {
|
||||
Some(claim) => match self.materializer.bind_working_directory(
|
||||
&claim.working_directory_id,
|
||||
claim.relative_cwd.as_deref(),
|
||||
) {
|
||||
Ok(binding) => Some(binding.status()),
|
||||
Err(diagnostic) => {
|
||||
return worker_runtime::execution::WorkerExecutionSpawnResult::Rejected(
|
||||
worker_runtime::execution::WorkerExecutionResult::rejected(
|
||||
worker_runtime::execution::WorkerExecutionOperation::Spawn,
|
||||
diagnostic.to_string(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
self.contexts
|
||||
.lock()
|
||||
.unwrap()
|
||||
|
|
@ -3760,10 +3940,7 @@ mod tests {
|
|||
self.backend_id(),
|
||||
),
|
||||
run_state: worker_runtime::execution::WorkerExecutionRunState::Idle,
|
||||
working_directory: request
|
||||
.working_directory
|
||||
.as_ref()
|
||||
.map(|binding| binding.status()),
|
||||
working_directory,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,10 +142,12 @@
|
|||
creatingWorkingDirectory = true;
|
||||
submitError = null;
|
||||
try {
|
||||
const response = await fetch(workerApiPath('/working-directories'), {
|
||||
const response = await fetch(
|
||||
workerApiPath(`/runtimes/${encodeURIComponent(runtimeId)}/working-directories`), {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
runtime_id: runtimeId,
|
||||
repository_id: workingDirectoryRepositoryId,
|
||||
selector: workingDirectorySelector || null,
|
||||
policy: { dirty_state: 'clean_point_only', cleanup: 'manual_or_worker_stop' },
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export type BrowserWorkerWorkingDirectorySelection = {
|
|||
};
|
||||
|
||||
export type BrowserWorkingDirectoryCreateRequest = {
|
||||
runtime_id: string;
|
||||
repository_id: string;
|
||||
selector?: string | null;
|
||||
policy?: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user