fix: defer restored workdir access until use
This commit is contained in:
@@ -6079,12 +6079,11 @@ impl WorkspaceApi {
|
||||
|
||||
/// Binds observation-based Restore intent to a Runtime-owned operation.
|
||||
///
|
||||
/// First queries existing owners/receipts without preparation, handling replay and recovery
|
||||
/// before new-launch eligibility. New intent rereads singleton, Workdir, and External grants
|
||||
/// to build references. SSH resources are issued only after Runtime admits the same observation
|
||||
/// token under its operation lock. Replay does not replace saved preparation; incomplete auth
|
||||
/// preparation remains ReconciliationRequired. Runtime owns lifecycle settlement, while the
|
||||
/// returned summary comes from current observation.
|
||||
/// Existing owners reach receipts/recovery before new-launch eligibility.
|
||||
/// Restore retains attachment references without opening providers, resolving
|
||||
/// Repository credentials, or requiring materialization. Workdir operations
|
||||
/// validate current authority and availability when a session is used.
|
||||
/// Runtime owns lifecycle settlement; summaries come from current observation.
|
||||
fn restore_workspace_worker(
|
||||
&self,
|
||||
worker: &RuntimeWorkerRef,
|
||||
@@ -6117,82 +6116,11 @@ impl WorkspaceApi {
|
||||
result.worker = self.current_restore_summary(worker, result.worker.is_some());
|
||||
return Ok(result);
|
||||
}
|
||||
let mut preparation = if let Some(preparation) = owned_preparation {
|
||||
let preparation = if let Some(preparation) = owned_preparation {
|
||||
preparation
|
||||
} else {
|
||||
self.store
|
||||
.require_current_worker_singleton_owner(&self.config.workspace_id, worker)?;
|
||||
let mut repository_access_workdirs = Vec::new();
|
||||
for link in self
|
||||
.store
|
||||
.list_worker_workdir_links(&self.config.workspace_id, worker)?
|
||||
.into_iter()
|
||||
.filter(|link| link.unlinked_at.is_none())
|
||||
{
|
||||
let record = self
|
||||
.store
|
||||
.get_workdir_registry(&self.config.workspace_id, &link.workdir_id)?
|
||||
.ok_or_else(|| Error::RuntimeOperationFailed {
|
||||
runtime_id: "workspace-backend".to_string(),
|
||||
code: "working_directory_not_found".to_string(),
|
||||
message: format!("Unknown Workdir `{}`", link.workdir_id),
|
||||
})?;
|
||||
match record.source {
|
||||
WorkdirRegistrySource::Repository {
|
||||
runtime_id,
|
||||
repository_id,
|
||||
} => {
|
||||
if self
|
||||
.require_configured_workspace_repository(&repository_id)?
|
||||
.source
|
||||
.kind
|
||||
== server_api::RepositorySourceKind::Ssh
|
||||
{
|
||||
repository_access_workdirs.push(
|
||||
runtime_api::RepositoryAccessWorkdirReference {
|
||||
runtime_id,
|
||||
working_directory_id: link.workdir_id.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
WorkdirRegistrySource::WorkspaceConfig { .. } => continue,
|
||||
WorkdirRegistrySource::ExternalGrant { grant_id } => {
|
||||
let grant = self
|
||||
.store
|
||||
.get_external_workdir_grant(&self.config.workspace_id, &grant_id)?
|
||||
.ok_or_else(|| Error::RuntimeOperationFailed {
|
||||
runtime_id: "external-workdir-provider".to_string(),
|
||||
code: "external_workdir_unavailable".to_string(),
|
||||
message: "External Workdir grant is unavailable".to_string(),
|
||||
})?;
|
||||
let grant_expired = external_workdir_grant_is_expired(&grant)?;
|
||||
let connection = self
|
||||
.external_workdir_providers
|
||||
.lock()
|
||||
.expect("External Workdir provider registry poisoned")
|
||||
.connection(&grant_id);
|
||||
if grant.status != "online"
|
||||
|| grant_expired
|
||||
|| !connection.is_some_and(|connection| {
|
||||
connection.generation == grant.generation
|
||||
&& connection.provider_instance_id == grant.provider_instance_id
|
||||
&& connection.workdir_id == link.workdir_id
|
||||
&& connection
|
||||
.expires_at
|
||||
.is_none_or(|expires_at| expires_at > Utc::now())
|
||||
})
|
||||
{
|
||||
return Err(Error::RuntimeOperationFailed {
|
||||
runtime_id: "external-workdir-provider".to_string(),
|
||||
code: "external_workdir_unavailable".to_string(),
|
||||
message: "External Workdir provider is offline".to_string(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let preparation = runtime_api::WorkerRestorePreparation {
|
||||
workspace_api: Some(runtime_contract_restore(
|
||||
self.workspace_api_ref(&worker.runtime_id),
|
||||
@@ -6201,7 +6129,7 @@ impl WorkspaceApi {
|
||||
restore_logical_workdir_attachments(self, worker)?,
|
||||
)?),
|
||||
repository_access: Vec::new(),
|
||||
repository_access_workdirs,
|
||||
repository_access_workdirs: Vec::new(),
|
||||
};
|
||||
let (result, preparation) = self
|
||||
.runtime
|
||||
@@ -6222,60 +6150,6 @@ impl WorkspaceApi {
|
||||
Error::Config("Runtime did not retain admitted Restore preparation".into())
|
||||
})?
|
||||
};
|
||||
// Only the admitted operation may issue SSH handles. Reissue from the pinned
|
||||
// credential snapshot after interruption; never renew candidate bindings.
|
||||
let completion =
|
||||
complete_restore_repository_access(self, &request.request_id, &mut preparation);
|
||||
if let Err(error) = completion {
|
||||
return Ok(InternalWorkerRestoreResult {
|
||||
state: server_api::WorkerRestoreState::ReconciliationRequired,
|
||||
worker: None,
|
||||
diagnostics: vec![RuntimeDiagnostic::new(
|
||||
"worker_restore_preparation_pending",
|
||||
"warning",
|
||||
sanitize_backend_error(&error.error.to_string()),
|
||||
)],
|
||||
});
|
||||
}
|
||||
// Persist the first resource snapshot at the admitted owner BEFORE any
|
||||
// Workdir Runtime authorization effect. Replays renew only its opaque handles.
|
||||
let (result, retained) = self
|
||||
.runtime
|
||||
.coordinate_worker_restore(
|
||||
worker,
|
||||
runtime_api::WorkerRestoreCoordinationRequest {
|
||||
expected_observation_token: request.expected_observation_token.clone(),
|
||||
request_id: request.request_id.clone(),
|
||||
preparation: Some(preparation),
|
||||
},
|
||||
)
|
||||
.map_err(RuntimeRegistryError::into_error)?;
|
||||
if let Some(mut result) = result {
|
||||
result.worker = self.current_restore_summary(worker, result.worker.is_some());
|
||||
return Ok(result);
|
||||
}
|
||||
let preparation = retained.ok_or_else(|| {
|
||||
Error::Config("Runtime did not retain Restore resource snapshot".into())
|
||||
})?;
|
||||
for access in &preparation.repository_access {
|
||||
let access: worker_runtime::catalog::WorkingDirectoryRepositoryAccessRequest =
|
||||
runtime_contract_restore(access.clone())?;
|
||||
let runtime_id = access.materialization.runtime_id.clone();
|
||||
if let Err(error) = self
|
||||
.runtime
|
||||
.authorize_working_directory_repository_access(&runtime_id, access)
|
||||
{
|
||||
return Ok(InternalWorkerRestoreResult {
|
||||
state: server_api::WorkerRestoreState::ReconciliationRequired,
|
||||
worker: None,
|
||||
diagnostics: vec![RuntimeDiagnostic::new(
|
||||
"worker_restore_repository_access_pending",
|
||||
"warning",
|
||||
sanitize_backend_error(&error.message()),
|
||||
)],
|
||||
});
|
||||
}
|
||||
}
|
||||
let mut restored = self
|
||||
.runtime
|
||||
.restore_worker(
|
||||
@@ -20315,44 +20189,30 @@ fn restore_logical_workdir_attachments(
|
||||
api: &WorkspaceApi,
|
||||
worker: &RuntimeWorkerRef,
|
||||
) -> ApiResult<Vec<LogicalWorkdirAttachment>> {
|
||||
api.store
|
||||
let mut attachments = Vec::new();
|
||||
for link in api
|
||||
.store
|
||||
.list_worker_workdir_links(&api.config.workspace_id, worker)?
|
||||
.into_iter()
|
||||
.filter(|link| link.unlinked_at.is_none())
|
||||
.filter(|link| {
|
||||
api.store
|
||||
.get_workdir_registry(&api.config.workspace_id, &link.workdir_id)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_none_or(|record| {
|
||||
!matches!(record.source, WorkdirRegistrySource::WorkspaceConfig { .. })
|
||||
})
|
||||
})
|
||||
.map(|link| {
|
||||
let workdir = api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, &link.workdir_id)?
|
||||
.ok_or_else(|| {
|
||||
Error::RegistryInconsistency(format!(
|
||||
"Worker attachment references unknown Workdir {}",
|
||||
link.workdir_id
|
||||
))
|
||||
})?;
|
||||
let capabilities = link
|
||||
.capabilities
|
||||
.intersection(workdir_source_capabilities(api, &workdir)?)
|
||||
.intersection(ticket_target_capabilities_for_workdir(
|
||||
api, worker, &workdir,
|
||||
)?);
|
||||
Ok(LogicalWorkdirAttachment {
|
||||
alias: workdir::WorkdirAttachmentAlias::new(link.alias)
|
||||
.map_err(|error| Error::InvalidInput(error.to_string()))?,
|
||||
working_directory_id: link.workdir_id,
|
||||
capabilities,
|
||||
{
|
||||
if api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, &link.workdir_id)?
|
||||
.is_some_and(|record| {
|
||||
matches!(record.source, WorkdirRegistrySource::WorkspaceConfig { .. })
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
.map_err(ApiError::from)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
attachments.push(LogicalWorkdirAttachment {
|
||||
alias: workdir::WorkdirAttachmentAlias::new(link.alias)
|
||||
.map_err(|error| Error::InvalidInput(error.to_string()))?,
|
||||
working_directory_id: link.workdir_id,
|
||||
capabilities: link.capabilities,
|
||||
});
|
||||
}
|
||||
Ok(attachments)
|
||||
}
|
||||
|
||||
fn prepare_runtime_worker_workdir_attachments(
|
||||
@@ -36648,81 +36508,6 @@ fn authorize_repository_materialization(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn complete_restore_repository_access(
|
||||
api: &WorkspaceApi,
|
||||
request_id: &str,
|
||||
preparation: &mut runtime_api::WorkerRestorePreparation,
|
||||
) -> ApiResult<()> {
|
||||
if preparation.repository_access.is_empty() {
|
||||
for reference in &preparation.repository_access_workdirs {
|
||||
if let Some(access) = repository_access_request_for_workdir(
|
||||
api,
|
||||
&reference.runtime_id,
|
||||
&reference.working_directory_id,
|
||||
&format!("worker-restore:{request_id}"),
|
||||
)? {
|
||||
preparation
|
||||
.repository_access
|
||||
.push(runtime_contract_restore(access)?);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
for prepared in &mut preparation.repository_access {
|
||||
let mut access: worker_runtime::catalog::WorkingDirectoryRepositoryAccessRequest =
|
||||
runtime_contract_restore(prepared.clone())?;
|
||||
let Some(ssh) = access.materialization.ssh.as_mut() else {
|
||||
continue;
|
||||
};
|
||||
let mut leases = Vec::new();
|
||||
for candidate in &ssh.credential_candidates {
|
||||
leases.push(
|
||||
api.repository_secrets
|
||||
.lease_ssh_materialization_access_fingerprints(
|
||||
api.workspace_id(),
|
||||
&candidate.credential_id,
|
||||
&candidate.public_key_fingerprint,
|
||||
&ssh.host_trust_id,
|
||||
&ssh.host_key_fingerprint,
|
||||
)?,
|
||||
);
|
||||
}
|
||||
let known_hosts_entry = leases
|
||||
.first()
|
||||
.ok_or_else(|| Error::Config("Admitted SSH snapshot has no candidates".into()))?
|
||||
.known_hosts_entry
|
||||
.clone();
|
||||
ssh.expires_at_epoch_seconds = repository_access_expiry();
|
||||
ssh.secret_resource = api
|
||||
.resource_broker
|
||||
.issue_repository_ssh_access_handle(
|
||||
api.workspace_id(),
|
||||
&access.materialization.runtime_id,
|
||||
format!(
|
||||
"repository-ssh-access:{}",
|
||||
access.materialization.operation_id
|
||||
),
|
||||
i64::try_from(ssh.expires_at_epoch_seconds).unwrap_or(i64::MAX),
|
||||
worker_runtime::resource::RepositorySshAccessSecret {
|
||||
credential_candidates: leases
|
||||
.into_iter()
|
||||
.map(
|
||||
|lease| worker_runtime::resource::RepositorySshAccessSecretCandidate {
|
||||
credential_id: lease.credential_id,
|
||||
public_key_fingerprint: lease.credential_fingerprint.clone(),
|
||||
private_key: lease.private_key.as_str().to_owned(),
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
known_hosts_entry,
|
||||
},
|
||||
)
|
||||
.map_err(|error| Error::Config(error.to_string()))?;
|
||||
*prepared = runtime_contract_restore(access)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn repository_access_request_for_workdir(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
@@ -37544,6 +37329,7 @@ mod tests {
|
||||
mod embedded_owned_execution_tests;
|
||||
mod external_checkout_tests;
|
||||
mod protocol_backpressure_tests;
|
||||
mod restore_workdir_tests;
|
||||
mod restored_parent_control_tests;
|
||||
mod subject_authorization_tests;
|
||||
mod subject_spawn_tests;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Restore entry -> real SQLite attachment registry -> embedded Runtime restore
|
||||
//! coordination. Only execution is deterministic; Workdir providers are absent.
|
||||
//! This suite covers restore preparation, not HTTP authentication or model turns.
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn restore_keeps_offline_external_and_unmaterialized_repository_attachments() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(temp.path());
|
||||
let (api, execution) = test_api_with_recording_backend(temp.path()).await;
|
||||
execution.accept_restores();
|
||||
let Json(created) = create_workspace_worker(
|
||||
State(api.clone()),
|
||||
HeaderMap::new(),
|
||||
Json(CreateWorkspaceWorkerRequest {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.into(),
|
||||
display_name: "Restore without providers".into(),
|
||||
singleton_key: None,
|
||||
profile: Some("builtin:coder".into()),
|
||||
ticket_assignment: None,
|
||||
initial_submit: Vec::new(),
|
||||
workdir_attachments: Vec::new(),
|
||||
feature_connections: Default::default(),
|
||||
control_operation_id: None,
|
||||
execution_grant: None,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let worker = RuntimeWorkerRef::new(&created.runtime_id, &created.worker_id);
|
||||
let grant = ExternalWorkdirGrantRecord {
|
||||
grant_id: "restore-external-grant".into(),
|
||||
workspace_id: TEST_WORKSPACE_ID.into(),
|
||||
workdir_id: "restore-external".into(),
|
||||
provider_instance_id: "offline-provider".into(),
|
||||
display_name: "Offline external".into(),
|
||||
permissions: "read_only".into(),
|
||||
created_by: test_browser_request_actor().account_id,
|
||||
created_at: TEST_CREATED_AT.into(),
|
||||
expires_at: None,
|
||||
generation: 1,
|
||||
status: "pending".into(),
|
||||
updated_at: TEST_CREATED_AT.into(),
|
||||
};
|
||||
for (alias, workdir_id, source) in [
|
||||
(
|
||||
"external",
|
||||
grant.workdir_id.as_str(),
|
||||
WorkdirRegistrySource::ExternalGrant {
|
||||
grant_id: grant.grant_id.clone(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"repository",
|
||||
"restore-missing-repository",
|
||||
WorkdirRegistrySource::Repository {
|
||||
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.into(),
|
||||
repository_id: test_repository_id(&api),
|
||||
},
|
||||
),
|
||||
] {
|
||||
let record = WorkdirRegistryRecord {
|
||||
workspace_id: TEST_WORKSPACE_ID.into(),
|
||||
workdir_id: workdir_id.into(),
|
||||
display_name: Some(alias.into()),
|
||||
source,
|
||||
creation_selector: None,
|
||||
creation_ref: None,
|
||||
creation_tree: None,
|
||||
current_selector: None,
|
||||
current_ref: None,
|
||||
current_tree: None,
|
||||
observed_at_epoch_seconds: None,
|
||||
materialization_status: "not_found".into(),
|
||||
cleanliness: "unknown".into(),
|
||||
created_at: TEST_CREATED_AT.into(),
|
||||
updated_at: TEST_CREATED_AT.into(),
|
||||
};
|
||||
if alias == "external" {
|
||||
api.store
|
||||
.create_external_workdir_grant(&grant, &record)
|
||||
.unwrap();
|
||||
assert!(
|
||||
api.store
|
||||
.activate_external_workdir_grant(
|
||||
TEST_WORKSPACE_ID,
|
||||
&grant.grant_id,
|
||||
&grant.provider_instance_id,
|
||||
1,
|
||||
TEST_CREATED_AT,
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
} else {
|
||||
api.store.upsert_workdir_registry(&record).unwrap();
|
||||
}
|
||||
api.store
|
||||
.attach_worker_workdir(&WorkerWorkdirLinkRecord {
|
||||
connection_id: String::new(),
|
||||
workspace_id: TEST_WORKSPACE_ID.into(),
|
||||
worker: worker.clone(),
|
||||
workdir_id: workdir_id.into(),
|
||||
alias: alias.into(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
linked_at: TEST_CREATED_AT.into(),
|
||||
unlinked_at: None,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let links = api
|
||||
.store
|
||||
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
|
||||
.unwrap();
|
||||
api.runtime
|
||||
.stop_worker(
|
||||
&worker,
|
||||
WorkerLifecycleRequest {
|
||||
reason: None,
|
||||
ticket_assignment: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let result = api
|
||||
.restore_workspace_worker(
|
||||
&worker,
|
||||
server_api::WorkerRestoreRequest {
|
||||
expected_observation_token: api
|
||||
.runtime
|
||||
.worker(&worker)
|
||||
.unwrap()
|
||||
.restore_observation_token
|
||||
.unwrap(),
|
||||
request_id: uuid::Uuid::now_v7().to_string(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(result.state, server_api::WorkerRestoreState::Accepted);
|
||||
let attachments = restore_logical_workdir_attachments(&api, &worker).unwrap();
|
||||
assert_eq!(attachments.len(), 2);
|
||||
assert!(
|
||||
attachments
|
||||
.iter()
|
||||
.any(|attachment| attachment.alias.as_str() == "external"
|
||||
&& attachment.working_directory_id == grant.workdir_id)
|
||||
);
|
||||
assert!(
|
||||
attachments
|
||||
.iter()
|
||||
.any(|attachment| attachment.alias.as_str() == "repository"
|
||||
&& attachment.working_directory_id == "restore-missing-repository")
|
||||
);
|
||||
assert_eq!(
|
||||
api.store
|
||||
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
|
||||
.unwrap(),
|
||||
links
|
||||
);
|
||||
let lock = current_worker_session_lock(&api, &worker);
|
||||
let guard = Arc::new(lock.lock_owned().await);
|
||||
for link in &links {
|
||||
let error = open_current_worker_workdir_session_locked(&api, &worker, link, &guard)
|
||||
.await
|
||||
.unwrap_err();
|
||||
match link.alias.as_str() {
|
||||
"external" => assert!(matches!(
|
||||
error,
|
||||
Error::RuntimeOperationFailed { code, .. }
|
||||
if code == "external_workdir_provider_offline"
|
||||
)),
|
||||
"repository" => assert!(matches!(
|
||||
error,
|
||||
Error::WorkdirSessionOpenFailed { transport, .. }
|
||||
if transport.code == workdir::http::WorkdirTransportErrorCode::Unavailable
|
||||
)),
|
||||
alias => panic!("unexpected attachment: {alias}"),
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
api.store
|
||||
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
|
||||
.unwrap(),
|
||||
links
|
||||
);
|
||||
api.runtime
|
||||
.stop_worker(
|
||||
&worker,
|
||||
WorkerLifecycleRequest {
|
||||
reason: None,
|
||||
ticket_assignment: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -1593,7 +1593,7 @@ async fn restore_guard_workspace_interrupted_preparation_recovers_admitted_snaps
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_workdir_owner() {
|
||||
async fn restore_keeps_cross_runtime_ssh_attachment_without_issuing_repository_access() {
|
||||
let workspace = tempfile::tempdir().unwrap();
|
||||
let mut config = test_server_config(workspace.path());
|
||||
let source = server_api::RepositorySource {
|
||||
@@ -1619,39 +1619,6 @@ async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_w
|
||||
.stop(&WorkerOperationContext::Backend, operation_lifecycle())
|
||||
.await
|
||||
.unwrap();
|
||||
let original_key = api
|
||||
.repository_secrets
|
||||
.generate_credential(
|
||||
api.workspace_id(),
|
||||
GenerateRepositorySshCredentialRequest {
|
||||
operation_id: "cross-runtime-default-key".into(),
|
||||
credential_id:
|
||||
crate::repository_access::WORKSPACE_DEFAULT_REPOSITORY_SSH_CREDENTIAL_ID.into(),
|
||||
name: "Restore default".into(),
|
||||
},
|
||||
"owner-account",
|
||||
)
|
||||
.unwrap();
|
||||
let public_key = api
|
||||
.repository_secrets
|
||||
.credential_public_key(api.workspace_id(), &original_key.credential_id)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.public_key;
|
||||
api.repository_secrets
|
||||
.put_host_trust(
|
||||
api.workspace_id(),
|
||||
PutRepositorySshHostTrustRequest {
|
||||
operation_id: "cross-runtime-trust".into(),
|
||||
host_trust_id: "cross-runtime-host".into(),
|
||||
hostname: "example.invalid".into(),
|
||||
port: 22,
|
||||
host_key: public_key,
|
||||
expected_fingerprint: None,
|
||||
},
|
||||
"owner-account",
|
||||
)
|
||||
.unwrap();
|
||||
let record = restore_test_workdir(
|
||||
&api,
|
||||
"cross-runtime-restore-workdir",
|
||||
@@ -1663,102 +1630,27 @@ async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_w
|
||||
peer.set_workdir_repository(&record.workdir_id, &test_repository_id(&api));
|
||||
api.store.upsert_workdir_registry(&record).unwrap();
|
||||
attach_restore_test_workdir(&api, &identity, &record);
|
||||
let links = api
|
||||
.store
|
||||
.list_worker_workdir_links(api.workspace_id(), &identity)
|
||||
.unwrap();
|
||||
let request = restore_request_for(&api, &identity, "cross-runtime-restore-request");
|
||||
assert_eq!(
|
||||
handle
|
||||
.restore(&WorkerOperationContext::Backend, request.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.state,
|
||||
server_api::WorkerRestoreState::ReconciliationRequired
|
||||
);
|
||||
assert!(
|
||||
execution.restore_operations.lock().unwrap().is_empty(),
|
||||
"credential completion must precede execution"
|
||||
);
|
||||
let first = peer.repository_access_requests.lock().unwrap()[0].clone();
|
||||
assert_eq!(
|
||||
first.materialization.runtime_id,
|
||||
WorkdirlessFixtureRuntime::RUNTIME_ID
|
||||
);
|
||||
assert_ne!(first.materialization.runtime_id, identity.runtime_id);
|
||||
// Rotate current host trust while the accepted operation is uncertain. Its
|
||||
// original credential/trust fingerprints, source and binding must remain pinned.
|
||||
let alternate = api
|
||||
.repository_secrets
|
||||
.generate_credential(
|
||||
api.workspace_id(),
|
||||
GenerateRepositorySshCredentialRequest {
|
||||
operation_id: "cross-runtime-alternate-key".into(),
|
||||
credential_id: "cross-runtime-alternate".into(),
|
||||
name: "Alternate".into(),
|
||||
},
|
||||
"owner-account",
|
||||
)
|
||||
.unwrap();
|
||||
let alternate_public = api
|
||||
.repository_secrets
|
||||
.credential_public_key(api.workspace_id(), &alternate.credential_id)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.public_key;
|
||||
let original_trust_fingerprint = first
|
||||
.materialization
|
||||
.ssh
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.host_key_fingerprint
|
||||
.clone();
|
||||
api.repository_secrets
|
||||
.put_host_trust(
|
||||
api.workspace_id(),
|
||||
PutRepositorySshHostTrustRequest {
|
||||
operation_id: "cross-runtime-trust-rotate".into(),
|
||||
host_trust_id: "cross-runtime-host".into(),
|
||||
hostname: "example.invalid".into(),
|
||||
port: 22,
|
||||
host_key: alternate_public,
|
||||
expected_fingerprint: Some(original_trust_fingerprint),
|
||||
},
|
||||
"owner-account",
|
||||
)
|
||||
.unwrap();
|
||||
*peer.reject_repository_access.lock().unwrap() = false;
|
||||
assert_eq!(
|
||||
handle
|
||||
.restore(&WorkerOperationContext::Backend, request.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.state,
|
||||
server_api::WorkerRestoreState::Accepted
|
||||
);
|
||||
let attempts = peer.repository_access_requests.lock().unwrap();
|
||||
assert_eq!(attempts.len(), 2);
|
||||
let second = &attempts[1];
|
||||
assert_eq!(
|
||||
second.materialization.operation_id,
|
||||
first.materialization.operation_id
|
||||
);
|
||||
let mut normalized = second.clone();
|
||||
let old_ssh = first.materialization.ssh.as_ref().unwrap();
|
||||
let new_ssh = normalized.materialization.ssh.as_mut().unwrap();
|
||||
assert_ne!(new_ssh.secret_resource.nonce, old_ssh.secret_resource.nonce);
|
||||
new_ssh.secret_resource = old_ssh.secret_resource.clone();
|
||||
new_ssh.expires_at_epoch_seconds = old_ssh.expires_at_epoch_seconds;
|
||||
assert_eq!(normalized, first, "only opaque handle and expiry may renew");
|
||||
drop(attempts);
|
||||
assert_eq!(execution.restore_operations.lock().unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
handle
|
||||
.restore(&WorkerOperationContext::Backend, request)
|
||||
.await
|
||||
.unwrap()
|
||||
.state,
|
||||
server_api::WorkerRestoreState::Accepted
|
||||
);
|
||||
assert_eq!(
|
||||
peer.repository_access_requests.lock().unwrap().len(),
|
||||
2,
|
||||
"terminal receipt must not renew access"
|
||||
);
|
||||
for _ in 0..2 {
|
||||
assert_eq!(
|
||||
handle
|
||||
.restore(&WorkerOperationContext::Backend, request.clone())
|
||||
.await
|
||||
.unwrap()
|
||||
.state,
|
||||
server_api::WorkerRestoreState::Accepted
|
||||
);
|
||||
assert!(peer.repository_access_requests.lock().unwrap().is_empty());
|
||||
assert_eq!(execution.restore_operations.lock().unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
api.store
|
||||
.list_worker_workdir_links(api.workspace_id(), &identity)
|
||||
.unwrap(),
|
||||
links
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user