fix: fence attachment grants and lifecycle cleanup
This commit is contained in:
@@ -1048,6 +1048,7 @@ pub trait WorkspaceWorkerRuntime: Send + Sync {
|
||||
_file_name: &str,
|
||||
_media_type: &str,
|
||||
_content: &[u8],
|
||||
_context: Option<&worker_runtime::UploadedFileUploadContext>,
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
Err(RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id().to_string(),
|
||||
@@ -1578,6 +1579,7 @@ impl RuntimeRegistry {
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
context: Option<&worker_runtime::UploadedFileUploadContext>,
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let runtime_id = worker.runtime_id.as_str();
|
||||
let worker_id = worker.worker_id.as_str();
|
||||
@@ -1592,7 +1594,7 @@ impl RuntimeRegistry {
|
||||
lookup.diagnostics,
|
||||
));
|
||||
}
|
||||
runtime.upload_worker_file(worker_id, file_name, media_type, content)
|
||||
runtime.upload_worker_file(worker_id, file_name, media_type, content, context)
|
||||
}
|
||||
|
||||
pub fn delete_worker_uploaded_file(
|
||||
@@ -2588,19 +2590,30 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
context: Option<&worker_runtime::UploadedFileUploadContext>,
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let worker_ref =
|
||||
self.worker_ref(worker_id)
|
||||
.ok_or_else(|| RuntimeRegistryError::UnknownWorker {
|
||||
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
|
||||
})?;
|
||||
self.runtime
|
||||
.upload_worker_file(&worker_ref, file_name, media_type, content)
|
||||
.map_err(|error| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: "embedded_worker_file_upload_failed".to_string(),
|
||||
message: error.to_string(),
|
||||
})
|
||||
let uploaded = match context {
|
||||
Some(context) => self.runtime.upload_worker_file_with_context(
|
||||
&worker_ref,
|
||||
file_name,
|
||||
media_type,
|
||||
content,
|
||||
context,
|
||||
),
|
||||
None => self
|
||||
.runtime
|
||||
.upload_worker_file(&worker_ref, file_name, media_type, content),
|
||||
};
|
||||
uploaded.map_err(|error| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
runtime_id: self.runtime_id.clone(),
|
||||
code: "embedded_worker_file_upload_failed".to_string(),
|
||||
message: error.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn delete_worker_uploaded_file(
|
||||
@@ -3665,13 +3678,24 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
|
||||
file_name: &str,
|
||||
media_type: &str,
|
||||
content: &[u8],
|
||||
context: Option<&worker_runtime::UploadedFileUploadContext>,
|
||||
) -> Result<protocol::UploadedFileRef, RuntimeRegistryError> {
|
||||
let path = format!(
|
||||
let mut path = format!(
|
||||
"/v1/workers/{}/attachments?file_name={}&media_type={}",
|
||||
url_path_segment_encode(worker_id),
|
||||
url_query_value_encode(file_name),
|
||||
url_query_value_encode(media_type),
|
||||
);
|
||||
if let Some(context) = context {
|
||||
path.push_str(&format!(
|
||||
"&upload_id={}&principal_id={}&workspace_id={}&runtime_id={}&owner_worker_id={}",
|
||||
url_query_value_encode(&context.upload_id),
|
||||
url_query_value_encode(&context.principal_id),
|
||||
url_query_value_encode(&context.workspace_id),
|
||||
url_query_value_encode(&context.runtime_id),
|
||||
url_query_value_encode(&context.worker_id),
|
||||
));
|
||||
}
|
||||
self.post_bytes::<RuntimeHttpUploadedFileResponse>(&path, content)
|
||||
.map(|response| response.file)
|
||||
.map_err(|diagnostic| RuntimeRegistryError::RuntimeOperationFailed {
|
||||
|
||||
@@ -502,6 +502,73 @@ async fn close_worker_workdir_sessions(
|
||||
}
|
||||
}
|
||||
|
||||
const ATTACHMENT_UPLOAD_GRANT_TTL_MS: u64 = 5 * 60 * 1_000;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AttachmentUploadGrant {
|
||||
workspace_id: String,
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
account_id: String,
|
||||
file_name: String,
|
||||
media_type: String,
|
||||
expires_at_ms: u64,
|
||||
state: AttachmentUploadGrantState,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum AttachmentUploadGrantState {
|
||||
Pending,
|
||||
Uploading,
|
||||
Cancelled,
|
||||
Completed(protocol::UploadedFileRef),
|
||||
}
|
||||
|
||||
impl AttachmentUploadGrant {
|
||||
fn claim(
|
||||
&mut self,
|
||||
workspace_id: &str,
|
||||
runtime_id: &str,
|
||||
worker_id: &str,
|
||||
account_id: &str,
|
||||
now_ms: u64,
|
||||
body_sha256: &str,
|
||||
) -> Result<Option<protocol::UploadedFileRef>> {
|
||||
if self.workspace_id != workspace_id
|
||||
|| self.runtime_id != runtime_id
|
||||
|| self.worker_id != worker_id
|
||||
|| self.account_id != account_id
|
||||
{
|
||||
return Err(Error::WorkspacePermissionDenied(
|
||||
"attachment upload grant scope mismatch".into(),
|
||||
));
|
||||
}
|
||||
if self.expires_at_ms <= now_ms {
|
||||
return Err(Error::InvalidInput(
|
||||
"attachment upload grant expired".into(),
|
||||
));
|
||||
}
|
||||
match &self.state {
|
||||
AttachmentUploadGrantState::Pending => {
|
||||
self.state = AttachmentUploadGrantState::Uploading;
|
||||
Ok(None)
|
||||
}
|
||||
AttachmentUploadGrantState::Uploading => Err(Error::RepositoryConflict(
|
||||
"attachment upload is already in progress".into(),
|
||||
)),
|
||||
AttachmentUploadGrantState::Cancelled => Err(Error::RepositoryConflict(
|
||||
"attachment upload grant was cancelled".into(),
|
||||
)),
|
||||
AttachmentUploadGrantState::Completed(file) if file.sha256 == body_sha256 => {
|
||||
Ok(Some(file.clone()))
|
||||
}
|
||||
AttachmentUploadGrantState::Completed(_) => Err(Error::RepositoryConflict(
|
||||
"attachment upload grant was already consumed".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WorkspaceApi {
|
||||
pub(crate) config: ServerConfig,
|
||||
@@ -524,6 +591,7 @@ pub struct WorkspaceApi {
|
||||
workdir_remove_locks: Arc<Mutex<HashMap<String, Arc<std::sync::Mutex<()>>>>>,
|
||||
workdir_remove_attempt_owner: WorkdirRemovalAttemptOwner,
|
||||
worker_control_locks: Arc<Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>>,
|
||||
attachment_upload_grants: Arc<Mutex<HashMap<String, AttachmentUploadGrant>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -1645,6 +1713,7 @@ impl WorkspaceApi {
|
||||
workdir_remove_locks: Arc::new(Mutex::new(HashMap::new())),
|
||||
workdir_remove_attempt_owner: current_workdir_removal_attempt_owner()?,
|
||||
worker_control_locks: Arc::new(Mutex::new(HashMap::new())),
|
||||
attachment_upload_grants: Arc::new(Mutex::new(HashMap::new())),
|
||||
};
|
||||
if let Some(dispatcher) = worker_remove_dispatcher {
|
||||
dispatcher
|
||||
@@ -2681,21 +2750,19 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
post(send_runtime_worker_input),
|
||||
)
|
||||
.route(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/attachments",
|
||||
post(upload_runtime_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||
delete(delete_runtime_worker_uploaded_file),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/input",
|
||||
post(scoped_send_runtime_worker_input),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments",
|
||||
post(scoped_upload_runtime_worker_file).layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-upload-grants",
|
||||
post(scoped_create_attachment_upload_grant),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachment-uploads/{upload_id}",
|
||||
put(scoped_upload_runtime_worker_file)
|
||||
.delete(scoped_cancel_attachment_upload)
|
||||
.layer(DefaultBodyLimit::max(MAX_WORKER_FILE_UPLOAD_BYTES)),
|
||||
)
|
||||
.route(
|
||||
"/api/w/{workspace_id}/runtimes/{runtime_id}/workers/{worker_id}/attachments/{artifact_id}",
|
||||
@@ -10756,6 +10823,8 @@ async fn scoped_execute_runtime_cleanup(
|
||||
struct WorkerFileUploadQuery {
|
||||
file_name: String,
|
||||
media_type: String,
|
||||
#[serde(default)]
|
||||
upload_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -10768,20 +10837,225 @@ struct WorkerFileDeleteResponse {
|
||||
deleted: bool,
|
||||
}
|
||||
|
||||
async fn scoped_upload_runtime_worker_file(
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct AttachmentUploadGrantResponse {
|
||||
upload_id: String,
|
||||
expires_at_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ScopedAttachmentUploadPath {
|
||||
workspace_id: String,
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
upload_id: String,
|
||||
}
|
||||
|
||||
fn attachment_body_sha256(body: &[u8]) -> String {
|
||||
Sha256::digest(body)
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn attachment_upload_now_ms() -> Result<u64> {
|
||||
u64::try_from(
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_err(|_| Error::InvalidInput("system clock predates UNIX epoch".into()))?
|
||||
.as_millis(),
|
||||
)
|
||||
.map_err(|_| Error::InvalidInput("attachment upload timestamp overflow".into()))
|
||||
}
|
||||
|
||||
async fn scoped_create_attachment_upload_grant(
|
||||
State(api): State<WorkspaceApi>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimeWorkerPath>,
|
||||
Query(query): Query<WorkerFileUploadQuery>,
|
||||
) -> ApiResult<Json<AttachmentUploadGrantResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
resolve_workspace_worker_reference(&api, &path.worker.runtime_id, &path.worker.worker_id)?;
|
||||
let now = attachment_upload_now_ms()?;
|
||||
let expires_at_ms = now.saturating_add(ATTACHMENT_UPLOAD_GRANT_TTL_MS);
|
||||
let upload_id = match query.upload_id {
|
||||
Some(upload_id) => Uuid::parse_str(&upload_id)
|
||||
.map_err(|_| Error::InvalidInput("attachment upload id must be a UUID".into()))?
|
||||
.to_string(),
|
||||
None => Uuid::now_v7().to_string(),
|
||||
};
|
||||
let mut grants = api
|
||||
.attachment_upload_grants
|
||||
.lock()
|
||||
.map_err(|_| Error::Store("attachment upload grant lock is poisoned".into()))?;
|
||||
grants.retain(|_, grant| grant.expires_at_ms >= now);
|
||||
if let Some(existing) = grants.get(&upload_id) {
|
||||
if existing.workspace_id == path.workspace_id
|
||||
&& existing.runtime_id == path.worker.runtime_id
|
||||
&& existing.worker_id == path.worker.worker_id
|
||||
&& existing.account_id == actor.account_id
|
||||
&& existing.file_name == query.file_name
|
||||
&& existing.media_type == query.media_type
|
||||
{
|
||||
return Ok(Json(AttachmentUploadGrantResponse {
|
||||
upload_id,
|
||||
expires_at_ms: existing.expires_at_ms,
|
||||
}));
|
||||
}
|
||||
return Err(Error::RepositoryConflict(
|
||||
"attachment upload id was reused with different intent".into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
grants.insert(
|
||||
upload_id.clone(),
|
||||
AttachmentUploadGrant {
|
||||
workspace_id: path.workspace_id,
|
||||
runtime_id: path.worker.runtime_id,
|
||||
worker_id: path.worker.worker_id,
|
||||
account_id: actor.account_id,
|
||||
file_name: query.file_name,
|
||||
media_type: query.media_type,
|
||||
expires_at_ms,
|
||||
state: AttachmentUploadGrantState::Pending,
|
||||
},
|
||||
);
|
||||
Ok(Json(AttachmentUploadGrantResponse {
|
||||
upload_id,
|
||||
expires_at_ms,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn scoped_upload_runtime_worker_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
AxumPath(path): AxumPath<ScopedAttachmentUploadPath>,
|
||||
body: Bytes,
|
||||
) -> ApiResult<Json<WorkerFileUploadResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
upload_runtime_worker_file(
|
||||
State(api),
|
||||
AxumPath((path.worker.runtime_id, path.worker.worker_id)),
|
||||
Query(query),
|
||||
body,
|
||||
)
|
||||
.await
|
||||
let body_sha256 = attachment_body_sha256(&body);
|
||||
let grant = {
|
||||
let mut grants = api
|
||||
.attachment_upload_grants
|
||||
.lock()
|
||||
.map_err(|_| Error::Store("attachment upload grant lock is poisoned".into()))?;
|
||||
let grant = grants
|
||||
.get_mut(&path.upload_id)
|
||||
.ok_or_else(|| Error::InvalidInput("attachment upload grant is unknown".into()))?;
|
||||
if let Some(file) = grant
|
||||
.claim(
|
||||
&path.workspace_id,
|
||||
&path.runtime_id,
|
||||
&path.worker_id,
|
||||
&actor.account_id,
|
||||
attachment_upload_now_ms()?,
|
||||
&body_sha256,
|
||||
)
|
||||
.map_err(ApiError::from)?
|
||||
{
|
||||
return Ok(Json(WorkerFileUploadResponse { file }));
|
||||
}
|
||||
grant.clone()
|
||||
};
|
||||
|
||||
let worker = resolve_workspace_worker_reference(&api, &path.runtime_id, &path.worker_id)?;
|
||||
let upload_context = worker_runtime::UploadedFileUploadContext {
|
||||
upload_id: path.upload_id.clone(),
|
||||
principal_id: grant.account_id.clone(),
|
||||
workspace_id: grant.workspace_id.clone(),
|
||||
runtime_id: grant.runtime_id.clone(),
|
||||
worker_id: grant.worker_id.clone(),
|
||||
};
|
||||
let uploaded = api.runtime.upload_worker_file(
|
||||
&worker,
|
||||
&grant.file_name,
|
||||
&grant.media_type,
|
||||
&body,
|
||||
Some(&upload_context),
|
||||
);
|
||||
let mut grants = api
|
||||
.attachment_upload_grants
|
||||
.lock()
|
||||
.map_err(|_| Error::Store("attachment upload grant lock is poisoned".into()))?;
|
||||
let current = grants
|
||||
.get_mut(&path.upload_id)
|
||||
.ok_or_else(|| Error::RepositoryConflict("attachment upload grant disappeared".into()))?;
|
||||
if matches!(current.state, AttachmentUploadGrantState::Cancelled) {
|
||||
let uploaded_file = uploaded.ok();
|
||||
grants.remove(&path.upload_id);
|
||||
drop(grants);
|
||||
if let Some(file) = uploaded_file {
|
||||
api.runtime
|
||||
.delete_worker_uploaded_file(&worker, &file.artifact_id)
|
||||
.map_err(|error| error.into_error())?;
|
||||
}
|
||||
return Err(Error::RepositoryConflict("attachment upload was cancelled".into()).into());
|
||||
}
|
||||
match uploaded {
|
||||
Ok(file) => {
|
||||
current.state = AttachmentUploadGrantState::Completed(file.clone());
|
||||
Ok(Json(WorkerFileUploadResponse { file }))
|
||||
}
|
||||
Err(error) => {
|
||||
current.state = AttachmentUploadGrantState::Pending;
|
||||
Err(error.into_error().into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AttachmentUploadCancelResponse {
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
async fn scoped_cancel_attachment_upload(
|
||||
State(api): State<WorkspaceApi>,
|
||||
Extension(actor): Extension<RequestActor>,
|
||||
AxumPath(path): AxumPath<ScopedAttachmentUploadPath>,
|
||||
) -> ApiResult<Json<AttachmentUploadCancelResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let completed = {
|
||||
let mut grants = api
|
||||
.attachment_upload_grants
|
||||
.lock()
|
||||
.map_err(|_| Error::Store("attachment upload grant lock is poisoned".into()))?;
|
||||
let grant = grants
|
||||
.get_mut(&path.upload_id)
|
||||
.ok_or_else(|| Error::InvalidInput("attachment upload grant is unknown".into()))?;
|
||||
if grant.workspace_id != path.workspace_id
|
||||
|| grant.runtime_id != path.runtime_id
|
||||
|| grant.worker_id != path.worker_id
|
||||
|| grant.account_id != actor.account_id
|
||||
{
|
||||
return Err(Error::WorkspacePermissionDenied(
|
||||
"attachment upload grant scope mismatch".into(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
match &grant.state {
|
||||
AttachmentUploadGrantState::Pending => {
|
||||
grants.remove(&path.upload_id);
|
||||
None
|
||||
}
|
||||
AttachmentUploadGrantState::Uploading => {
|
||||
grant.state = AttachmentUploadGrantState::Cancelled;
|
||||
None
|
||||
}
|
||||
AttachmentUploadGrantState::Cancelled => None,
|
||||
AttachmentUploadGrantState::Completed(file) => {
|
||||
let file = file.clone();
|
||||
grants.remove(&path.upload_id);
|
||||
Some(file)
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(file) = completed {
|
||||
let worker = resolve_workspace_worker_reference(&api, &path.runtime_id, &path.worker_id)?;
|
||||
api.runtime
|
||||
.delete_worker_uploaded_file(&worker, &file.artifact_id)
|
||||
.map_err(|error| error.into_error())?;
|
||||
}
|
||||
Ok(Json(AttachmentUploadCancelResponse { cancelled: true }))
|
||||
}
|
||||
|
||||
async fn scoped_delete_runtime_worker_uploaded_file(
|
||||
@@ -13323,20 +13597,6 @@ async fn send_runtime_worker_input(
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
async fn upload_runtime_worker_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id)): AxumPath<(String, String)>,
|
||||
Query(query): Query<WorkerFileUploadQuery>,
|
||||
body: Bytes,
|
||||
) -> ApiResult<Json<WorkerFileUploadResponse>> {
|
||||
let worker = resolve_workspace_worker_reference(&api, &runtime_id, &worker_id)?;
|
||||
let file = api
|
||||
.runtime
|
||||
.upload_worker_file(&worker, &query.file_name, &query.media_type, &body)
|
||||
.map_err(|err| err.into_error())?;
|
||||
Ok(Json(WorkerFileUploadResponse { file }))
|
||||
}
|
||||
|
||||
async fn delete_runtime_worker_uploaded_file(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath((runtime_id, worker_id, artifact_id)): AxumPath<(String, String, String)>,
|
||||
@@ -16084,6 +16344,77 @@ mod tests {
|
||||
SqliteWorkspaceStore, TrustedRuntimeRecord, UserRecord, WorkspaceRecord,
|
||||
};
|
||||
|
||||
fn completed_upload_file(sha256: &str) -> protocol::UploadedFileRef {
|
||||
protocol::UploadedFileRef {
|
||||
artifact_id: "019ca7c8-57b6-7f05-8edf-524147aba7b3".into(),
|
||||
file_name: "notes.txt".into(),
|
||||
media_type: "text/plain".into(),
|
||||
created_at_ms: 1,
|
||||
availability: protocol::UploadedFileAvailability::Available,
|
||||
byte_len: 1,
|
||||
sha256: sha256.into(),
|
||||
source_entry_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pending_upload_grant() -> AttachmentUploadGrant {
|
||||
AttachmentUploadGrant {
|
||||
workspace_id: "workspace-1".into(),
|
||||
runtime_id: "runtime-1".into(),
|
||||
worker_id: "worker-1".into(),
|
||||
account_id: "account-1".into(),
|
||||
file_name: "notes.txt".into(),
|
||||
media_type: "text/plain".into(),
|
||||
expires_at_ms: 100,
|
||||
state: AttachmentUploadGrantState::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_upload_grant_is_scoped_expiring_and_idempotent() {
|
||||
let mut grant = pending_upload_grant();
|
||||
assert!(matches!(
|
||||
grant.claim("workspace-2", "runtime-1", "worker-1", "account-1", 1, "a"),
|
||||
Err(Error::WorkspacePermissionDenied(_))
|
||||
));
|
||||
assert!(matches!(
|
||||
pending_upload_grant().claim(
|
||||
"workspace-1",
|
||||
"runtime-1",
|
||||
"worker-1",
|
||||
"account-1",
|
||||
101,
|
||||
"a"
|
||||
),
|
||||
Err(Error::InvalidInput(_))
|
||||
));
|
||||
|
||||
assert!(
|
||||
grant
|
||||
.claim("workspace-1", "runtime-1", "worker-1", "account-1", 1, "a")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
assert!(matches!(
|
||||
grant.claim("workspace-1", "runtime-1", "worker-1", "account-1", 1, "a"),
|
||||
Err(Error::RepositoryConflict(_))
|
||||
));
|
||||
|
||||
grant.state = AttachmentUploadGrantState::Completed(completed_upload_file("a"));
|
||||
assert_eq!(
|
||||
grant
|
||||
.claim("workspace-1", "runtime-1", "worker-1", "account-1", 1, "a")
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.artifact_id,
|
||||
"019ca7c8-57b6-7f05-8edf-524147aba7b3"
|
||||
);
|
||||
assert!(matches!(
|
||||
grant.claim("workspace-1", "runtime-1", "worker-1", "account-1", 1, "b"),
|
||||
Err(Error::RepositoryConflict(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_session_survives_attachment_refresh_until_worker_revocation() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user