feat: distribute latest workspace config to runtimes
This commit is contained in:
@@ -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.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_profile_source_archive(
|
|
||||||
&self,
|
|
||||||
location: &ProfileSourceArchiveHttpRef,
|
|
||||||
request_audience: Option<&str>,
|
|
||||||
) -> 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),
|
|
||||||
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());
|
let embedded = bundle.profile_source_archive.as_ref().ok_or_else(|| {
|
||||||
archive
|
"resolved Workspace Config is missing its profile source archive".to_string()
|
||||||
.verify()
|
})?;
|
||||||
.map_err(|err| format!("failed to verify fetched profile source archive: {err}"))
|
if &embedded.reference != archive {
|
||||||
|
return Err(
|
||||||
|
"Workspace Config profile source archive reference mismatch".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
embedded.verify().map_err(|err| {
|
||||||
|
format!("failed to verify Workspace Config 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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
);
|
|
||||||
let path = format!(
|
|
||||||
"/api/w/{TEST_WORKSPACE_ID}/profile-source-archives/{}",
|
|
||||||
handle.digest
|
|
||||||
);
|
|
||||||
let proof = worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity)
|
|
||||||
.issue(
|
.issue(
|
||||||
"server-test",
|
"server-test",
|
||||||
TEST_WORKSPACE_ID,
|
TEST_WORKSPACE_ID,
|
||||||
None,
|
None,
|
||||||
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION,
|
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION,
|
||||||
"GET",
|
"GET",
|
||||||
&path,
|
&target,
|
||||||
b"",
|
b"",
|
||||||
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
|
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
|
||||||
30,
|
30,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap()
|
||||||
let response = build_router(api)
|
};
|
||||||
|
let app = build_router(api);
|
||||||
|
let response = app
|
||||||
|
.clone()
|
||||||
.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,
|
||||||
|
|||||||
Reference in New Issue
Block a user