1use super::{
2 HealthCheckKind, IsolationLevel, MountKind, NetworkMode, RuntimeUnitClass, RuntimeUnitSpec,
3};
4use crate::ProviderId;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeSet;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ResourceControl {
11 Cpu,
12 Memory,
13 Pids,
14 EphemeralStorage,
15 ExecutionTimeout,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum RuntimeFeature {
21 DurableIdentity,
22 Stop,
23 Remove,
24 ServiceTcp,
25 ServiceUdp,
26 Logs,
27 Exec,
28 Usage,
29 Attestation,
30 IdentityAttachment,
31 SecretReferences,
32 OutputArtifacts,
33 ServiceLifecycle,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(deny_unknown_fields)]
40pub struct RuntimeCapabilities {
41 pub schema: String,
42 pub provider_id: ProviderId,
43 pub provider_build: String,
44 pub unit_classes: Vec<RuntimeUnitClass>,
45 pub artifact_media_types: Vec<String>,
46 pub isolation_levels: Vec<IsolationLevel>,
47 pub network_modes: Vec<NetworkMode>,
48 pub mount_kinds: Vec<MountKind>,
49 pub health_check_kinds: Vec<HealthCheckKind>,
50 pub resource_controls: Vec<ResourceControl>,
51 pub features: Vec<RuntimeFeature>,
52}
53
54impl RuntimeCapabilities {
55 pub const SCHEMA: &'static str = "a3s.runtime.capabilities.v6";
56
57 pub fn validate(&self) -> Result<(), String> {
58 if self.schema != Self::SCHEMA {
59 return Err(format!(
60 "unsupported Runtime capabilities schema {:?}",
61 self.schema
62 ));
63 }
64 super::validate_nonempty("provider_build", &self.provider_build, 255)?;
65 if self.unit_classes.is_empty()
66 || self.artifact_media_types.is_empty()
67 || self.isolation_levels.is_empty()
68 || self.resource_controls.is_empty()
69 {
70 return Err("Runtime capabilities omit a required capability family".into());
71 }
72 ensure_unique("unit class", &self.unit_classes)?;
73 ensure_unique("artifact media type", &self.artifact_media_types)?;
74 ensure_unique("isolation level", &self.isolation_levels)?;
75 ensure_unique("network mode", &self.network_modes)?;
76 ensure_unique("mount kind", &self.mount_kinds)?;
77 ensure_unique("health check kind", &self.health_check_kinds)?;
78 ensure_unique("resource control", &self.resource_controls)?;
79 ensure_unique("feature", &self.features)?;
80 let service_mode = self.network_modes.contains(&NetworkMode::Service);
81 let service_protocol = self.supports_feature(RuntimeFeature::ServiceTcp)
82 || self.supports_feature(RuntimeFeature::ServiceUdp);
83 if service_mode != service_protocol {
84 return Err(
85 "Runtime Service networking and service protocol capabilities must be advertised together"
86 .into(),
87 );
88 }
89 if self.supports_feature(RuntimeFeature::ServiceLifecycle)
90 && (!self.unit_classes.contains(&RuntimeUnitClass::Service)
91 || self.health_check_kinds.is_empty()
92 || !self.supports_feature(RuntimeFeature::Stop))
93 {
94 return Err(
95 "Runtime Service lifecycle requires Service, health, and stop capabilities".into(),
96 );
97 }
98 for media_type in &self.artifact_media_types {
99 super::validate_nonempty("artifact media type", media_type, 255)?;
100 }
101 Ok(())
102 }
103
104 pub fn supports_feature(&self, feature: RuntimeFeature) -> bool {
105 self.features.contains(&feature)
106 }
107
108 pub fn missing_for(&self, spec: &RuntimeUnitSpec) -> Result<Vec<String>, String> {
109 self.validate()?;
110 spec.validate()?;
111 let mut missing = Vec::new();
112 if !self.unit_classes.contains(&spec.class) {
113 missing.push(format!("unit_class:{:?}", spec.class));
114 }
115 if !self
116 .artifact_media_types
117 .contains(&spec.artifact.media_type)
118 {
119 missing.push(format!("artifact_media_type:{}", spec.artifact.media_type));
120 }
121 if !self.isolation_levels.contains(&spec.isolation) {
122 missing.push(format!("isolation:{:?}", spec.isolation));
123 }
124 if !self.network_modes.contains(&spec.network.mode) {
125 missing.push(format!("network_mode:{:?}", spec.network.mode));
126 }
127 for protocol in spec.network.ports.iter().map(|port| port.protocol) {
128 let feature = match protocol {
129 super::TransportProtocol::Tcp => RuntimeFeature::ServiceTcp,
130 super::TransportProtocol::Udp => RuntimeFeature::ServiceUdp,
131 };
132 if !self.supports_feature(feature) {
133 missing.push(format!("feature:{feature:?}"));
134 }
135 }
136 for kind in spec.mounts.iter().map(|mount| mount.source.kind()) {
137 if !self.mount_kinds.contains(&kind) {
138 missing.push(format!("mount_kind:{kind:?}"));
139 }
140 }
141 if let Some(health) = &spec.health {
142 let kind = health.probe.kind();
143 if !self.health_check_kinds.contains(&kind) {
144 missing.push(format!("health_check:{kind:?}"));
145 }
146 }
147 if let Some(lifecycle) = &spec.service_lifecycle {
148 let kind = lifecycle.liveness.probe.kind();
149 if !self.health_check_kinds.contains(&kind) {
150 missing.push(format!("health_check:{kind:?}"));
151 }
152 }
153 for required in [
154 ResourceControl::Cpu,
155 ResourceControl::Memory,
156 ResourceControl::Pids,
157 ] {
158 if !self.resource_controls.contains(&required) {
159 missing.push(format!("resource_control:{required:?}"));
160 }
161 }
162 if spec.resources.ephemeral_storage_bytes.is_some()
163 && !self
164 .resource_controls
165 .contains(&ResourceControl::EphemeralStorage)
166 {
167 missing.push("resource_control:EphemeralStorage".into());
168 }
169 if spec.resources.execution_timeout_ms.is_some()
170 && !self
171 .resource_controls
172 .contains(&ResourceControl::ExecutionTimeout)
173 {
174 missing.push("resource_control:ExecutionTimeout".into());
175 }
176 if !self.supports_feature(RuntimeFeature::DurableIdentity) {
177 missing.push("feature:DurableIdentity".into());
178 }
179 if !spec.secrets.is_empty() && !self.supports_feature(RuntimeFeature::SecretReferences) {
180 missing.push("feature:SecretReferences".into());
181 }
182 if !spec.outputs.is_empty() && !self.supports_feature(RuntimeFeature::OutputArtifacts) {
183 missing.push("feature:OutputArtifacts".into());
184 }
185 if spec.isolation == IsolationLevel::Confidential
186 && !self.supports_feature(RuntimeFeature::Attestation)
187 {
188 missing.push("feature:Attestation".into());
189 }
190 if spec.identity_attachment_digest.is_some()
191 && !self.supports_feature(RuntimeFeature::IdentityAttachment)
192 {
193 missing.push("feature:IdentityAttachment".into());
194 }
195 if spec.service_lifecycle.is_some()
196 && !self.supports_feature(RuntimeFeature::ServiceLifecycle)
197 {
198 missing.push("feature:ServiceLifecycle".into());
199 }
200 missing.sort();
201 missing.dedup();
202 Ok(missing)
203 }
204}
205
206fn ensure_unique<T>(label: &str, values: &[T]) -> Result<(), String>
207where
208 T: Ord + Clone,
209{
210 let unique = values.iter().cloned().collect::<BTreeSet<_>>();
211 if unique.len() != values.len() {
212 return Err(format!(
213 "Runtime capabilities contain duplicate {label} values"
214 ));
215 }
216 Ok(())
217}