Update public backend API surface
Private source commit: ba3f7ce2b6d9
This commit is contained in:
parent
784463622c
commit
26fdcb9d84
34 changed files with 5473 additions and 72 deletions
398
crates/clusterflux-client/src/protocol.rs
Normal file
398
crates/clusterflux-client/src/protocol.rs
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use clusterflux_core::{
|
||||
AgentId, ApiErrorCategory, ApiErrorCode, ArtifactId, Authorization, Digest, DownloadLink,
|
||||
LimitKind, NodeId, ProcessId, ProjectId, ResourceLimits, TaskInstanceId, TenantId, UserId,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::types::*;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub(crate) enum AuthenticatedRequest {
|
||||
AuthStatus,
|
||||
RevokeCliSession,
|
||||
CreateProject {
|
||||
project: ProjectId,
|
||||
name: String,
|
||||
},
|
||||
SelectProject {
|
||||
project: ProjectId,
|
||||
},
|
||||
ListProjects,
|
||||
RegisterAgentPublicKey {
|
||||
agent: AgentId,
|
||||
public_key: String,
|
||||
},
|
||||
ListAgentPublicKeys,
|
||||
RotateAgentPublicKey {
|
||||
agent: AgentId,
|
||||
public_key: String,
|
||||
},
|
||||
RevokeAgentPublicKey {
|
||||
agent: AgentId,
|
||||
},
|
||||
CreateNodeEnrollmentGrant {
|
||||
ttl_seconds: u64,
|
||||
},
|
||||
ListNodeSummaries {
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
},
|
||||
RevokeNodeCredential {
|
||||
node: NodeId,
|
||||
},
|
||||
ListProcessSummaries {
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
},
|
||||
CancelProcess {
|
||||
process: ProcessId,
|
||||
},
|
||||
AbortProcess {
|
||||
process: ProcessId,
|
||||
launch_attempt: Option<String>,
|
||||
},
|
||||
QuotaStatus,
|
||||
ListTaskEvents {
|
||||
process: Option<ProcessId>,
|
||||
},
|
||||
ListTaskSnapshots {
|
||||
process: ProcessId,
|
||||
},
|
||||
ListRecentLogs {
|
||||
process: ProcessId,
|
||||
task: Option<TaskInstanceId>,
|
||||
after_sequence: Option<u64>,
|
||||
limit: u32,
|
||||
},
|
||||
RestartTask {
|
||||
process: ProcessId,
|
||||
task: TaskInstanceId,
|
||||
replacement_bundle: Option<TaskReplacementBundle>,
|
||||
},
|
||||
ResolveTaskFailure {
|
||||
process: ProcessId,
|
||||
task: TaskInstanceId,
|
||||
resolution: TaskFailureResolution,
|
||||
},
|
||||
DebugAttach {
|
||||
process: ProcessId,
|
||||
},
|
||||
CreateDebugEpoch {
|
||||
process: ProcessId,
|
||||
stopped_task: TaskInstanceId,
|
||||
reason: String,
|
||||
},
|
||||
ResumeDebugEpoch {
|
||||
process: ProcessId,
|
||||
epoch: u64,
|
||||
},
|
||||
InspectDebugEpoch {
|
||||
process: ProcessId,
|
||||
epoch: u64,
|
||||
},
|
||||
ListArtifacts {
|
||||
process: Option<ProcessId>,
|
||||
cursor: Option<String>,
|
||||
limit: u32,
|
||||
},
|
||||
GetArtifact {
|
||||
artifact: ArtifactId,
|
||||
},
|
||||
CreateArtifactDownloadLink {
|
||||
artifact: ArtifactId,
|
||||
max_bytes: u64,
|
||||
ttl_seconds: u64,
|
||||
},
|
||||
OpenArtifactDownloadStream {
|
||||
artifact: ArtifactId,
|
||||
max_bytes: u64,
|
||||
token_digest: Digest,
|
||||
chunk_bytes: u64,
|
||||
},
|
||||
RevokeArtifactDownloadLink {
|
||||
artifact: ArtifactId,
|
||||
token_digest: Digest,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub(crate) enum LoginRequest {
|
||||
BeginWebBrowserLogin {},
|
||||
ExchangeWebLoginHandoff {
|
||||
transaction_id: String,
|
||||
handoff_code: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub(crate) struct WireBrowserSession {
|
||||
pub tenant: TenantId,
|
||||
pub project: ProjectId,
|
||||
pub user: UserId,
|
||||
pub session_secret: String,
|
||||
pub expires_at_epoch_seconds: u64,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub(crate) enum WireResponse {
|
||||
AuthStatus {
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
actor: UserId,
|
||||
authenticated: bool,
|
||||
account_status: String,
|
||||
suspended: bool,
|
||||
disabled: bool,
|
||||
deleted: bool,
|
||||
manual_review: bool,
|
||||
sanitized_reason: Option<String>,
|
||||
next_actions: Vec<String>,
|
||||
},
|
||||
CliSessionRevoked {},
|
||||
ProjectCreated {
|
||||
project: Project,
|
||||
},
|
||||
ProjectSelected {
|
||||
project: Project,
|
||||
},
|
||||
Projects {
|
||||
projects: Vec<Project>,
|
||||
},
|
||||
AgentPublicKey {
|
||||
record: AgentPublicKey,
|
||||
},
|
||||
AgentPublicKeys {
|
||||
records: Vec<AgentPublicKey>,
|
||||
},
|
||||
NodeEnrollmentGrantCreated {
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
grant: String,
|
||||
scope: String,
|
||||
expires_at_epoch_seconds: u64,
|
||||
},
|
||||
NodeSummaries {
|
||||
nodes: Vec<NodeSummary>,
|
||||
next_cursor: Option<String>,
|
||||
},
|
||||
NodeCredentialRevoked {
|
||||
node: NodeId,
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
actor: UserId,
|
||||
descriptor_removed: bool,
|
||||
queued_assignments_removed: usize,
|
||||
},
|
||||
ProcessSummaries {
|
||||
processes: Vec<ProcessSummary>,
|
||||
next_cursor: Option<String>,
|
||||
},
|
||||
ProcessCancellationRequested {
|
||||
process: ProcessId,
|
||||
cancelled_tasks: Vec<TaskCancellationTarget>,
|
||||
affected_nodes: Vec<NodeId>,
|
||||
},
|
||||
ProcessAborted {
|
||||
process: ProcessId,
|
||||
aborted_tasks: Vec<TaskCancellationTarget>,
|
||||
affected_nodes: Vec<NodeId>,
|
||||
},
|
||||
QuotaStatus {
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
actor: UserId,
|
||||
policy_label: Option<String>,
|
||||
limits: ResourceLimits,
|
||||
window_seconds: BTreeMap<LimitKind, u64>,
|
||||
usage: BTreeMap<LimitKind, u64>,
|
||||
window_started_epoch_seconds: BTreeMap<LimitKind, u64>,
|
||||
},
|
||||
TaskEvents {
|
||||
events: Vec<TaskCompletionEvent>,
|
||||
},
|
||||
TaskSnapshots {
|
||||
snapshots: Vec<TaskAttemptSnapshot>,
|
||||
},
|
||||
RecentLogs {
|
||||
entries: Vec<RecentLogEntry>,
|
||||
next_sequence: Option<u64>,
|
||||
history_truncated: bool,
|
||||
},
|
||||
TaskRestart {
|
||||
process: ProcessId,
|
||||
task: TaskInstanceId,
|
||||
restarted_task_instance: Option<TaskInstanceId>,
|
||||
restarted_attempt_id: Option<String>,
|
||||
actor: UserId,
|
||||
accepted: bool,
|
||||
clean_boundary_available: bool,
|
||||
active_task: bool,
|
||||
completed_event_observed: bool,
|
||||
requires_whole_process_restart: bool,
|
||||
message: String,
|
||||
audit_event: DebugAuditEvent,
|
||||
},
|
||||
TaskFailureResolved {
|
||||
process: ProcessId,
|
||||
task: TaskInstanceId,
|
||||
attempt_id: String,
|
||||
resolution: TaskFailureResolution,
|
||||
},
|
||||
DebugAttach {
|
||||
process: ProcessId,
|
||||
actor: UserId,
|
||||
authorization: Authorization,
|
||||
audit_event: DebugAuditEvent,
|
||||
},
|
||||
DebugEpoch {
|
||||
process: ProcessId,
|
||||
actor: UserId,
|
||||
epoch: u64,
|
||||
command: String,
|
||||
affected_tasks: Vec<TaskCancellationTarget>,
|
||||
all_stop_requested: bool,
|
||||
audit_event: DebugAuditEvent,
|
||||
},
|
||||
DebugEpochStatus {
|
||||
process: ProcessId,
|
||||
actor: UserId,
|
||||
epoch: u64,
|
||||
command: String,
|
||||
expected_tasks: Vec<TaskCancellationTarget>,
|
||||
acknowledgements: Vec<DebugParticipantAcknowledgement>,
|
||||
fully_frozen: bool,
|
||||
partially_frozen: bool,
|
||||
fully_resumed: bool,
|
||||
failed: bool,
|
||||
failure_messages: Vec<String>,
|
||||
audit_event: DebugAuditEvent,
|
||||
},
|
||||
Artifacts {
|
||||
artifacts: Vec<ArtifactSummary>,
|
||||
next_cursor: Option<String>,
|
||||
},
|
||||
Artifact {
|
||||
artifact: ArtifactSummary,
|
||||
},
|
||||
ArtifactDownloadLink {
|
||||
link: DownloadLink,
|
||||
},
|
||||
ArtifactDownloadLinkRevoked {},
|
||||
ArtifactDownloadStream {
|
||||
#[serde(rename = "link")]
|
||||
_link: DownloadLink,
|
||||
content_bytes_available: bool,
|
||||
content_offset: Option<u64>,
|
||||
content_eof: bool,
|
||||
content_base64: Option<String>,
|
||||
},
|
||||
WebBrowserLoginStarted {
|
||||
transaction_id: String,
|
||||
authorization_url: String,
|
||||
expires_at_epoch_seconds: u64,
|
||||
},
|
||||
WebBrowserSession {
|
||||
session: WireBrowserSession,
|
||||
},
|
||||
Error {
|
||||
code: ApiErrorCode,
|
||||
category: ApiErrorCategory,
|
||||
message: String,
|
||||
retryable: bool,
|
||||
request_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OperationFixture {
|
||||
operation: String,
|
||||
boundary: String,
|
||||
request: Value,
|
||||
response: Value,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn website_operation_contract_fixtures_cover_every_typed_request() {
|
||||
let fixtures: Vec<OperationFixture> =
|
||||
serde_json::from_str(include_str!("../tests/fixtures/web_operations.json")).unwrap();
|
||||
let expected = BTreeSet::from([
|
||||
"abort_process",
|
||||
"auth_status",
|
||||
"begin_web_browser_login",
|
||||
"cancel_process",
|
||||
"create_artifact_download_link",
|
||||
"create_debug_epoch",
|
||||
"create_node_enrollment_grant",
|
||||
"create_project",
|
||||
"debug_attach",
|
||||
"exchange_web_login_handoff",
|
||||
"get_artifact",
|
||||
"inspect_debug_epoch",
|
||||
"list_agent_public_keys",
|
||||
"list_artifacts",
|
||||
"list_node_summaries",
|
||||
"list_process_summaries",
|
||||
"list_projects",
|
||||
"list_recent_logs",
|
||||
"list_task_events",
|
||||
"list_task_snapshots",
|
||||
"open_artifact_download_stream",
|
||||
"quota_status",
|
||||
"register_agent_public_key",
|
||||
"resolve_task_failure",
|
||||
"restart_task",
|
||||
"resume_debug_epoch",
|
||||
"revoke_agent_public_key",
|
||||
"revoke_artifact_download_link",
|
||||
"revoke_cli_session",
|
||||
"revoke_node_credential",
|
||||
"rotate_agent_public_key",
|
||||
"select_project",
|
||||
])
|
||||
.into_iter()
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
let mut observed = BTreeSet::new();
|
||||
|
||||
for fixture in fixtures {
|
||||
assert_eq!(fixture.request["type"], fixture.operation);
|
||||
let round_trip = match fixture.boundary.as_str() {
|
||||
"control" => {
|
||||
let typed: AuthenticatedRequest =
|
||||
serde_json::from_value(fixture.request.clone()).unwrap();
|
||||
serde_json::to_value(typed).unwrap()
|
||||
}
|
||||
"login" => {
|
||||
let typed: LoginRequest =
|
||||
serde_json::from_value(fixture.request.clone()).unwrap();
|
||||
serde_json::to_value(typed).unwrap()
|
||||
}
|
||||
boundary => panic!("unknown fixture boundary {boundary}"),
|
||||
};
|
||||
assert_eq!(round_trip, fixture.request);
|
||||
let response: WireResponse = serde_json::from_value(fixture.response).unwrap();
|
||||
assert!(
|
||||
!matches!(response, WireResponse::Error { .. }),
|
||||
"{} must carry its operation-specific success response fixture",
|
||||
fixture.operation
|
||||
);
|
||||
assert!(observed.insert(fixture.operation));
|
||||
}
|
||||
assert_eq!(observed, expected);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue