1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use sha2::{Digest as _, Sha256};
5use utoipa::ToSchema;
6
7pub const RUNTIME_OPERATIONS_PROTOCOL: &str = "lenso.system-plane.runtime-operations.v1";
8pub const RUNTIME_OPERATIONS_PATH: &str = "/system-plane/v1/runtime-operations";
9pub const RUNTIME_OPERATIONS_FEATURE_FUNCTION_RETRY: &str = "function-run-retry";
10pub const RUNTIME_OPERATIONS_FEATURE_OUTBOX_RETRY: &str = "outbox-event-retry";
11pub const RUNTIME_OPERATIONS_FEATURE_EVIDENCE: &str = "operation-evidence";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
14#[serde(rename_all = "snake_case")]
15pub enum RuntimeOperationTargetKind {
16 FunctionRun,
17 OutboxEvent,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
21#[serde(rename_all = "snake_case")]
22pub enum RuntimeOperationTargetStatus {
23 Pending,
24 Processing,
25 Running,
26 Completed,
27 Published,
28 Failed,
29 Dead,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum RuntimeOperationDesiredOutcome {
35 Retry,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
39#[serde(rename_all = "snake_case")]
40pub enum ManagementActorKind {
41 Operator,
42 Automation,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
46#[serde(rename_all = "camelCase", deny_unknown_fields)]
47pub struct RuntimeOperationTarget {
48 pub kind: RuntimeOperationTargetKind,
49 pub target_id: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct RuntimeOperationTargetSnapshot {
55 pub protocol: String,
56 pub service_id: String,
57 pub service_revision: String,
58 pub target: RuntimeOperationTarget,
59 pub target_revision: String,
60 pub observed_at_unix_ms: u64,
61 pub target_name: String,
62 pub status: RuntimeOperationTargetStatus,
63 pub attempts: u32,
64 pub max_attempts: u32,
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct ManagementActor {
70 pub kind: ManagementActorKind,
71 pub subject: String,
72 pub delegated_authority_digest: String,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct ManagementApproval {
78 pub approval_id: String,
79 pub approval_digest: String,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ManagementIntent {
85 pub protocol: String,
86 pub intent_id: String,
87 pub service_id: String,
88 pub service_revision: String,
89 pub target: RuntimeOperationTarget,
90 pub desired_outcome: RuntimeOperationDesiredOutcome,
91 pub expected_target_revision: String,
92 pub actor: ManagementActor,
93 pub approvals: Vec<ManagementApproval>,
94 pub deadline_unix_ms: u64,
95 pub idempotency_key: String,
96 pub capability_contract_id: String,
97 pub capability_schema_digest: String,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
101#[serde(rename_all = "snake_case")]
102pub enum RuntimeOperationRisk {
103 DuplicateExternalEffect,
104 RepeatedBusinessNotification,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
108#[serde(rename_all = "snake_case")]
109pub enum RuntimeOperationAvailabilityImpact {
110 None,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
114#[serde(rename_all = "snake_case")]
115pub enum RuntimeOperationCompensationSupport {
116 NotAvailable,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
120#[serde(rename_all = "camelCase", deny_unknown_fields)]
121pub struct RuntimeOperationPlanReceipt {
122 pub protocol: String,
123 pub intent_digest: String,
124 pub plan_digest: String,
125 pub service_id: String,
126 pub service_revision: String,
127 pub target: RuntimeOperationTarget,
128 pub expected_target_revision: String,
129 pub expected_effects: Vec<String>,
130 pub risks: Vec<RuntimeOperationRisk>,
131 pub availability_impact: RuntimeOperationAvailabilityImpact,
132 pub compensation_support: RuntimeOperationCompensationSupport,
133 pub approval_required: bool,
134 pub expires_at_unix_ms: u64,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
138#[serde(rename_all = "camelCase", deny_unknown_fields)]
139pub struct RuntimeOperationSubmission {
140 pub intent: ManagementIntent,
141 pub plan: RuntimeOperationPlanReceipt,
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
145#[serde(rename_all = "snake_case")]
146pub enum RuntimeOperationState {
147 Accepted,
148 Succeeded,
149 Rejected,
150 Failed,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
154#[serde(rename_all = "camelCase", deny_unknown_fields)]
155pub struct RuntimeOperationAcknowledgement {
156 pub protocol: String,
157 pub operation_id: String,
158 pub idempotency_key: String,
159 pub intent_digest: String,
160 pub plan_digest: String,
161 pub state: RuntimeOperationState,
162 pub accepted_at_unix_ms: u64,
163 pub authorization_epoch: u64,
164 pub enrollment_receipt_digest: String,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
168#[serde(rename_all = "camelCase", deny_unknown_fields)]
169pub struct RuntimeOperationEvidence {
170 pub protocol: String,
171 pub operation_id: String,
172 pub sequence: u64,
173 pub state: RuntimeOperationState,
174 pub recorded_at_unix_ms: u64,
175 pub service_id: String,
176 pub service_revision: String,
177 pub target: RuntimeOperationTarget,
178 pub target_revision_before: String,
179 pub target_revision_after: Option<String>,
180 pub code: String,
181 pub message: String,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
185#[serde(rename_all = "camelCase", deny_unknown_fields)]
186pub struct RuntimeOperationEvidencePage {
187 pub protocol: String,
188 pub operation_id: String,
189 pub items: Vec<RuntimeOperationEvidence>,
190 pub next_cursor: Option<String>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
194#[serde(rename_all = "camelCase", deny_unknown_fields)]
195pub struct RuntimeOperationRecovery {
196 pub protocol: String,
197 pub acknowledgement: RuntimeOperationAcknowledgement,
198 pub latest_evidence: RuntimeOperationEvidence,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
202#[serde(tag = "kind", content = "document", rename_all = "snake_case")]
203pub enum RuntimeOperationsMessage {
204 TargetSnapshot(RuntimeOperationTargetSnapshot),
205 Intent(ManagementIntent),
206 PlanReceipt(RuntimeOperationPlanReceipt),
207 Submission(RuntimeOperationSubmission),
208 Acknowledgement(RuntimeOperationAcknowledgement),
209 Evidence(RuntimeOperationEvidence),
210 EvidencePage(RuntimeOperationEvidencePage),
211 Recovery(RuntimeOperationRecovery),
212}
213
214#[must_use]
215pub fn management_intent_digest(intent: &ManagementIntent) -> String {
216 digest_json(intent)
217}
218
219#[must_use]
220pub fn runtime_operation_plan_digest(plan: &RuntimeOperationPlanReceipt) -> String {
221 let mut unsigned = plan.clone();
222 unsigned.plan_digest.clear();
223 digest_json(&unsigned)
224}
225
226#[must_use]
227pub fn runtime_operations_schema() -> Value {
228 let mut schema = serde_json::to_value(schemars::schema_for!(RuntimeOperationsMessage))
229 .expect("Runtime Operations schema serializes");
230 schema["$id"] = Value::String(
231 "https://contracts.lenso.local/system-plane/lenso.system-plane.runtime-operations.v1.schema.json"
232 .to_owned(),
233 );
234 schema["title"] = Value::String("Lenso Runtime Operations Messages".to_owned());
235 for definition in [
236 "RuntimeOperationTargetSnapshot",
237 "ManagementIntent",
238 "RuntimeOperationPlanReceipt",
239 "RuntimeOperationAcknowledgement",
240 "RuntimeOperationEvidence",
241 "RuntimeOperationEvidencePage",
242 "RuntimeOperationRecovery",
243 ] {
244 schema["$defs"][definition]["properties"]["protocol"] =
245 json!({ "const": RUNTIME_OPERATIONS_PROTOCOL });
246 }
247 patch_digest(&mut schema, "ManagementActor", "delegatedAuthorityDigest");
248 patch_digest(&mut schema, "ManagementApproval", "approvalDigest");
249 patch_digest(&mut schema, "ManagementIntent", "expectedTargetRevision");
250 patch_digest(&mut schema, "ManagementIntent", "capabilitySchemaDigest");
251 patch_digest(&mut schema, "RuntimeOperationPlanReceipt", "intentDigest");
252 patch_digest(&mut schema, "RuntimeOperationPlanReceipt", "planDigest");
253 patch_digest(
254 &mut schema,
255 "RuntimeOperationPlanReceipt",
256 "expectedTargetRevision",
257 );
258 patch_digest(
259 &mut schema,
260 "RuntimeOperationAcknowledgement",
261 "intentDigest",
262 );
263 patch_digest(&mut schema, "RuntimeOperationAcknowledgement", "planDigest");
264 patch_digest(
265 &mut schema,
266 "RuntimeOperationAcknowledgement",
267 "enrollmentReceiptDigest",
268 );
269 patch_digest(
270 &mut schema,
271 "RuntimeOperationEvidence",
272 "targetRevisionBefore",
273 );
274 schema
275}
276
277#[must_use]
278pub fn runtime_operations_schema_digest() -> String {
279 digest_bytes(
280 &serde_json::to_vec(&runtime_operations_schema())
281 .expect("Runtime Operations schema serializes to bytes"),
282 )
283}
284
285fn patch_digest(schema: &mut Value, definition: &str, field: &str) {
286 schema["$defs"][definition]["properties"][field]["pattern"] = json!("^sha256:[0-9a-f]{64}$");
287}
288
289fn digest_json<T: Serialize>(value: &T) -> String {
290 digest_bytes(&serde_json::to_vec(value).expect("Runtime Operations document serializes"))
291}
292
293fn digest_bytes(bytes: &[u8]) -> String {
294 format!("sha256:{}", hex(&Sha256::digest(bytes)))
295}
296
297fn hex(bytes: &[u8]) -> String {
298 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
299}