feat: distribute latest workspace config to runtimes

This commit is contained in:
2026-09-07 02:40:03 +09:00
parent 31f7d39647
commit f1baea1705
11 changed files with 656 additions and 403 deletions
+4 -14
View File
@@ -15,32 +15,22 @@ pub enum ProfileSelector {
Named(String), Named(String),
} }
/// 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. /// Profile source material available to a Runtime during Worker creation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum ProfileSourceArchiveSource { pub enum ProfileSourceArchiveSource {
/// Backend-internal embedded runtimes may receive already-built archive bytes. /// Backend-internal embedded runtimes may receive already-built archive bytes.
Embedded { archive: ProfileSourceArchive }, Embedded { archive: ProfileSourceArchive },
/// Standalone runtimes fetch/cache the tar archive over HTTP. /// Standalone runtimes resolve this immutable archive from the latest
Http { /// Workspace Config bundle before creating the Worker.
location: ProfileSourceArchiveHttpRef, WorkspaceConfig { archive: ProfileSourceArchiveRef },
},
} }
impl ProfileSourceArchiveSource { impl ProfileSourceArchiveSource {
pub fn reference(&self) -> ProfileSourceArchiveRef { pub fn reference(&self) -> ProfileSourceArchiveRef {
match self { match self {
Self::Embedded { archive } => archive.reference.clone(), Self::Embedded { archive } => archive.reference.clone(),
Self::Http { location } => location.archive.clone(), Self::WorkspaceConfig { archive } => archive.clone(),
} }
} }
} }
@@ -9,6 +9,11 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
pub const CONFIG_BUNDLE_DIGEST_ALGORITHM: &str = "sha256"; pub const CONFIG_BUNDLE_DIGEST_ALGORITHM: &str = "sha256";
pub const WORKSPACE_CONFIG_ETAG_PREFIX: &str = "workspace-config:";
pub fn workspace_config_etag(digest: &str) -> String {
format!("\"{WORKSPACE_CONFIG_ETAG_PREFIX}{digest}\"")
}
/// Backend-synced Profile/config bundle stored by a Runtime. /// Backend-synced Profile/config bundle stored by a Runtime.
/// ///
+32 -1
View File
@@ -1,6 +1,7 @@
use crate::catalog::{ use crate::catalog::{
RepositoryRefObservation, RepositoryRefObservationRequest, ConfigBundleRef, ProfileSelector, RepositoryRefObservation, RepositoryRefObservationRequest,
WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest, WorkingDirectoryStatus, WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest, WorkingDirectoryStatus,
WorkspaceApiRef,
}; };
use crate::config_bundle::ConfigBundle; use crate::config_bundle::ConfigBundle;
use crate::error::RuntimeError; use crate::error::RuntimeError;
@@ -265,6 +266,22 @@ pub struct WorkerExecutionRestoreRequest {
pub config_bundle: Option<ConfigBundle>, pub config_bundle: Option<ConfigBundle>,
} }
/// Runtime-side request to refresh the latest Workspace Config before Worker creation.
#[derive(Clone, Debug)]
pub struct WorkspaceConfigFetchRequest {
pub workspace_api: WorkspaceApiRef,
pub profile: ProfileSelector,
pub expected: ConfigBundleRef,
pub cached: Option<ConfigBundleRef>,
}
/// Result of a conditional Workspace Config fetch.
#[derive(Clone, Debug)]
pub enum WorkspaceConfigFetchResult {
NotModified,
Modified(ConfigBundle),
}
/// Backend outcome for Worker spawn/restore operations. /// Backend outcome for Worker spawn/restore operations.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum WorkerExecutionSpawnResult { pub enum WorkerExecutionSpawnResult {
@@ -291,6 +308,13 @@ impl WorkerExecutionSpawnResult {
pub trait WorkerExecutionBackend: Send + Sync + 'static { pub trait WorkerExecutionBackend: Send + Sync + 'static {
fn backend_id(&self) -> &str; fn backend_id(&self) -> &str;
fn fetch_workspace_config(
&self,
_request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
Err("execution backend does not support Workspace Config fetching".to_string())
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult; fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult;
fn restore_worker( fn restore_worker(
@@ -472,6 +496,13 @@ impl WorkerExecutionBackendRef {
}) })
} }
pub(crate) fn fetch_workspace_config(
&self,
request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
self.backend.fetch_workspace_config(request)
}
pub(crate) fn spawn_worker( pub(crate) fn spawn_worker(
&self, &self,
request: WorkerExecutionSpawnRequest, request: WorkerExecutionSpawnRequest,
+28 -28
View File
@@ -2694,22 +2694,22 @@ mod tests {
create_fingerprint: "test-create".to_string(), create_fingerprint: "test-create".to_string(),
profile, profile,
display_name: None, display_name: None,
profile_source: crate::catalog::ProfileSourceArchiveSource::Http { profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded {
location: crate::catalog::ProfileSourceArchiveHttpRef { archive: crate::profile_archive::ProfileSourceArchive::build(
url: "http://127.0.0.1/profile-source.tar".to_string(), crate::profile_archive::ProfileSourceArchiveInput {
etag: None,
archive: crate::profile_archive::ProfileSourceArchiveRef {
id: "test-profile-source".to_string(), id: "test-profile-source".to_string(),
digest: "test-digest".to_string(), entrypoints: std::collections::BTreeMap::from([(
size_bytes: 0, "builtin:coder".to_string(),
source_graph: crate::profile_archive::ProfileSourceGraphSummary { "profiles/coder.dcdl".to_string(),
source_count: 0, )]),
total_source_bytes: 0, imports: std::collections::BTreeMap::new(),
entrypoints: std::collections::BTreeMap::new(), sources: std::collections::BTreeMap::from([(
import_count: 0, "profiles/coder.dcdl".to_string(),
}, "{}".to_string(),
)]),
}, },
}, )
.unwrap(),
}, },
config_bundle: Some(ConfigBundleRef { config_bundle: Some(ConfigBundleRef {
id: bundle.metadata.id, id: bundle.metadata.id,
@@ -3312,22 +3312,22 @@ mod ws_tests {
create_fingerprint: "test-create".to_string(), create_fingerprint: "test-create".to_string(),
profile: ProfileSelector::Builtin("builtin:companion".to_string()), profile: ProfileSelector::Builtin("builtin:companion".to_string()),
display_name: None, display_name: None,
profile_source: crate::catalog::ProfileSourceArchiveSource::Http { profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded {
location: crate::catalog::ProfileSourceArchiveHttpRef { archive: crate::profile_archive::ProfileSourceArchive::build(
url: "http://127.0.0.1/profile-source.tar".to_string(), crate::profile_archive::ProfileSourceArchiveInput {
etag: None,
archive: crate::profile_archive::ProfileSourceArchiveRef {
id: "test-profile-source".to_string(), id: "test-profile-source".to_string(),
digest: "test-digest".to_string(), entrypoints: std::collections::BTreeMap::from([(
size_bytes: 0, "builtin:coder".to_string(),
source_graph: crate::profile_archive::ProfileSourceGraphSummary { "profiles/coder.dcdl".to_string(),
source_count: 0, )]),
total_source_bytes: 0, imports: std::collections::BTreeMap::new(),
entrypoints: std::collections::BTreeMap::new(), sources: std::collections::BTreeMap::from([(
import_count: 0, "profiles/coder.dcdl".to_string(),
}, "{}".to_string(),
)]),
}, },
}, )
.unwrap(),
}, },
config_bundle: Some(ConfigBundleRef { config_bundle: Some(ConfigBundleRef {
id: bundle.metadata.id, id: bundle.metadata.id,
-19
View File
@@ -6,8 +6,6 @@ use crate::identity::WorkerId;
use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef, sha256_hex}; use crate::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveRef, sha256_hex};
use async_trait::async_trait; use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
pub const PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE: &str = pub const PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE: &str =
"application/vnd.yoi.profile-source-archive+tar"; "application/vnd.yoi.profile-source-archive+tar";
@@ -266,23 +264,6 @@ impl BackendResourceClient for HttpBackendResourceClient {
} }
} }
#[derive(Default, Debug)]
pub struct ProfileSourceArchiveCache {
archives: Mutex<HashMap<String, ProfileSourceArchive>>,
}
impl ProfileSourceArchiveCache {
pub fn get(&self, digest: &str) -> Option<ProfileSourceArchive> {
self.archives.lock().ok()?.get(digest).cloned()
}
pub fn insert(&self, archive: ProfileSourceArchive) {
if let Ok(mut archives) = self.archives.lock() {
archives.insert(archive.reference.digest.clone(), archive);
}
}
}
pub fn build_profile_source_archive_fetch_request( pub fn build_profile_source_archive_fetch_request(
handle: BackendResourceHandle, handle: BackendResourceHandle,
runtime_id: &str, runtime_id: &str,
+228 -29
View File
@@ -1,8 +1,9 @@
use crate::catalog::{ use crate::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, RepositoryRefObservation, ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
RepositoryRefObservationRequest, WorkerDetail, WorkerLifecycleAck, WorkerRestoreIntent, RepositoryRefObservation, RepositoryRefObservationRequest, WorkerDetail, WorkerLifecycleAck,
WorkerStatus, WorkerSummary, WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest, WorkerRestoreIntent, WorkerStatus, WorkerSummary, WorkingDirectoryRepositoryAccessRequest,
WorkingDirectoryStatus as CatalogWorkingDirectoryStatus, WorkspaceApiRef, WorkingDirectoryRequest, WorkingDirectoryStatus as CatalogWorkingDirectoryStatus,
WorkspaceApiRef,
}; };
use crate::config_bundle::{ use crate::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle, ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle,
@@ -14,7 +15,7 @@ use crate::execution::WorkerExecutionRestoreRequest;
use crate::execution::{ use crate::execution::{
WorkerExecutionBackend, WorkerExecutionBackendRef, WorkerExecutionHandle, WorkerExecutionBackend, WorkerExecutionBackendRef, WorkerExecutionHandle,
WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionSpawnRequest, WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionSpawnRequest,
WorkerExecutionSpawnResult, WorkerExecutionSpawnResult, WorkspaceConfigFetchRequest, WorkspaceConfigFetchResult,
}; };
#[cfg(feature = "fs-store")] #[cfg(feature = "fs-store")]
use crate::fs_store::{ use crate::fs_store::{
@@ -643,11 +644,147 @@ impl Runtime {
self.create_worker_with_workspace(request, Some(scope)) self.create_worker_with_workspace(request, Some(scope))
} }
fn existing_worker_for_create(
&self,
request: &CreateWorkerRequest,
scope: Option<&RuntimeWorkspaceScope>,
) -> Result<Option<WorkerDetail>, RuntimeError> {
let mut state = self.lock()?;
state.ensure_running()?;
validate_create_worker_request(request)?;
validate_create_workspace_scope(request, scope.map(|scope| scope.workspace_id.as_str()))?;
if let Some(scope) = scope {
state.ensure_workspace_owner(scope, false)?;
}
let workspace_id = scope.map(|scope| scope.workspace_id.as_str());
if let Some(existing) = state.workers.get(&request.worker_id) {
if existing.workspace_id.as_deref() != workspace_id {
return Err(RuntimeError::InvalidRequest(format!(
"worker {} already belongs to another Workspace scope",
request.worker_id
)));
}
if existing.request.create_fingerprint != request.create_fingerprint {
return Err(RuntimeError::InvalidRequest(format!(
"worker {} was already created with a different fingerprint",
request.worker_id
)));
}
return Ok(Some(existing.detail()));
}
Ok(None)
}
fn refresh_workspace_config(&self, request: &CreateWorkerRequest) -> Result<(), RuntimeError> {
let ProfileSourceArchiveSource::WorkspaceConfig {
archive: expected_archive,
} = &request.profile_source
else {
return Ok(());
};
let expected = request.config_bundle.as_ref().ok_or_else(|| {
RuntimeError::InvalidRequest(
"Workspace Config profile source requires a config_bundle reference".to_string(),
)
})?;
let workspace_api = request.workspace_api.clone().ok_or_else(|| {
RuntimeError::InvalidRequest(
"Workspace Config profile source requires a Workspace API reference".to_string(),
)
})?;
let profile_key = match &request.profile {
ProfileSelector::Builtin(value) | ProfileSelector::Named(value) => value.clone(),
};
let cache_key = format!(
"{}\u{0}{}\u{0}{}",
workspace_api.base_url, workspace_api.workspace_id, profile_key
);
let fetch_gate = {
let mut state = self.lock()?;
state
.workspace_config_fetch_gates
.entry(cache_key.clone())
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
};
let _fetch_guard = fetch_gate.lock().map_err(|_| RuntimeError::StatePoisoned)?;
let (backend, cached) = {
let state = self.lock()?;
let cached_reference = state
.workspace_config_latest
.get(&cache_key)
.unwrap_or(expected);
let cached = state
.config_bundles
.get(&cached_reference.id)
.filter(|bundle| bundle.metadata.digest == cached_reference.digest)
.map(|bundle| ConfigBundleRef {
id: bundle.metadata.id.clone(),
digest: bundle.metadata.digest.clone(),
});
(state.execution_backend.clone(), cached)
};
let backend = backend.ok_or_else(|| RuntimeError::ExecutionBackendUnavailable {
message: "Workspace Config refresh requires an execution backend".to_string(),
})?;
let fetched = backend
.fetch_workspace_config(WorkspaceConfigFetchRequest {
workspace_api,
profile: request.profile.clone(),
expected: expected.clone(),
cached: cached.clone(),
})
.map_err(|message| {
RuntimeError::InvalidRequest(format!(
"failed to refresh latest Workspace Config: {message}"
))
})?;
let resolved = match fetched {
WorkspaceConfigFetchResult::NotModified => cached.ok_or_else(|| {
RuntimeError::InvalidRequest(
"latest Workspace Config returned not-modified without a cached bundle"
.to_string(),
)
})?,
WorkspaceConfigFetchResult::Modified(bundle) => {
self.store_config_bundle(bundle)?.reference
}
};
if &resolved != expected {
return Err(RuntimeError::InvalidRequest(format!(
"latest Workspace Config changed while Worker creation was being prepared: expected '{}@{}', got '{}@{}'",
expected.id, expected.digest, resolved.id, resolved.digest
)));
}
let mut state = self.lock()?;
let bundle = state.config_bundles.get(&resolved.id).ok_or_else(|| {
RuntimeError::InvalidRequest(
"latest Workspace Config was not available after refresh".to_string(),
)
})?;
let archive = bundle.profile_source_archive.as_ref().ok_or_else(|| {
RuntimeError::InvalidRequest(
"latest Workspace Config is missing its profile source archive".to_string(),
)
})?;
if &archive.reference != expected_archive {
return Err(RuntimeError::InvalidRequest(
"latest Workspace Config profile source archive reference mismatch".to_string(),
));
}
state.workspace_config_latest.insert(cache_key, resolved);
Ok(())
}
fn create_worker_with_workspace( fn create_worker_with_workspace(
&self, &self,
request: CreateWorkerRequest, request: CreateWorkerRequest,
scope: Option<&RuntimeWorkspaceScope>, scope: Option<&RuntimeWorkspaceScope>,
) -> Result<WorkerDetail, RuntimeError> { ) -> Result<WorkerDetail, RuntimeError> {
if let Some(existing) = self.existing_worker_for_create(&request, scope)? {
return Ok(existing);
}
self.refresh_workspace_config(&request)?;
let (backend, worker_ref, spawn_request) = { let (backend, worker_ref, spawn_request) = {
let mut state = self.lock()?; let mut state = self.lock()?;
state.ensure_running()?; state.ensure_running()?;
@@ -2170,6 +2307,8 @@ struct RuntimeState {
workers: BTreeMap<WorkerId, WorkerRecord>, workers: BTreeMap<WorkerId, WorkerRecord>,
workspace_owners: BTreeMap<String, String>, workspace_owners: BTreeMap<String, String>,
config_bundles: BTreeMap<String, ConfigBundle>, config_bundles: BTreeMap<String, ConfigBundle>,
workspace_config_latest: BTreeMap<String, ConfigBundleRef>,
workspace_config_fetch_gates: BTreeMap<String, Arc<Mutex<()>>>,
diagnostics: Vec<RuntimeDiagnostic>, diagnostics: Vec<RuntimeDiagnostic>,
subscription_revision: u64, subscription_revision: u64,
worker_subject_revisions: BTreeMap<WorkerId, u64>, worker_subject_revisions: BTreeMap<WorkerId, u64>,
@@ -2198,6 +2337,8 @@ impl RuntimeState {
workers: BTreeMap::new(), workers: BTreeMap::new(),
workspace_owners: BTreeMap::new(), workspace_owners: BTreeMap::new(),
config_bundles: BTreeMap::new(), config_bundles: BTreeMap::new(),
workspace_config_latest: BTreeMap::new(),
workspace_config_fetch_gates: BTreeMap::new(),
diagnostics: Vec::new(), diagnostics: Vec::new(),
subscription_revision: 0, subscription_revision: 0,
worker_subject_revisions: BTreeMap::new(), worker_subject_revisions: BTreeMap::new(),
@@ -2227,6 +2368,8 @@ impl RuntimeState {
workers: BTreeMap::new(), workers: BTreeMap::new(),
workspace_owners: BTreeMap::new(), workspace_owners: BTreeMap::new(),
config_bundles: BTreeMap::new(), config_bundles: BTreeMap::new(),
workspace_config_latest: BTreeMap::new(),
workspace_config_fetch_gates: BTreeMap::new(),
diagnostics: Vec::new(), diagnostics: Vec::new(),
subscription_revision: 0, subscription_revision: 0,
worker_subject_revisions: BTreeMap::new(), worker_subject_revisions: BTreeMap::new(),
@@ -2286,6 +2429,8 @@ impl RuntimeState {
next_diagnostic_id, next_diagnostic_id,
workers, workers,
config_bundles: BTreeMap::new(), config_bundles: BTreeMap::new(),
workspace_config_latest: BTreeMap::new(),
workspace_config_fetch_gates: BTreeMap::new(),
workspace_owners: persisted.workspace_owners, workspace_owners: persisted.workspace_owners,
diagnostics, diagnostics,
subscription_revision: 0, subscription_revision: 0,
@@ -3126,15 +3271,10 @@ fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), R
RuntimeError::InvalidRequest(format!("profile_source archive is invalid: {err}")) RuntimeError::InvalidRequest(format!("profile_source archive is invalid: {err}"))
})?; })?;
} }
crate::catalog::ProfileSourceArchiveSource::Http { location } => { crate::catalog::ProfileSourceArchiveSource::WorkspaceConfig { archive } => {
if location.url.trim().is_empty() { if archive.digest.trim().is_empty() {
return Err(RuntimeError::InvalidRequest( return Err(RuntimeError::InvalidRequest(
"profile_source.location.url must not be empty".to_string(), "profile_source.archive.digest 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(),
)); ));
} }
} }
@@ -3494,6 +3634,21 @@ mod tests {
assert!(validate_create_worker_request(&request).is_ok()); assert!(validate_create_worker_request(&request).is_ok());
} }
fn test_profile_source_archive() -> crate::profile_archive::ProfileSourceArchive {
crate::profile_archive::ProfileSourceArchive::build(
crate::profile_archive::ProfileSourceArchiveInput {
id: "test-profile-source".to_string(),
entrypoints: BTreeMap::from([(
"builtin:coder".to_string(),
"profiles/coder.dcdl".to_string(),
)]),
imports: BTreeMap::new(),
sources: BTreeMap::from([("profiles/coder.dcdl".to_string(), "{}".to_string())]),
},
)
.unwrap()
}
fn task_request(_objective: &str) -> CreateWorkerRequest { fn task_request(_objective: &str) -> CreateWorkerRequest {
let profile = ProfileSelector::Builtin("builtin:coder".to_string()); let profile = ProfileSelector::Builtin("builtin:coder".to_string());
let bundle = test_bundle_for_profile(profile.clone()); let bundle = test_bundle_for_profile(profile.clone());
@@ -3502,22 +3657,8 @@ mod tests {
create_fingerprint: "test-create".to_string(), create_fingerprint: "test-create".to_string(),
profile, profile,
display_name: None, display_name: None,
profile_source: crate::catalog::ProfileSourceArchiveSource::Http { profile_source: crate::catalog::ProfileSourceArchiveSource::Embedded {
location: crate::catalog::ProfileSourceArchiveHttpRef { archive: test_profile_source_archive(),
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 { config_bundle: Some(ConfigBundleRef {
id: bundle.metadata.id, id: bundle.metadata.id,
@@ -3853,6 +3994,8 @@ mod tests {
restore_count: Mutex<u64>, restore_count: Mutex<u64>,
run_generations: Mutex<Vec<u64>>, run_generations: Mutex<Vec<u64>>,
config_bundles: Mutex<Vec<Option<ConfigBundle>>>, config_bundles: Mutex<Vec<Option<ConfigBundle>>>,
workspace_config_fetches: Mutex<Vec<WorkspaceConfigFetchRequest>>,
workspace_config_results: Mutex<Vec<WorkspaceConfigFetchResult>>,
contexts: Mutex<BTreeMap<WorkerId, WorkerExecutionContext>>, contexts: Mutex<BTreeMap<WorkerId, WorkerExecutionContext>>,
dispatched_inputs: Mutex<Vec<WorkerInput>>, dispatched_inputs: Mutex<Vec<WorkerInput>>,
repository_accesses: Mutex<Vec<WorkingDirectoryRepositoryAccessRequest>>, repository_accesses: Mutex<Vec<WorkingDirectoryRepositoryAccessRequest>>,
@@ -3898,6 +4041,18 @@ mod tests {
"test-execution-backend" "test-execution-backend"
} }
fn fetch_workspace_config(
&self,
request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
self.workspace_config_fetches.lock().unwrap().push(request);
let mut results = self.workspace_config_results.lock().unwrap();
if results.is_empty() {
return Err("no Workspace Config fetch result configured".to_string());
}
Ok(results.remove(0))
}
fn create_working_directory( fn create_working_directory(
&self, &self,
request: &WorkingDirectoryRequest, request: &WorkingDirectoryRequest,
@@ -4585,6 +4740,50 @@ mod tests {
); );
} }
#[test]
fn remote_create_refreshes_workspace_config_and_revalidates_cached_etag() {
let backend = Arc::new(TestExecutionBackend::default());
let runtime =
Runtime::with_execution_backend(RuntimeOptions::default(), backend.clone()).unwrap();
let mut bundle = test_bundle();
let archive = test_profile_source_archive();
bundle.profile_source_archive = Some(archive.clone());
bundle = bundle.with_computed_digest();
backend.workspace_config_results.lock().unwrap().extend([
WorkspaceConfigFetchResult::Modified(bundle.clone()),
WorkspaceConfigFetchResult::NotModified,
]);
let archive = archive.reference;
let request = |objective: &str| {
let mut request = bundled_task_request(objective, &bundle);
request.profile_source = ProfileSourceArchiveSource::WorkspaceConfig {
archive: archive.clone(),
};
request.workspace_api = Some(WorkspaceApiRef {
workspace_id: "workspace-test".to_string(),
base_url: "https://workspace.example".to_string(),
});
request
};
let first = request("first refresh");
let second = request("cached refresh");
runtime.create_worker(first).unwrap();
runtime.create_worker(second.clone()).unwrap();
runtime.create_worker(second).unwrap();
let fetches = backend.workspace_config_fetches.lock().unwrap();
assert_eq!(fetches.len(), 2);
assert_eq!(fetches[0].cached, None);
assert_eq!(
fetches[1].cached,
Some(ConfigBundleRef {
id: bundle.metadata.id.clone(),
digest: bundle.metadata.digest.clone(),
})
);
}
#[test] #[test]
fn restore_does_not_require_recorded_config_bundle() { fn restore_does_not_require_recorded_config_bundle() {
let (runtime, backend) = runtime_and_backend(); let (runtime, backend) = runtime_and_backend();
+126 -112
View File
@@ -19,18 +19,19 @@ use crate::auth::{
RuntimeIdentityMaterial, RuntimeRequestSourceSigner, unix_now_seconds, RuntimeIdentityMaterial, RuntimeRequestSourceSigner, unix_now_seconds,
}; };
use crate::catalog::{ use crate::catalog::{
CreateWorkerRequest, ProfileSourceArchiveHttpRef, ProfileSourceArchiveSource, CreateWorkerRequest, ProfileSourceArchiveSource, RepositoryRefObservation,
RepositoryRefObservation, RepositoryRefObservationRequest, RepositoryRefObservationRequest, WorkingDirectoryRepositoryAccessRequest,
WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest, WorkingDirectoryStatus, WorkingDirectoryRequest, WorkingDirectoryStatus,
}; };
use crate::config_bundle::{ConfigBundle, workspace_config_etag};
use crate::execution::{ use crate::execution::{
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation,
WorkerExecutionRestoreRequest, WorkerExecutionResult, WorkerExecutionSpawnRequest, WorkerExecutionRestoreRequest, WorkerExecutionResult, WorkerExecutionSpawnRequest,
WorkerExecutionSpawnResult, WorkerExecutionSpawnResult, WorkspaceConfigFetchRequest, WorkspaceConfigFetchResult,
}; };
use crate::identity::WorkerRef; use crate::identity::WorkerRef;
use crate::interaction::{WorkerInput, WorkerInputKind}; use crate::interaction::{WorkerInput, WorkerInputKind};
use crate::resource::{BackendResourceClient, ProfileSourceArchiveCache}; use crate::resource::BackendResourceClient;
use crate::worker_source::{ use crate::worker_source::{
EmbeddedWorkerMutationDispatcher, RuntimeOwnedWorkspaceClient, RuntimeWorkerMutationForwarder, EmbeddedWorkerMutationDispatcher, RuntimeOwnedWorkspaceClient, RuntimeWorkerMutationForwarder,
}; };
@@ -38,6 +39,8 @@ use crate::working_directory::{
WorkingDirectoryBinding, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer, WorkingDirectoryBinding, WorkingDirectoryDiagnostic, WorkingDirectoryMaterializer,
}; };
use async_trait::async_trait; use async_trait::async_trait;
#[cfg(feature = "http-server")]
use futures::StreamExt;
#[cfg(test)] #[cfg(test)]
use protocol::WorkerStatus; use protocol::WorkerStatus;
use protocol::{Event, Method, Segment, WorkerCommandEnvelope}; use protocol::{Event, Method, Segment, WorkerCommandEnvelope};
@@ -86,6 +89,8 @@ use worker::{
const DEFAULT_BACKEND_ID: &str = "worker-crate"; const DEFAULT_BACKEND_ID: &str = "worker-crate";
const RUNTIME_TASK_TIMEOUT: Duration = Duration::from_secs(10); const RUNTIME_TASK_TIMEOUT: Duration = Duration::from_secs(10);
const WORKSPACE_CONFIG_HTTP_TIMEOUT: Duration = Duration::from_secs(8);
const MAX_WORKSPACE_CONFIG_RESPONSE_BYTES: usize = 72 * 1024 * 1024;
// Keep this below the adapter task timeout so a failed acknowledgement task // Keep this below the adapter task timeout so a failed acknowledgement task
// returns a typed execution error instead of leaving the outer waiter to time out. // returns a typed execution error instead of leaving the outer waiter to time out.
const USER_INPUT_COMMIT_TIMEOUT: Duration = Duration::from_secs(9); const USER_INPUT_COMMIT_TIMEOUT: Duration = Duration::from_secs(9);
@@ -107,6 +112,13 @@ pub trait RuntimeWorkerFactory: Send + Sync + 'static {
Ok(()) Ok(())
} }
async fn fetch_workspace_config(
&self,
_request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
Err("Runtime Worker factory does not support Workspace Config fetching".to_string())
}
async fn spawn_controller( async fn spawn_controller(
&self, &self,
request: WorkerExecutionSpawnRequest, request: WorkerExecutionSpawnRequest,
@@ -307,7 +319,6 @@ pub struct ProfileRuntimeWorkerFactory {
profile_base_dir: PathBuf, profile_base_dir: PathBuf,
worker_aggregate_root: Option<PathBuf>, worker_aggregate_root: Option<PathBuf>,
resource_client: Option<Arc<dyn BackendResourceClient>>, resource_client: Option<Arc<dyn BackendResourceClient>>,
profile_archive_cache: Arc<ProfileSourceArchiveCache>,
prompt_projection_cache: Arc<WorkspacePromptProjectionCache>, prompt_projection_cache: Arc<WorkspacePromptProjectionCache>,
runtime_id: Option<String>, runtime_id: Option<String>,
worker_mutation_identity: Option<RuntimeIdentityMaterial>, worker_mutation_identity: Option<RuntimeIdentityMaterial>,
@@ -324,7 +335,6 @@ impl ProfileRuntimeWorkerFactory {
profile_base_dir, profile_base_dir,
worker_aggregate_root: None, worker_aggregate_root: None,
resource_client: None, resource_client: None,
profile_archive_cache: Arc::new(ProfileSourceArchiveCache::default()),
prompt_projection_cache: Arc::new(WorkspacePromptProjectionCache::default()), prompt_projection_cache: Arc::new(WorkspacePromptProjectionCache::default()),
runtime_id: None, runtime_id: None,
worker_mutation_identity: None, worker_mutation_identity: None,
@@ -486,62 +496,28 @@ impl ProfileRuntimeWorkerFactory {
async fn resolve_profile_source_archive( async fn resolve_profile_source_archive(
&self, &self,
source: &ProfileSourceArchiveSource, source: &ProfileSourceArchiveSource,
request_audience: Option<&str>, config_bundle: Option<&ConfigBundle>,
) -> Result<crate::profile_archive::VerifiedProfileSourceArchive, String> { ) -> Result<crate::profile_archive::VerifiedProfileSourceArchive, String> {
match source { match source {
ProfileSourceArchiveSource::Embedded { archive } => archive ProfileSourceArchiveSource::Embedded { archive } => archive
.verify() .verify()
.map_err(|err| format!("failed to verify embedded profile source archive: {err}")), .map_err(|err| format!("failed to verify embedded profile source archive: {err}")),
ProfileSourceArchiveSource::Http { location } => { ProfileSourceArchiveSource::WorkspaceConfig { archive } => {
self.fetch_profile_source_archive(location, request_audience) let bundle = config_bundle.ok_or_else(|| {
.await "Workspace Config profile source requires a resolved Config Bundle".to_string()
} })?;
} let embedded = bundle.profile_source_archive.as_ref().ok_or_else(|| {
} "resolved Workspace Config is missing its profile source archive".to_string()
})?;
async fn fetch_profile_source_archive( if &embedded.reference != archive {
&self, return Err(
location: &ProfileSourceArchiveHttpRef, "Workspace Config profile source archive reference mismatch".to_string()
request_audience: Option<&str>, );
) -> Result<crate::profile_archive::VerifiedProfileSourceArchive, String> { }
if let Some(cached) = self.profile_archive_cache.get(&location.archive.digest) { embedded.verify().map_err(|err| {
let response = fetch_profile_source_archive_http( format!("failed to verify Workspace Config profile source archive: {err}")
location,
Some(&location.archive.digest),
self.worker_mutation_identity.as_ref(),
self.runtime_request_audience
.as_deref()
.or(request_audience),
)
.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,
self.worker_mutation_identity.as_ref(),
self.runtime_request_audience
.as_deref()
.or(request_audience),
)
.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}"))
} }
} }
} }
@@ -627,91 +603,112 @@ impl RuntimeWorkspaceBackendRef {
} }
#[cfg(feature = "http-server")] #[cfg(feature = "http-server")]
async fn fetch_profile_source_archive_http( async fn fetch_workspace_config_http(
location: &ProfileSourceArchiveHttpRef, request: &WorkspaceConfigFetchRequest,
cached_digest: Option<&str>,
identity: Option<&RuntimeIdentityMaterial>, identity: Option<&RuntimeIdentityMaterial>,
audience: Option<&str>, audience: Option<&str>,
) -> Result<Option<crate::profile_archive::ProfileSourceArchive>, String> { ) -> Result<WorkspaceConfigFetchResult, String> {
let client = reqwest::Client::new(); let mut url = reqwest::Url::parse(&request.workspace_api.base_url)
let url = reqwest::Url::parse(&location.url) .map_err(|error| format!("Workspace API base URL is invalid: {error}"))?;
.map_err(|error| format!("profile source archive URL is invalid: {error}"))?; url.set_path(&format!(
"/api/w/{}/runtime-config",
request.workspace_api.workspace_id
));
let profile = match &request.profile {
crate::catalog::ProfileSelector::Builtin(value)
| crate::catalog::ProfileSelector::Named(value) => value.clone(),
};
url.query_pairs_mut().append_pair("profile", &profile);
let path = url.path().to_owned(); let path = url.path().to_owned();
let workspace_id = path let request_target = match url.query() {
.split('/') Some(query) => format!("{path}?{query}"),
.collect::<Vec<_>>() None => path.clone(),
.windows(2) };
.find_map(|parts| (parts[0] == "w").then_some(parts[1])) let client = reqwest::Client::builder()
.filter(|value| !value.is_empty()) .connect_timeout(Duration::from_secs(3))
.ok_or_else(|| "profile source archive URL is not workspace-scoped".to_owned())?; .timeout(WORKSPACE_CONFIG_HTTP_TIMEOUT)
let mut request = client.get(url); .build()
.map_err(|error| format!("failed to build Workspace Config HTTP client: {error}"))?;
let mut http_request = client.get(url);
if let Some(identity) = identity { if let Some(identity) = identity {
let audience = audience.ok_or_else(|| { let audience = audience
"profile source archive request proof audience is unavailable".to_owned() .ok_or_else(|| "Workspace Config request proof audience is unavailable".to_owned())?;
})?;
let proof = RuntimeRequestSourceSigner::from_identity(identity) let proof = RuntimeRequestSourceSigner::from_identity(identity)
.issue( .issue(
audience, audience,
workspace_id, &request.workspace_api.workspace_id,
None, None,
BACKEND_RESOURCE_FETCH_PERMISSION, BACKEND_RESOURCE_FETCH_PERMISSION,
"GET", "GET",
&path, &request_target,
b"", b"",
i64::try_from(unix_now_seconds()).unwrap_or(i64::MAX), i64::try_from(unix_now_seconds()).unwrap_or(i64::MAX),
30, 30,
) )
.map_err(|error| error.to_string())?; .map_err(|error| error.to_string())?;
request = request.header(RUNTIME_REQUEST_SOURCE_PROOF_HEADER, proof); http_request = http_request.header(RUNTIME_REQUEST_SOURCE_PROOF_HEADER, proof);
} }
if cached_digest == Some(location.archive.digest.as_str()) { if let Some(cached) = request.cached.as_ref() {
if let Some(etag) = location.etag.as_deref() { http_request = http_request.header(
request = request.header(reqwest::header::IF_NONE_MATCH, etag); reqwest::header::IF_NONE_MATCH,
} workspace_config_etag(&cached.digest),
);
} }
let response = request
let response = http_request
.send() .send()
.await .await
.map_err(|err| format!("failed to fetch profile source archive: {err}"))?; .map_err(|error| format!("failed to fetch latest Workspace Config: {error}"))?;
if response.status() == reqwest::StatusCode::NOT_MODIFIED { if response.status() == reqwest::StatusCode::NOT_MODIFIED {
return Ok(None); return Ok(WorkspaceConfigFetchResult::NotModified);
} }
if !response.status().is_success() { if !response.status().is_success() {
let status = response.status();
return Err(format!( return Err(format!(
"profile source archive fetch failed with HTTP {status}" "latest Workspace Config fetch failed with HTTP {}",
response.status()
)); ));
} }
let bytes = response if response
.bytes() .content_length()
.await .is_some_and(|size| size > MAX_WORKSPACE_CONFIG_RESPONSE_BYTES as u64)
.map_err(|err| format!("failed to read profile source archive response: {err}"))? {
.to_vec(); return Err("latest Workspace Config response exceeds the size limit".to_string());
let archive = crate::profile_archive::ProfileSourceArchive { }
reference: location.archive.clone(), let response_etag = response
content: bytes, .headers()
}; .get(reqwest::header::ETAG)
if archive.content.len() as u64 != archive.reference.size_bytes { .and_then(|value| value.to_str().ok())
.map(str::to_string)
.ok_or_else(|| "latest Workspace Config response is missing its ETag".to_string())?;
let mut body = Vec::new();
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk =
chunk.map_err(|error| format!("failed to read latest Workspace Config: {error}"))?;
if body.len().saturating_add(chunk.len()) > MAX_WORKSPACE_CONFIG_RESPONSE_BYTES {
return Err("latest Workspace Config response exceeds the size limit".to_string());
}
body.extend_from_slice(&chunk);
}
let bundle = serde_json::from_slice::<ConfigBundle>(&body)
.map_err(|error| format!("failed to decode latest Workspace Config: {error}"))?;
let expected_etag = workspace_config_etag(&bundle.metadata.digest);
if response_etag != expected_etag {
return Err(format!( return Err(format!(
"profile source archive size mismatch: expected {}, got {}", "latest Workspace Config ETag mismatch: expected {expected_etag}, got {response_etag}"
archive.reference.size_bytes,
archive.content.len()
)); ));
} }
Ok(Some(archive)) Ok(WorkspaceConfigFetchResult::Modified(bundle))
} }
#[cfg(not(feature = "http-server"))] #[cfg(not(feature = "http-server"))]
async fn fetch_profile_source_archive_http( async fn fetch_workspace_config_http(
_location: &ProfileSourceArchiveHttpRef, _request: &WorkspaceConfigFetchRequest,
_cached_digest: Option<&str>,
_identity: Option<&RuntimeIdentityMaterial>, _identity: Option<&RuntimeIdentityMaterial>,
_audience: Option<&str>, _audience: Option<&str>,
) -> Result<Option<crate::profile_archive::ProfileSourceArchive>, String> { ) -> Result<WorkspaceConfigFetchResult, String> {
Err( Err("Workspace Config fetch requires the worker-runtime http-server feature".to_string())
"HTTP profile source archive fetch requires the worker-runtime http-server feature"
.to_string(),
)
} }
fn runtime_local_workdir_session( fn runtime_local_workdir_session(
@@ -803,6 +800,18 @@ fn validate_worker_memory_settings(
#[async_trait] #[async_trait]
impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory { impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
async fn fetch_workspace_config(
&self,
request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
fetch_workspace_config_http(
&request,
self.worker_mutation_identity.as_ref(),
self.runtime_request_audience.as_deref(),
)
.await
}
fn observe_workspace_prompt_projection( fn observe_workspace_prompt_projection(
&self, &self,
projection: worker::WorkspacePromptProjection, projection: worker::WorkspacePromptProjection,
@@ -856,10 +865,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
let archive = self let archive = self
.resolve_profile_source_archive( .resolve_profile_source_archive(
&request.request.profile_source, &request.request.profile_source,
request request.config_bundle.as_ref(),
.workspace_scope
.as_ref()
.map(|scope| scope.server_id.as_str()),
) )
.await?; .await?;
let (mut manifest, mut loader) = { let (mut manifest, mut loader) = {
@@ -1557,6 +1563,14 @@ where
&self.backend_id &self.backend_id
} }
fn fetch_workspace_config(
&self,
request: WorkspaceConfigFetchRequest,
) -> Result<WorkspaceConfigFetchResult, String> {
let factory = self.factory.clone();
self.run_on_adapter_runtime(async move { factory.fetch_workspace_config(request).await })
}
fn observe_workspace_prompt_projection( fn observe_workspace_prompt_projection(
&self, &self,
projection: worker::WorkspacePromptProjection, projection: worker::WorkspacePromptProjection,
+32 -102
View File
@@ -1,5 +1,7 @@
use crate::Error; use crate::Error;
use crate::resource_broker::{BackendResourceBroker, BackendResourceTarget}; use crate::resource_broker::BackendResourceBroker;
#[cfg(test)]
use crate::resource_broker::BackendResourceTarget;
use chrono::Utc; use chrono::Utc;
use protocol::Segment; use protocol::Segment;
use reqwest::blocking::{Client as BlockingHttpClient, RequestBuilder}; use reqwest::blocking::{Client as BlockingHttpClient, RequestBuilder};
@@ -24,8 +26,8 @@ use workdir::{
use worker_runtime::RuntimeWorkspaceScope; use worker_runtime::RuntimeWorkspaceScope;
use worker_runtime::auth::{CapabilityTokenSigner, capability_claims}; use worker_runtime::auth::{CapabilityTokenSigner, capability_claims};
use worker_runtime::catalog::{ use worker_runtime::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveHttpRef, ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
ProfileSourceArchiveSource, RepositoryRefObservation, RepositoryRefObservationRequest, RepositoryRefObservation, RepositoryRefObservationRequest,
WorkerDetail as EmbeddedWorkerDetail, WorkerStatus as EmbeddedWorkerStatus, WorkerDetail as EmbeddedWorkerDetail, WorkerStatus as EmbeddedWorkerStatus,
WorkingDirectoryClaim, WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest, WorkingDirectoryClaim, WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest,
WorkingDirectoryStatus, WorkingDirectorySummary, WorkspaceApiRef, WorkingDirectoryStatus, WorkingDirectorySummary, WorkspaceApiRef,
@@ -57,7 +59,7 @@ use worker_runtime::interaction::{
WorkerInput as EmbeddedWorkerInput, WorkerInputKind as EmbeddedWorkerInputKind, WorkerInput as EmbeddedWorkerInput, WorkerInputKind as EmbeddedWorkerInputKind,
}; };
use worker_runtime::management::{RuntimeOptions as EmbeddedRuntimeOptions, RuntimeStatus}; use worker_runtime::management::{RuntimeOptions as EmbeddedRuntimeOptions, RuntimeStatus};
use worker_runtime::profile_archive::{ProfileSourceArchive, ProfileSourceArchiveInput}; use worker_runtime::profile_archive::ProfileSourceArchive;
use worker_runtime::retention::{ use worker_runtime::retention::{
WorkerRetentionExecutionRequest, WorkerRetentionExecutionResult, WorkerRetentionInventory, WorkerRetentionExecutionRequest, WorkerRetentionExecutionResult, WorkerRetentionInventory,
}; };
@@ -1481,7 +1483,9 @@ impl RuntimeRegistry {
_ => {} _ => {}
} }
let runtime = self.runtime(runtime_id)?; let runtime = self.runtime(runtime_id)?;
if let Some(bundle) = request.resolved_config_bundle.clone() { if runtime_id == EMBEDDED_RUNTIME_ID
&& let Some(bundle) = request.resolved_config_bundle.clone()
{
let sync = runtime.sync_config_bundle(bundle); let sync = runtime.sync_config_bundle(bundle);
if sync.state != WorkerOperationState::Accepted { if sync.state != WorkerOperationState::Accepted {
let message = sync let message = sync
@@ -2972,7 +2976,6 @@ pub struct RemoteWorkerRuntime {
runtime_id: String, runtime_id: String,
display_name: String, display_name: String,
base_url: String, base_url: String,
backend_base_url: String,
workspace_id: String, workspace_id: String,
bearer_token: Option<String>, bearer_token: Option<String>,
auth: Option<RemoteRuntimeAuthConfig>, auth: Option<RemoteRuntimeAuthConfig>,
@@ -3049,7 +3052,7 @@ impl RemoteWorkerRuntime {
pub fn new( pub fn new(
config: RemoteRuntimeConfig, config: RemoteRuntimeConfig,
workspace_id: String, workspace_id: String,
backend_base_url: String, _backend_base_url: String,
) -> Result<Self, RuntimeRegistryError> { ) -> Result<Self, RuntimeRegistryError> {
validate_backend_identifier("runtime_id", &config.runtime_id)?; validate_backend_identifier("runtime_id", &config.runtime_id)?;
let base_url = config.base_url.trim_end_matches('/').to_string(); let base_url = config.base_url.trim_end_matches('/').to_string();
@@ -3077,7 +3080,6 @@ impl RemoteWorkerRuntime {
runtime_id: config.runtime_id, runtime_id: config.runtime_id,
display_name: config.display_name, display_name: config.display_name,
base_url, base_url,
backend_base_url: backend_base_url.trim_end_matches('/').to_string(),
workspace_id, workspace_id,
bearer_token: config.bearer_token, bearer_token: config.bearer_token,
auth: config.auth, auth: config.auth,
@@ -3722,28 +3724,24 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
}; };
} }
let profile = request.profile.clone(); let profile = request.profile.clone();
let profile_source = match profile_source_archive_http_source( let profile_source_archive = match profile_source_archive_for_request(&request, &profile) {
&request, Ok(archive) => archive,
&profile,
&self.workspace_id,
Some(self.runtime_id.as_str()),
&self.resource_broker,
&self.backend_base_url,
) {
Ok(source) => source,
Err(error) => { Err(error) => {
return WorkerSpawnResult { return WorkerSpawnResult {
state: WorkerOperationState::Rejected, state: WorkerOperationState::Rejected,
worker: None, worker: None,
acceptance_evidence: Vec::new(), acceptance_evidence: Vec::new(),
diagnostics: vec![diagnostic( diagnostics: vec![diagnostic(
"remote_profile_source_archive_invalid", "remote_workspace_config_invalid",
DiagnosticSeverity::Error, DiagnosticSeverity::Error,
error, error,
)], )],
}; };
} }
}; };
let profile_source = ProfileSourceArchiveSource::WorkspaceConfig {
archive: profile_source_archive.reference,
};
let workspace_api = match required_worker_workspace_api(&request) { let workspace_api = match required_worker_workspace_api(&request) {
Ok(workspace_api) => workspace_api, Ok(workspace_api) => workspace_api,
Err(diagnostic) => { Err(diagnostic) => {
@@ -4088,7 +4086,7 @@ fn profile_source_archive_for_request(
{ {
return Ok(archive); return Ok(archive);
} }
builtin_profile_source_archive(profile) crate::profile_settings::builtin_profile_source_archive(profile)
} }
fn profile_source_archive_source( fn profile_source_archive_source(
@@ -4100,36 +4098,14 @@ fn profile_source_archive_source(
}) })
} }
fn profile_source_archive_http_source( #[cfg(test)]
fn profile_source_archive_workspace_config_source(
request: &WorkerSpawnRequest, request: &WorkerSpawnRequest,
profile: &ProfileSelector, profile: &ProfileSelector,
workspace_id: &str,
runtime_id: Option<&str>,
resource_broker: &BackendResourceBroker,
backend_base_url: &str,
) -> Result<ProfileSourceArchiveSource, String> { ) -> Result<ProfileSourceArchiveSource, String> {
let archive = profile_source_archive_for_request(request, profile)?; let archive = profile_source_archive_for_request(request, profile)?;
let target = runtime_id Ok(ProfileSourceArchiveSource::WorkspaceConfig {
.map(BackendResourceTarget::Runtime) archive: archive.reference,
.unwrap_or(BackendResourceTarget::Workspace);
let _handle = resource_broker.issue_profile_source_archive_handle(
workspace_id.to_string(),
target,
archive.clone(),
);
let etag = format!("\"profile-source:{}\"", archive.reference.digest);
let url = format!(
"{}/api/w/{}/profile-source-archives/{}",
backend_base_url.trim_end_matches('/'),
workspace_id,
archive.reference.digest
);
Ok(ProfileSourceArchiveSource::Http {
location: ProfileSourceArchiveHttpRef {
url,
etag: Some(etag),
archive: archive.reference.clone(),
},
}) })
} }
@@ -4154,7 +4130,7 @@ fn builtin_profile_config_bundle(
.unwrap_or_else(|| "default".to_string()) .unwrap_or_else(|| "default".to_string())
.replace([':', '/', ' '], "-") .replace([':', '/', ' '], "-")
); );
let archive = builtin_profile_source_archive(profile)?; let archive = crate::profile_settings::builtin_profile_source_archive(profile)?;
let (profile_source_archive, profile_source_archive_handle) = match archive_transport { let (profile_source_archive, profile_source_archive_handle) = match archive_transport {
ProfileSourceArchiveTransport::Inline => (Some(archive), None), ProfileSourceArchiveTransport::Inline => (Some(archive), None),
ProfileSourceArchiveTransport::BackendResourceHandle => { ProfileSourceArchiveTransport::BackendResourceHandle => {
@@ -4208,39 +4184,6 @@ fn embedded_profile_label(profile: &ProfileSelector) -> Option<String> {
}) })
} }
fn builtin_profile_source_archive(
profile: &ProfileSelector,
) -> Result<ProfileSourceArchive, String> {
let selected_profile = match profile {
ProfileSelector::Builtin(name) => {
if name.starts_with("builtin:") {
name.clone()
} else {
format!("builtin:{name}")
}
}
ProfileSelector::Named(name) => {
return Err(format!(
"embedded runtime does not provide named Profile `{name}`"
));
}
};
let catalog = manifest::builtin_profile_catalog_snapshot();
if !catalog.entrypoints.contains_key(&selected_profile) {
return Err(format!(
"embedded runtime does not provide Profile `{selected_profile}`"
));
}
ProfileSourceArchive::build(ProfileSourceArchiveInput {
id: catalog.id.to_owned(),
sources: catalog.sources,
entrypoints: catalog.entrypoints,
imports: catalog.imports,
})
.map_err(|error| format!("failed to build built-in Profile source archive: {error}"))
}
const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation"; const MEMORY_CONSOLIDATION_PROFILE: &str = "memory-consolidation";
const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation"; const MEMORY_CONSOLIDATION_SINGLETON_KEY: &str = "workspace-memory-consolidation";
const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator"; const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator";
@@ -4940,7 +4883,7 @@ mod tests {
fn resolved_project_profile_archive_is_used_for_runtime_delivery() { fn resolved_project_profile_archive_is_used_for_runtime_delivery() {
let broker = BackendResourceBroker::default(); let broker = BackendResourceBroker::default();
let builtin_selector = ProfileSelector::Builtin("builtin:coder".to_string()); let builtin_selector = ProfileSelector::Builtin("builtin:coder".to_string());
let archive = builtin_profile_source_archive(&builtin_selector) let archive = crate::profile_settings::builtin_profile_source_archive(&builtin_selector)
.expect("build stand-in project profile archive"); .expect("build stand-in project profile archive");
let mut bundle = builtin_profile_config_bundle( let mut bundle = builtin_profile_config_bundle(
&builtin_selector, &builtin_selector,
@@ -4962,30 +4905,17 @@ mod tests {
} }
#[test] #[test]
fn remote_profile_source_archive_url_uses_workspace_id_not_host_id() { fn remote_profile_source_uses_workspace_config_archive_reference() {
let broker = BackendResourceBroker::default();
let runtime_id = "remote:test";
let request = embedded_spawn_request(); let request = embedded_spawn_request();
let source = profile_source_archive_http_source( let profile = ProfileSelector::Builtin("builtin:coder".to_string());
&request, let expected = profile_source_archive_for_request(&request, &profile)
&ProfileSelector::Builtin("builtin:coder".to_string()), .unwrap()
"workspace-actual", .reference;
Some(runtime_id), let source = profile_source_archive_workspace_config_source(&request, &profile).unwrap();
&broker, assert_eq!(
"http://127.0.0.1:8787/", source,
) ProfileSourceArchiveSource::WorkspaceConfig { archive: expected }
.unwrap();
let ProfileSourceArchiveSource::Http { location } = source else {
panic!("remote profile source should be HTTP fetched");
};
assert!(
location.url.starts_with(
"http://127.0.0.1:8787/api/w/workspace-actual/profile-source-archives/"
),
"{}",
location.url
); );
assert!(!location.url.contains("remote-runtime"), "{}", location.url);
} }
#[test] #[test]
@@ -4,7 +4,7 @@ use std::path::{Component, Path, PathBuf};
use std::time::UNIX_EPOCH; use std::time::UNIX_EPOCH;
use config_source::{ConfigContentType, ConfigSchemaContribution, VirtualPath}; use config_source::{ConfigContentType, ConfigSchemaContribution, VirtualPath};
use manifest::{ProfileSource, resolve_profile_artifact_value}; use manifest::{ProfileSource, builtin_profile_catalog_snapshot, resolve_profile_artifact_value};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use worker::EffectivePromptCatalog; use worker::EffectivePromptCatalog;
@@ -361,6 +361,38 @@ pub fn build_virtual_profile_config_bundle(
) )
} }
pub(crate) fn builtin_profile_source_archive(
profile: &worker_runtime::catalog::ProfileSelector,
) -> std::result::Result<ProfileSourceArchive, String> {
let selected_profile = match profile {
worker_runtime::catalog::ProfileSelector::Builtin(value) => {
if value.starts_with("builtin:") {
value.clone()
} else {
format!("builtin:{value}")
}
}
worker_runtime::catalog::ProfileSelector::Named(value) => {
return Err(format!(
"builtin profile source catalog has no named entrypoint for '{value}'"
));
}
};
let catalog = builtin_profile_catalog_snapshot();
if !catalog.entrypoints.contains_key(&selected_profile) {
return Err(format!(
"builtin profile source catalog has no entrypoint for '{selected_profile}'"
));
}
ProfileSourceArchive::build(ProfileSourceArchiveInput {
id: catalog.id.to_owned(),
entrypoints: catalog.entrypoints,
imports: catalog.imports,
sources: catalog.sources,
})
.map_err(|error| format!("failed to build builtin profile source archive: {error}"))
}
pub fn build_virtual_profile_config_bundle_with_prompt_projection( pub fn build_virtual_profile_config_bundle_with_prompt_projection(
projection: &ProfileConfigProjection, projection: &ProfileConfigProjection,
state: &WorkspaceConfigState, state: &WorkspaceConfigState,
@@ -371,13 +403,17 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection(
) -> Result<Option<ConfigBundle>> { ) -> Result<Option<ConfigBundle>> {
validate_prompt_projection_matches_state(workspace_id, state, prompt_projection)?; validate_prompt_projection_matches_state(workspace_id, state, prompt_projection)?;
let prompt_catalog = prompt_projection.catalog().clone(); let prompt_catalog = prompt_projection.catalog().clone();
let archive = projection
.entries
.get(selector)
.map(|entry| build_virtual_profile_archive(selector, entry, &projection.sources, state))
.transpose()?;
let profile_selector = selector_for_builtin_candidate(selector) let profile_selector = selector_for_builtin_candidate(selector)
.unwrap_or_else(|| worker_runtime::catalog::ProfileSelector::Named(selector.to_string())); .unwrap_or_else(|| worker_runtime::catalog::ProfileSelector::Named(selector.to_string()));
let archive = match projection.entries.get(selector) {
Some(entry) => Some(build_virtual_profile_archive(
selector,
entry,
&projection.sources,
state,
)?),
None => Some(builtin_profile_source_archive(&profile_selector).map_err(Error::Store)?),
};
let bundle_id = virtual_profile_bundle_id( let bundle_id = virtual_profile_bundle_id(
state, state,
workspace_id, workspace_id,
@@ -1,15 +1,12 @@
use super::*; use super::*;
use protocol::subscription::{SubscriptionWorkerIds, SubscriptionWorkerState}; use protocol::subscription::{SubscriptionWorkerIds, SubscriptionWorkerState};
use worker_runtime::Runtime; use worker_runtime::Runtime;
use worker_runtime::catalog::{ use worker_runtime::catalog::{CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource};
CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveHttpRef, ProfileSourceArchiveSource,
};
use worker_runtime::execution::{ use worker_runtime::execution::{
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult, WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult, WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult,
}; };
use worker_runtime::identity::WorkerId; use worker_runtime::identity::WorkerId;
use worker_runtime::profile_archive::{ProfileSourceArchiveRef, ProfileSourceGraphSummary};
#[derive(Debug)] #[derive(Debug)]
struct TestExecutionBackend; struct TestExecutionBackend;
@@ -57,22 +54,11 @@ fn create_request(name: &str) -> CreateWorkerRequest {
profile: ProfileSelector::Builtin("builtin:companion".to_string()), profile: ProfileSelector::Builtin("builtin:companion".to_string()),
display_name: Some(name.to_string()), display_name: Some(name.to_string()),
config_bundle: None, config_bundle: None,
profile_source: ProfileSourceArchiveSource::Http { profile_source: ProfileSourceArchiveSource::Embedded {
location: ProfileSourceArchiveHttpRef { archive: crate::profile_settings::builtin_profile_source_archive(
url: "http://127.0.0.1/profiles/test".to_string(), &ProfileSelector::Builtin("builtin:default".to_string()),
etag: None, )
archive: ProfileSourceArchiveRef { .unwrap(),
id: "test-profile-source".to_string(),
digest: "test-digest".to_string(),
size_bytes: 0,
source_graph: ProfileSourceGraphSummary {
source_count: 0,
total_source_bytes: 0,
entrypoints: std::collections::BTreeMap::new(),
import_count: 0,
},
},
},
}, },
initial_input: None, initial_input: None,
working_directory_request: None, working_directory_request: None,
+153 -72
View File
@@ -6,7 +6,9 @@ use std::sync::{Arc, Mutex, RwLock, Weak};
use axum::body::Bytes; use axum::body::Bytes;
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
use axum::extract::{DefaultBodyLimit, Extension, Path as AxumPath, Query, Request, State}; use axum::extract::{DefaultBodyLimit, Extension, Path as AxumPath, Query, Request, State};
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE}; use axum::http::header::{
CACHE_CONTROL, CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE,
};
use axum::http::{HeaderMap, Method, StatusCode, Uri}; use axum::http::{HeaderMap, Method, StatusCode, Uri};
use axum::middleware::{self, Next}; use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
@@ -1596,7 +1598,10 @@ async fn authorize_scoped_workspace_request(
{ {
worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION
} else if path.starts_with("/api/runtime/v1/workspaces/") } else if path.starts_with("/api/runtime/v1/workspaces/")
|| path.contains("/profile-source-archives/") || path
.split('?')
.next()
.is_some_and(|path| path.ends_with("/runtime-config"))
{ {
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION
} else { } else {
@@ -1705,7 +1710,10 @@ async fn authorize_workspace_api_request(
{ {
worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION
} else if path.starts_with("/api/runtime/v1/workspaces/") } else if path.starts_with("/api/runtime/v1/workspaces/")
|| path.contains("/profile-source-archives/") || path
.split('?')
.next()
.is_some_and(|path| path.ends_with("/runtime-config"))
{ {
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION
} else { } else {
@@ -3112,8 +3120,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
.route("/api/hosts", get(list_hosts)) .route("/api/hosts", get(list_hosts))
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts)) .route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
.route( .route(
"/api/w/{workspace_id}/profile-source-archives/{digest}", "/api/w/{workspace_id}/runtime-config",
get(scoped_get_profile_source_archive), get(get_latest_workspace_runtime_config),
) )
.route( .route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories", "/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories",
@@ -3752,12 +3760,6 @@ struct ScopedRepositoryHostTrustPath {
host_trust_id: String, host_trust_id: String,
} }
#[derive(Debug, Deserialize)]
struct ScopedProfileArchivePath {
workspace_id: String,
digest: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ScopedHostPath { struct ScopedHostPath {
workspace_id: String, workspace_id: String,
@@ -8940,30 +8942,99 @@ async fn scoped_list_hosts(
list_hosts(State(api)).await list_hosts(State(api)).await
} }
async fn scoped_get_profile_source_archive( #[derive(Debug, Deserialize)]
struct LatestWorkspaceRuntimeConfigQuery {
profile: String,
}
async fn get_latest_workspace_runtime_config(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedProfileArchivePath>, AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<LatestWorkspaceRuntimeConfigQuery>,
headers: HeaderMap, headers: HeaderMap,
) -> ApiResult<(StatusCode, HeaderMap, Vec<u8>)> { source: Option<Extension<crate::worker_source::VerifiedRuntimeRequestSource>>,
validate_workspace_scope(&api, &path.workspace_id)?; ) -> Response {
let archive = api let Some(Extension(_source)) = source else {
.resource_broker return (
.profile_source_archive(&path.digest) StatusCode::FORBIDDEN,
.ok_or_else(|| Error::Store("profile source archive not found".to_string()))?; Json(serde_json::json!({ "error": "runtime_config_source_required" })),
let etag = format!("\"profile-source:{}\"", archive.reference.digest); )
.into_response();
};
if let Err(error) = validate_workspace_scope(&api, &path.workspace_id) {
return error.into_response();
}
let config_state = match api.config_store.load_workspace_config(&path.workspace_id) {
Ok(Some(config)) => config,
Ok(None) => {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "workspace_config_not_found" })),
)
.into_response();
}
Err(error) => return ApiError::from(error).into_response(),
};
let profile_projection = match crate::profile_settings::project_profiles_from_workspace_config(
&path.workspace_id,
&config_state,
) {
Ok(projection) => projection,
Err(error) => return ApiError::from(error).into_response(),
};
if crate::profile_settings::selector_for_workspace_candidate(
&profile_projection,
&query.profile,
)
.is_none()
{
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "runtime_config_profile_not_found" })),
)
.into_response();
};
let prompt_catalog = match api
.prompt_projection_cache
.resolve(&path.workspace_id, &config_state)
{
Ok(catalog) => catalog,
Err(error) => return ApiError::from(error).into_response(),
};
let Some(bundle) =
(match crate::profile_settings::build_virtual_profile_config_bundle_with_prompt_projection(
&profile_projection,
&config_state,
&path.workspace_id,
&api.config.workspace_created_at,
&query.profile,
prompt_catalog.as_ref(),
) {
Ok(bundle) => bundle,
Err(error) => return ApiError::from(error).into_response(),
})
else {
return (
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "runtime_config_profile_not_found" })),
)
.into_response();
};
let etag = worker_runtime::config_bundle::workspace_config_etag(&bundle.metadata.digest);
let mut response_headers = HeaderMap::new();
response_headers.insert(ETAG, etag.parse().expect("Workspace Config ETag is valid"));
response_headers.insert(
CACHE_CONTROL,
"no-cache".parse().expect("valid cache policy"),
);
if headers if headers
.get(IF_NONE_MATCH) .get(IF_NONE_MATCH)
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.is_some_and(|value| value.split(',').any(|candidate| candidate.trim() == etag)) .is_some_and(|value| value.split(',').any(|candidate| candidate.trim() == etag))
{ {
let mut response_headers = HeaderMap::new(); return (StatusCode::NOT_MODIFIED, response_headers).into_response();
response_headers.insert(ETAG, etag.parse().unwrap());
return Ok((StatusCode::NOT_MODIFIED, response_headers, Vec::new()));
} }
let mut response_headers = HeaderMap::new(); (StatusCode::OK, response_headers, Json(bundle)).into_response()
response_headers.insert(ETAG, etag.parse().unwrap());
response_headers.insert(CONTENT_TYPE, "application/x-tar".parse().unwrap());
Ok((StatusCode::OK, response_headers, archive.content))
} }
async fn scoped_list_runtimes( async fn scoped_list_runtimes(
@@ -25846,42 +25917,39 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn runtime_signed_profile_source_archive_fetch_uses_resource_permission() { async fn runtime_fetches_latest_workspace_config_with_etag_revalidation() {
let workspace = tempfile::tempdir().unwrap(); let workspace = tempfile::tempdir().unwrap();
init_clean_git_workspace(workspace.path()); init_clean_git_workspace(workspace.path());
let mut api = test_api(workspace.path()).await; let mut api = test_api(workspace.path()).await;
let identity = let identity =
worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-test").unwrap(); worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-test").unwrap();
configure_runtime_request_auth(&mut api, &identity, "runtime-test"); configure_runtime_request_auth(&mut api, &identity, "runtime-test");
let handle = api.resource_broker.issue_profile_source_archive_handle( let target =
TEST_WORKSPACE_ID, format!("/api/w/{TEST_WORKSPACE_ID}/runtime-config?profile=builtin%3Acompanion");
crate::resource_broker::BackendResourceTarget::Runtime("runtime-test"), let issue = || {
test_profile_archive(), worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity)
); .issue(
let path = format!( "server-test",
"/api/w/{TEST_WORKSPACE_ID}/profile-source-archives/{}", TEST_WORKSPACE_ID,
handle.digest None,
); worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION,
let proof = worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity) "GET",
.issue( &target,
"server-test", b"",
TEST_WORKSPACE_ID, i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
None, 30,
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION, )
"GET", .unwrap()
&path, };
b"", let app = build_router(api);
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX), let response = app
30, .clone()
)
.unwrap();
let response = build_router(api)
.oneshot( .oneshot(
Request::builder() Request::builder()
.uri(path) .uri(&target)
.header( .header(
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER, worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
proof, issue(),
) )
.body(Body::empty()) .body(Body::empty())
.unwrap(), .unwrap(),
@@ -25889,12 +25957,34 @@ mod tests {
.await .await
.unwrap(); .unwrap();
assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.status(), StatusCode::OK);
let etag = response.headers().get(ETAG).unwrap().clone();
assert_eq!(response.headers().get(CACHE_CONTROL).unwrap(), "no-cache");
let bundle: worker_runtime::config_bundle::ConfigBundle =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
assert!(bundle.profile_source_archive.is_some());
assert_eq!( assert_eq!(
response.headers().get(ETAG).unwrap().to_str().unwrap(), etag.to_str().unwrap(),
format!("\"profile-source:{}\"", handle.digest) worker_runtime::config_bundle::workspace_config_etag(&bundle.metadata.digest)
); );
let response = app
.oneshot(
Request::builder()
.uri(&target)
.header(
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
issue(),
)
.header(IF_NONE_MATCH, etag)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
assert!( assert!(
!to_bytes(response.into_body(), usize::MAX) to_bytes(response.into_body(), usize::MAX)
.await .await
.unwrap() .unwrap()
.is_empty() .is_empty()
@@ -25994,22 +26084,13 @@ mod tests {
"builtin:companion".to_string(), "builtin:companion".to_string(),
), ),
display_name: None, display_name: None,
profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Http { profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Embedded {
location: worker_runtime::catalog::ProfileSourceArchiveHttpRef { archive: crate::profile_settings::builtin_profile_source_archive(
url: "http://127.0.0.1/profile-source.tar".to_string(), &worker_runtime::catalog::ProfileSelector::Builtin(
etag: None, "builtin:default".to_string(),
archive: worker_runtime::profile_archive::ProfileSourceArchiveRef { ),
id: "test-profile-source".to_string(), )
digest: "test-digest".to_string(), .unwrap(),
size_bytes: 0,
source_graph: worker_runtime::profile_archive::ProfileSourceGraphSummary {
source_count: 0,
total_source_bytes: 0,
entrypoints: std::collections::BTreeMap::new(),
import_count: 0,
},
},
},
}, },
config_bundle: Some(worker_runtime::catalog::ConfigBundleRef { config_bundle: Some(worker_runtime::catalog::ConfigBundleRef {
id: bundle.metadata.id, id: bundle.metadata.id,