1use lenso_contracts::{
2 ConsoleContributionAction, ModuleConfigActivation, ModuleConfigFieldType,
3 ModuleConfigMutability, ModuleConfigScope, digest_json,
4};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7use serde_json::{Value, json};
8use std::collections::{BTreeMap, BTreeSet};
9use utoipa::ToSchema;
10
11pub const MODULE_OPERATIONS_PROTOCOL: &str = "lenso.system-plane.module-operations.v1";
12pub const MODULE_OPERATIONS_PATH: &str = "/system-plane/v1/modules";
13pub const MODULE_OPERATIONS_FEATURE_INVENTORY_READ: &str = "module.inventory.read";
14pub const MODULE_OPERATIONS_FEATURE_CONTRIBUTIONS_RESOLVE: &str =
15 "module.action-contributions.resolve";
16pub const MODULE_OPERATIONS_FEATURE_CONFIG_READ: &str = "module.config.read";
17pub const MODULE_OPERATIONS_FEATURE_CONFIG_WRITE: &str = "module.config.write";
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
24#[serde(rename_all = "camelCase", deny_unknown_fields)]
25pub struct ManagedServiceContext {
26 pub system_id: String,
27 pub service_id: String,
28 pub environment_id: String,
29 pub target_service_principal: String,
30 pub caller_module_id: String,
31 pub delegated_actor_subject: String,
32 pub delegated_authority_digest: String,
33 #[serde(default)]
34 pub capabilities: BTreeSet<String>,
35}
36
37impl ManagedServiceContext {
38 #[must_use]
39 pub fn new(
40 system_id: impl Into<String>,
41 service_id: impl Into<String>,
42 environment_id: impl Into<String>,
43 target_service_principal: impl Into<String>,
44 caller_module_id: impl Into<String>,
45 delegated_actor_subject: impl Into<String>,
46 delegated_authority_digest: impl Into<String>,
47 capabilities: impl IntoIterator<Item = impl Into<String>>,
48 ) -> Self {
49 Self {
50 system_id: system_id.into(),
51 service_id: service_id.into(),
52 environment_id: environment_id.into(),
53 target_service_principal: target_service_principal.into(),
54 caller_module_id: caller_module_id.into(),
55 delegated_actor_subject: delegated_actor_subject.into(),
56 delegated_authority_digest: delegated_authority_digest.into(),
57 capabilities: capabilities.into_iter().map(Into::into).collect(),
58 }
59 }
60
61 pub fn digest(&self) -> Result<String, serde_json::Error> {
64 digest_json(self)
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct ModuleInventoryRequest {
71 pub context: ManagedServiceContext,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
75#[serde(rename_all = "camelCase", deny_unknown_fields)]
76pub struct ModuleInventorySnapshot {
77 pub protocol: String,
78 pub context: ManagedServiceContext,
79 pub service_revision: String,
80 pub snapshot_revision: String,
81 pub schema_digest: String,
82 pub modules: Vec<ModuleInventoryModule>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct ModuleInventoryModule {
88 pub module_id: String,
89 pub version: String,
90 pub release_digest: String,
91 pub manifest_digest: String,
92 pub delivery: ModuleInventoryDelivery,
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub dependency_module_ids: Vec<String>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 pub routes: Vec<ModuleInventoryRoute>,
97 #[serde(default, skip_serializing_if = "Vec::is_empty")]
98 pub runtime_functions: Vec<String>,
99 pub runtime_status: ModuleRuntimeStatus,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub console_ui: Option<ModuleInventoryConsoleUi>,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
105#[serde(rename_all = "snake_case")]
106pub enum ModuleInventoryDelivery {
107 Linked,
108 Service,
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
112#[serde(rename_all = "snake_case")]
113pub enum ModuleRuntimeStatus {
114 Active,
115 Disabled,
116 Degraded,
117 Failed,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
121#[serde(rename_all = "camelCase", deny_unknown_fields)]
122pub struct ModuleInventoryRoute {
123 pub method: String,
124 pub path: String,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub capability: Option<String>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
130#[serde(rename_all = "camelCase", deny_unknown_fields)]
131pub struct ModuleInventoryConsoleUi {
132 pub format: String,
133 pub protocol_major: u32,
134 pub artifact_digest: String,
135 pub entry: String,
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
137 pub style_assets: Vec<String>,
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
141#[serde(rename_all = "camelCase", deny_unknown_fields)]
142pub struct ActionContributionResolutionRequest {
143 pub context: ManagedServiceContext,
144 pub slot: String,
145 pub slot_version: u32,
146 #[serde(default)]
147 pub slot_context: BTreeMap<String, Value>,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
151#[serde(rename_all = "camelCase", deny_unknown_fields)]
152pub struct ActionContributionResolution {
153 pub protocol: String,
154 pub context: ManagedServiceContext,
155 pub slot: String,
156 pub slot_version: u32,
157 pub contributions: Vec<ResolvedActionContribution>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
161#[serde(rename_all = "camelCase", deny_unknown_fields)]
162pub struct ResolvedActionContribution {
163 pub contributing_module_id: String,
164 pub target: String,
165 pub target_version: u32,
166 pub label: String,
167 pub action: ConsoleContributionAction,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
169 pub icon: Option<String>,
170 #[serde(default)]
171 pub required_capabilities: Vec<String>,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
175#[serde(rename_all = "camelCase", deny_unknown_fields)]
176pub struct ModuleConfigReadRequest {
177 pub context: ManagedServiceContext,
178 pub module_id: String,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub keys: Vec<String>,
181}
182
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
184#[serde(rename_all = "camelCase", deny_unknown_fields)]
185pub struct ModuleConfigReadResponse {
186 pub protocol: String,
187 pub context: ManagedServiceContext,
188 pub module_id: String,
189 pub values: Vec<ModuleConfigValue>,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194pub struct ModuleConfigValue {
195 pub key: String,
196 pub field_type: ModuleConfigFieldType,
197 pub scope: ModuleConfigScope,
198 pub mutability: ModuleConfigMutability,
199 pub activation: ModuleConfigActivation,
200 pub sensitive: bool,
201 pub present: bool,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub value: Option<Value>,
205}
206
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
208#[serde(rename_all = "camelCase", deny_unknown_fields)]
209pub struct ModuleConfigWriteRequest {
210 pub context: ManagedServiceContext,
211 pub module_id: String,
212 pub values: Vec<ModuleConfigWriteValue>,
213}
214
215#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
216#[serde(rename_all = "camelCase", deny_unknown_fields)]
217pub struct ModuleConfigWriteValue {
218 pub key: String,
219 pub value: Value,
220}
221
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224pub struct ModuleConfigWriteResponse {
225 pub protocol: String,
226 pub operation_id: String,
227 pub context: ManagedServiceContext,
228 pub module_id: String,
229 pub target_revision_before: String,
230 pub target_revision_after: String,
231 pub authorization_digest: String,
232 pub evidence: Vec<ModuleConfigAuditEvidence>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
236#[serde(rename_all = "camelCase", deny_unknown_fields)]
237pub struct ModuleConfigAuditEvidence {
238 pub sequence: u64,
239 pub operation_id: String,
240 pub module_id: String,
241 pub key: String,
242 pub sensitive: bool,
243 pub old_value_digest: Option<String>,
244 pub new_value_digest: String,
245 pub recorded_at_unix_ms: u64,
246}
247
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, ToSchema)]
249#[serde(tag = "kind", content = "document", rename_all = "snake_case")]
250pub enum ModuleOperationsMessage {
251 InventoryRequest(ModuleInventoryRequest),
252 Inventory(ModuleInventorySnapshot),
253 ContributionsRequest(ActionContributionResolutionRequest),
254 Contributions(ActionContributionResolution),
255 ConfigReadRequest(ModuleConfigReadRequest),
256 ConfigRead(ModuleConfigReadResponse),
257 ConfigWriteRequest(ModuleConfigWriteRequest),
258 ConfigWrite(ModuleConfigWriteResponse),
259}
260
261#[must_use]
262pub fn module_operations_schema() -> Value {
263 let mut schema = serde_json::to_value(schemars::schema_for!(ModuleOperationsMessage))
264 .expect("Module Operations schema must serialize");
265 schema["$id"] = Value::String(
266 "https://contracts.lenso.local/system-plane/lenso.system-plane.module-operations.v1.schema.json"
267 .to_owned(),
268 );
269 schema["title"] = Value::String("Lenso Managed Service Module Operations".to_owned());
270 for definition in [
271 "ModuleInventorySnapshot",
272 "ActionContributionResolution",
273 "ModuleConfigReadResponse",
274 "ModuleConfigWriteResponse",
275 ] {
276 if let Some(properties) = schema["$defs"][definition]["properties"].as_object_mut() {
277 properties.insert(
278 "protocol".to_owned(),
279 json!({ "type": "string", "const": MODULE_OPERATIONS_PROTOCOL }),
280 );
281 }
282 }
283 schema
284}
285
286#[must_use]
287pub fn module_operations_schema_digest() -> String {
288 digest_json(&module_operations_schema()).expect("Module Operations schema is digestible")
289}