feat: distribute latest workspace config to runtimes
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
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 protocol::Segment;
|
||||
use reqwest::blocking::{Client as BlockingHttpClient, RequestBuilder};
|
||||
@@ -24,8 +26,8 @@ use workdir::{
|
||||
use worker_runtime::RuntimeWorkspaceScope;
|
||||
use worker_runtime::auth::{CapabilityTokenSigner, capability_claims};
|
||||
use worker_runtime::catalog::{
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveHttpRef,
|
||||
ProfileSourceArchiveSource, RepositoryRefObservation, RepositoryRefObservationRequest,
|
||||
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
|
||||
RepositoryRefObservation, RepositoryRefObservationRequest,
|
||||
WorkerDetail as EmbeddedWorkerDetail, WorkerStatus as EmbeddedWorkerStatus,
|
||||
WorkingDirectoryClaim, WorkingDirectoryRepositoryAccessRequest, WorkingDirectoryRequest,
|
||||
WorkingDirectoryStatus, WorkingDirectorySummary, WorkspaceApiRef,
|
||||
@@ -57,7 +59,7 @@ use worker_runtime::interaction::{
|
||||
WorkerInput as EmbeddedWorkerInput, WorkerInputKind as EmbeddedWorkerInputKind,
|
||||
};
|
||||
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::{
|
||||
WorkerRetentionExecutionRequest, WorkerRetentionExecutionResult, WorkerRetentionInventory,
|
||||
};
|
||||
@@ -1481,7 +1483,9 @@ impl RuntimeRegistry {
|
||||
_ => {}
|
||||
}
|
||||
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);
|
||||
if sync.state != WorkerOperationState::Accepted {
|
||||
let message = sync
|
||||
@@ -2972,7 +2976,6 @@ pub struct RemoteWorkerRuntime {
|
||||
runtime_id: String,
|
||||
display_name: String,
|
||||
base_url: String,
|
||||
backend_base_url: String,
|
||||
workspace_id: String,
|
||||
bearer_token: Option<String>,
|
||||
auth: Option<RemoteRuntimeAuthConfig>,
|
||||
@@ -3049,7 +3052,7 @@ impl RemoteWorkerRuntime {
|
||||
pub fn new(
|
||||
config: RemoteRuntimeConfig,
|
||||
workspace_id: String,
|
||||
backend_base_url: String,
|
||||
_backend_base_url: String,
|
||||
) -> Result<Self, RuntimeRegistryError> {
|
||||
validate_backend_identifier("runtime_id", &config.runtime_id)?;
|
||||
let base_url = config.base_url.trim_end_matches('/').to_string();
|
||||
@@ -3077,7 +3080,6 @@ impl RemoteWorkerRuntime {
|
||||
runtime_id: config.runtime_id,
|
||||
display_name: config.display_name,
|
||||
base_url,
|
||||
backend_base_url: backend_base_url.trim_end_matches('/').to_string(),
|
||||
workspace_id,
|
||||
bearer_token: config.bearer_token,
|
||||
auth: config.auth,
|
||||
@@ -3722,28 +3724,24 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||
};
|
||||
}
|
||||
let profile = request.profile.clone();
|
||||
let profile_source = match profile_source_archive_http_source(
|
||||
&request,
|
||||
&profile,
|
||||
&self.workspace_id,
|
||||
Some(self.runtime_id.as_str()),
|
||||
&self.resource_broker,
|
||||
&self.backend_base_url,
|
||||
) {
|
||||
Ok(source) => source,
|
||||
let profile_source_archive = match profile_source_archive_for_request(&request, &profile) {
|
||||
Ok(archive) => archive,
|
||||
Err(error) => {
|
||||
return WorkerSpawnResult {
|
||||
state: WorkerOperationState::Rejected,
|
||||
worker: None,
|
||||
acceptance_evidence: Vec::new(),
|
||||
diagnostics: vec![diagnostic(
|
||||
"remote_profile_source_archive_invalid",
|
||||
"remote_workspace_config_invalid",
|
||||
DiagnosticSeverity::Error,
|
||||
error,
|
||||
)],
|
||||
};
|
||||
}
|
||||
};
|
||||
let profile_source = ProfileSourceArchiveSource::WorkspaceConfig {
|
||||
archive: profile_source_archive.reference,
|
||||
};
|
||||
let workspace_api = match required_worker_workspace_api(&request) {
|
||||
Ok(workspace_api) => workspace_api,
|
||||
Err(diagnostic) => {
|
||||
@@ -4088,7 +4086,7 @@ fn profile_source_archive_for_request(
|
||||
{
|
||||
return Ok(archive);
|
||||
}
|
||||
builtin_profile_source_archive(profile)
|
||||
crate::profile_settings::builtin_profile_source_archive(profile)
|
||||
}
|
||||
|
||||
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,
|
||||
profile: &ProfileSelector,
|
||||
workspace_id: &str,
|
||||
runtime_id: Option<&str>,
|
||||
resource_broker: &BackendResourceBroker,
|
||||
backend_base_url: &str,
|
||||
) -> Result<ProfileSourceArchiveSource, String> {
|
||||
let archive = profile_source_archive_for_request(request, profile)?;
|
||||
let target = runtime_id
|
||||
.map(BackendResourceTarget::Runtime)
|
||||
.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(),
|
||||
},
|
||||
Ok(ProfileSourceArchiveSource::WorkspaceConfig {
|
||||
archive: archive.reference,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4154,7 +4130,7 @@ fn builtin_profile_config_bundle(
|
||||
.unwrap_or_else(|| "default".to_string())
|
||||
.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 {
|
||||
ProfileSourceArchiveTransport::Inline => (Some(archive), None),
|
||||
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_SINGLETON_KEY: &str = "workspace-memory-consolidation";
|
||||
const WORKSPACE_ORCHESTRATOR_PROFILE: &str = "orchestrator";
|
||||
@@ -4940,7 +4883,7 @@ mod tests {
|
||||
fn resolved_project_profile_archive_is_used_for_runtime_delivery() {
|
||||
let broker = BackendResourceBroker::default();
|
||||
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");
|
||||
let mut bundle = builtin_profile_config_bundle(
|
||||
&builtin_selector,
|
||||
@@ -4962,30 +4905,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_profile_source_archive_url_uses_workspace_id_not_host_id() {
|
||||
let broker = BackendResourceBroker::default();
|
||||
let runtime_id = "remote:test";
|
||||
fn remote_profile_source_uses_workspace_config_archive_reference() {
|
||||
let request = embedded_spawn_request();
|
||||
let source = profile_source_archive_http_source(
|
||||
&request,
|
||||
&ProfileSelector::Builtin("builtin:coder".to_string()),
|
||||
"workspace-actual",
|
||||
Some(runtime_id),
|
||||
&broker,
|
||||
"http://127.0.0.1:8787/",
|
||||
)
|
||||
.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
|
||||
let profile = ProfileSelector::Builtin("builtin:coder".to_string());
|
||||
let expected = profile_source_archive_for_request(&request, &profile)
|
||||
.unwrap()
|
||||
.reference;
|
||||
let source = profile_source_archive_workspace_config_source(&request, &profile).unwrap();
|
||||
assert_eq!(
|
||||
source,
|
||||
ProfileSourceArchiveSource::WorkspaceConfig { archive: expected }
|
||||
);
|
||||
assert!(!location.url.contains("remote-runtime"), "{}", location.url);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::path::{Component, Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
|
||||
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 sha2::{Digest, Sha256};
|
||||
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(
|
||||
projection: &ProfileConfigProjection,
|
||||
state: &WorkspaceConfigState,
|
||||
@@ -371,13 +403,17 @@ pub fn build_virtual_profile_config_bundle_with_prompt_projection(
|
||||
) -> Result<Option<ConfigBundle>> {
|
||||
validate_prompt_projection_matches_state(workspace_id, state, prompt_projection)?;
|
||||
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)
|
||||
.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(
|
||||
state,
|
||||
workspace_id,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use super::*;
|
||||
use protocol::subscription::{SubscriptionWorkerIds, SubscriptionWorkerState};
|
||||
use worker_runtime::Runtime;
|
||||
use worker_runtime::catalog::{
|
||||
CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveHttpRef, ProfileSourceArchiveSource,
|
||||
};
|
||||
use worker_runtime::catalog::{CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource};
|
||||
use worker_runtime::execution::{
|
||||
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
|
||||
WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult,
|
||||
};
|
||||
use worker_runtime::identity::WorkerId;
|
||||
use worker_runtime::profile_archive::{ProfileSourceArchiveRef, ProfileSourceGraphSummary};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestExecutionBackend;
|
||||
@@ -57,22 +54,11 @@ fn create_request(name: &str) -> CreateWorkerRequest {
|
||||
profile: ProfileSelector::Builtin("builtin:companion".to_string()),
|
||||
display_name: Some(name.to_string()),
|
||||
config_bundle: None,
|
||||
profile_source: ProfileSourceArchiveSource::Http {
|
||||
location: ProfileSourceArchiveHttpRef {
|
||||
url: "http://127.0.0.1/profiles/test".to_string(),
|
||||
etag: None,
|
||||
archive: ProfileSourceArchiveRef {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
profile_source: ProfileSourceArchiveSource::Embedded {
|
||||
archive: crate::profile_settings::builtin_profile_source_archive(
|
||||
&ProfileSelector::Builtin("builtin:default".to_string()),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
initial_input: None,
|
||||
working_directory_request: None,
|
||||
|
||||
@@ -6,7 +6,9 @@ use std::sync::{Arc, Mutex, RwLock, Weak};
|
||||
use axum::body::Bytes;
|
||||
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
|
||||
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::middleware::{self, Next};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
@@ -1596,7 +1598,10 @@ async fn authorize_scoped_workspace_request(
|
||||
{
|
||||
worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION
|
||||
} 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
|
||||
} else {
|
||||
@@ -1705,7 +1710,10 @@ async fn authorize_workspace_api_request(
|
||||
{
|
||||
worker_runtime::auth::WORKSPACE_WORKER_DISCOVERY_PERMISSION
|
||||
} 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
|
||||
} else {
|
||||
@@ -3112,8 +3120,8 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||
.route("/api/hosts", get(list_hosts))
|
||||
.route("/api/w/{workspace_id}/hosts", get(scoped_list_hosts))
|
||||
.route(
|
||||
"/api/w/{workspace_id}/profile-source-archives/{digest}",
|
||||
get(scoped_get_profile_source_archive),
|
||||
"/api/w/{workspace_id}/runtime-config",
|
||||
get(get_latest_workspace_runtime_config),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/working-directories",
|
||||
@@ -3752,12 +3760,6 @@ struct ScopedRepositoryHostTrustPath {
|
||||
host_trust_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedProfileArchivePath {
|
||||
workspace_id: String,
|
||||
digest: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedHostPath {
|
||||
workspace_id: String,
|
||||
@@ -8940,30 +8942,99 @@ async fn scoped_list_hosts(
|
||||
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>,
|
||||
AxumPath(path): AxumPath<ScopedProfileArchivePath>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Query(query): Query<LatestWorkspaceRuntimeConfigQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> ApiResult<(StatusCode, HeaderMap, Vec<u8>)> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let archive = api
|
||||
.resource_broker
|
||||
.profile_source_archive(&path.digest)
|
||||
.ok_or_else(|| Error::Store("profile source archive not found".to_string()))?;
|
||||
let etag = format!("\"profile-source:{}\"", archive.reference.digest);
|
||||
source: Option<Extension<crate::worker_source::VerifiedRuntimeRequestSource>>,
|
||||
) -> Response {
|
||||
let Some(Extension(_source)) = source else {
|
||||
return (
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(serde_json::json!({ "error": "runtime_config_source_required" })),
|
||||
)
|
||||
.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
|
||||
.get(IF_NONE_MATCH)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value.split(',').any(|candidate| candidate.trim() == etag))
|
||||
{
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert(ETAG, etag.parse().unwrap());
|
||||
return Ok((StatusCode::NOT_MODIFIED, response_headers, Vec::new()));
|
||||
return (StatusCode::NOT_MODIFIED, response_headers).into_response();
|
||||
}
|
||||
let mut response_headers = HeaderMap::new();
|
||||
response_headers.insert(ETAG, etag.parse().unwrap());
|
||||
response_headers.insert(CONTENT_TYPE, "application/x-tar".parse().unwrap());
|
||||
Ok((StatusCode::OK, response_headers, archive.content))
|
||||
(StatusCode::OK, response_headers, Json(bundle)).into_response()
|
||||
}
|
||||
|
||||
async fn scoped_list_runtimes(
|
||||
@@ -25846,42 +25917,39 @@ mod tests {
|
||||
}
|
||||
|
||||
#[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();
|
||||
init_clean_git_workspace(workspace.path());
|
||||
let mut api = test_api(workspace.path()).await;
|
||||
let identity =
|
||||
worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-test").unwrap();
|
||||
configure_runtime_request_auth(&mut api, &identity, "runtime-test");
|
||||
let handle = api.resource_broker.issue_profile_source_archive_handle(
|
||||
TEST_WORKSPACE_ID,
|
||||
crate::resource_broker::BackendResourceTarget::Runtime("runtime-test"),
|
||||
test_profile_archive(),
|
||||
);
|
||||
let path = format!(
|
||||
"/api/w/{TEST_WORKSPACE_ID}/profile-source-archives/{}",
|
||||
handle.digest
|
||||
);
|
||||
let proof = worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity)
|
||||
.issue(
|
||||
"server-test",
|
||||
TEST_WORKSPACE_ID,
|
||||
None,
|
||||
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION,
|
||||
"GET",
|
||||
&path,
|
||||
b"",
|
||||
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
|
||||
30,
|
||||
)
|
||||
.unwrap();
|
||||
let response = build_router(api)
|
||||
let target =
|
||||
format!("/api/w/{TEST_WORKSPACE_ID}/runtime-config?profile=builtin%3Acompanion");
|
||||
let issue = || {
|
||||
worker_runtime::auth::RuntimeRequestSourceSigner::from_identity(&identity)
|
||||
.issue(
|
||||
"server-test",
|
||||
TEST_WORKSPACE_ID,
|
||||
None,
|
||||
worker_runtime::auth::BACKEND_RESOURCE_FETCH_PERMISSION,
|
||||
"GET",
|
||||
&target,
|
||||
b"",
|
||||
i64::try_from(worker_runtime::auth::unix_now_seconds()).unwrap_or(i64::MAX),
|
||||
30,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let app = build_router(api);
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri(path)
|
||||
.uri(&target)
|
||||
.header(
|
||||
worker_runtime::auth::RUNTIME_REQUEST_SOURCE_PROOF_HEADER,
|
||||
proof,
|
||||
issue(),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
@@ -25889,12 +25957,34 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
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!(
|
||||
response.headers().get(ETAG).unwrap().to_str().unwrap(),
|
||||
format!("\"profile-source:{}\"", handle.digest)
|
||||
etag.to_str().unwrap(),
|
||||
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!(
|
||||
!to_bytes(response.into_body(), usize::MAX)
|
||||
to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
@@ -25994,22 +26084,13 @@ mod tests {
|
||||
"builtin:companion".to_string(),
|
||||
),
|
||||
display_name: None,
|
||||
profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Http {
|
||||
location: worker_runtime::catalog::ProfileSourceArchiveHttpRef {
|
||||
url: "http://127.0.0.1/profile-source.tar".to_string(),
|
||||
etag: None,
|
||||
archive: worker_runtime::profile_archive::ProfileSourceArchiveRef {
|
||||
id: "test-profile-source".to_string(),
|
||||
digest: "test-digest".to_string(),
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
profile_source: worker_runtime::catalog::ProfileSourceArchiveSource::Embedded {
|
||||
archive: crate::profile_settings::builtin_profile_source_archive(
|
||||
&worker_runtime::catalog::ProfileSelector::Builtin(
|
||||
"builtin:default".to_string(),
|
||||
),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
config_bundle: Some(worker_runtime::catalog::ConfigBundleRef {
|
||||
id: bundle.metadata.id,
|
||||
|
||||
Reference in New Issue
Block a user