feat: migrate runtime worker domains to ServerApi
This commit is contained in:
@@ -88,28 +88,25 @@ impl BackendRuntimeTarget {
|
|||||||
content: Vec<u8>,
|
content: Vec<u8>,
|
||||||
) -> Result<protocol::UploadedFileRef, BackendRuntimeClientError> {
|
) -> Result<protocol::UploadedFileRef, BackendRuntimeClientError> {
|
||||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||||
|
let client = runtime_server_api_client(&self.base_url, &api)?;
|
||||||
|
let grant = client
|
||||||
|
.runtime_worker_attachment_upload_grant(
|
||||||
|
self.workspace_id.clone(),
|
||||||
|
self.runtime_id.clone(),
|
||||||
|
self.worker_id.clone(),
|
||||||
|
server_api::WorkerFileUploadQuery {
|
||||||
|
file_name: file_name.to_string(),
|
||||||
|
media_type: media_type.to_string(),
|
||||||
|
upload_id: Some(upload_id.to_string()),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let worker_path = format!(
|
let worker_path = format!(
|
||||||
"/api/w/{}/runtimes/{}/workers/{}",
|
"/api/w/{}/runtimes/{}/workers/{}",
|
||||||
path_segment_encode(&self.workspace_id),
|
path_segment_encode(&self.workspace_id),
|
||||||
path_segment_encode(&self.runtime_id),
|
path_segment_encode(&self.runtime_id),
|
||||||
path_segment_encode(&self.worker_id),
|
path_segment_encode(&self.worker_id),
|
||||||
);
|
);
|
||||||
let grant_path = format!(
|
|
||||||
"{worker_path}/attachment-upload-grants?file_name={}&media_type={}&upload_id={}",
|
|
||||||
path_segment_encode(file_name),
|
|
||||||
path_segment_encode(media_type),
|
|
||||||
path_segment_encode(&upload_id),
|
|
||||||
);
|
|
||||||
let grant_response = api
|
|
||||||
.request(HttpMethod::POST, &grant_path)?
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(BackendRuntimeClientError::Http)?;
|
|
||||||
api.check_status(grant_response.status())?;
|
|
||||||
let grant = grant_response
|
|
||||||
.json::<AttachmentUploadGrantResponse>()
|
|
||||||
.await
|
|
||||||
.map_err(BackendRuntimeClientError::Http)?;
|
|
||||||
let upload_path = format!(
|
let upload_path = format!(
|
||||||
"{worker_path}/attachment-uploads/{}",
|
"{worker_path}/attachment-uploads/{}",
|
||||||
path_segment_encode(&grant.upload_id),
|
path_segment_encode(&grant.upload_id),
|
||||||
@@ -133,19 +130,14 @@ impl BackendRuntimeTarget {
|
|||||||
upload_id: &str,
|
upload_id: &str,
|
||||||
) -> Result<(), BackendRuntimeClientError> {
|
) -> Result<(), BackendRuntimeClientError> {
|
||||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||||
let path = format!(
|
runtime_server_api_client(&self.base_url, &api)?
|
||||||
"/api/w/{}/runtimes/{}/workers/{}/attachment-uploads/{}",
|
.runtime_worker_attachment_upload_cancel(
|
||||||
path_segment_encode(&self.workspace_id),
|
self.workspace_id.clone(),
|
||||||
path_segment_encode(&self.runtime_id),
|
self.runtime_id.clone(),
|
||||||
path_segment_encode(&self.worker_id),
|
self.worker_id.clone(),
|
||||||
path_segment_encode(upload_id),
|
upload_id.to_string(),
|
||||||
);
|
)
|
||||||
let response = api
|
.await?;
|
||||||
.request(HttpMethod::DELETE, &path)?
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(BackendRuntimeClientError::Http)?;
|
|
||||||
api.check_status(response.status())?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,30 +146,18 @@ impl BackendRuntimeTarget {
|
|||||||
artifact_id: &str,
|
artifact_id: &str,
|
||||||
) -> Result<(), BackendRuntimeClientError> {
|
) -> Result<(), BackendRuntimeClientError> {
|
||||||
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
let api = BackendApiClient::from_stored_token(&self.base_url)?;
|
||||||
let path = format!(
|
runtime_server_api_client(&self.base_url, &api)?
|
||||||
"/api/w/{}/runtimes/{}/workers/{}/attachments/{}",
|
.runtime_worker_attachment_delete(
|
||||||
path_segment_encode(&self.workspace_id),
|
self.workspace_id.clone(),
|
||||||
path_segment_encode(&self.runtime_id),
|
self.runtime_id.clone(),
|
||||||
path_segment_encode(&self.worker_id),
|
self.worker_id.clone(),
|
||||||
path_segment_encode(artifact_id),
|
artifact_id.to_string(),
|
||||||
);
|
)
|
||||||
let response = api
|
.await?;
|
||||||
.request(HttpMethod::DELETE, &path)?
|
|
||||||
.send()
|
|
||||||
.await
|
|
||||||
.map_err(BackendRuntimeClientError::Http)?;
|
|
||||||
api.check_status(response.status())?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct AttachmentUploadGrantResponse {
|
|
||||||
upload_id: String,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
expires_at_ms: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct UploadedFileResponse {
|
struct UploadedFileResponse {
|
||||||
file: protocol::UploadedFileRef,
|
file: protocol::UploadedFileRef,
|
||||||
@@ -280,6 +260,7 @@ pub enum BackendRuntimeClientError {
|
|||||||
InvalidTarget(String),
|
InvalidTarget(String),
|
||||||
Api(BackendApiClientError),
|
Api(BackendApiClientError),
|
||||||
SessionApi(server_api::ServerApiClientError),
|
SessionApi(server_api::ServerApiClientError),
|
||||||
|
ContractApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
Protocol(String),
|
Protocol(String),
|
||||||
}
|
}
|
||||||
@@ -290,6 +271,7 @@ impl fmt::Display for BackendRuntimeClientError {
|
|||||||
Self::InvalidTarget(message) => f.write_str(message),
|
Self::InvalidTarget(message) => f.write_str(message),
|
||||||
Self::Api(error) => write!(f, "{error}"),
|
Self::Api(error) => write!(f, "{error}"),
|
||||||
Self::SessionApi(error) => write!(f, "{error}"),
|
Self::SessionApi(error) => write!(f, "{error}"),
|
||||||
|
Self::ContractApi(error) => write!(f, "{error}"),
|
||||||
Self::Http(error) => write!(f, "{error}"),
|
Self::Http(error) => write!(f, "{error}"),
|
||||||
Self::Protocol(message) => f.write_str(message),
|
Self::Protocol(message) => f.write_str(message),
|
||||||
}
|
}
|
||||||
@@ -304,6 +286,16 @@ impl From<server_api::ServerApiClientError> for BackendRuntimeClientError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<server_api::client_support::ClientError<server_api::RepositoryApiError>>
|
||||||
|
for BackendRuntimeClientError
|
||||||
|
{
|
||||||
|
fn from(
|
||||||
|
value: server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||||
|
) -> Self {
|
||||||
|
Self::ContractApi(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<BackendApiClientError> for BackendRuntimeClientError {
|
impl From<BackendApiClientError> for BackendRuntimeClientError {
|
||||||
fn from(error: BackendApiClientError) -> Self {
|
fn from(error: BackendApiClientError) -> Self {
|
||||||
Self::Api(error)
|
Self::Api(error)
|
||||||
@@ -318,6 +310,34 @@ impl From<reqwest::Error> for BackendRuntimeClientError {
|
|||||||
|
|
||||||
const WORKER_SESSION_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
const WORKER_SESSION_RESPONSE_LIMIT: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
fn runtime_server_api_client(
|
||||||
|
base_url: &str,
|
||||||
|
backend: &BackendApiClient,
|
||||||
|
) -> Result<server_api::ServerApiClient<StoredBearerAuthorizer>, BackendRuntimeClientError> {
|
||||||
|
server_api::ServerApiClient::builder(base_url)
|
||||||
|
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?
|
||||||
|
.client(backend.asynchronous_client())
|
||||||
|
.authorizer(StoredBearerAuthorizer {
|
||||||
|
authorization: backend.authorization_header_value(),
|
||||||
|
})
|
||||||
|
.response_body_limit(WORKER_SESSION_RESPONSE_LIMIT)
|
||||||
|
.build()
|
||||||
|
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_api_error_is_auth(
|
||||||
|
error: &server_api::client_support::ClientError<server_api::RepositoryApiError>,
|
||||||
|
) -> bool {
|
||||||
|
matches!(
|
||||||
|
error,
|
||||||
|
server_api::client_support::ClientError::Public { status, .. }
|
||||||
|
if matches!(
|
||||||
|
*status,
|
||||||
|
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct StoredBearerAuthorizer {
|
struct StoredBearerAuthorizer {
|
||||||
authorization: String,
|
authorization: String,
|
||||||
@@ -351,15 +371,7 @@ async fn observe_backend_worker_session_with_client(
|
|||||||
target: &BackendRuntimeTarget,
|
target: &BackendRuntimeTarget,
|
||||||
backend: &BackendApiClient,
|
backend: &BackendApiClient,
|
||||||
) -> Result<server_api::WorkspaceWorkerSessionResponse, BackendRuntimeClientError> {
|
) -> Result<server_api::WorkspaceWorkerSessionResponse, BackendRuntimeClientError> {
|
||||||
let client = server_api::ServerApiClient::builder(&target.base_url)
|
runtime_server_api_client(&target.base_url, backend)?
|
||||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?
|
|
||||||
.authorizer(StoredBearerAuthorizer {
|
|
||||||
authorization: backend.authorization_header_value(),
|
|
||||||
})
|
|
||||||
.response_body_limit(WORKER_SESSION_RESPONSE_LIMIT)
|
|
||||||
.build()
|
|
||||||
.map_err(|error| BackendRuntimeClientError::Protocol(error.to_string()))?;
|
|
||||||
client
|
|
||||||
.worker_session(
|
.worker_session(
|
||||||
target.workspace_id.clone(),
|
target.workspace_id.clone(),
|
||||||
target.runtime_id.clone(),
|
target.runtime_id.clone(),
|
||||||
@@ -426,80 +438,50 @@ pub async fn list_backend_workers(
|
|||||||
) -> Result<BackendRuntimeListResponse<BackendWorkerSummary>, BackendRuntimeClientError> {
|
) -> Result<BackendRuntimeListResponse<BackendWorkerSummary>, BackendRuntimeClientError> {
|
||||||
validate_list_target(target)?;
|
validate_list_target(target)?;
|
||||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
if let Some(runtime_id) = target.runtime_id.as_deref() {
|
let client = runtime_server_api_client(&target.base_url, &api)?;
|
||||||
let path = backend_runtime_workers_path(
|
let workspace_id = target
|
||||||
target
|
|
||||||
.workspace_id
|
.workspace_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.expect("validated Backend Workspace scope"),
|
.expect("validated Backend Workspace scope");
|
||||||
runtime_id,
|
if let Some(runtime_id) = target.runtime_id.as_deref() {
|
||||||
);
|
return client
|
||||||
let response = api.request(HttpMethod::GET, &path)?.send().await?;
|
.runtime_worker_list(
|
||||||
api.check_status(response.status())?;
|
workspace_id.to_string(),
|
||||||
return Ok(response
|
runtime_id.to_string(),
|
||||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
server_api::RuntimeWorkersQuery::default(),
|
||||||
.await?);
|
)
|
||||||
|
.await
|
||||||
|
.map(runtime_worker_list_response)
|
||||||
|
.map_err(Into::into);
|
||||||
}
|
}
|
||||||
|
|
||||||
let runtime_path = backend_runtimes_path(
|
let runtimes = client.runtime_list(workspace_id.to_string()).await?;
|
||||||
target
|
|
||||||
.workspace_id
|
|
||||||
.as_deref()
|
|
||||||
.expect("validated Backend Workspace scope"),
|
|
||||||
);
|
|
||||||
let response = api.request(HttpMethod::GET, &runtime_path)?.send().await?;
|
|
||||||
api.check_status(response.status())?;
|
|
||||||
let runtimes = response
|
|
||||||
.json::<BackendRuntimeListResponse<BackendRuntimeSummary>>()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
let mut diagnostics = runtimes.diagnostics;
|
let mut diagnostics = runtimes.diagnostics;
|
||||||
for runtime in runtimes.items {
|
for resource in runtimes.items {
|
||||||
let path = backend_runtime_workers_path(
|
let runtime = resource.runtime;
|
||||||
target
|
match client
|
||||||
.workspace_id
|
.runtime_worker_list(
|
||||||
.as_deref()
|
workspace_id.to_string(),
|
||||||
.expect("validated Backend Workspace scope"),
|
runtime.runtime_id.clone(),
|
||||||
&runtime.runtime_id,
|
server_api::RuntimeWorkersQuery::default(),
|
||||||
);
|
)
|
||||||
let response = match api.request(HttpMethod::GET, &path)?.send().await {
|
.await
|
||||||
Ok(response) => response,
|
{
|
||||||
Err(error) => {
|
Ok(response) => {
|
||||||
diagnostics.push(BackendDiagnostic {
|
diagnostics.extend(response.diagnostics);
|
||||||
|
items.extend(response.items);
|
||||||
|
}
|
||||||
|
Err(error) if server_api_error_is_auth(&error) => return Err(error.into()),
|
||||||
|
Err(error) => diagnostics.push(BackendDiagnostic {
|
||||||
code: "runtime_worker_list_failed".to_string(),
|
code: "runtime_worker_list_failed".to_string(),
|
||||||
severity: BackendDiagnosticSeverity::Error,
|
severity: BackendDiagnosticSeverity::Error,
|
||||||
message: format!(
|
message: format!(
|
||||||
"failed to list workers for runtime {}: {error}",
|
"failed to list workers for runtime {}: {error}",
|
||||||
runtime.runtime_id
|
runtime.runtime_id
|
||||||
),
|
),
|
||||||
});
|
}),
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if matches!(
|
|
||||||
response.status(),
|
|
||||||
reqwest::StatusCode::UNAUTHORIZED | reqwest::StatusCode::FORBIDDEN
|
|
||||||
) {
|
|
||||||
api.check_status(response.status())?;
|
|
||||||
}
|
|
||||||
if !response.status().is_success() {
|
|
||||||
diagnostics.push(BackendDiagnostic {
|
|
||||||
code: "runtime_worker_list_failed".to_string(),
|
|
||||||
severity: BackendDiagnosticSeverity::Error,
|
|
||||||
message: format!(
|
|
||||||
"failed to list workers for runtime {}: Backend returned HTTP {}",
|
|
||||||
runtime.runtime_id,
|
|
||||||
response.status().as_u16()
|
|
||||||
),
|
|
||||||
});
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let response = response
|
|
||||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
|
||||||
.await?;
|
|
||||||
diagnostics.extend(response.diagnostics);
|
|
||||||
items.extend(response.items);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(BackendRuntimeListResponse {
|
Ok(BackendRuntimeListResponse {
|
||||||
@@ -521,21 +503,21 @@ pub async fn list_backend_stopped_workers(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
let path = backend_runtime_workers_path(
|
runtime_server_api_client(&target.base_url, &api)?
|
||||||
|
.runtime_worker_list(
|
||||||
target
|
target
|
||||||
.workspace_id
|
.workspace_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.expect("validated Backend Workspace scope"),
|
.expect("validated Backend Workspace scope")
|
||||||
runtime_id,
|
.to_string(),
|
||||||
);
|
runtime_id.to_string(),
|
||||||
let response = api
|
server_api::RuntimeWorkersQuery {
|
||||||
.request(HttpMethod::GET, &format!("{path}?status=stopped"))?
|
status: Some(server_api::RuntimeWorkersStatusFilter::Stopped),
|
||||||
.send()
|
},
|
||||||
.await?;
|
)
|
||||||
api.check_status(response.status())?;
|
.await
|
||||||
Ok(response
|
.map(runtime_worker_list_response)
|
||||||
.json::<BackendRuntimeListResponse<BackendWorkerSummary>>()
|
.map_err(Into::into)
|
||||||
.await?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn restore_backend_worker(
|
pub async fn restore_backend_worker(
|
||||||
@@ -543,18 +525,18 @@ pub async fn restore_backend_worker(
|
|||||||
) -> Result<BackendWorkerRestoreResponse, BackendRuntimeClientError> {
|
) -> Result<BackendWorkerRestoreResponse, BackendRuntimeClientError> {
|
||||||
validate_target(target)?;
|
validate_target(target)?;
|
||||||
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
let api = BackendApiClient::from_stored_token(&target.base_url)?;
|
||||||
let path = backend_runtime_worker_restore_path(
|
runtime_server_api_client(&target.base_url, &api)?
|
||||||
&target.workspace_id,
|
.runtime_worker_restore(
|
||||||
&target.runtime_id,
|
target.workspace_id.clone(),
|
||||||
&target.worker_id,
|
target.runtime_id.clone(),
|
||||||
);
|
target.worker_id.clone(),
|
||||||
let response = api
|
server_api::RestoreTicketAssignmentQuery {
|
||||||
.request(HttpMethod::POST, &path)?
|
ticket_id: None,
|
||||||
.json(&serde_json::json!({}))
|
assignment_operation_id: None,
|
||||||
.send()
|
},
|
||||||
.await?;
|
)
|
||||||
let response = api.require_success(response).await?;
|
.await
|
||||||
Ok(response.json::<BackendWorkerRestoreResponse>().await?)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect_backend_runtime(
|
pub async fn connect_backend_runtime(
|
||||||
@@ -599,6 +581,18 @@ fn protocol_connect_error_message(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_worker_list_response(
|
||||||
|
response: server_api::RuntimeWorkerListResponse,
|
||||||
|
) -> BackendRuntimeListResponse<BackendWorkerSummary> {
|
||||||
|
BackendRuntimeListResponse {
|
||||||
|
workspace_id: response.workspace_id,
|
||||||
|
limit: response.limit,
|
||||||
|
items: response.items,
|
||||||
|
source: response.source,
|
||||||
|
diagnostics: response.diagnostics,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeClientError> {
|
fn validate_target(target: &BackendRuntimeTarget) -> Result<(), BackendRuntimeClientError> {
|
||||||
if target.base_url.trim().is_empty() {
|
if target.base_url.trim().is_empty() {
|
||||||
return Err(BackendRuntimeClientError::InvalidTarget(
|
return Err(BackendRuntimeClientError::InvalidTarget(
|
||||||
@@ -697,31 +691,6 @@ fn backend_workspace_workers_launch_options_path(workspace_id: &str) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn backend_runtimes_path(workspace_id: &str) -> String {
|
|
||||||
format!("/api/w/{}/runtimes", path_segment_encode(workspace_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn backend_runtime_workers_path(workspace_id: &str, runtime_id: &str) -> String {
|
|
||||||
format!(
|
|
||||||
"/api/w/{}/runtimes/{}/workers",
|
|
||||||
path_segment_encode(workspace_id),
|
|
||||||
path_segment_encode(runtime_id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn backend_runtime_worker_restore_path(
|
|
||||||
workspace_id: &str,
|
|
||||||
runtime_id: &str,
|
|
||||||
worker_id: &str,
|
|
||||||
) -> String {
|
|
||||||
format!(
|
|
||||||
"/api/w/{}/runtimes/{}/workers/{}/restore",
|
|
||||||
path_segment_encode(workspace_id),
|
|
||||||
path_segment_encode(runtime_id),
|
|
||||||
path_segment_encode(worker_id)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn protocol_ws_request(
|
fn protocol_ws_request(
|
||||||
target: &BackendRuntimeTarget,
|
target: &BackendRuntimeTarget,
|
||||||
api: &BackendApiClient,
|
api: &BackendApiClient,
|
||||||
@@ -1051,21 +1020,4 @@ mod tests {
|
|||||||
serde_json::json!(64);
|
serde_json::json!(64);
|
||||||
assert!(serde_json::from_value::<BackendWorkerSummary>(stale).is_err());
|
assert!(serde_json::from_value::<BackendWorkerSummary>(stale).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn workers_path_requires_workspace_scope_for_status_queries() {
|
|
||||||
let path = backend_runtime_workers_path("team main", "runtime/one");
|
|
||||||
assert_eq!(
|
|
||||||
format!("{path}?status=stopped"),
|
|
||||||
"/api/w/team%20main/runtimes/runtime%2Fone/workers?status=stopped"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn restore_worker_path_requires_workspace_scope() {
|
|
||||||
assert_eq!(
|
|
||||||
backend_runtime_worker_restore_path("team main", "runtime/one", "worker one"),
|
|
||||||
"/api/w/team%20main/runtimes/runtime%2Fone/workers/worker%20one/restore"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ pub enum BackendWorkspaceClientError {
|
|||||||
Api(BackendApiClientError),
|
Api(BackendApiClientError),
|
||||||
Http(reqwest::Error),
|
Http(reqwest::Error),
|
||||||
ServerApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
ServerApi(server_api::client_support::ClientError<server_api::RepositoryApiError>),
|
||||||
|
RuntimeManagementApi(
|
||||||
|
server_api::client_support::ClientError<server_api::RuntimeManagementApiError>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for BackendWorkspaceClientError {
|
impl fmt::Display for BackendWorkspaceClientError {
|
||||||
@@ -40,6 +43,7 @@ impl fmt::Display for BackendWorkspaceClientError {
|
|||||||
Self::Api(error) => write!(f, "{error}"),
|
Self::Api(error) => write!(f, "{error}"),
|
||||||
Self::Http(error) => write!(f, "{error}"),
|
Self::Http(error) => write!(f, "{error}"),
|
||||||
Self::ServerApi(error) => write!(f, "{error}"),
|
Self::ServerApi(error) => write!(f, "{error}"),
|
||||||
|
Self::RuntimeManagementApi(error) => write!(f, "{error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,6 +95,29 @@ pub(crate) fn server_client_error(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn runtime_management_client_error(
|
||||||
|
backend: &BackendApiClient,
|
||||||
|
error: server_api::client_support::ClientError<server_api::RuntimeManagementApiError>,
|
||||||
|
) -> BackendWorkspaceClientError {
|
||||||
|
let status = match &error {
|
||||||
|
server_api::client_support::ClientError::Public { status, .. } => Some(*status),
|
||||||
|
server_api::client_support::ClientError::Failure(_) => None,
|
||||||
|
};
|
||||||
|
match status {
|
||||||
|
Some(reqwest::StatusCode::UNAUTHORIZED) => {
|
||||||
|
BackendWorkspaceClientError::Api(BackendApiClientError::Unauthorized {
|
||||||
|
origin: backend.origin().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Some(reqwest::StatusCode::FORBIDDEN) => {
|
||||||
|
BackendWorkspaceClientError::Api(BackendApiClientError::Forbidden {
|
||||||
|
origin: backend.origin().clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => BackendWorkspaceClientError::RuntimeManagementApi(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct ServerBearerAuthorizer {
|
pub(crate) struct ServerBearerAuthorizer {
|
||||||
authorization: String,
|
authorization: String,
|
||||||
|
|||||||
@@ -345,24 +345,40 @@ impl BackendWorkspaceProductClient {
|
|||||||
pub fn list_runtimes(
|
pub fn list_runtimes(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<ListResponse<WorkspaceRuntimeResource>, BackendWorkspaceClientError> {
|
) -> Result<ListResponse<WorkspaceRuntimeResource>, BackendWorkspaceClientError> {
|
||||||
self.get_json("/runtimes")
|
let workspace_id = self.workspace_id.clone();
|
||||||
|
let response =
|
||||||
|
self.generated(move |client| async move { client.runtime_list(workspace_id).await })?;
|
||||||
|
Ok(ListResponse {
|
||||||
|
workspace_id: response.workspace_id,
|
||||||
|
limit: response.limit,
|
||||||
|
items: response.items,
|
||||||
|
source: response.source,
|
||||||
|
diagnostics: response.diagnostics,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn runtime_detail(
|
pub fn runtime_detail(
|
||||||
&self,
|
&self,
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
||||||
self.get_json(&format!("/runtimes/{}", encode_path_segment(runtime_id)))
|
let workspace_id = self.workspace_id.clone();
|
||||||
|
let runtime_id = runtime_id.to_string();
|
||||||
|
self.generated(move |client| async move {
|
||||||
|
client.runtime_detail(workspace_id, runtime_id).await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reveal_runtime_trust_key(
|
pub fn reveal_runtime_trust_key(
|
||||||
&self,
|
&self,
|
||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
) -> Result<RuntimeTrustKeyRevealResponse, BackendWorkspaceClientError> {
|
) -> Result<RuntimeTrustKeyRevealResponse, BackendWorkspaceClientError> {
|
||||||
self.get_json(&format!(
|
let workspace_id = self.workspace_id.clone();
|
||||||
"/runtimes/{}/trust-key",
|
let runtime_id = runtime_id.to_string();
|
||||||
encode_path_segment(runtime_id)
|
self.generated(move |client| async move {
|
||||||
))
|
client
|
||||||
|
.runtime_trust_key_reveal(workspace_id, runtime_id)
|
||||||
|
.await
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn revoke_runtime_trust_key(
|
pub fn revoke_runtime_trust_key(
|
||||||
@@ -370,11 +386,20 @@ impl BackendWorkspaceProductClient {
|
|||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
request: &RevokeRuntimeTrustKeyRequest,
|
request: &RevokeRuntimeTrustKeyRequest,
|
||||||
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
|
||||||
self.send_json(
|
let client = crate::backend_workspace::server_api_client(&self.api)?;
|
||||||
Method::DELETE,
|
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||||
&format!("/runtimes/{}/trust-key", encode_path_segment(runtime_id)),
|
.enable_all()
|
||||||
Some(request),
|
.build()
|
||||||
)
|
.map_err(|error| BackendWorkspaceClientError::InvalidTarget(error.to_string()))?;
|
||||||
|
runtime
|
||||||
|
.block_on(client.runtime_trust_key_revoke(
|
||||||
|
self.workspace_id.clone(),
|
||||||
|
runtime_id.to_string(),
|
||||||
|
request.clone(),
|
||||||
|
))
|
||||||
|
.map_err(|error| {
|
||||||
|
crate::backend_workspace::runtime_management_client_error(&self.api, error)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> {
|
pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use server_api::repository_openapi_typescript::{
|
||||||
|
INPUT_PATH, RUNTIME_OUTPUT_PATH, generate_runtime_typescript,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(error) = run() {
|
||||||
|
eprintln!("{error}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), String> {
|
||||||
|
let mut check = false;
|
||||||
|
for argument in std::env::args().skip(1) {
|
||||||
|
match argument.as_str() {
|
||||||
|
"--check" => check = true,
|
||||||
|
"--help" | "-h" => {
|
||||||
|
println!(
|
||||||
|
"Generate the bounded Runtime API TypeScript artifact.\n\n\
|
||||||
|
Usage: cargo run -q -p server-api --example generate_runtime_api_types -- [--check]"
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
other => return Err(format!("unknown argument `{other}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
let input_path = root.join(INPUT_PATH);
|
||||||
|
let output_path = root.join(RUNTIME_OUTPUT_PATH);
|
||||||
|
let input = fs::read_to_string(&input_path)
|
||||||
|
.map_err(|error| format!("failed to read {}: {error}", input_path.display()))?;
|
||||||
|
let generated = generate_runtime_typescript(&input)
|
||||||
|
.map_err(|error| format!("failed to generate Runtime API types: {error}"))?;
|
||||||
|
|
||||||
|
if check {
|
||||||
|
let checked_in = fs::read_to_string(&output_path)
|
||||||
|
.map_err(|error| format!("failed to read {}: {error}", output_path.display()))?;
|
||||||
|
if checked_in != generated {
|
||||||
|
return Err(format!(
|
||||||
|
"{} is stale; regenerate it without --check",
|
||||||
|
output_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
println!("{} is current", output_path.display());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let temporary_path = output_path.with_extension("ts.tmp");
|
||||||
|
fs::write(&temporary_path, generated)
|
||||||
|
.map_err(|error| format!("failed to write {}: {error}", temporary_path.display()))?;
|
||||||
|
fs::rename(&temporary_path, &output_path)
|
||||||
|
.map_err(|error| format!("failed to replace {}: {error}", output_path.display()))?;
|
||||||
|
println!("generated {}", output_path.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
+1132
-124
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,11 @@ pub const WORKDIR_GENERATOR_VERSION: &str = "1";
|
|||||||
pub const WORKDIR_GENERATOR_OPTIONS: &str =
|
pub const WORKDIR_GENERATOR_OPTIONS: &str =
|
||||||
"workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
"workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||||
pub const WORKDIR_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/workdir-api.ts";
|
pub const WORKDIR_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/workdir-api.ts";
|
||||||
|
pub const RUNTIME_GENERATOR_NAME: &str = "yoi-runtime-openapi-typescript";
|
||||||
|
pub const RUNTIME_GENERATOR_VERSION: &str = "1";
|
||||||
|
pub const RUNTIME_GENERATOR_OPTIONS: &str =
|
||||||
|
"runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output";
|
||||||
|
pub const RUNTIME_OUTPUT_PATH: &str = "web/workspace/src/lib/generated/runtime-api.ts";
|
||||||
|
|
||||||
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0;
|
||||||
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
|
const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991.0;
|
||||||
@@ -119,6 +124,48 @@ pub fn generate_workdir_typescript(input: &str) -> Result<String, GenerationErro
|
|||||||
Ok(output)
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_runtime_typescript(input: &str) -> Result<String, GenerationError> {
|
||||||
|
let document: Value = serde_json::from_str(input)
|
||||||
|
.map_err(|error| GenerationError::invalid(format!("invalid OpenAPI JSON: {error}")))?;
|
||||||
|
let root = object(&document, "OpenAPI document")?;
|
||||||
|
if root.get("openapi").and_then(Value::as_str) != Some("3.1.0") {
|
||||||
|
return Err(GenerationError::invalid(
|
||||||
|
"Runtime TypeScript generation requires OpenAPI 3.1.0",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let info = object_field(root, "info", "OpenAPI document")?;
|
||||||
|
let contract_version = required_string(info, "version", "OpenAPI info")?;
|
||||||
|
let contract_digest = required_string(info, "x-yoi-source-digest", "OpenAPI info")?;
|
||||||
|
validate_contract_digest(contract_digest)?;
|
||||||
|
let schemas = object_field(
|
||||||
|
object_field(root, "components", "OpenAPI document")?,
|
||||||
|
"schemas",
|
||||||
|
"OpenAPI components",
|
||||||
|
)?;
|
||||||
|
let paths = object_field(root, "paths", "OpenAPI document")?;
|
||||||
|
let roots = validate_runtime_operations(paths)?;
|
||||||
|
let closure = schema_closure(schemas, &roots)?;
|
||||||
|
|
||||||
|
let mut output = format!(
|
||||||
|
"// @generated by {RUNTIME_GENERATOR_NAME}@{RUNTIME_GENERATOR_VERSION}. Do not edit.\n\
|
||||||
|
// Input: {INPUT_PATH}\n\
|
||||||
|
// Input contract: {contract_version} ({contract_digest})\n\
|
||||||
|
// Options: {RUNTIME_GENERATOR_OPTIONS}\n\
|
||||||
|
// Output: {RUNTIME_OUTPUT_PATH}\n\
|
||||||
|
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check\n\n"
|
||||||
|
);
|
||||||
|
for name in closure {
|
||||||
|
let schema = schemas
|
||||||
|
.get(&name)
|
||||||
|
.ok_or_else(|| GenerationError::invalid(format!("missing schema `{name}`")))?;
|
||||||
|
let declaration = render_schema(schema, &format!("schema `{name}`"))?;
|
||||||
|
output.push_str(&format!("export type {name} = {declaration};\n\n"));
|
||||||
|
}
|
||||||
|
output.pop();
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
|
fn validate_contract_digest(digest: &str) -> Result<(), GenerationError> {
|
||||||
let Some(hex) = digest.strip_prefix("sha256:") else {
|
let Some(hex) = digest.strip_prefix("sha256:") else {
|
||||||
return Err(GenerationError::invalid(
|
return Err(GenerationError::invalid(
|
||||||
@@ -347,6 +394,364 @@ fn validate_workdir_operations(
|
|||||||
validate_operation_contracts(paths, &operations)
|
validate_operation_contracts(paths, &operations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_runtime_operations(
|
||||||
|
paths: &Map<String, Value>,
|
||||||
|
) -> Result<BTreeSet<String>, GenerationError> {
|
||||||
|
let operations = [
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes",
|
||||||
|
"get",
|
||||||
|
"runtime_list_alias",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeListResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes",
|
||||||
|
"get",
|
||||||
|
"runtime_list",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkspaceRuntimeListResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes",
|
||||||
|
"post",
|
||||||
|
"runtime_create",
|
||||||
|
Some("CreateRemoteRuntimeRequest"),
|
||||||
|
&[
|
||||||
|
("200", "WorkspaceRuntimeResource"),
|
||||||
|
("201", "WorkspaceRuntimeResource"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||||
|
"get",
|
||||||
|
"runtime_detail",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkspaceRuntimeDetail")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||||
|
"post",
|
||||||
|
"runtime_update",
|
||||||
|
Some("UpdateRemoteRuntimeRequest"),
|
||||||
|
&[("200", "WorkspaceRuntimeDetail")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}",
|
||||||
|
"delete",
|
||||||
|
"runtime_remove",
|
||||||
|
Some("RemoveRuntimeRequest"),
|
||||||
|
&[("200", "RuntimeRemovalOperationResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
|
||||||
|
"get",
|
||||||
|
"runtime_trust_key_reveal",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeTrustKeyRevealResponse")],
|
||||||
|
),
|
||||||
|
RuntimeOperationContract {
|
||||||
|
error: "RuntimeManagementApiError",
|
||||||
|
..runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
|
||||||
|
"delete",
|
||||||
|
"runtime_trust_key_revoke",
|
||||||
|
Some("RevokeRuntimeTrustKeyRequest"),
|
||||||
|
&[("200", "WorkspaceRuntimeDetail")],
|
||||||
|
)
|
||||||
|
},
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/connection-tests",
|
||||||
|
"post",
|
||||||
|
"runtime_connection_test",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeConnectionTestResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers",
|
||||||
|
"get",
|
||||||
|
"runtime_worker_list_alias",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeWorkerListResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_create_alias",
|
||||||
|
Some("RuntimeWorkerSpawnRequest"),
|
||||||
|
&[("200", "RuntimeWorkerSpawnResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
|
||||||
|
"get",
|
||||||
|
"runtime_worker_list",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeWorkerListResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_create",
|
||||||
|
Some("RuntimeWorkerSpawnRequest"),
|
||||||
|
&[("200", "RuntimeWorkerSpawnResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/config-bundles",
|
||||||
|
"post",
|
||||||
|
"runtime_config_bundle_sync_alias",
|
||||||
|
Some("RuntimeConfigBundleSyncRequest"),
|
||||||
|
&[("200", "RuntimeConfigBundleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles",
|
||||||
|
"post",
|
||||||
|
"runtime_config_bundle_sync",
|
||||||
|
Some("RuntimeConfigBundleSyncRequest"),
|
||||||
|
&[("200", "RuntimeConfigBundleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
|
||||||
|
"get",
|
||||||
|
"runtime_config_bundle_availability_alias",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeConfigBundleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/config-bundles/{bundle_id}/availability",
|
||||||
|
"get",
|
||||||
|
"runtime_config_bundle_availability",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeConfigBundleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}",
|
||||||
|
"get",
|
||||||
|
"runtime_worker_detail_alias",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeWorkerShowResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}",
|
||||||
|
"get",
|
||||||
|
"runtime_worker_detail",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeWorkerShowResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}/restore",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_restore_alias",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkerRestoreResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/restore",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_restore",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkerRestoreResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/pin",
|
||||||
|
"put",
|
||||||
|
"runtime_worker_pin",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkerRetentionResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/pin",
|
||||||
|
"delete",
|
||||||
|
"runtime_worker_unpin",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkerRetentionResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-plan",
|
||||||
|
"get",
|
||||||
|
"runtime_cleanup_plan",
|
||||||
|
None,
|
||||||
|
&[("200", "RuntimeCleanupPlanResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-executions",
|
||||||
|
"post",
|
||||||
|
"runtime_cleanup_execute",
|
||||||
|
Some("ExecuteRuntimeCleanupRequest"),
|
||||||
|
&[("200", "RuntimeCleanupExecutionResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_input_alias",
|
||||||
|
Some("RuntimeWorkerInputRequest"),
|
||||||
|
&[("200", "RuntimeWorkerInputResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_input",
|
||||||
|
Some("RuntimeWorkerInputRequest"),
|
||||||
|
&[("200", "RuntimeWorkerInputResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-upload-grants",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_attachment_upload_grant",
|
||||||
|
None,
|
||||||
|
&[("200", "AttachmentUploadGrantResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-uploads/{upload_id}",
|
||||||
|
"delete",
|
||||||
|
"runtime_worker_attachment_upload_cancel",
|
||||||
|
None,
|
||||||
|
&[("200", "AttachmentUploadCancelResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||||
|
"delete",
|
||||||
|
"runtime_worker_attachment_delete",
|
||||||
|
None,
|
||||||
|
&[("200", "WorkerFileDeleteResponse")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}/completions",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_completions_alias",
|
||||||
|
Some("RuntimeWorkerCompletionsRequest"),
|
||||||
|
&[("200", "RuntimeWorkerCompletionsResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/completions",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_completions",
|
||||||
|
Some("RuntimeWorkerCompletionsRequest"),
|
||||||
|
&[("200", "RuntimeWorkerCompletionsResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}/stop",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_stop_alias",
|
||||||
|
Some("RuntimeWorkerLifecycleRequest"),
|
||||||
|
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/stop",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_stop",
|
||||||
|
Some("RuntimeWorkerLifecycleRequest"),
|
||||||
|
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/runtimes/{runtime_id}/workers/{worker_id}/cancel",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_cancel_alias",
|
||||||
|
Some("RuntimeWorkerLifecycleRequest"),
|
||||||
|
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||||
|
),
|
||||||
|
runtime_operation(
|
||||||
|
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/cancel",
|
||||||
|
"post",
|
||||||
|
"runtime_worker_cancel",
|
||||||
|
Some("RuntimeWorkerLifecycleRequest"),
|
||||||
|
&[("200", "RuntimeWorkerLifecycleResult")],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
validate_runtime_operation_contracts(paths, &operations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn runtime_operation(
|
||||||
|
path: &'static str,
|
||||||
|
method: &'static str,
|
||||||
|
operation_id: &'static str,
|
||||||
|
request: Option<&'static str>,
|
||||||
|
successes: &'static [(&'static str, &'static str)],
|
||||||
|
) -> RuntimeOperationContract {
|
||||||
|
RuntimeOperationContract {
|
||||||
|
path,
|
||||||
|
method,
|
||||||
|
operation_id,
|
||||||
|
request,
|
||||||
|
successes,
|
||||||
|
error: "RepositoryApiError",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_runtime_operation_contracts(
|
||||||
|
paths: &Map<String, Value>,
|
||||||
|
operations: &[RuntimeOperationContract],
|
||||||
|
) -> Result<BTreeSet<String>, GenerationError> {
|
||||||
|
let mut roots = BTreeSet::new();
|
||||||
|
for operation in operations {
|
||||||
|
let path = object_field(paths, operation.path, "OpenAPI paths")?;
|
||||||
|
let value = object_field(path, operation.method, operation.path)?;
|
||||||
|
if value.get("operationId").and_then(Value::as_str) != Some(operation.operation_id) {
|
||||||
|
return Err(GenerationError::invalid(format!(
|
||||||
|
"{} {} must retain operationId `{}`",
|
||||||
|
operation.method, operation.path, operation.operation_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
match operation.request {
|
||||||
|
Some(expected) => {
|
||||||
|
let request = object_field(value, "requestBody", operation.operation_id)?;
|
||||||
|
require_schema_ref(
|
||||||
|
json_content_schema(request, operation.operation_id)?,
|
||||||
|
expected,
|
||||||
|
operation.operation_id,
|
||||||
|
)?;
|
||||||
|
roots.insert(expected.to_owned());
|
||||||
|
}
|
||||||
|
None if value.contains_key("requestBody") => {
|
||||||
|
return Err(GenerationError::invalid(format!(
|
||||||
|
"{} unexpectedly gained a request body",
|
||||||
|
operation.operation_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
let responses = object_field(value, "responses", operation.operation_id)?;
|
||||||
|
let successes = operation
|
||||||
|
.successes
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<BTreeMap<_, _>>();
|
||||||
|
for (status, response) in responses {
|
||||||
|
let response = object(
|
||||||
|
response,
|
||||||
|
&format!("{} response {status}", operation.operation_id),
|
||||||
|
)?;
|
||||||
|
let expected = successes
|
||||||
|
.get(status.as_str())
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(operation.error);
|
||||||
|
require_schema_ref(
|
||||||
|
json_content_schema(response, operation.operation_id)?,
|
||||||
|
expected,
|
||||||
|
operation.operation_id,
|
||||||
|
)?;
|
||||||
|
roots.insert(expected.to_owned());
|
||||||
|
}
|
||||||
|
for (status, expected) in successes {
|
||||||
|
if !responses.contains_key(status) {
|
||||||
|
return Err(GenerationError::invalid(format!(
|
||||||
|
"{} is missing success response {status} ({expected})",
|
||||||
|
operation.operation_id
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(roots)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RuntimeOperationContract {
|
||||||
|
path: &'static str,
|
||||||
|
method: &'static str,
|
||||||
|
operation_id: &'static str,
|
||||||
|
request: Option<&'static str>,
|
||||||
|
successes: &'static [(&'static str, &'static str)],
|
||||||
|
error: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_operation_contracts(
|
fn validate_operation_contracts(
|
||||||
paths: &Map<String, Value>,
|
paths: &Map<String, Value>,
|
||||||
operations: &[OperationContract],
|
operations: &[OperationContract],
|
||||||
@@ -686,6 +1091,21 @@ fn render_schema(value: &Value, context: &str) -> Result<String, GenerationError
|
|||||||
.map(|branches| branches.join(" | "));
|
.map(|branches| branches.join(" | "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let disjoint_types = branches
|
||||||
|
.iter()
|
||||||
|
.map(|branch| branch.as_object()?.get("type")?.as_str())
|
||||||
|
.collect::<Option<Vec<_>>>();
|
||||||
|
if let Some(types) = disjoint_types {
|
||||||
|
let unique = types.iter().copied().collect::<BTreeSet<_>>();
|
||||||
|
if outer_type.is_none() && unique.len() == branches.len() {
|
||||||
|
return branches
|
||||||
|
.iter()
|
||||||
|
.map(|branch| render_schema(branch, context))
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map(|branches| branches.join(" | "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut rendered = Vec::with_capacity(branches.len());
|
let mut rendered = Vec::with_capacity(branches.len());
|
||||||
let mut seen_literals = BTreeSet::new();
|
let mut seen_literals = BTreeSet::new();
|
||||||
for branch in branches {
|
for branch in branches {
|
||||||
@@ -1007,6 +1427,44 @@ mod tests {
|
|||||||
const OPENAPI: &str = include_str!("../../../openapi/server-api.json");
|
const OPENAPI: &str = include_str!("../../../openapi/server-api.json");
|
||||||
const TYPESCRIPT: &str =
|
const TYPESCRIPT: &str =
|
||||||
include_str!("../../../web/workspace/src/lib/generated/repository-api.ts");
|
include_str!("../../../web/workspace/src/lib/generated/repository-api.ts");
|
||||||
|
const RUNTIME_TYPESCRIPT: &str =
|
||||||
|
include_str!("../../../web/workspace/src/lib/generated/runtime-api.ts");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn checked_in_runtime_typescript_is_current() {
|
||||||
|
assert_eq!(
|
||||||
|
generate_runtime_typescript(OPENAPI).unwrap(),
|
||||||
|
RUNTIME_TYPESCRIPT,
|
||||||
|
"regenerate with `cargo run -q -p server-api --example generate_runtime_api_types`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_generator_validates_operation_ownership_and_schema_closure() {
|
||||||
|
let output = generate_runtime_typescript(OPENAPI).unwrap();
|
||||||
|
assert!(output.contains(&format!(
|
||||||
|
"@generated by {RUNTIME_GENERATOR_NAME}@{RUNTIME_GENERATOR_VERSION}"
|
||||||
|
)));
|
||||||
|
assert!(output.contains("export type RuntimeCleanupPlanResponse ="));
|
||||||
|
assert!(output.contains("export type RuntimeWorkerLifecycleResult ="));
|
||||||
|
assert!(output.contains("export type RuntimeWorkerSpawnAcceptanceRequirement = \"socket_ready\" | { run_accepted:"));
|
||||||
|
assert!(!output.contains("export type UnmigratedDomain ="));
|
||||||
|
|
||||||
|
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||||
|
document["paths"]["/api/w/{workspace_id}/runtimes/{runtime_id}/cleanup-plan"]["get"]["operationId"] =
|
||||||
|
serde_json::json!("manual_cleanup_plan");
|
||||||
|
let error = generate_runtime_typescript(&document.to_string()).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("runtime_cleanup_plan"));
|
||||||
|
|
||||||
|
let mut document: Value = serde_json::from_str(OPENAPI).unwrap();
|
||||||
|
document["components"]["schemas"]["RuntimeWorkerSpawnAcceptanceRequirement"]
|
||||||
|
["oneOf"]
|
||||||
|
.as_array_mut()
|
||||||
|
.unwrap()
|
||||||
|
.push(serde_json::json!({"type": "object", "properties": {}, "additionalProperties": false}));
|
||||||
|
let error = generate_runtime_typescript(&document.to_string()).unwrap_err();
|
||||||
|
assert!(error.to_string().contains("ambiguous oneOf"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn checked_in_repository_typescript_is_current() {
|
fn checked_in_repository_typescript_is_current() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+6807
-1
File diff suppressed because it is too large
Load Diff
@@ -359,207 +359,3 @@ export type RepositoryLogResponse = {
|
|||||||
items: Array<GitCommitSummary>;
|
items: Array<GitCommitSummary>;
|
||||||
diagnostics: Array<Diagnostic>;
|
diagnostics: Array<Diagnostic>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
|
|
||||||
|
|
||||||
export type RuntimeSourceStatus = "active" | "reserved";
|
|
||||||
|
|
||||||
export type RuntimeIdentityAuthority =
|
|
||||||
| "runtime_registry_projection"
|
|
||||||
| "server_runtime_configuration";
|
|
||||||
|
|
||||||
export type RuntimeSourceSummary = {
|
|
||||||
kind: RuntimeSourceKind;
|
|
||||||
status: RuntimeSourceStatus;
|
|
||||||
identity_authority: RuntimeIdentityAuthority;
|
|
||||||
note: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeSummary = {
|
|
||||||
runtime_id: string;
|
|
||||||
label: string;
|
|
||||||
kind: string;
|
|
||||||
status: string;
|
|
||||||
source: RuntimeSourceSummary;
|
|
||||||
host_ids: Array<string>;
|
|
||||||
worker_creation_available: boolean;
|
|
||||||
os: string;
|
|
||||||
arch: string;
|
|
||||||
diagnostics: Array<Diagnostic>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceRuntimeBindingState =
|
|
||||||
| "configured"
|
|
||||||
| "verified"
|
|
||||||
| "revoked";
|
|
||||||
|
|
||||||
export type RuntimeConnectionDisplayState =
|
|
||||||
| "configured"
|
|
||||||
| "verified"
|
|
||||||
| "unavailable"
|
|
||||||
| "revoked";
|
|
||||||
|
|
||||||
export type RuntimeVerificationOutcome =
|
|
||||||
| "verified"
|
|
||||||
| "challenge_issued"
|
|
||||||
| "verification_failed"
|
|
||||||
| "connectivity_failed";
|
|
||||||
|
|
||||||
export type RuntimeVerificationEvidenceSummary = {
|
|
||||||
verified_at: string | null;
|
|
||||||
last_checked_at: string;
|
|
||||||
last_outcome: RuntimeVerificationOutcome;
|
|
||||||
binding_revision: number;
|
|
||||||
workspace_key_id: string;
|
|
||||||
workspace_identity_revision: number;
|
|
||||||
workspace_trust_generation: number;
|
|
||||||
runtime_public_key_fingerprint: string;
|
|
||||||
runtime_identity_revision: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceRuntimeBindingSummary = {
|
|
||||||
state: WorkspaceRuntimeBindingState;
|
|
||||||
connection_state: RuntimeConnectionDisplayState;
|
|
||||||
revision: number;
|
|
||||||
workspace_key_id?: string | null;
|
|
||||||
workspace_key_generation?: number | null;
|
|
||||||
verification?: RuntimeVerificationEvidenceSummary | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeManagementSummary = {
|
|
||||||
built_in: boolean;
|
|
||||||
config_managed: boolean;
|
|
||||||
removable: boolean;
|
|
||||||
endpoint_configured: boolean;
|
|
||||||
token_ref_configured: boolean;
|
|
||||||
binding?: WorkspaceRuntimeBindingSummary | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceRuntimeResource = {
|
|
||||||
management: RuntimeManagementSummary;
|
|
||||||
runtime_id: string;
|
|
||||||
label: string;
|
|
||||||
kind: string;
|
|
||||||
status: string;
|
|
||||||
source: RuntimeSourceSummary;
|
|
||||||
host_ids: Array<string>;
|
|
||||||
worker_creation_available: boolean;
|
|
||||||
os: string;
|
|
||||||
arch: string;
|
|
||||||
diagnostics: Array<Diagnostic>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
|
|
||||||
|
|
||||||
export type RuntimeTrustKeyState = {
|
|
||||||
status: RuntimeTrustKeyStatus;
|
|
||||||
fingerprint?: string | null;
|
|
||||||
revision?: number | null;
|
|
||||||
created_at?: string | null;
|
|
||||||
updated_at?: string | null;
|
|
||||||
revoked_at?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeTrustAuditAction =
|
|
||||||
| "created"
|
|
||||||
| "replaced"
|
|
||||||
| "reactivated"
|
|
||||||
| "revoked";
|
|
||||||
|
|
||||||
export type RuntimeTrustAuditEntry = {
|
|
||||||
action: RuntimeTrustAuditAction;
|
|
||||||
actor_account_id: string;
|
|
||||||
old_fingerprint?: string | null;
|
|
||||||
new_fingerprint?: string | null;
|
|
||||||
revision: number;
|
|
||||||
at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type WorkspaceRuntimeDetail = {
|
|
||||||
workspace_id: string;
|
|
||||||
runtime: WorkspaceRuntimeResource;
|
|
||||||
endpoint?: string | null;
|
|
||||||
trust_key: RuntimeTrustKeyState;
|
|
||||||
recent_audit: Array<RuntimeTrustAuditEntry>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeTrustKeyRevealResponse = { public_key: string };
|
|
||||||
|
|
||||||
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
|
|
||||||
|
|
||||||
export type RemoveRuntimeRequest = {
|
|
||||||
operation_id: string;
|
|
||||||
expected_binding_revision: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeRemovalOperationState =
|
|
||||||
| "pending"
|
|
||||||
| "cleanup_pending"
|
|
||||||
| "succeeded"
|
|
||||||
| "failed";
|
|
||||||
|
|
||||||
export type RuntimeRemovalOperationResponse = {
|
|
||||||
operation_id: string;
|
|
||||||
workspace_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
state: RuntimeRemovalOperationState;
|
|
||||||
binding_removed: boolean;
|
|
||||||
runtime_registration_removed: boolean | null;
|
|
||||||
failure_category?: string | null;
|
|
||||||
created_at: string;
|
|
||||||
updated_at: string;
|
|
||||||
completed_at?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
|
|
||||||
|
|
||||||
export type RuntimeTrustConflictResponse = {
|
|
||||||
error: RuntimeTrustConflictKind;
|
|
||||||
message: string;
|
|
||||||
current_revision?: number;
|
|
||||||
current_fingerprint?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimePublicIdentityBundle = {
|
|
||||||
identity_id: string;
|
|
||||||
public_key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CreateRemoteRuntimeRequest = {
|
|
||||||
public_bundle: RuntimePublicIdentityBundle;
|
|
||||||
display_name?: string | null;
|
|
||||||
endpoint: string;
|
|
||||||
expected_revision?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UpdateRemoteRuntimeRequest = {
|
|
||||||
display_name?: string | null;
|
|
||||||
endpoint: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeConnectionTestStatus = "compatible" | "failed";
|
|
||||||
|
|
||||||
export type RuntimeConnectionTestFailureKind =
|
|
||||||
| "authentication"
|
|
||||||
| "authorization"
|
|
||||||
| "network_unreachable"
|
|
||||||
| "timeout"
|
|
||||||
| "tls_or_transport"
|
|
||||||
| "malformed_response"
|
|
||||||
| "protocol_version_mismatch"
|
|
||||||
| "runtime_identity_mismatch"
|
|
||||||
| "configuration";
|
|
||||||
|
|
||||||
export type RuntimeConnectionTestResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
binding_revision: number;
|
|
||||||
connection_state: RuntimeConnectionDisplayState;
|
|
||||||
verification: RuntimeVerificationEvidenceSummary | null;
|
|
||||||
checked_at: string;
|
|
||||||
status: RuntimeConnectionTestStatus;
|
|
||||||
failure_kind: RuntimeConnectionTestFailureKind | null;
|
|
||||||
expected_protocol_version: number;
|
|
||||||
actual_protocol_version: number | null;
|
|
||||||
diagnostics: Array<Diagnostic>;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
|
||||||
// Input: openapi/server-api.json
|
// Input: openapi/server-api.json
|
||||||
// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59)
|
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||||
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
|
||||||
// Output: web/workspace/src/lib/generated/repository-api.ts
|
// Output: web/workspace/src/lib/generated/repository-api.ts
|
||||||
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
|
||||||
|
// Input: openapi/server-api.json
|
||||||
|
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||||
|
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||||
|
// Output: web/workspace/src/lib/generated/runtime-api.ts
|
||||||
|
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
|
||||||
|
|
||||||
|
export type AttachmentUploadCancelResponse = { cancelled: boolean; };
|
||||||
|
|
||||||
|
export type AttachmentUploadGrantResponse = { expires_at_ms: number; upload_id: string; };
|
||||||
|
|
||||||
|
export type CleanupTargetKind = "worker_delete" | "workdir_clean_cleanup" | "workdir_dirty_discard" | "workdir_record_delete";
|
||||||
|
|
||||||
|
export type CleanupWorkdirCandidate = { action: CleanupTargetKind; blocking_reason?: string | null; cleanliness: CleanupWorkdirCleanliness; estimated_reclaim_bytes?: number | null; file_status: CleanupWorkdirFileStatus; linked_running_worker_ids: Array<string>; linked_worker_ids: Array<string>; pinned_linked: boolean; reason: string; repository_key: string; running_linked: boolean; runtime_id: string; target_id: string; workdir_id: string; };
|
||||||
|
|
||||||
|
export type CleanupWorkdirCleanliness = "clean" | "dirty" | "unknown";
|
||||||
|
|
||||||
|
export type CleanupWorkdirFileStatus = "pending" | "present" | "active" | "cleanup_pending" | "not_found" | "corrupted" | "failed" | "unknown";
|
||||||
|
|
||||||
|
export type CleanupWorkerCandidate = { action: CleanupTargetKind; blocking_reason?: string | null; estimated_reclaim_bytes?: number | null; linked_workdir_ids: Array<string>; pinned: boolean; reason: string; retention_state: string; running_linked: boolean; runtime_id: string; runtime_worker_id: string; target_id: string; worker_id: string; };
|
||||||
|
|
||||||
|
export type CreateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; expected_revision?: number | null; public_bundle: RuntimePublicIdentityBundle; };
|
||||||
|
|
||||||
|
export type Diagnostic = { code: string; message: string; severity: DiagnosticSeverity; };
|
||||||
|
|
||||||
|
export type DiagnosticSeverity = "info" | "warning" | "error";
|
||||||
|
|
||||||
|
export type ExecuteRuntimeCleanupRequest = { confirm_dirty_discard_target_ids?: Array<string>; expected_plan_digest: string; expected_plan_revision: string; workdir_target_ids?: Array<string>; worker_target_ids?: Array<string>; };
|
||||||
|
|
||||||
|
export type RemoveRuntimeRequest = { expected_binding_revision: number; operation_id: string; };
|
||||||
|
|
||||||
|
export type RepositoryApiError = { diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||||
|
|
||||||
|
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number; };
|
||||||
|
|
||||||
|
export type RuntimeCleanupExecutionResponse = { diagnostics: Array<Diagnostic>; executed_at: string; plan_after: RuntimeCleanupPlanResponse; results: Array<RuntimeCleanupExecutionResult>; runtime_id: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeCleanupExecutionResult = { action: CleanupTargetKind; message: string; status: string; target_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeCleanupPlanResponse = { diagnostics: Array<Diagnostic>; digest: string; generated_at: string; revision: string; runtime_id: string; workdirs: Array<CleanupWorkdirCandidate>; workers: Array<CleanupWorkerCandidate>; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeConfigBundleResult = { availability?: unknown; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; };
|
||||||
|
|
||||||
|
export type RuntimeConfigBundleSyncRequest = { bundle: unknown; };
|
||||||
|
|
||||||
|
export type RuntimeConnectionDisplayState = "configured" | "verified" | "unavailable" | "revoked";
|
||||||
|
|
||||||
|
export type RuntimeConnectionTestFailureKind = "authentication" | "authorization" | "network_unreachable" | "timeout" | "tls_or_transport" | "malformed_response" | "protocol_version_mismatch" | "runtime_identity_mismatch" | "configuration";
|
||||||
|
|
||||||
|
export type RuntimeConnectionTestResponse = { actual_protocol_version?: number | null; binding_revision: number; checked_at: string; connection_state: RuntimeConnectionDisplayState; diagnostics?: Array<Diagnostic>; expected_protocol_version: number; failure_kind?: RuntimeConnectionTestFailureKind | null; runtime_id: string; status: RuntimeConnectionTestStatus; verification?: RuntimeVerificationEvidenceSummary | null; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeConnectionTestStatus = "compatible" | "failed";
|
||||||
|
|
||||||
|
export type RuntimeIdentityAuthority = "runtime_registry_projection" | "server_runtime_configuration";
|
||||||
|
|
||||||
|
export type RuntimeListResponse = { diagnostics?: Array<Diagnostic>; items: Array<RuntimeSummary>; limit: number; source: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeManagementApiError = { current_fingerprint?: string | null; current_revision?: number | null; diagnostics?: Array<Diagnostic>; error: string; message: string; };
|
||||||
|
|
||||||
|
export type RuntimeManagementSummary = { binding?: WorkspaceRuntimeBindingSummary | null; built_in: boolean; config_managed: boolean; endpoint_configured: boolean; removable: boolean; token_ref_configured: boolean; };
|
||||||
|
|
||||||
|
export type RuntimeProfileSelector = { kind: "builtin"; value: string; } | { kind: "named"; value: string; };
|
||||||
|
|
||||||
|
export type RuntimePublicIdentityBundle = { identity_id: string; public_key: string; };
|
||||||
|
|
||||||
|
export type RuntimeRemovalOperationResponse = { binding_removed: boolean; completed_at?: string | null; created_at: string; failure_category?: string | null; operation_id: string; runtime_id: string; runtime_registration_removed?: boolean | null; state: RuntimeRemovalOperationState; updated_at: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeRemovalOperationState = "pending" | "cleanup_pending" | "succeeded" | "failed";
|
||||||
|
|
||||||
|
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
|
||||||
|
|
||||||
|
export type RuntimeSourceStatus = "active" | "reserved";
|
||||||
|
|
||||||
|
export type RuntimeSourceSummary = { identity_authority: RuntimeIdentityAuthority; kind: RuntimeSourceKind; note: string; status: RuntimeSourceStatus; };
|
||||||
|
|
||||||
|
export type RuntimeSummary = { arch: string; diagnostics?: Array<Diagnostic>; host_ids?: Array<string>; kind: string; label: string; os: string; runtime_id: string; source: RuntimeSourceSummary; status: string; worker_creation_available: boolean; };
|
||||||
|
|
||||||
|
export type RuntimeTicketWorkerRole = "intake" | "orchestrator" | "coder" | "reviewer";
|
||||||
|
|
||||||
|
export type RuntimeTrustAuditAction = "created" | "replaced" | "reactivated" | "revoked";
|
||||||
|
|
||||||
|
export type RuntimeTrustAuditEntry = { action: RuntimeTrustAuditAction; actor_account_id: string; at: string; new_fingerprint?: string | null; old_fingerprint?: string | null; revision: number; };
|
||||||
|
|
||||||
|
export type RuntimeTrustKeyRevealResponse = { public_key: string; };
|
||||||
|
|
||||||
|
export type RuntimeTrustKeyState = { created_at?: string | null; fingerprint?: string | null; revision?: number | null; revoked_at?: string | null; status: RuntimeTrustKeyStatus; updated_at?: string | null; };
|
||||||
|
|
||||||
|
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
|
||||||
|
|
||||||
|
export type RuntimeVerificationEvidenceSummary = { binding_revision: number; last_checked_at: string; last_outcome: RuntimeVerificationOutcome; runtime_identity_revision: number; runtime_public_key_fingerprint: string; verified_at?: string | null; workspace_identity_revision: number; workspace_key_id: string; workspace_trust_generation: number; };
|
||||||
|
|
||||||
|
export type RuntimeVerificationOutcome = "verified" | "challenge_issued" | "verification_failed" | "connectivity_failed";
|
||||||
|
|
||||||
|
export type RuntimeWorkerCompletionsRequest = { kind: unknown; prefix?: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerCompletionsResult = { diagnostics?: Array<Diagnostic>; entries: Array<unknown>; kind: unknown; prefix: string; runtime_id: string; worker_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerInputRequest = { content: string; kind?: string | null; segments?: Array<unknown> | null; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerInputResult = { diagnostics?: Array<Diagnostic>; runtime_id: string; state: WorkerOperationState; worker_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerLifecycleRequest = { reason?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerLifecycleResult = { diagnostics?: Array<Diagnostic>; runtime_id: string; state: WorkerOperationState; worker_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkerSummary>; limit: number; source: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerShowResponse = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; updated_at: string; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnAcceptanceEvidence = { detail: string; kind: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnAcceptanceRequirement = "socket_ready" | { run_accepted: { expected_segments: number; }; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnIntent = { kind: "workspace_companion"; } | { kind: "workspace_orchestrator"; } | { kind: "workspace_coding"; } | { kind: "ticket_role"; role: RuntimeTicketWorkerRole; ticket_id: string; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnRequest = { acceptance: RuntimeWorkerSpawnAcceptanceRequirement; initial_submit?: Array<unknown>; intent: RuntimeWorkerSpawnIntent; profile: RuntimeProfileSelector; requested_worker_name?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; workdir_attachment_requests?: Array<RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest>; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnResponse = { acceptance_evidence?: Array<RuntimeWorkerSpawnAcceptanceEvidence>; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; worker?: RuntimeWorkerSummary | null; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest = { alias: string; working_directory: RuntimeWorkerSpawnWorkingDirectoryRequest; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSpawnWorkingDirectoryRequest = { repository_key: string; selector?: string | null; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<unknown>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
|
||||||
|
|
||||||
|
export type RuntimeWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };
|
||||||
|
|
||||||
|
export type UpdateRemoteRuntimeRequest = { display_name?: string | null; endpoint: string; };
|
||||||
|
|
||||||
|
export type WorkerCapabilitySummary = { can_spawn_followup: boolean; can_stop: boolean; };
|
||||||
|
|
||||||
|
export type WorkerFileDeleteResponse = { deleted: boolean; };
|
||||||
|
|
||||||
|
export type WorkerImplementationSummary = { display_hint: string; kind: string; };
|
||||||
|
|
||||||
|
export type WorkerOperationState = "accepted" | "unsupported" | "rejected";
|
||||||
|
|
||||||
|
export type WorkerRestoreResponse = { result: WorkerRestoreResult; runtime_id: string; worker_id: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type WorkerRestoreResult = { diagnostics?: Array<Diagnostic>; state: WorkerRestoreState; worker?: WorkerSummary | null; };
|
||||||
|
|
||||||
|
export type WorkerRestoreState = "accepted" | "rejected" | "rolled_back" | "reconciliation_required";
|
||||||
|
|
||||||
|
export type WorkerRetentionResponse = { pinned: boolean; retention_state: string; runtime_id: string; worker_id: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type WorkerSummary = { capabilities: WorkerCapabilitySummary; diagnostics?: Array<Diagnostic>; display_name?: string; host_id: string; implementation: WorkerImplementationSummary; label: string; last_seen_at?: string | null; pinned?: boolean; profile?: string | null; resource_key: string; retention_state?: string; runtime_id: string; singleton_key?: string | null; state: string; tags?: Array<string>; workdir_attachments?: Array<WorkerWorkdirAttachmentSummary>; worker_id: string; worker_state?: unknown; workspace: WorkerWorkspaceSummary; };
|
||||||
|
|
||||||
|
export type WorkerWorkdirAttachmentSummary = { alias: string; working_directory: WorkingDirectorySummary; };
|
||||||
|
|
||||||
|
export type WorkerWorkspaceSummary = { identity: string; visibility: string; workspace_id?: string | null; };
|
||||||
|
|
||||||
|
export type WorkingDirectoryCleanupTarget = { kind: string; repository_key: string; working_directory_id: string; };
|
||||||
|
|
||||||
|
export type WorkingDirectoryMaterializerKind = "runtime_git_clone" | "client_hosted_external";
|
||||||
|
|
||||||
|
export type WorkingDirectoryOccupancy = { display_name: string; linked_at: string; runtime_id: string; worker_id: string; };
|
||||||
|
|
||||||
|
export type WorkingDirectorySource = { kind: "repository"; repository_key: string; } | { grant_id: string; kind: "external_grant"; };
|
||||||
|
|
||||||
|
export type WorkingDirectoryStatusKind = "active" | "cleanup_pending" | "corrupted" | "not_found" | "unknown";
|
||||||
|
|
||||||
|
export type WorkingDirectorySummary = { cleanliness?: string | null; cleanup_target?: WorkingDirectoryCleanupTarget | null; creation_ref?: string | null; creation_selector?: string | null; creation_tree?: string | null; current_ref?: string | null; current_selector?: string | null; current_tree?: string | null; display_name?: string | null; materializer_kind: WorkingDirectoryMaterializerKind; observed_at_epoch_seconds?: number | null; occupied_by?: WorkingDirectoryOccupancy | null; source: WorkingDirectorySource; status: WorkingDirectoryStatusKind; working_directory_id: string; };
|
||||||
|
|
||||||
|
export type WorkspaceRuntimeBindingState = "configured" | "verified" | "revoked";
|
||||||
|
|
||||||
|
export type WorkspaceRuntimeBindingSummary = { connection_state: RuntimeConnectionDisplayState; revision: number; state: WorkspaceRuntimeBindingState; verification?: RuntimeVerificationEvidenceSummary | null; workspace_key_generation?: number | null; workspace_key_id?: string | null; };
|
||||||
|
|
||||||
|
export type WorkspaceRuntimeDetail = { endpoint?: string | null; recent_audit?: Array<RuntimeTrustAuditEntry>; runtime: WorkspaceRuntimeResource; trust_key: RuntimeTrustKeyState; workspace_id: string; };
|
||||||
|
|
||||||
|
export type WorkspaceRuntimeListResponse = { diagnostics?: Array<Diagnostic>; items: Array<WorkspaceRuntimeResource>; limit: number; source: string; workspace_id: string; };
|
||||||
|
|
||||||
|
export type WorkspaceRuntimeResource = { arch: string; diagnostics?: Array<Diagnostic>; host_ids?: Array<string>; kind: string; label: string; management: RuntimeManagementSummary; os: string; runtime_id: string; source: RuntimeSourceSummary; status: string; worker_creation_available: boolean; };
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
|
||||||
// Input: openapi/server-api.json
|
// Input: openapi/server-api.json
|
||||||
// Input contract: 0.1.0 (sha256:e3f776a17079ce202725bb97de607426f4bfad9afe390ba7bbf76186e871be59)
|
// Input contract: 0.1.0 (sha256:67e56d1ffd3b27bc8653ef791bd68b5150ac7d507ccbe4e65b2e00320834d718)
|
||||||
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
|
||||||
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
// Output: web/workspace/src/lib/generated/workdir-api.ts
|
||||||
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type {
|
|||||||
RuntimeConnectionTestFailureKind,
|
RuntimeConnectionTestFailureKind,
|
||||||
RuntimeConnectionTestResponse,
|
RuntimeConnectionTestResponse,
|
||||||
RuntimeVerificationEvidenceSummary,
|
RuntimeVerificationEvidenceSummary,
|
||||||
} from "$lib/generated/legacy-server-api";
|
} from "$lib/generated/runtime-api";
|
||||||
|
|
||||||
const RESPONSE_KEYS = [
|
const RESPONSE_KEYS = [
|
||||||
"workspace_id",
|
"workspace_id",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
RevokeRuntimeTrustKeyRequest,
|
RevokeRuntimeTrustKeyRequest,
|
||||||
RuntimeConnectionDisplayState,
|
RuntimeConnectionDisplayState,
|
||||||
RuntimeIdentityAuthority,
|
RuntimeIdentityAuthority,
|
||||||
|
RuntimeManagementApiError,
|
||||||
RuntimeManagementSummary,
|
RuntimeManagementSummary,
|
||||||
RuntimeRemovalOperationResponse,
|
RuntimeRemovalOperationResponse,
|
||||||
RuntimeRemovalOperationState,
|
RuntimeRemovalOperationState,
|
||||||
@@ -13,8 +14,6 @@ import type {
|
|||||||
RuntimeSourceSummary,
|
RuntimeSourceSummary,
|
||||||
RuntimeTrustAuditAction,
|
RuntimeTrustAuditAction,
|
||||||
RuntimeTrustAuditEntry,
|
RuntimeTrustAuditEntry,
|
||||||
RuntimeTrustConflictKind,
|
|
||||||
RuntimeTrustConflictResponse,
|
|
||||||
RuntimeTrustKeyRevealResponse,
|
RuntimeTrustKeyRevealResponse,
|
||||||
RuntimeTrustKeyState,
|
RuntimeTrustKeyState,
|
||||||
RuntimeTrustKeyStatus,
|
RuntimeTrustKeyStatus,
|
||||||
@@ -23,12 +22,24 @@ import type {
|
|||||||
WorkspaceRuntimeBindingState,
|
WorkspaceRuntimeBindingState,
|
||||||
WorkspaceRuntimeBindingSummary,
|
WorkspaceRuntimeBindingSummary,
|
||||||
WorkspaceRuntimeDetail,
|
WorkspaceRuntimeDetail,
|
||||||
|
WorkspaceRuntimeListResponse,
|
||||||
WorkspaceRuntimeResource,
|
WorkspaceRuntimeResource,
|
||||||
} from "$lib/generated/legacy-server-api.ts";
|
} from "$lib/generated/runtime-api.ts";
|
||||||
import type { ListResponse } from "$lib/workspace/sidebar/types";
|
|
||||||
import { workspaceApiPath } from "./http.ts";
|
import { workspaceApiPath } from "./http.ts";
|
||||||
|
|
||||||
export type WorkspaceRuntimeList = ListResponse<WorkspaceRuntimeResource>;
|
export type WorkspaceRuntimeList = Omit<WorkspaceRuntimeListResponse, "diagnostics"> & {
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
};
|
||||||
|
export type ValidatedWorkspaceRuntimeDetail = Omit<
|
||||||
|
WorkspaceRuntimeDetail,
|
||||||
|
"recent_audit"
|
||||||
|
> & {
|
||||||
|
recent_audit: RuntimeTrustAuditEntry[];
|
||||||
|
};
|
||||||
|
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
|
||||||
|
export type RuntimeTrustConflictResponse = RuntimeManagementApiError & {
|
||||||
|
error: RuntimeTrustConflictKind;
|
||||||
|
};
|
||||||
|
|
||||||
const LIMITS = {
|
const LIMITS = {
|
||||||
runtimeItems: 200,
|
runtimeItems: 200,
|
||||||
@@ -690,7 +701,7 @@ export function parseWorkspaceRuntimeList(
|
|||||||
|
|
||||||
export function parseWorkspaceRuntimeDetail(
|
export function parseWorkspaceRuntimeDetail(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): WorkspaceRuntimeDetail {
|
): ValidatedWorkspaceRuntimeDetail {
|
||||||
const response = object(value, "Runtime detail response");
|
const response = object(value, "Runtime detail response");
|
||||||
exactKeys(
|
exactKeys(
|
||||||
response,
|
response,
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
import type {
|
||||||
|
CleanupTargetKind,
|
||||||
|
CleanupWorkdirCandidate,
|
||||||
|
CleanupWorkdirCleanliness,
|
||||||
|
CleanupWorkdirFileStatus,
|
||||||
|
CleanupWorkerCandidate,
|
||||||
|
Diagnostic,
|
||||||
|
RuntimeCleanupExecutionResponse,
|
||||||
|
RuntimeCleanupExecutionResult,
|
||||||
|
RuntimeCleanupPlanResponse,
|
||||||
|
RuntimeWorkerLifecycleResult,
|
||||||
|
WorkerOperationState,
|
||||||
|
WorkerRetentionResponse,
|
||||||
|
} from "$lib/generated/runtime-api";
|
||||||
|
|
||||||
|
const MAX_STRING_BYTES = 4_096;
|
||||||
|
const MAX_CANDIDATES = 1_000;
|
||||||
|
const MAX_RESULTS = 2_000;
|
||||||
|
const MAX_DIAGNOSTICS = 64;
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
type JsonObject = Record<string, unknown>;
|
||||||
|
|
||||||
|
const TARGET_KINDS = new Set<CleanupTargetKind>([
|
||||||
|
"worker_delete",
|
||||||
|
"workdir_clean_cleanup",
|
||||||
|
"workdir_dirty_discard",
|
||||||
|
"workdir_record_delete",
|
||||||
|
]);
|
||||||
|
const FILE_STATUSES = new Set<CleanupWorkdirFileStatus>([
|
||||||
|
"pending",
|
||||||
|
"present",
|
||||||
|
"active",
|
||||||
|
"cleanup_pending",
|
||||||
|
"not_found",
|
||||||
|
"corrupted",
|
||||||
|
"failed",
|
||||||
|
"unknown",
|
||||||
|
]);
|
||||||
|
const CLEANLINESS = new Set<CleanupWorkdirCleanliness>([
|
||||||
|
"clean",
|
||||||
|
"dirty",
|
||||||
|
"unknown",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export class RuntimeWorkerValidationError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message.slice(0, 256));
|
||||||
|
this.name = "RuntimeWorkerValidationError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(path: string, message: string): never {
|
||||||
|
throw new RuntimeWorkerValidationError(`${path} ${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function object(value: unknown, path: string): JsonObject {
|
||||||
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||||
|
return fail(path, "must be an object");
|
||||||
|
}
|
||||||
|
return value as JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exactKeys(
|
||||||
|
value: JsonObject,
|
||||||
|
required: readonly string[],
|
||||||
|
optional: readonly string[],
|
||||||
|
path: string,
|
||||||
|
): void {
|
||||||
|
const allowed = new Set([...required, ...optional]);
|
||||||
|
for (const key of Object.keys(value)) {
|
||||||
|
if (!allowed.has(key)) fail(`${path}.${key}`, "is not part of the wire contract");
|
||||||
|
}
|
||||||
|
for (const key of required) {
|
||||||
|
if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function boundedString(value: unknown, path: string, allowEmpty = false): string {
|
||||||
|
if (typeof value !== "string") return fail(path, "must be a string");
|
||||||
|
if (!allowEmpty && value.length === 0) return fail(path, "must not be empty");
|
||||||
|
if (encoder.encode(value).byteLength > MAX_STRING_BYTES) {
|
||||||
|
return fail(path, `must be at most ${MAX_STRING_BYTES} UTF-8 bytes`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function boolean(value: unknown, path: string): boolean {
|
||||||
|
if (typeof value !== "boolean") return fail(path, "must be a boolean");
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function array(value: unknown, path: string, max: number): unknown[] {
|
||||||
|
if (!Array.isArray(value)) return fail(path, "must be an array");
|
||||||
|
if (value.length > max) return fail(path, `must contain at most ${max} items`);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function strings(value: unknown, path: string): string[] {
|
||||||
|
return array(value, path, MAX_CANDIDATES).map((entry, index) =>
|
||||||
|
boundedString(entry, `${path}[${index}]`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNullableString(value: unknown, path: string): string | null | undefined {
|
||||||
|
if (value === undefined || value === null) return value;
|
||||||
|
return boundedString(value, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNullableSafeInteger(value: unknown, path: string): number | null | undefined {
|
||||||
|
if (value === undefined || value === null) return value;
|
||||||
|
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
||||||
|
return fail(path, "must be a non-negative safe integer");
|
||||||
|
}
|
||||||
|
return value as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function targetKind(value: unknown, path: string): CleanupTargetKind {
|
||||||
|
if (typeof value !== "string" || !TARGET_KINDS.has(value as CleanupTargetKind)) {
|
||||||
|
return fail(path, "has an unknown cleanup action");
|
||||||
|
}
|
||||||
|
return value as CleanupTargetKind;
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnostic(value: unknown, path: string): Diagnostic {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["code", "severity", "message"], [], path);
|
||||||
|
const severity = boundedString(item.severity, `${path}.severity`);
|
||||||
|
if (severity !== "info" && severity !== "warning" && severity !== "error") {
|
||||||
|
fail(`${path}.severity`, "has an unknown value");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
code: boundedString(item.code, `${path}.code`),
|
||||||
|
severity,
|
||||||
|
message: boundedString(item.message, `${path}.message`, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function diagnostics(value: unknown, path: string): Diagnostic[] {
|
||||||
|
return array(value, path, MAX_DIAGNOSTICS).map((entry, index) =>
|
||||||
|
diagnostic(entry, `${path}[${index}]`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function workerCandidate(value: unknown, path: string): CleanupWorkerCandidate {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"target_id",
|
||||||
|
"action",
|
||||||
|
"worker_id",
|
||||||
|
"runtime_worker_id",
|
||||||
|
"runtime_id",
|
||||||
|
"reason",
|
||||||
|
"pinned",
|
||||||
|
"retention_state",
|
||||||
|
"linked_workdir_ids",
|
||||||
|
"running_linked",
|
||||||
|
],
|
||||||
|
["blocking_reason", "estimated_reclaim_bytes"],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
target_id: boundedString(item.target_id, `${path}.target_id`),
|
||||||
|
action: targetKind(item.action, `${path}.action`),
|
||||||
|
worker_id: boundedString(item.worker_id, `${path}.worker_id`),
|
||||||
|
runtime_worker_id: boundedString(item.runtime_worker_id, `${path}.runtime_worker_id`),
|
||||||
|
runtime_id: boundedString(item.runtime_id, `${path}.runtime_id`),
|
||||||
|
reason: boundedString(item.reason, `${path}.reason`, true),
|
||||||
|
blocking_reason: optionalNullableString(item.blocking_reason, `${path}.blocking_reason`),
|
||||||
|
pinned: boolean(item.pinned, `${path}.pinned`),
|
||||||
|
retention_state: boundedString(item.retention_state, `${path}.retention_state`),
|
||||||
|
linked_workdir_ids: strings(item.linked_workdir_ids, `${path}.linked_workdir_ids`),
|
||||||
|
running_linked: boolean(item.running_linked, `${path}.running_linked`),
|
||||||
|
estimated_reclaim_bytes: optionalNullableSafeInteger(
|
||||||
|
item.estimated_reclaim_bytes,
|
||||||
|
`${path}.estimated_reclaim_bytes`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function workdirCandidate(value: unknown, path: string): CleanupWorkdirCandidate {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"target_id",
|
||||||
|
"action",
|
||||||
|
"workdir_id",
|
||||||
|
"runtime_id",
|
||||||
|
"repository_key",
|
||||||
|
"reason",
|
||||||
|
"linked_worker_ids",
|
||||||
|
"linked_running_worker_ids",
|
||||||
|
"running_linked",
|
||||||
|
"pinned_linked",
|
||||||
|
"file_status",
|
||||||
|
"cleanliness",
|
||||||
|
],
|
||||||
|
["blocking_reason", "estimated_reclaim_bytes"],
|
||||||
|
path,
|
||||||
|
);
|
||||||
|
const fileStatus = boundedString(item.file_status, `${path}.file_status`);
|
||||||
|
const cleanliness = boundedString(item.cleanliness, `${path}.cleanliness`);
|
||||||
|
if (!FILE_STATUSES.has(fileStatus as CleanupWorkdirFileStatus)) {
|
||||||
|
fail(`${path}.file_status`, "has an unknown value");
|
||||||
|
}
|
||||||
|
if (!CLEANLINESS.has(cleanliness as CleanupWorkdirCleanliness)) {
|
||||||
|
fail(`${path}.cleanliness`, "has an unknown value");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
target_id: boundedString(item.target_id, `${path}.target_id`),
|
||||||
|
action: targetKind(item.action, `${path}.action`),
|
||||||
|
workdir_id: boundedString(item.workdir_id, `${path}.workdir_id`),
|
||||||
|
runtime_id: boundedString(item.runtime_id, `${path}.runtime_id`),
|
||||||
|
repository_key: boundedString(item.repository_key, `${path}.repository_key`),
|
||||||
|
reason: boundedString(item.reason, `${path}.reason`, true),
|
||||||
|
blocking_reason: optionalNullableString(item.blocking_reason, `${path}.blocking_reason`),
|
||||||
|
linked_worker_ids: strings(item.linked_worker_ids, `${path}.linked_worker_ids`),
|
||||||
|
linked_running_worker_ids: strings(
|
||||||
|
item.linked_running_worker_ids,
|
||||||
|
`${path}.linked_running_worker_ids`,
|
||||||
|
),
|
||||||
|
running_linked: boolean(item.running_linked, `${path}.running_linked`),
|
||||||
|
pinned_linked: boolean(item.pinned_linked, `${path}.pinned_linked`),
|
||||||
|
file_status: fileStatus as CleanupWorkdirFileStatus,
|
||||||
|
cleanliness: cleanliness as CleanupWorkdirCleanliness,
|
||||||
|
estimated_reclaim_bytes: optionalNullableSafeInteger(
|
||||||
|
item.estimated_reclaim_bytes,
|
||||||
|
`${path}.estimated_reclaim_bytes`,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseWorkerRetentionResponse(value: unknown): WorkerRetentionResponse {
|
||||||
|
const item = object(value, "Worker retention response");
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
["workspace_id", "runtime_id", "worker_id", "pinned", "retention_state"],
|
||||||
|
[],
|
||||||
|
"Worker retention response",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: boundedString(item.workspace_id, "Worker retention response.workspace_id"),
|
||||||
|
runtime_id: boundedString(item.runtime_id, "Worker retention response.runtime_id"),
|
||||||
|
worker_id: boundedString(item.worker_id, "Worker retention response.worker_id"),
|
||||||
|
pinned: boolean(item.pinned, "Worker retention response.pinned"),
|
||||||
|
retention_state: boundedString(
|
||||||
|
item.retention_state,
|
||||||
|
"Worker retention response.retention_state",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const WORKER_OPERATION_STATES = new Set<WorkerOperationState>([
|
||||||
|
"accepted",
|
||||||
|
"unsupported",
|
||||||
|
"rejected",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function parseRuntimeWorkerLifecycleResult(
|
||||||
|
value: unknown,
|
||||||
|
): RuntimeWorkerLifecycleResult {
|
||||||
|
const item = object(value, "Runtime Worker lifecycle result");
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
["state", "runtime_id", "worker_id", "diagnostics"],
|
||||||
|
[],
|
||||||
|
"Runtime Worker lifecycle result",
|
||||||
|
);
|
||||||
|
const state = boundedString(item.state, "Runtime Worker lifecycle result.state");
|
||||||
|
if (!WORKER_OPERATION_STATES.has(state as WorkerOperationState)) {
|
||||||
|
fail("Runtime Worker lifecycle result.state", "has an unknown value");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
state: state as WorkerOperationState,
|
||||||
|
runtime_id: boundedString(
|
||||||
|
item.runtime_id,
|
||||||
|
"Runtime Worker lifecycle result.runtime_id",
|
||||||
|
),
|
||||||
|
worker_id: boundedString(
|
||||||
|
item.worker_id,
|
||||||
|
"Runtime Worker lifecycle result.worker_id",
|
||||||
|
),
|
||||||
|
diagnostics: diagnostics(
|
||||||
|
item.diagnostics,
|
||||||
|
"Runtime Worker lifecycle result.diagnostics",
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRuntimeCleanupPlan(value: unknown): RuntimeCleanupPlanResponse {
|
||||||
|
const item = object(value, "Runtime cleanup plan");
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
[
|
||||||
|
"workspace_id",
|
||||||
|
"runtime_id",
|
||||||
|
"generated_at",
|
||||||
|
"revision",
|
||||||
|
"digest",
|
||||||
|
"workers",
|
||||||
|
"workdirs",
|
||||||
|
"diagnostics",
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
"Runtime cleanup plan",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: boundedString(item.workspace_id, "Runtime cleanup plan.workspace_id"),
|
||||||
|
runtime_id: boundedString(item.runtime_id, "Runtime cleanup plan.runtime_id"),
|
||||||
|
generated_at: boundedString(item.generated_at, "Runtime cleanup plan.generated_at"),
|
||||||
|
revision: boundedString(item.revision, "Runtime cleanup plan.revision"),
|
||||||
|
digest: boundedString(item.digest, "Runtime cleanup plan.digest"),
|
||||||
|
workers: array(item.workers, "Runtime cleanup plan.workers", MAX_CANDIDATES).map(
|
||||||
|
(entry, index) => workerCandidate(entry, `Runtime cleanup plan.workers[${index}]`),
|
||||||
|
),
|
||||||
|
workdirs: array(item.workdirs, "Runtime cleanup plan.workdirs", MAX_CANDIDATES).map(
|
||||||
|
(entry, index) => workdirCandidate(entry, `Runtime cleanup plan.workdirs[${index}]`),
|
||||||
|
),
|
||||||
|
diagnostics: diagnostics(item.diagnostics, "Runtime cleanup plan.diagnostics"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function executionResult(value: unknown, path: string): RuntimeCleanupExecutionResult {
|
||||||
|
const item = object(value, path);
|
||||||
|
exactKeys(item, ["target_id", "action", "status", "message"], [], path);
|
||||||
|
return {
|
||||||
|
target_id: boundedString(item.target_id, `${path}.target_id`),
|
||||||
|
action: targetKind(item.action, `${path}.action`),
|
||||||
|
status: boundedString(item.status, `${path}.status`),
|
||||||
|
message: boundedString(item.message, `${path}.message`, true),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRuntimeCleanupExecution(
|
||||||
|
value: unknown,
|
||||||
|
): RuntimeCleanupExecutionResponse {
|
||||||
|
const item = object(value, "Runtime cleanup execution");
|
||||||
|
exactKeys(
|
||||||
|
item,
|
||||||
|
["workspace_id", "runtime_id", "executed_at", "results", "plan_after", "diagnostics"],
|
||||||
|
[],
|
||||||
|
"Runtime cleanup execution",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
workspace_id: boundedString(item.workspace_id, "Runtime cleanup execution.workspace_id"),
|
||||||
|
runtime_id: boundedString(item.runtime_id, "Runtime cleanup execution.runtime_id"),
|
||||||
|
executed_at: boundedString(item.executed_at, "Runtime cleanup execution.executed_at"),
|
||||||
|
results: array(item.results, "Runtime cleanup execution.results", MAX_RESULTS).map(
|
||||||
|
(entry, index) => executionResult(entry, `Runtime cleanup execution.results[${index}]`),
|
||||||
|
),
|
||||||
|
plan_after: parseRuntimeCleanupPlan(item.plan_after),
|
||||||
|
diagnostics: diagnostics(item.diagnostics, "Runtime cleanup execution.diagnostics"),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { AttachmentUploadGrantResponse } from "$lib/generated/runtime-api.ts";
|
||||||
import type { UploadedFileRef } from "$lib/generated/protocol.ts";
|
import type { UploadedFileRef } from "$lib/generated/protocol.ts";
|
||||||
|
|
||||||
export const MAX_UPLOADED_FILE_BYTES = 10 * 1024 * 1024;
|
export const MAX_UPLOADED_FILE_BYTES = 10 * 1024 * 1024;
|
||||||
@@ -133,10 +134,19 @@ export function uploadAttachment(
|
|||||||
|
|
||||||
function isUploadGrantResponse(
|
function isUploadGrantResponse(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
): value is { upload_id: string; expires_at_ms: number } {
|
): value is AttachmentUploadGrantResponse {
|
||||||
return !!value && typeof value === "object" &&
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
||||||
"upload_id" in value && typeof value.upload_id === "string" &&
|
const keys = Object.keys(value);
|
||||||
"expires_at_ms" in value && typeof value.expires_at_ms === "number";
|
if (
|
||||||
|
keys.length !== 2 || !keys.includes("upload_id") ||
|
||||||
|
!keys.includes("expires_at_ms")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const grant = value as Record<string, unknown>;
|
||||||
|
return typeof grant.upload_id === "string" && grant.upload_id.length > 0 &&
|
||||||
|
grant.upload_id.length <= 256 && Number.isSafeInteger(grant.expires_at_ms) &&
|
||||||
|
(grant.expires_at_ms as number) >= 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUploadedFileResponse(
|
function isUploadedFileResponse(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { WorkspaceRuntimeResource } from "$lib/generated/legacy-server-api";
|
import type { WorkspaceRuntimeResource } from "$lib/generated/runtime-api";
|
||||||
|
|
||||||
export type RepositorySshProbeSelection<T> = Readonly<{
|
export type RepositorySshProbeSelection<T> = Readonly<{
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import type {
|
||||||
|
CleanupTargetKind,
|
||||||
|
CleanupWorkdirCandidate,
|
||||||
|
CleanupWorkerCandidate,
|
||||||
|
RuntimeCleanupExecutionResponse,
|
||||||
|
RuntimeCleanupPlanResponse,
|
||||||
|
} from "$lib/generated/runtime-api";
|
||||||
import type {
|
import type {
|
||||||
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
|
BrowserCreateWorkerResponse as SharedBrowserCreateWorkerResponse,
|
||||||
BrowserWorkerWorkingDirectorySelection
|
BrowserWorkerWorkingDirectorySelection
|
||||||
@@ -123,67 +130,12 @@ export type WorkerLaunchProfileCandidate = SharedWorkerLaunchProfileCandidate;
|
|||||||
export type WorkingDirectoryRepositoryOption =
|
export type WorkingDirectoryRepositoryOption =
|
||||||
SharedWorkingDirectoryRepositoryOption;
|
SharedWorkingDirectoryRepositoryOption;
|
||||||
|
|
||||||
export type CleanupTargetKind =
|
export type {
|
||||||
| "worker_delete"
|
CleanupTargetKind,
|
||||||
| "workdir_clean_cleanup"
|
CleanupWorkdirCandidate,
|
||||||
| "workdir_dirty_discard"
|
CleanupWorkerCandidate,
|
||||||
| "workdir_record_delete";
|
RuntimeCleanupExecutionResponse,
|
||||||
|
RuntimeCleanupPlanResponse,
|
||||||
export type CleanupWorkerCandidate = {
|
|
||||||
target_id: string;
|
|
||||||
action: CleanupTargetKind;
|
|
||||||
worker_id: string;
|
|
||||||
runtime_worker_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
reason: string;
|
|
||||||
blocking_reason?: string | null;
|
|
||||||
pinned: boolean;
|
|
||||||
retention_state: string;
|
|
||||||
linked_workdir_ids: string[];
|
|
||||||
running_linked: boolean;
|
|
||||||
estimated_reclaim_bytes?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CleanupWorkdirCandidate = {
|
|
||||||
target_id: string;
|
|
||||||
action: CleanupTargetKind;
|
|
||||||
workdir_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
repository_key: string;
|
|
||||||
reason: string;
|
|
||||||
blocking_reason?: string | null;
|
|
||||||
linked_worker_ids: string[];
|
|
||||||
linked_running_worker_ids: string[];
|
|
||||||
running_linked: boolean;
|
|
||||||
pinned_linked: boolean;
|
|
||||||
file_status: string;
|
|
||||||
cleanliness: string;
|
|
||||||
estimated_reclaim_bytes?: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeCleanupPlanResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
generated_at: string;
|
|
||||||
revision: string;
|
|
||||||
digest: string;
|
|
||||||
workers: CleanupWorkerCandidate[];
|
|
||||||
workdirs: CleanupWorkdirCandidate[];
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RuntimeCleanupExecutionResponse = {
|
|
||||||
workspace_id: string;
|
|
||||||
runtime_id: string;
|
|
||||||
executed_at: string;
|
|
||||||
results: {
|
|
||||||
target_id: string;
|
|
||||||
action: CleanupTargetKind;
|
|
||||||
status: string;
|
|
||||||
message: string;
|
|
||||||
}[];
|
|
||||||
plan_after: RuntimeCleanupPlanResponse;
|
|
||||||
diagnostics: Diagnostic[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrowserWorkerWorkingDirectorySelection =
|
export type BrowserWorkerWorkingDirectorySelection =
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
import { workspaceApiPath } from "$lib/workspace/api/http";
|
import { workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
|
import {
|
||||||
|
parseRuntimeCleanupExecution,
|
||||||
|
parseRuntimeCleanupPlan,
|
||||||
|
parseRuntimeWorkerLifecycleResult,
|
||||||
|
} from "$lib/workspace/api/runtime-workers";
|
||||||
import type {
|
import type {
|
||||||
Diagnostic,
|
Diagnostic,
|
||||||
RuntimeCleanupExecutionResponse,
|
|
||||||
RuntimeCleanupPlanResponse,
|
|
||||||
Worker,
|
Worker,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
type FetchFn = typeof fetch;
|
type FetchFn = typeof fetch;
|
||||||
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
|
type WorkerActionTarget = Pick<Worker, "runtime_id" | "worker_id" | "state">;
|
||||||
|
|
||||||
type WorkerLifecycleResponse = {
|
|
||||||
state: string;
|
|
||||||
diagnostics?: Diagnostic[];
|
|
||||||
};
|
|
||||||
|
|
||||||
function workerPath(workspaceId: string, worker: WorkerActionTarget): string {
|
function workerPath(workspaceId: string, worker: WorkerActionTarget): string {
|
||||||
return workspaceApiPath(
|
return workspaceApiPath(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
@@ -62,7 +60,7 @@ export async function stopSidebarWorker(
|
|||||||
});
|
});
|
||||||
if (!response.ok) throw new Error(await responseError(response));
|
if (!response.ok) throw new Error(await responseError(response));
|
||||||
|
|
||||||
const result = await response.json() as WorkerLifecycleResponse;
|
const result = parseRuntimeWorkerLifecycleResult(await response.json());
|
||||||
if (result.state !== "accepted") {
|
if (result.state !== "accepted") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
diagnosticMessage(result.diagnostics, `Worker stop was ${result.state}`),
|
diagnosticMessage(result.diagnostics, `Worker stop was ${result.state}`),
|
||||||
@@ -81,7 +79,7 @@ export async function deleteSidebarWorker(
|
|||||||
);
|
);
|
||||||
if (!planResponse.ok) throw new Error(await responseError(planResponse));
|
if (!planResponse.ok) throw new Error(await responseError(planResponse));
|
||||||
|
|
||||||
const plan = await planResponse.json() as RuntimeCleanupPlanResponse;
|
const plan = parseRuntimeCleanupPlan(await planResponse.json());
|
||||||
const candidate = plan.workers.find((item) =>
|
const candidate = plan.workers.find((item) =>
|
||||||
item.runtime_id === worker.runtime_id &&
|
item.runtime_id === worker.runtime_id &&
|
||||||
item.runtime_worker_id === worker.worker_id
|
item.runtime_worker_id === worker.worker_id
|
||||||
@@ -107,8 +105,7 @@ export async function deleteSidebarWorker(
|
|||||||
throw new Error(await responseError(executionResponse));
|
throw new Error(await responseError(executionResponse));
|
||||||
}
|
}
|
||||||
|
|
||||||
const execution = await executionResponse
|
const execution = parseRuntimeCleanupExecution(await executionResponse.json());
|
||||||
.json() as RuntimeCleanupExecutionResponse;
|
|
||||||
const outcome = execution.results.find((result) =>
|
const outcome = execution.results.find((result) =>
|
||||||
result.target_id === candidate.target_id
|
result.target_id === candidate.target_id
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
RuntimeConnectionTestResponse,
|
RuntimeConnectionTestResponse,
|
||||||
RuntimePublicIdentityBundle,
|
RuntimePublicIdentityBundle,
|
||||||
WorkspaceRuntimeResource,
|
WorkspaceRuntimeResource,
|
||||||
} from '$lib/generated/legacy-server-api';
|
} from '$lib/generated/runtime-api';
|
||||||
import {
|
import {
|
||||||
createRemoteRuntime,
|
createRemoteRuntime,
|
||||||
previewRuntimePublicKeyFingerprint,
|
previewRuntimePublicKeyFingerprint,
|
||||||
@@ -397,12 +397,12 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{@const currentResult = currentTestResult(runtime)}
|
{@const currentResult = currentTestResult(runtime)}
|
||||||
{#if runtime.diagnostics.length > 0 || currentResult}
|
{#if (runtime.diagnostics ?? []).length > 0 || currentResult}
|
||||||
<tr class="settings-runtime-detail-row">
|
<tr class="settings-runtime-detail-row">
|
||||||
<td colspan="7">
|
<td colspan="7">
|
||||||
{#if runtime.diagnostics.length > 0}
|
{#if (runtime.diagnostics ?? []).length > 0}
|
||||||
<ul class="settings-diagnostics-list">
|
<ul class="settings-diagnostics-list">
|
||||||
{#each runtime.diagnostics as diagnostic}
|
{#each runtime.diagnostics ?? [] as diagnostic}
|
||||||
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
|
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
|
||||||
<strong>{diagnostic.code}</strong>
|
<strong>{diagnostic.code}</strong>
|
||||||
<span>{diagnostic.message}</span>
|
<span>{diagnostic.message}</span>
|
||||||
@@ -413,8 +413,8 @@
|
|||||||
{#if currentResult}
|
{#if currentResult}
|
||||||
<div class:failed={currentResult.status === 'failed'} class="settings-test-result">
|
<div class:failed={currentResult.status === 'failed'} class="settings-test-result">
|
||||||
<strong>Connection test: {connectionTestSummary(currentResult)}</strong>
|
<strong>Connection test: {connectionTestSummary(currentResult)}</strong>
|
||||||
{#if currentResult.diagnostics[0]}
|
{#if (currentResult.diagnostics ?? [])[0]}
|
||||||
<span>{currentResult.diagnostics[0].message}</span>
|
<span>{(currentResult.diagnostics ?? [])[0].message}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<small>Checked {new Date(currentResult.checked_at).toLocaleString()}</small>
|
<small>Checked {new Date(currentResult.checked_at).toLocaleString()}</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type {
|
import type {
|
||||||
RevokeRuntimeTrustKeyRequest,
|
RevokeRuntimeTrustKeyRequest,
|
||||||
RuntimeTrustKeyStatus,
|
RuntimeTrustKeyStatus,
|
||||||
} from '$lib/generated/legacy-server-api';
|
} from '$lib/generated/runtime-api';
|
||||||
import {
|
import {
|
||||||
createRemoteRuntime,
|
createRemoteRuntime,
|
||||||
removeRemoteRuntime,
|
removeRemoteRuntime,
|
||||||
@@ -414,9 +414,9 @@
|
|||||||
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
|
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
|
||||||
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
|
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{#if runtime.diagnostics.length > 0}
|
{#if (runtime.diagnostics ?? []).length > 0}
|
||||||
<ul class="settings-diagnostics-list">
|
<ul class="settings-diagnostics-list">
|
||||||
{#each runtime.diagnostics as diagnostic}
|
{#each runtime.diagnostics ?? [] as diagnostic}
|
||||||
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
|
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
|
||||||
<strong>{diagnostic.code}</strong>
|
<strong>{diagnostic.code}</strong>
|
||||||
<span>{diagnostic.message}</span>
|
<span>{diagnostic.message}</span>
|
||||||
|
|||||||
+5
-8
@@ -1,10 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import { parseRuntimeCleanupExecution } from '$lib/workspace/api/runtime-workers';
|
||||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import type {
|
import type {
|
||||||
CleanupWorkdirCandidate,
|
CleanupWorkdirCandidate,
|
||||||
RuntimeCleanupExecutionResponse,
|
|
||||||
RuntimeCleanupPlanResponse,
|
RuntimeCleanupPlanResponse,
|
||||||
WorkingDirectorySummary,
|
WorkingDirectorySummary,
|
||||||
} from '$lib/workspace/sidebar/types';
|
} from '$lib/workspace/sidebar/types';
|
||||||
@@ -83,14 +83,11 @@
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
|
const payload = await response.json().catch(() => null);
|
||||||
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
||||||
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
|
const execution = parseRuntimeCleanupExecution(payload);
|
||||||
cleanupPlan = (payload as RuntimeCleanupExecutionResponse).plan_after;
|
cleanupPlan = execution.plan_after;
|
||||||
}
|
const result = execution.results.find((entry) => entry.target_id === candidate.target_id);
|
||||||
const result = payload && typeof payload === 'object' && 'results' in payload
|
|
||||||
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
|
|
||||||
: undefined;
|
|
||||||
if (!result || result.status !== 'deleted') {
|
if (!result || result.status !== 'deleted') {
|
||||||
throw new Error(result?.message ?? 'Runtime did not delete the selected Workdir');
|
throw new Error(result?.message ?? 'Runtime did not delete the selected Workdir');
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -1,18 +1,17 @@
|
|||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
|
import { parseWorkspaceRuntimeList } from "$lib/workspace/api/runtime-management";
|
||||||
|
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
|
||||||
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
|
import { parseWorkingDirectoryListResponse } from "$lib/workspace/api/workdirs";
|
||||||
import type {
|
|
||||||
ListResponse,
|
|
||||||
Runtime,
|
|
||||||
RuntimeCleanupPlanResponse,
|
|
||||||
} from "$lib/workspace/sidebar/types";
|
|
||||||
import type { PageLoad } from "./$types";
|
import type { PageLoad } from "./$types";
|
||||||
|
|
||||||
export const load: PageLoad = async ({ fetch, params }) => {
|
export const load: PageLoad = async ({ fetch, params }) => {
|
||||||
const runtimeId = params.runtimeId;
|
const runtimeId = params.runtimeId;
|
||||||
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
|
const [runtimes, workdirs, cleanupPlan] = await Promise.all([
|
||||||
loadJson<ListResponse<Runtime>>(
|
loadJson(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(params.workspaceId, "/runtimes"),
|
workspaceApiPath(params.workspaceId, "/runtimes"),
|
||||||
|
undefined,
|
||||||
|
parseWorkspaceRuntimeList,
|
||||||
),
|
),
|
||||||
loadJson(
|
loadJson(
|
||||||
fetch,
|
fetch,
|
||||||
@@ -23,12 +22,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||||||
undefined,
|
undefined,
|
||||||
parseWorkingDirectoryListResponse,
|
parseWorkingDirectoryListResponse,
|
||||||
),
|
),
|
||||||
loadJson<RuntimeCleanupPlanResponse>(
|
loadJson(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(
|
workspaceApiPath(
|
||||||
params.workspaceId,
|
params.workspaceId,
|
||||||
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
|
parseRuntimeCleanupPlan,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
import { pushWorkspaceAlert } from '$lib/workspace/alerts/store';
|
||||||
import { workspaceApiPath } from '$lib/workspace/api/http';
|
import { workspaceApiPath } from '$lib/workspace/api/http';
|
||||||
|
import {
|
||||||
|
parseRuntimeCleanupExecution,
|
||||||
|
parseRuntimeCleanupPlan,
|
||||||
|
parseWorkerRetentionResponse,
|
||||||
|
} from '$lib/workspace/api/runtime-workers';
|
||||||
import { workerHref } from '$lib/workspace/resource-links';
|
import { workerHref } from '$lib/workspace/resource-links';
|
||||||
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
import { formatCurrentWorkdirRevision } from '$lib/workspace/settings/workdir-revision';
|
||||||
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
|
import { canOpenWorkerConsole } from '$lib/workspace/sidebar/workers';
|
||||||
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
|
import { liveWorkerState } from '$lib/workspace/sidebar/worker-state';
|
||||||
import type { CleanupWorkerCandidate, RuntimeCleanupExecutionResponse, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
|
import type { CleanupWorkerCandidate, RuntimeCleanupPlanResponse, Worker } from '$lib/workspace/sidebar/types';
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from './$types';
|
||||||
|
|
||||||
type WorkerActionKind = 'pin' | 'delete';
|
type WorkerActionKind = 'pin' | 'delete';
|
||||||
@@ -55,7 +60,7 @@
|
|||||||
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
||||||
);
|
);
|
||||||
if (!response.ok) return;
|
if (!response.ok) return;
|
||||||
const plan = (await response.json()) as RuntimeCleanupPlanResponse;
|
const plan = parseRuntimeCleanupPlan(await response.json());
|
||||||
cleanupPlans = { ...cleanupPlans, [runtimeId]: plan };
|
cleanupPlans = { ...cleanupPlans, [runtimeId]: plan };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +80,9 @@
|
|||||||
pushWorkspaceAlert('error', errorMessage(payload, response.statusText), { title: 'Worker pin failed' });
|
pushWorkspaceAlert('error', errorMessage(payload, response.statusText), { title: 'Worker pin failed' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
worker.pinned = Boolean(payload?.pinned);
|
const retention = parseWorkerRetentionResponse(payload);
|
||||||
worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal');
|
worker.pinned = retention.pinned;
|
||||||
|
worker.retention_state = retention.retention_state;
|
||||||
await refreshCleanupPlan(worker.runtime_id);
|
await refreshCleanupPlan(worker.runtime_id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Worker pin failed', {
|
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Worker pin failed', {
|
||||||
@@ -112,15 +118,11 @@
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
|
const payload = await response.json().catch(() => null);
|
||||||
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
||||||
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
|
const execution = parseRuntimeCleanupExecution(payload);
|
||||||
const execution = payload as RuntimeCleanupExecutionResponse;
|
|
||||||
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: execution.plan_after };
|
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: execution.plan_after };
|
||||||
}
|
const result = execution.results.find((entry) => entry.target_id === candidate.target_id);
|
||||||
const result = payload && typeof payload === 'object' && 'results' in payload
|
|
||||||
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
|
|
||||||
: undefined;
|
|
||||||
if (!result || result.status !== 'deleted') {
|
if (!result || result.status !== 'deleted') {
|
||||||
throw new Error(result?.message ?? 'Runtime did not delete the selected Worker');
|
throw new Error(result?.message ?? 'Runtime did not delete the selected Worker');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
|
||||||
|
import { parseRuntimeCleanupPlan } from "$lib/workspace/api/runtime-workers";
|
||||||
import type {
|
import type {
|
||||||
ListResponse,
|
ListResponse,
|
||||||
RuntimeCleanupPlanResponse,
|
RuntimeCleanupPlanResponse,
|
||||||
@@ -16,12 +17,14 @@ export const load: PageLoad = async ({ fetch, params }) => {
|
|||||||
);
|
);
|
||||||
const cleanupPlanEntries = await Promise.all(
|
const cleanupPlanEntries = await Promise.all(
|
||||||
runtimeIds.map(async (runtimeId) => {
|
runtimeIds.map(async (runtimeId) => {
|
||||||
const cleanupPlan = await loadJson<RuntimeCleanupPlanResponse>(
|
const cleanupPlan = await loadJson(
|
||||||
fetch,
|
fetch,
|
||||||
workspaceApiPath(
|
workspaceApiPath(
|
||||||
params.workspaceId,
|
params.workspaceId,
|
||||||
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
`/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`,
|
||||||
),
|
),
|
||||||
|
undefined,
|
||||||
|
parseRuntimeCleanupPlan,
|
||||||
);
|
);
|
||||||
return [runtimeId, cleanupPlan] as const;
|
return [runtimeId, cleanupPlan] as const;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { assert, assertEquals } from "jsr:@std/assert";
|
import { assert, assertEquals } from "jsr:@std/assert";
|
||||||
import type { WorkspaceRuntimeResource } from "../src/lib/generated/legacy-server-api.ts";
|
import type { WorkspaceRuntimeResource } from "../src/lib/generated/runtime-api.ts";
|
||||||
import { parseRepositorySshConnectionProbeResponse } from "../src/lib/workspace/api/workspace-model.ts";
|
import { parseRepositorySshConnectionProbeResponse } from "../src/lib/workspace/api/workspace-model.ts";
|
||||||
import {
|
import {
|
||||||
changeRepositorySshProbeRuntime,
|
changeRepositorySshProbeRuntime,
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ Deno.test("sidebar Stop uses the workspace-scoped Worker lifecycle endpoint", as
|
|||||||
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
const requests: Array<{ url: string; init?: RequestInit }> = [];
|
||||||
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||||
requests.push({ url: input.toString(), init });
|
requests.push({ url: input.toString(), init });
|
||||||
return jsonResponse({ state: "accepted", diagnostics: [] });
|
return jsonResponse({
|
||||||
|
state: "accepted",
|
||||||
|
runtime_id: worker.runtime_id,
|
||||||
|
worker_id: worker.worker_id,
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
|
|
||||||
await stopSidebarWorker("team space", worker, fetchFn);
|
await stopSidebarWorker("team space", worker, fetchFn);
|
||||||
@@ -76,7 +81,13 @@ Deno.test("sidebar Stop rejects non-accepted lifecycle responses", async () => {
|
|||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
jsonResponse({
|
jsonResponse({
|
||||||
state: "rejected",
|
state: "rejected",
|
||||||
diagnostics: [{ severity: "error", message: "Worker cannot stop" }],
|
runtime_id: worker.runtime_id,
|
||||||
|
worker_id: worker.worker_id,
|
||||||
|
diagnostics: [{
|
||||||
|
code: "cannot_stop",
|
||||||
|
severity: "error",
|
||||||
|
message: "Worker cannot stop",
|
||||||
|
}],
|
||||||
}),
|
}),
|
||||||
)) as typeof fetch;
|
)) as typeof fetch;
|
||||||
|
|
||||||
@@ -92,25 +103,48 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
|
|||||||
requests.push({ url: input.toString(), init });
|
requests.push({ url: input.toString(), init });
|
||||||
if (requests.length === 1) {
|
if (requests.length === 1) {
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
revision: 7,
|
workspace_id: "team",
|
||||||
|
runtime_id: worker.runtime_id,
|
||||||
|
generated_at: "2026-01-01T00:00:00Z",
|
||||||
|
revision: "7",
|
||||||
digest: "digest-7",
|
digest: "digest-7",
|
||||||
candidates: [],
|
|
||||||
workers: [{
|
workers: [{
|
||||||
target_id: "worker-target",
|
target_id: "worker-target",
|
||||||
|
action: "worker_delete",
|
||||||
|
worker_id: worker.worker_id,
|
||||||
runtime_id: worker.runtime_id,
|
runtime_id: worker.runtime_id,
|
||||||
runtime_worker_id: worker.worker_id,
|
runtime_worker_id: worker.worker_id,
|
||||||
|
reason: "stopped worker",
|
||||||
blocking_reason: null,
|
blocking_reason: null,
|
||||||
|
pinned: false,
|
||||||
|
retention_state: "stopped",
|
||||||
|
linked_workdir_ids: [],
|
||||||
|
running_linked: false,
|
||||||
}],
|
}],
|
||||||
workdirs: [],
|
workdirs: [],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
|
workspace_id: "team",
|
||||||
|
runtime_id: worker.runtime_id,
|
||||||
|
executed_at: "2026-01-01T00:00:01Z",
|
||||||
results: [{
|
results: [{
|
||||||
target_id: "worker-target",
|
target_id: "worker-target",
|
||||||
|
action: "worker_delete",
|
||||||
status: "deleted",
|
status: "deleted",
|
||||||
message: null,
|
message: "",
|
||||||
}],
|
}],
|
||||||
|
plan_after: {
|
||||||
|
workspace_id: "team",
|
||||||
|
runtime_id: worker.runtime_id,
|
||||||
|
generated_at: "2026-01-01T00:00:01Z",
|
||||||
|
revision: "8",
|
||||||
|
digest: "digest-8",
|
||||||
|
workers: [],
|
||||||
|
workdirs: [],
|
||||||
|
diagnostics: [],
|
||||||
|
},
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
});
|
});
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
@@ -123,7 +157,7 @@ Deno.test("sidebar Delete executes the authoritative runtime cleanup plan", asyn
|
|||||||
]);
|
]);
|
||||||
assertEquals(requests[1]?.init?.method, "POST");
|
assertEquals(requests[1]?.init?.method, "POST");
|
||||||
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
|
assertEquals(JSON.parse(String(requests[1]?.init?.body)), {
|
||||||
expected_plan_revision: 7,
|
expected_plan_revision: "7",
|
||||||
expected_plan_digest: "digest-7",
|
expected_plan_digest: "digest-7",
|
||||||
worker_target_ids: ["worker-target"],
|
worker_target_ids: ["worker-target"],
|
||||||
workdir_target_ids: [],
|
workdir_target_ids: [],
|
||||||
@@ -135,14 +169,23 @@ Deno.test("sidebar Delete reports cleanup-plan blocking reasons", async () => {
|
|||||||
const fetchFn = (() =>
|
const fetchFn = (() =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
jsonResponse({
|
jsonResponse({
|
||||||
revision: 8,
|
workspace_id: "team",
|
||||||
|
runtime_id: worker.runtime_id,
|
||||||
|
generated_at: "2026-01-01T00:00:00Z",
|
||||||
|
revision: "8",
|
||||||
digest: "digest-8",
|
digest: "digest-8",
|
||||||
candidates: [],
|
|
||||||
workers: [{
|
workers: [{
|
||||||
target_id: "worker-target",
|
target_id: "worker-target",
|
||||||
|
action: "worker_delete",
|
||||||
|
worker_id: worker.worker_id,
|
||||||
runtime_id: worker.runtime_id,
|
runtime_id: worker.runtime_id,
|
||||||
runtime_worker_id: worker.worker_id,
|
runtime_worker_id: worker.worker_id,
|
||||||
|
reason: "stopped worker",
|
||||||
blocking_reason: "Worker is pinned",
|
blocking_reason: "Worker is pinned",
|
||||||
|
pinned: true,
|
||||||
|
retention_state: "pinned",
|
||||||
|
linked_workdir_ids: [],
|
||||||
|
running_linked: false,
|
||||||
}],
|
}],
|
||||||
workdirs: [],
|
workdirs: [],
|
||||||
diagnostics: [],
|
diagnostics: [],
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
declare const Deno: {
|
||||||
|
test(name: string, fn: () => void | Promise<void>): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseRuntimeCleanupExecution,
|
||||||
|
parseRuntimeCleanupPlan,
|
||||||
|
parseRuntimeWorkerLifecycleResult,
|
||||||
|
parseWorkerRetentionResponse,
|
||||||
|
} from "../src/lib/workspace/api/runtime-workers.ts";
|
||||||
|
|
||||||
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertThrows(operation: () => unknown, expected: string): void {
|
||||||
|
try {
|
||||||
|
operation();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (message.includes(expected)) return;
|
||||||
|
throw new Error(`expected error containing ${expected}, received ${message}`);
|
||||||
|
}
|
||||||
|
throw new Error("expected operation to throw");
|
||||||
|
}
|
||||||
|
|
||||||
|
function plan() {
|
||||||
|
return {
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
generated_at: "2026-09-01T00:00:00Z",
|
||||||
|
revision: "revision-a",
|
||||||
|
digest: "digest-a",
|
||||||
|
workers: [{
|
||||||
|
target_id: "worker-target",
|
||||||
|
action: "worker_delete",
|
||||||
|
worker_id: "W-1",
|
||||||
|
runtime_worker_id: "worker-1",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
reason: "stopped",
|
||||||
|
blocking_reason: null,
|
||||||
|
pinned: false,
|
||||||
|
retention_state: "active",
|
||||||
|
linked_workdir_ids: [],
|
||||||
|
running_linked: false,
|
||||||
|
estimated_reclaim_bytes: 10,
|
||||||
|
}],
|
||||||
|
workdirs: [{
|
||||||
|
target_id: "workdir-target",
|
||||||
|
action: "workdir_clean_cleanup",
|
||||||
|
workdir_id: "workdir-1",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
repository_key: "main",
|
||||||
|
reason: "unused",
|
||||||
|
blocking_reason: null,
|
||||||
|
linked_worker_ids: [],
|
||||||
|
linked_running_worker_ids: [],
|
||||||
|
running_linked: false,
|
||||||
|
pinned_linked: false,
|
||||||
|
file_status: "active",
|
||||||
|
cleanliness: "clean",
|
||||||
|
estimated_reclaim_bytes: null,
|
||||||
|
}],
|
||||||
|
diagnostics: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Deno.test("Runtime cleanup parsers accept the generated exact response shapes", () => {
|
||||||
|
const parsedPlan = parseRuntimeCleanupPlan(plan());
|
||||||
|
assert(parsedPlan.workers[0]?.runtime_worker_id === "worker-1", "worker candidate missing");
|
||||||
|
|
||||||
|
const execution = parseRuntimeCleanupExecution({
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
executed_at: "2026-09-01T00:01:00Z",
|
||||||
|
results: [{
|
||||||
|
target_id: "worker-target",
|
||||||
|
action: "worker_delete",
|
||||||
|
status: "deleted",
|
||||||
|
message: "deleted",
|
||||||
|
}],
|
||||||
|
plan_after: plan(),
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
assert(execution.results[0]?.status === "deleted", "execution result missing");
|
||||||
|
|
||||||
|
const lifecycle = parseRuntimeWorkerLifecycleResult({
|
||||||
|
state: "accepted",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
worker_id: "worker-1",
|
||||||
|
diagnostics: [],
|
||||||
|
});
|
||||||
|
assert(lifecycle.state === "accepted", "lifecycle state missing");
|
||||||
|
|
||||||
|
const retention = parseWorkerRetentionResponse({
|
||||||
|
workspace_id: "workspace-a",
|
||||||
|
runtime_id: "runtime-a",
|
||||||
|
worker_id: "worker-1",
|
||||||
|
pinned: true,
|
||||||
|
retention_state: "pinned",
|
||||||
|
});
|
||||||
|
assert(retention.pinned, "retention state missing");
|
||||||
|
});
|
||||||
|
|
||||||
|
Deno.test("Runtime cleanup parsers reject unknown fields and unsafe values", () => {
|
||||||
|
assertThrows(
|
||||||
|
() => parseRuntimeCleanupPlan({ ...plan(), unexpected: true }),
|
||||||
|
"not part of the wire contract",
|
||||||
|
);
|
||||||
|
assertThrows(
|
||||||
|
() => parseRuntimeCleanupPlan({ ...plan(), workers: new Array(1_001).fill(plan().workers[0]) }),
|
||||||
|
"at most 1000",
|
||||||
|
);
|
||||||
|
assertThrows(
|
||||||
|
() => parseRuntimeCleanupPlan({ ...plan(), workers: [{ ...plan().workers[0], estimated_reclaim_bytes: Number.MAX_SAFE_INTEGER + 1 }] }),
|
||||||
|
"safe integer",
|
||||||
|
);
|
||||||
|
assertThrows(
|
||||||
|
() => parseRuntimeWorkerLifecycleResult({ state: "future", runtime_id: "runtime-a", worker_id: "worker-1", diagnostics: [] }),
|
||||||
|
"unknown value",
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user