1use serde::{Deserialize, Serialize};
2use std::collections::{btree_map::Entry, BTreeMap, BTreeSet};
3use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4
5use super::observation::RuntimeObservation;
6
7const SERVICE_ENDPOINT_CLAIM_PREFIX: &str = "a3s.runtime.service-endpoint.";
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum NetworkMode {
12 None,
13 Outbound,
14 Service,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum TransportProtocol {
20 Tcp,
21 Udp,
22}
23
24impl TransportProtocol {
25 pub const fn as_str(self) -> &'static str {
26 match self {
27 Self::Tcp => "tcp",
28 Self::Udp => "udp",
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(deny_unknown_fields)]
35pub struct RuntimePort {
36 pub name: String,
37 pub container_port: u16,
38 pub protocol: TransportProtocol,
39}
40
41impl RuntimePort {
42 pub(crate) fn validate(&self) -> Result<(), String> {
43 validate_service_port_name(&self.name)?;
44 if self.container_port == 0 {
45 return Err("container_port must be positive".into());
46 }
47 Ok(())
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct RuntimeServiceEndpoint {
61 pub port_name: String,
62 pub protocol: TransportProtocol,
63 pub address: IpAddr,
64 pub port: u16,
65}
66
67impl RuntimeServiceEndpoint {
68 pub fn new(
69 port_name: impl Into<String>,
70 protocol: TransportProtocol,
71 address: IpAddr,
72 port: u16,
73 ) -> Result<Self, String> {
74 let endpoint = Self {
75 port_name: port_name.into(),
76 protocol,
77 address,
78 port,
79 };
80 endpoint.validate()?;
81 Ok(endpoint)
82 }
83
84 pub fn node_local_tcp(port_name: impl Into<String>, port: u16) -> Result<Self, String> {
85 Self::new(
86 port_name,
87 TransportProtocol::Tcp,
88 IpAddr::V4(Ipv4Addr::LOCALHOST),
89 port,
90 )
91 }
92
93 pub fn validate(&self) -> Result<(), String> {
94 validate_service_port_name(&self.port_name)?;
95 if !self.address.is_loopback() || self.port == 0 {
96 return Err(
97 "Runtime service endpoint must use an explicit positive loopback socket".into(),
98 );
99 }
100 Ok(())
101 }
102
103 pub fn socket_addr(&self) -> SocketAddr {
104 SocketAddr::new(self.address, self.port)
105 }
106
107 pub fn claim_key(&self) -> String {
108 format!("{SERVICE_ENDPOINT_CLAIM_PREFIX}{}", self.port_name)
109 }
110
111 pub fn claim_value(&self) -> String {
112 format!("{}://{}", self.protocol.as_str(), self.socket_addr())
113 }
114
115 pub fn insert_claim(&self, claims: &mut BTreeMap<String, String>) -> Result<(), String> {
116 self.validate()?;
117 let key = self.claim_key();
118 let value = self.claim_value();
119 match claims.entry(key) {
120 Entry::Vacant(entry) => {
121 entry.insert(value);
122 Ok(())
123 }
124 Entry::Occupied(entry) if entry.get() == &value => Ok(()),
125 Entry::Occupied(_) => Err(format!(
126 "Runtime evidence contains conflicting service endpoint {:?}",
127 self.port_name
128 )),
129 }
130 }
131
132 pub fn from_observation(
133 observation: &RuntimeObservation,
134 port_name: &str,
135 ) -> Result<Self, String> {
136 observation.validate()?;
137 validate_service_port_name(port_name)?;
138 let key = format!("{SERVICE_ENDPOINT_CLAIM_PREFIX}{port_name}");
139 let value = observation
140 .evidence
141 .as_ref()
142 .and_then(|evidence| evidence.claims.get(&key))
143 .ok_or_else(|| {
144 format!("Runtime observation has no service endpoint for port {port_name:?}")
145 })?;
146 Self::from_claim(port_name, value)
147 }
148
149 pub(crate) fn from_claims(claims: &BTreeMap<String, String>) -> Result<Vec<Self>, String> {
150 claims
151 .iter()
152 .filter_map(|(key, value)| {
153 key.strip_prefix(SERVICE_ENDPOINT_CLAIM_PREFIX)
154 .map(|port_name| Self::from_claim(port_name, value))
155 })
156 .collect()
157 }
158
159 pub(crate) fn remove_claims(claims: &mut BTreeMap<String, String>) {
160 claims.retain(|key, _| !key.starts_with(SERVICE_ENDPOINT_CLAIM_PREFIX));
161 }
162
163 fn from_claim(port_name: &str, value: &str) -> Result<Self, String> {
164 let (protocol, socket) = if let Some(socket) = value.strip_prefix("tcp://") {
165 (TransportProtocol::Tcp, socket)
166 } else if let Some(socket) = value.strip_prefix("udp://") {
167 (TransportProtocol::Udp, socket)
168 } else {
169 return Err("Runtime service endpoint claim has an unsupported protocol".into());
170 };
171 let socket = socket
172 .parse::<SocketAddr>()
173 .map_err(|error| format!("Runtime service endpoint claim is invalid: {error}"))?;
174 let endpoint = Self::new(port_name, protocol, socket.ip(), socket.port())?;
175 if endpoint.claim_value() != value {
176 return Err("Runtime service endpoint claim is not canonical".into());
177 }
178 Ok(endpoint)
179 }
180}
181
182fn validate_service_port_name(value: &str) -> Result<(), String> {
183 super::validate_name("service port name", value)?;
184 if SERVICE_ENDPOINT_CLAIM_PREFIX.len() + value.len() > 255 {
185 return Err("Runtime service port name exceeds the endpoint evidence key bound".into());
186 }
187 Ok(())
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(deny_unknown_fields)]
192pub struct RuntimeNetworkSpec {
193 pub mode: NetworkMode,
194 pub ports: Vec<RuntimePort>,
195}
196
197impl RuntimeNetworkSpec {
198 pub(crate) fn validate(&self) -> Result<(), String> {
199 if self.ports.len() > 64 {
200 return Err("Runtime unit declares more than 64 ports".into());
201 }
202 if self.mode != NetworkMode::Service && !self.ports.is_empty() {
203 return Err("declared ports require service network mode".into());
204 }
205 let mut names = BTreeSet::new();
206 let mut sockets = BTreeSet::new();
207 for port in &self.ports {
208 port.validate()?;
209 if !names.insert(&port.name) {
210 return Err(format!("duplicate Runtime port name {:?}", port.name));
211 }
212 if !sockets.insert((port.container_port, port.protocol)) {
213 return Err(format!(
214 "duplicate Runtime port socket {}/{}",
215 port.container_port,
216 match port.protocol {
217 TransportProtocol::Tcp => "tcp",
218 TransportProtocol::Udp => "udp",
219 }
220 ));
221 }
222 }
223 Ok(())
224 }
225
226 pub(crate) fn has_port(&self, name: &str) -> bool {
227 self.ports.iter().any(|port| port.name == name)
228 }
229}