runtime: fetch profile archives for remote workers
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
use crate::execution::WorkerExecutionStatus;
|
||||
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
|
||||
use crate::interaction::WorkerInput;
|
||||
use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Profile selector boundary. This is a selector, not a resolved config bundle.
|
||||
/// Profile selector boundary. This is a selector, not a resolved runtime config.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
||||
pub enum ProfileSelector {
|
||||
@@ -19,7 +20,36 @@ impl Default for ProfileSelector {
|
||||
}
|
||||
}
|
||||
|
||||
/// Backend-synced config bundle reference used during Worker creation.
|
||||
/// Runtime fetch/caching metadata for a Backend-authored Decodal profile source archive.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ProfileSourceArchiveHttpRef {
|
||||
pub url: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub etag: Option<String>,
|
||||
pub archive: ProfileSourceArchiveRef,
|
||||
}
|
||||
|
||||
/// Profile source material available to a Runtime during Worker creation.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ProfileSourceArchiveSource {
|
||||
/// Backend-internal embedded runtimes may receive already-built archive bytes.
|
||||
Embedded { archive: ProfileSourceArchive },
|
||||
/// Standalone runtimes fetch/cache the tar archive over HTTP.
|
||||
Http {
|
||||
location: ProfileSourceArchiveHttpRef,
|
||||
},
|
||||
}
|
||||
|
||||
impl ProfileSourceArchiveSource {
|
||||
pub fn reference(&self) -> ProfileSourceArchiveRef {
|
||||
match self {
|
||||
Self::Embedded { archive } => archive.reference.clone(),
|
||||
Self::Http { location } => location.archive.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ConfigBundleRef {
|
||||
pub id: String,
|
||||
@@ -139,16 +169,17 @@ pub struct WorkingDirectoryStatus {
|
||||
///
|
||||
/// Browser/product launch semantics are resolved by a backend before this
|
||||
/// request is built. The request contains only durable Runtime identity inputs:
|
||||
/// a backend-decided profile selector, a previously synced ConfigBundle identity,
|
||||
/// optional initial user input that is committed in the same transaction as
|
||||
/// Worker catalog/transcript persistence, and an optional working directory
|
||||
/// request that preserves RepositoryPoint-style semantics for runtime-side
|
||||
/// materialization. Browser-facing status for materialized working directories is
|
||||
/// summarized without exposing raw host paths.
|
||||
/// a backend-decided profile selector, the Decodal profile source archive source
|
||||
/// used to resolve that selector, optional initial user input committed with the
|
||||
/// Worker catalog/transcript persistence, and an optional Runtime-owned working
|
||||
/// directory binding. Browser-facing status for materialized working directories
|
||||
/// is summarized without exposing raw host paths.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct CreateWorkerRequest {
|
||||
pub profile: ProfileSelector,
|
||||
pub config_bundle: ConfigBundleRef,
|
||||
pub profile_source: ProfileSourceArchiveSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_bundle: Option<ConfigBundleRef>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub initial_input: Option<WorkerInput>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -181,7 +212,9 @@ pub struct WorkerSummary {
|
||||
pub status: WorkerStatus,
|
||||
pub execution: WorkerExecutionStatus,
|
||||
pub profile: ProfileSelector,
|
||||
pub config_bundle: ConfigBundleRef,
|
||||
pub profile_source: ProfileSourceArchiveRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_bundle: Option<ConfigBundleRef>,
|
||||
pub transcript_len: usize,
|
||||
pub last_event_id: u64,
|
||||
}
|
||||
@@ -195,7 +228,9 @@ pub struct WorkerDetail {
|
||||
pub status: WorkerStatus,
|
||||
pub execution: WorkerExecutionStatus,
|
||||
pub profile: ProfileSelector,
|
||||
pub config_bundle: ConfigBundleRef,
|
||||
pub profile_source: ProfileSourceArchiveRef,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub config_bundle: Option<ConfigBundleRef>,
|
||||
pub transcript_len: usize,
|
||||
pub last_event_id: u64,
|
||||
}
|
||||
|
||||
@@ -933,10 +933,27 @@ mod tests {
|
||||
let bundle = test_bundle(profile.clone());
|
||||
CreateWorkerRequest {
|
||||
profile,
|
||||
config_bundle: ConfigBundleRef {
|
||||
profile_source: crate::catalog::ProfileSourceArchiveSource::Http {
|
||||
location: crate::catalog::ProfileSourceArchiveHttpRef {
|
||||
url: "http://127.0.0.1/profile-source.tar".to_string(),
|
||||
etag: None,
|
||||
archive: crate::profile_archive::ProfileSourceArchiveRef {
|
||||
id: "test-profile-source".to_string(),
|
||||
digest: "test-digest".to_string(),
|
||||
size_bytes: 0,
|
||||
source_graph: crate::profile_archive::ProfileSourceGraphSummary {
|
||||
source_count: 0,
|
||||
total_source_bytes: 0,
|
||||
entrypoints: std::collections::BTreeMap::new(),
|
||||
import_count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config_bundle: Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id,
|
||||
digest: bundle.metadata.digest,
|
||||
},
|
||||
}),
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
working_directory: None,
|
||||
@@ -1220,10 +1237,27 @@ mod ws_tests {
|
||||
let bundle = ws_test_bundle(ProfileSelector::RuntimeDefault);
|
||||
CreateWorkerRequest {
|
||||
profile: ProfileSelector::RuntimeDefault,
|
||||
config_bundle: ConfigBundleRef {
|
||||
profile_source: crate::catalog::ProfileSourceArchiveSource::Http {
|
||||
location: crate::catalog::ProfileSourceArchiveHttpRef {
|
||||
url: "http://127.0.0.1/profile-source.tar".to_string(),
|
||||
etag: None,
|
||||
archive: crate::profile_archive::ProfileSourceArchiveRef {
|
||||
id: "test-profile-source".to_string(),
|
||||
digest: "test-digest".to_string(),
|
||||
size_bytes: 0,
|
||||
source_graph: crate::profile_archive::ProfileSourceGraphSummary {
|
||||
source_count: 0,
|
||||
total_source_bytes: 0,
|
||||
entrypoints: std::collections::BTreeMap::new(),
|
||||
import_count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config_bundle: Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id,
|
||||
digest: bundle.metadata.digest,
|
||||
},
|
||||
}),
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
working_directory: None,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail, WorkerLifecycleAck,
|
||||
WorkerStatus, WorkerSummary, WorkingDirectoryRequest,
|
||||
ConfigBundleRef, CreateWorkerRequest, WorkerDetail, WorkerLifecycleAck, WorkerStatus,
|
||||
WorkerSummary, WorkingDirectoryRequest,
|
||||
WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
|
||||
};
|
||||
use crate::config_bundle::{
|
||||
@@ -308,7 +308,7 @@ impl Runtime {
|
||||
.map_err(|diagnostic| RuntimeError::InvalidRequest(diagnostic.to_string()))
|
||||
}
|
||||
|
||||
/// Create a Worker through the canonical ConfigBundle + execution backend path.
|
||||
/// Create a Worker through the canonical profile-source + execution backend path.
|
||||
pub fn create_worker(
|
||||
&self,
|
||||
request: CreateWorkerRequest,
|
||||
@@ -318,13 +318,6 @@ impl Runtime {
|
||||
state.ensure_running()?;
|
||||
validate_create_worker_request(&request)?;
|
||||
state.validate_worker_config_boundary(&request)?;
|
||||
let config_bundle = state
|
||||
.config_bundles
|
||||
.get(&request.config_bundle.id)
|
||||
.cloned()
|
||||
.ok_or_else(|| RuntimeError::ConfigBundleMissing {
|
||||
bundle_id: request.config_bundle.id.clone(),
|
||||
})?;
|
||||
let backend = state.execution_backend.clone().ok_or_else(|| {
|
||||
RuntimeError::ExecutionBackendUnavailable {
|
||||
message: "worker creation requires an execution backend".to_string(),
|
||||
@@ -370,7 +363,7 @@ impl Runtime {
|
||||
request,
|
||||
context: self.execution_context(worker_ref.clone()),
|
||||
working_directory: None,
|
||||
config_bundle: Some(config_bundle),
|
||||
config_bundle: None,
|
||||
};
|
||||
(backend, worker_ref, spawn_request)
|
||||
};
|
||||
@@ -1317,23 +1310,8 @@ impl RuntimeState {
|
||||
|
||||
fn validate_worker_config_boundary(
|
||||
&self,
|
||||
request: &CreateWorkerRequest,
|
||||
_request: &CreateWorkerRequest,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let reference = &request.config_bundle;
|
||||
let availability = self.check_config_bundle_ref(reference)?;
|
||||
let bundle = self
|
||||
.config_bundles
|
||||
.get(&availability.reference.id)
|
||||
.ok_or_else(|| RuntimeError::ConfigBundleMissing {
|
||||
bundle_id: availability.reference.id.clone(),
|
||||
})?;
|
||||
if !bundle.contains_profile(&request.profile) {
|
||||
return Err(RuntimeError::InvalidProfileSelector {
|
||||
profile: profile_label(&request.profile),
|
||||
bundle_id: Some(reference.id.clone()),
|
||||
message: "profile selector is not declared by synced config bundle".to_string(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1571,6 +1549,7 @@ impl WorkerRecord {
|
||||
status: self.status,
|
||||
execution: self.execution.clone(),
|
||||
profile: self.request.profile.clone(),
|
||||
profile_source: self.request.profile_source.reference(),
|
||||
config_bundle: self.request.config_bundle.clone(),
|
||||
transcript_len: self.transcript.len(),
|
||||
last_event_id: self.last_event_id,
|
||||
@@ -1585,6 +1564,7 @@ impl WorkerRecord {
|
||||
status: self.status,
|
||||
execution: self.execution.clone(),
|
||||
profile: self.request.profile.clone(),
|
||||
profile_source: self.request.profile_source.reference(),
|
||||
config_bundle: self.request.config_bundle.clone(),
|
||||
transcript_len: self.transcript.len(),
|
||||
last_event_id: self.last_event_id,
|
||||
@@ -1606,24 +1586,25 @@ impl WorkerRecord {
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_label(selector: &ProfileSelector) -> String {
|
||||
match selector {
|
||||
ProfileSelector::RuntimeDefault => "runtime_default".to_string(),
|
||||
ProfileSelector::Builtin(value) => value.clone(),
|
||||
ProfileSelector::Named(value) => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), RuntimeError> {
|
||||
if request.config_bundle.id.trim().is_empty() {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"config_bundle.id must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if request.config_bundle.digest.trim().is_empty() {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"config_bundle.digest must not be empty".to_string(),
|
||||
));
|
||||
match &request.profile_source {
|
||||
crate::catalog::ProfileSourceArchiveSource::Embedded { archive } => {
|
||||
archive.verify().map_err(|err| {
|
||||
RuntimeError::InvalidRequest(format!("profile_source archive is invalid: {err}"))
|
||||
})?;
|
||||
}
|
||||
crate::catalog::ProfileSourceArchiveSource::Http { location } => {
|
||||
if location.url.trim().is_empty() {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"profile_source.location.url must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if location.archive.digest.trim().is_empty() {
|
||||
return Err(RuntimeError::InvalidRequest(
|
||||
"profile_source.location.archive.digest must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(input) = &request.initial_input {
|
||||
if input.kind != WorkerInputKind::User {
|
||||
@@ -1669,10 +1650,27 @@ mod tests {
|
||||
let bundle = test_bundle_for_profile(profile.clone());
|
||||
CreateWorkerRequest {
|
||||
profile,
|
||||
config_bundle: ConfigBundleRef {
|
||||
profile_source: crate::catalog::ProfileSourceArchiveSource::Http {
|
||||
location: crate::catalog::ProfileSourceArchiveHttpRef {
|
||||
url: "http://127.0.0.1/profile-source.tar".to_string(),
|
||||
etag: None,
|
||||
archive: crate::profile_archive::ProfileSourceArchiveRef {
|
||||
id: "test-profile-source".to_string(),
|
||||
digest: "test-digest".to_string(),
|
||||
size_bytes: 0,
|
||||
source_graph: crate::profile_archive::ProfileSourceGraphSummary {
|
||||
source_count: 0,
|
||||
total_source_bytes: 0,
|
||||
entrypoints: BTreeMap::new(),
|
||||
import_count: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config_bundle: Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id,
|
||||
digest: bundle.metadata.digest,
|
||||
},
|
||||
}),
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
working_directory: None,
|
||||
@@ -1806,10 +1804,10 @@ mod tests {
|
||||
|
||||
fn bundled_task_request(objective: &str, bundle: &ConfigBundle) -> CreateWorkerRequest {
|
||||
let mut request = task_request(objective);
|
||||
request.config_bundle = ConfigBundleRef {
|
||||
request.config_bundle = Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id.clone(),
|
||||
digest: bundle.metadata.digest.clone(),
|
||||
};
|
||||
});
|
||||
request
|
||||
}
|
||||
|
||||
@@ -1820,7 +1818,7 @@ mod tests {
|
||||
|
||||
assert_eq!(detail.worker_ref.runtime_id, runtime.runtime_id().unwrap());
|
||||
assert_eq!(detail.status, WorkerStatus::Running);
|
||||
assert_eq!(detail.config_bundle.id, "bundle-1");
|
||||
assert_eq!(detail.config_bundle.as_ref().unwrap().id, "bundle-1");
|
||||
|
||||
let list = runtime.list_workers().unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
@@ -1852,7 +1850,7 @@ mod tests {
|
||||
let detail = runtime
|
||||
.create_worker(bundled_task_request("synced", &bundle))
|
||||
.unwrap();
|
||||
assert_eq!(detail.config_bundle, availability.reference);
|
||||
assert_eq!(detail.config_bundle, Some(availability.reference));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1860,11 +1858,6 @@ mod tests {
|
||||
let runtime = Runtime::new_memory();
|
||||
let bundle = test_bundle();
|
||||
|
||||
let missing = runtime
|
||||
.create_worker(bundled_task_request("missing", &bundle))
|
||||
.unwrap_err();
|
||||
assert!(matches!(missing, RuntimeError::ConfigBundleMissing { .. }));
|
||||
|
||||
runtime.store_config_bundle(bundle.clone()).unwrap();
|
||||
let mismatch = runtime
|
||||
.check_config_bundle(&ConfigBundleRef {
|
||||
@@ -1877,14 +1870,6 @@ mod tests {
|
||||
RuntimeError::ConfigBundleDigestMismatch { .. }
|
||||
));
|
||||
|
||||
let mut bad_profile = bundled_task_request("bad profile", &bundle);
|
||||
bad_profile.profile = ProfileSelector::Builtin("builtin:reviewer".to_string());
|
||||
let invalid_profile = runtime.create_worker(bad_profile).unwrap_err();
|
||||
assert!(matches!(
|
||||
invalid_profile,
|
||||
RuntimeError::InvalidProfileSelector { .. }
|
||||
));
|
||||
|
||||
let mut unsupported = test_bundle();
|
||||
unsupported.declarations.push(ConfigDeclaration {
|
||||
kind: ConfigDeclarationKind::Unsupported,
|
||||
@@ -1947,12 +1932,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_worker_missing_config_bundle_is_rejected_before_backend() {
|
||||
fn create_worker_without_execution_backend_is_rejected_before_persisting_worker() {
|
||||
let runtime = Runtime::new_memory();
|
||||
let error = runtime
|
||||
.create_worker(task_request("missing bundle"))
|
||||
.create_worker(task_request("missing backend"))
|
||||
.unwrap_err();
|
||||
assert!(matches!(error, RuntimeError::ConfigBundleMissing { .. }));
|
||||
assert!(matches!(
|
||||
error,
|
||||
RuntimeError::ExecutionBackendUnavailable { .. }
|
||||
));
|
||||
assert!(runtime.list_workers().unwrap().is_empty());
|
||||
}
|
||||
|
||||
|
||||
@@ -14,17 +14,16 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::catalog::{WorkingDirectoryRequest, WorkingDirectoryStatus};
|
||||
use crate::config_bundle::verified_profile_source_archive;
|
||||
use crate::catalog::{
|
||||
ProfileSourceArchiveHttpRef, ProfileSourceArchiveSource, WorkingDirectoryRequest,
|
||||
WorkingDirectoryStatus,
|
||||
};
|
||||
use crate::execution::{
|
||||
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
|
||||
WorkerExecutionRunState, WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult,
|
||||
};
|
||||
use crate::interaction::{WorkerInput, WorkerInputKind};
|
||||
use crate::resource::{
|
||||
BackendResourceClient, BackendResourceError, ProfileSourceArchiveCache,
|
||||
build_profile_source_archive_fetch_request, profile_source_archive_from_response,
|
||||
};
|
||||
use crate::resource::{BackendResourceClient, ProfileSourceArchiveCache};
|
||||
use crate::working_directory::{
|
||||
WorkingDirectoryBinding, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
|
||||
};
|
||||
@@ -170,54 +169,103 @@ impl ProfileRuntimeWorkerFactory {
|
||||
}
|
||||
async fn resolve_profile_source_archive(
|
||||
&self,
|
||||
bundle: &crate::config_bundle::ConfigBundle,
|
||||
request: &WorkerExecutionSpawnRequest,
|
||||
source: &ProfileSourceArchiveSource,
|
||||
) -> Result<crate::profile_archive::VerifiedProfileSourceArchive, String> {
|
||||
if let Some(archive) = verified_profile_source_archive(bundle)
|
||||
.map_err(|err| format!("failed to verify profile source archive: {err}"))?
|
||||
{
|
||||
return Ok(archive);
|
||||
}
|
||||
let handle = bundle
|
||||
.profile_source_archive_handle
|
||||
.clone()
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"config bundle {} does not contain a ProfileSourceArchive resource handle",
|
||||
bundle.metadata.id
|
||||
)
|
||||
})?;
|
||||
let client = self.resource_client.as_ref().ok_or_else(|| {
|
||||
format!(
|
||||
"config bundle {} requires a Backend resource client for profile source archive fetch",
|
||||
bundle.metadata.id
|
||||
)
|
||||
})?;
|
||||
let fetch_request = build_profile_source_archive_fetch_request(
|
||||
handle.clone(),
|
||||
&request.worker_ref.runtime_id,
|
||||
Some(&request.worker_ref.worker_id),
|
||||
);
|
||||
let response = client
|
||||
.fetch_resource(fetch_request)
|
||||
.await
|
||||
.map_err(format_backend_resource_error)?;
|
||||
let fetched_archive = profile_source_archive_from_response(&handle, response)
|
||||
.map_err(format_backend_resource_error)?;
|
||||
if let Some(cached) = self.profile_archive_cache.get(&handle.digest) {
|
||||
return cached
|
||||
match source {
|
||||
ProfileSourceArchiveSource::Embedded { archive } => archive
|
||||
.verify()
|
||||
.map_err(|err| format!("failed to verify cached profile source archive: {err}"));
|
||||
.map_err(|err| format!("failed to verify embedded profile source archive: {err}")),
|
||||
ProfileSourceArchiveSource::Http { location } => {
|
||||
self.fetch_profile_source_archive(location).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_profile_source_archive(
|
||||
&self,
|
||||
location: &ProfileSourceArchiveHttpRef,
|
||||
) -> Result<crate::profile_archive::VerifiedProfileSourceArchive, String> {
|
||||
if let Some(cached) = self.profile_archive_cache.get(&location.archive.digest) {
|
||||
let response =
|
||||
fetch_profile_source_archive_http(location, Some(&location.archive.digest)).await?;
|
||||
if let Some(fetched) = response {
|
||||
self.profile_archive_cache.insert(fetched.clone());
|
||||
fetched.verify().map_err(|err| {
|
||||
format!("failed to verify fetched profile source archive: {err}")
|
||||
})
|
||||
} else {
|
||||
cached
|
||||
.verify()
|
||||
.map_err(|err| format!("failed to verify cached profile source archive: {err}"))
|
||||
}
|
||||
} else {
|
||||
let archive = fetch_profile_source_archive_http(location, None)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
"profile source archive HTTP revalidation returned 304 without a cached archive"
|
||||
.to_string()
|
||||
})?;
|
||||
self.profile_archive_cache.insert(archive.clone());
|
||||
archive
|
||||
.verify()
|
||||
.map_err(|err| format!("failed to verify fetched profile source archive: {err}"))
|
||||
}
|
||||
self.profile_archive_cache.insert(fetched_archive.clone());
|
||||
fetched_archive
|
||||
.verify()
|
||||
.map_err(|err| format!("failed to verify fetched profile source archive: {err}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_backend_resource_error(error: BackendResourceError) -> String {
|
||||
format!("backend resource fetch failed: {error}")
|
||||
#[cfg(feature = "http-server")]
|
||||
async fn fetch_profile_source_archive_http(
|
||||
location: &ProfileSourceArchiveHttpRef,
|
||||
cached_digest: Option<&str>,
|
||||
) -> Result<Option<crate::profile_archive::ProfileSourceArchive>, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.get(&location.url);
|
||||
if cached_digest == Some(location.archive.digest.as_str()) {
|
||||
if let Some(etag) = location.etag.as_deref() {
|
||||
request = request.header(reqwest::header::IF_NONE_MATCH, etag);
|
||||
}
|
||||
}
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|err| format!("failed to fetch profile source archive: {err}"))?;
|
||||
if response.status() == reqwest::StatusCode::NOT_MODIFIED {
|
||||
return Ok(None);
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
return Err(format!(
|
||||
"profile source archive fetch failed with HTTP {status}"
|
||||
));
|
||||
}
|
||||
let bytes = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|err| format!("failed to read profile source archive response: {err}"))?
|
||||
.to_vec();
|
||||
let archive = crate::profile_archive::ProfileSourceArchive {
|
||||
reference: location.archive.clone(),
|
||||
content: bytes,
|
||||
};
|
||||
if archive.content.len() as u64 != archive.reference.size_bytes {
|
||||
return Err(format!(
|
||||
"profile source archive size mismatch: expected {}, got {}",
|
||||
archive.reference.size_bytes,
|
||||
archive.content.len()
|
||||
));
|
||||
}
|
||||
Ok(Some(archive))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "http-server"))]
|
||||
async fn fetch_profile_source_archive_http(
|
||||
_location: &ProfileSourceArchiveHttpRef,
|
||||
_cached_digest: Option<&str>,
|
||||
) -> Result<Option<crate::profile_archive::ProfileSourceArchive>, String> {
|
||||
Err(
|
||||
"HTTP profile source archive fetch requires the worker-runtime http-server feature"
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@@ -238,11 +286,11 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
.as_ref()
|
||||
.map(|binding| binding.cwd().to_path_buf())
|
||||
.unwrap_or_else(|| self.cwd.clone());
|
||||
let (mut manifest, loader) = if let Some(bundle) = request.config_bundle.as_ref() {
|
||||
let selector = profile.as_deref().unwrap_or("builtin:default");
|
||||
let archive = self
|
||||
.resolve_profile_source_archive(bundle, &request)
|
||||
.await?;
|
||||
let selector = profile.as_deref().unwrap_or("builtin:default");
|
||||
let archive = self
|
||||
.resolve_profile_source_archive(&request.request.profile_source)
|
||||
.await?;
|
||||
let (mut manifest, loader) = {
|
||||
let manifest = archive
|
||||
.resolve_profile(selector, &worker_root, &worker_name)
|
||||
.map_err(|err| format!("failed to resolve profile source archive: {err}"))?;
|
||||
@@ -251,15 +299,6 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
&worker_root,
|
||||
&worker_name,
|
||||
)?
|
||||
} else {
|
||||
// Compatibility/debug fallback for direct CLI tests. Normal Browser/Backend launch
|
||||
// supplies a ProfileSourceArchive inside the Runtime config bundle and must not use
|
||||
// Runtime-local filesystem profile discovery.
|
||||
worker::entrypoint::resolve_runtime_profile_manifest(
|
||||
profile.as_deref(),
|
||||
&self.profile_base_dir,
|
||||
&worker_name,
|
||||
)?
|
||||
};
|
||||
manifest.worker.name = worker_name;
|
||||
|
||||
@@ -899,7 +938,7 @@ mod tests {
|
||||
label: Some("adapter-test".to_string()),
|
||||
}],
|
||||
declarations: Vec::new(),
|
||||
profile_source_archive: None,
|
||||
profile_source_archive: Some(sample_profile_archive()),
|
||||
profile_source_archive_handle: None,
|
||||
}
|
||||
.with_computed_digest()
|
||||
@@ -1010,10 +1049,13 @@ mod tests {
|
||||
worker_ref: worker_ref.clone(),
|
||||
request: CreateWorkerRequest {
|
||||
profile: ProfileSelector::RuntimeDefault,
|
||||
config_bundle: ConfigBundleRef {
|
||||
profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded {
|
||||
archive: bundle.profile_source_archive.clone().unwrap(),
|
||||
},
|
||||
config_bundle: Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id.clone(),
|
||||
digest: bundle.metadata.digest.clone(),
|
||||
},
|
||||
}),
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
working_directory: None,
|
||||
@@ -1028,10 +1070,13 @@ mod tests {
|
||||
let bundle = test_bundle();
|
||||
CreateWorkerRequest {
|
||||
profile: ProfileSelector::RuntimeDefault,
|
||||
config_bundle: ConfigBundleRef {
|
||||
profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded {
|
||||
archive: bundle.profile_source_archive.clone().unwrap(),
|
||||
},
|
||||
config_bundle: Some(ConfigBundleRef {
|
||||
id: bundle.metadata.id,
|
||||
digest: bundle.metadata.digest,
|
||||
},
|
||||
}),
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
working_directory: None,
|
||||
@@ -1077,34 +1122,16 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cached_profile_archive_still_requires_backend_authorization() {
|
||||
let archive = sample_profile_archive();
|
||||
let handle = handle_for_archive(&archive);
|
||||
let call_count = Arc::new(AtomicUsize::new(0));
|
||||
let client = SequencedResourceClient {
|
||||
responses: Arc::new(Mutex::new(VecDeque::from([
|
||||
Ok(response_for_archive(&handle, &archive)),
|
||||
Err(crate::resource::BackendResourceError::Expired),
|
||||
]))),
|
||||
call_count: call_count.clone(),
|
||||
async fn embedded_profile_source_archive_does_not_require_backend_resource_fetch() {
|
||||
let factory = ProfileRuntimeWorkerFactory::new(tempfile::tempdir().unwrap().path());
|
||||
let bundle = test_bundle();
|
||||
let source = crate::catalog::ProfileSourceArchiveSource::Embedded {
|
||||
archive: bundle.profile_source_archive.clone().unwrap(),
|
||||
};
|
||||
let factory = ProfileRuntimeWorkerFactory::new(tempfile::tempdir().unwrap().path())
|
||||
.with_resource_client(Arc::new(client));
|
||||
let mut bundle = test_bundle();
|
||||
bundle.profile_source_archive_handle = Some(handle);
|
||||
bundle = bundle.with_computed_digest();
|
||||
|
||||
factory
|
||||
.resolve_profile_source_archive(&bundle, &spawn_request_with_bundle(bundle.clone()))
|
||||
.resolve_profile_source_archive(&source)
|
||||
.await
|
||||
.expect("first fetch should authorize and cache archive");
|
||||
let err = factory
|
||||
.resolve_profile_source_archive(&bundle, &spawn_request_with_bundle(bundle.clone()))
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.contains("expired"), "unexpected error: {err}");
|
||||
assert_eq!(call_count.load(Ordering::SeqCst), 2);
|
||||
.expect("embedded archive should resolve without Backend resource client");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user