Skip to main content

lenso_service/
system_plane.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::{Value, json};
4use std::collections::{BTreeSet, HashSet};
5use utoipa::ToSchema;
6
7pub mod enrollment;
8pub mod runtime_observability;
9pub mod runtime_operations;
10
11pub use enrollment::*;
12pub use runtime_observability::*;
13pub use runtime_operations::*;
14
15pub const CORE_PROTOCOL: &str = "lenso.system-plane.v1";
16pub const CORE_PATH: &str = "/system-plane/v1";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20pub struct CoreDocument {
21    pub protocol: String,
22    #[schema(min_length = 1)]
23    pub service_id: String,
24    #[schema(min_length = 1)]
25    pub service_principal: String,
26    #[schema(min_length = 1)]
27    pub service_revision: String,
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub capabilities: Vec<CapabilityAdvertisement>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
33#[serde(rename_all = "camelCase", deny_unknown_fields)]
34pub struct CapabilityAdvertisement {
35    #[schema(pattern = r"^lenso\.system-plane\.[a-z0-9]+(?:[.-][a-z0-9]+)*\.v[1-9][0-9]*$")]
36    pub contract_id: String,
37    #[schema(minimum = 1)]
38    pub major_version: u32,
39    #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
40    pub feature_ids: BTreeSet<String>,
41    #[schema(pattern = r"^sha256:[0-9a-f]{64}$")]
42    pub schema_digest: String,
43    #[schema(pattern = r"^/system-plane/v1/[a-z0-9]+(?:[/-][a-z0-9]+)*$")]
44    pub endpoint: String,
45}
46
47#[derive(
48    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
49)]
50#[serde(rename_all = "snake_case")]
51pub enum CoreIssueCode {
52    InvalidProtocol,
53    MissingServiceIdentity,
54    MissingServicePrincipal,
55    MissingServiceRevision,
56    InvalidCapabilityContractId,
57    InvalidCapabilityMajorVersion,
58    CapabilityMajorVersionMismatch,
59    InvalidFeatureId,
60    InvalidSchemaDigest,
61    InvalidEndpointReference,
62    DuplicateCapability,
63}
64
65#[derive(
66    Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
67)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct CoreIssue {
70    pub code: CoreIssueCode,
71    pub path: String,
72    pub message: String,
73    pub next_action: String,
74}
75
76#[must_use]
77pub fn validate_core_document(document: &CoreDocument) -> Vec<CoreIssue> {
78    let mut issues = Vec::new();
79
80    if document.protocol != CORE_PROTOCOL {
81        push_issue(
82            &mut issues,
83            CoreIssueCode::InvalidProtocol,
84            "$.protocol",
85            format!("protocol must be `{CORE_PROTOCOL}`"),
86            "Use the supported System Plane Core Protocol identifier.",
87        );
88    }
89    validate_present(
90        &document.service_id,
91        CoreIssueCode::MissingServiceIdentity,
92        "$.serviceId",
93        "serviceId must identify the managed Service",
94        "Publish the stable logical Service identity.",
95        &mut issues,
96    );
97    validate_present(
98        &document.service_principal,
99        CoreIssueCode::MissingServicePrincipal,
100        "$.servicePrincipal",
101        "servicePrincipal must identify the managed Service authority",
102        "Publish the stable Service Principal independently from endpoint and Workload identity.",
103        &mut issues,
104    );
105    validate_present(
106        &document.service_revision,
107        CoreIssueCode::MissingServiceRevision,
108        "$.serviceRevision",
109        "serviceRevision must identify the advertised Service state",
110        "Publish a stable revision that changes when the Core advertisement changes.",
111        &mut issues,
112    );
113
114    let mut advertised_contracts = HashSet::new();
115    for (index, capability) in document.capabilities.iter().enumerate() {
116        let base = format!("$.capabilities[{index}]");
117        let contract_major = capability_major_version(&capability.contract_id);
118
119        if contract_major.is_none() {
120            push_issue(
121                &mut issues,
122                CoreIssueCode::InvalidCapabilityContractId,
123                format!("{base}.contractId"),
124                "contractId must match `lenso.system-plane.<capability>.v<major>`",
125                "Publish a stable capability-specific System Plane Contract identifier.",
126            );
127        }
128        if capability.major_version == 0 {
129            push_issue(
130                &mut issues,
131                CoreIssueCode::InvalidCapabilityMajorVersion,
132                format!("{base}.majorVersion"),
133                "majorVersion must be greater than zero",
134                "Publish the supported major version for this Capability Contract.",
135            );
136        } else if contract_major.is_some_and(|major| major != capability.major_version) {
137            push_issue(
138                &mut issues,
139                CoreIssueCode::CapabilityMajorVersionMismatch,
140                format!("{base}.majorVersion"),
141                "majorVersion must match the version suffix in contractId",
142                "Make the advertised major version and Contract identifier agree.",
143            );
144        }
145
146        for feature_id in &capability.feature_ids {
147            if !valid_dotted_id(feature_id) {
148                push_issue(
149                    &mut issues,
150                    CoreIssueCode::InvalidFeatureId,
151                    format!("{base}.featureIds"),
152                    format!("feature identifier `{feature_id}` is not canonical"),
153                    "Use lowercase dot-separated identifiers with alphanumeric or hyphenated segments.",
154                );
155            }
156        }
157        if !valid_sha256_digest(&capability.schema_digest) {
158            push_issue(
159                &mut issues,
160                CoreIssueCode::InvalidSchemaDigest,
161                format!("{base}.schemaDigest"),
162                "schemaDigest must be a lowercase `sha256:<64 hex>` digest",
163                "Publish the digest of the exact Capability Contract schema.",
164            );
165        }
166        if !valid_capability_endpoint(&capability.endpoint) {
167            push_issue(
168                &mut issues,
169                CoreIssueCode::InvalidEndpointReference,
170                format!("{base}.endpoint"),
171                "endpoint must be a relative subpath below `/system-plane/v1/`",
172                "Publish a capability endpoint inside the managed Service System Plane namespace.",
173            );
174        }
175        if !advertised_contracts.insert(capability.contract_id.as_str()) {
176            push_issue(
177                &mut issues,
178                CoreIssueCode::DuplicateCapability,
179                format!("{base}.contractId"),
180                format!(
181                    "Capability Contract `{}` is advertised more than once",
182                    capability.contract_id
183                ),
184                "Publish each exact Capability Contract identity once.",
185            );
186        }
187    }
188
189    issues
190}
191
192#[must_use]
193pub fn core_document_schema() -> Value {
194    let mut schema = serde_json::to_value(schemars::schema_for!(CoreDocument))
195        .expect("System Plane Core schema serializes");
196    schema["$id"] = Value::String(
197        "https://contracts.lenso.local/system-plane/lenso.system-plane.v1.schema.json".to_owned(),
198    );
199    schema["title"] = Value::String("Lenso System Plane Core Document".to_owned());
200    schema["properties"]["protocol"] = json!({ "const": CORE_PROTOCOL });
201    for field in ["serviceId", "servicePrincipal", "serviceRevision"] {
202        schema["properties"][field]["minLength"] = json!(1);
203    }
204    schema["$defs"]["CapabilityAdvertisement"]["properties"]["contractId"]["pattern"] =
205        json!(r"^lenso\.system-plane\.[a-z0-9]+(?:[.-][a-z0-9]+)*\.v[1-9][0-9]*$");
206    schema["$defs"]["CapabilityAdvertisement"]["properties"]["majorVersion"]["minimum"] = json!(1);
207    schema["$defs"]["CapabilityAdvertisement"]["properties"]["featureIds"]["items"] = json!({
208        "type": "string",
209        "pattern": r"^[a-z0-9]+(?:[.-][a-z0-9]+)*$"
210    });
211    schema["$defs"]["CapabilityAdvertisement"]["properties"]["schemaDigest"]["pattern"] =
212        json!(r"^sha256:[0-9a-f]{64}$");
213    schema["$defs"]["CapabilityAdvertisement"]["properties"]["endpoint"]["pattern"] =
214        json!(r"^/system-plane/v1/[a-z0-9]+(?:[/-][a-z0-9]+)*$");
215    schema
216}
217
218fn validate_present(
219    value: &str,
220    code: CoreIssueCode,
221    path: &str,
222    message: &str,
223    next_action: &str,
224    issues: &mut Vec<CoreIssue>,
225) {
226    if value.trim().is_empty() {
227        push_issue(issues, code, path, message, next_action);
228    }
229}
230
231fn capability_major_version(contract_id: &str) -> Option<u32> {
232    let remainder = contract_id.strip_prefix("lenso.system-plane.")?;
233    let (capability, version) = remainder.rsplit_once(".v")?;
234    if !valid_dotted_id(capability) {
235        return None;
236    }
237    let major = version.parse::<u32>().ok()?;
238    (major > 0 && version == major.to_string()).then_some(major)
239}
240
241fn valid_dotted_id(value: &str) -> bool {
242    !value.is_empty() && value.split('.').all(valid_id_segment)
243}
244
245fn valid_id_segment(segment: &str) -> bool {
246    !segment.is_empty()
247        && !segment.starts_with('-')
248        && !segment.ends_with('-')
249        && !segment.contains("--")
250        && segment
251            .bytes()
252            .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
253}
254
255fn valid_sha256_digest(value: &str) -> bool {
256    value.strip_prefix("sha256:").is_some_and(|digest| {
257        digest.len() == 64
258            && digest
259                .bytes()
260                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
261    })
262}
263
264fn valid_capability_endpoint(value: &str) -> bool {
265    value
266        .strip_prefix("/system-plane/v1/")
267        .is_some_and(|suffix| {
268            !suffix.is_empty()
269                && !suffix.starts_with('/')
270                && !suffix.contains("//")
271                && !suffix.contains(['?', '#'])
272                && suffix.split('/').all(valid_id_segment)
273        })
274}
275
276fn push_issue(
277    issues: &mut Vec<CoreIssue>,
278    code: CoreIssueCode,
279    path: impl Into<String>,
280    message: impl Into<String>,
281    next_action: impl Into<String>,
282) {
283    issues.push(CoreIssue {
284        code,
285        path: path.into(),
286        message: message.into(),
287        next_action: next_action.into(),
288    });
289}