running_process/broker/protocol_v2/
io.rs1use std::path::{Path, PathBuf};
19
20use prost::Message as _;
21
22use crate::broker::lifecycle::names::validate_service_name;
23use crate::broker::secure_dir;
24use crate::broker::server::service_def_loader::{
25 ensure_service_definition_dir, service_definition_dir, ServiceDefinitionError,
26};
27
28use super::{BrokerIsolation, ServiceDefinition};
29
30pub const SERVICE_DEF_V2_EXTENSION: &str = "servicedef.v2";
34
35#[must_use]
41pub fn service_definition_dir_v2() -> PathBuf {
42 service_definition_dir()
43}
44
45pub fn service_definition_path_v2(
52 root: &Path,
53 service_name: &str,
54) -> Result<PathBuf, ServiceDefinitionError> {
55 validate_service_name(service_name)?;
56 Ok(root.join(format!("{service_name}.{SERVICE_DEF_V2_EXTENSION}")))
57}
58
59pub fn write_service_definition_v2(
72 root: &Path,
73 definition: &ServiceDefinition,
74) -> Result<PathBuf, ServiceDefinitionError> {
75 ensure_service_definition_dir(root)?;
76 let path = service_definition_path_v2(root, &definition.service_name)?;
77 std::fs::write(&path, definition.encode_to_vec())?;
78 Ok(path)
79}
80
81#[derive(Debug, Clone)]
88pub struct ServiceDefinitionBuilder {
89 definition: ServiceDefinition,
90}
91
92impl ServiceDefinitionBuilder {
93 #[must_use]
101 pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
102 Self {
103 definition: ServiceDefinition {
104 service_name: service_name.into(),
105 binary_path: binary_path.into(),
106 isolation: BrokerIsolation::SharedBroker as i32,
107 ..Default::default()
108 },
109 }
110 }
111
112 #[must_use]
115 pub fn private_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
116 Self {
117 definition: ServiceDefinition {
118 service_name: service_name.into(),
119 binary_path: binary_path.into(),
120 isolation: BrokerIsolation::PrivateBroker as i32,
121 ..Default::default()
122 },
123 }
124 }
125
126 #[must_use]
129 pub fn explicit_instance(
130 service_name: impl Into<String>,
131 binary_path: impl Into<String>,
132 instance: impl Into<String>,
133 ) -> Self {
134 Self {
135 definition: ServiceDefinition {
136 service_name: service_name.into(),
137 binary_path: binary_path.into(),
138 isolation: BrokerIsolation::ExplicitInstance as i32,
139 explicit_instance: instance.into(),
140 ..Default::default()
141 },
142 }
143 }
144
145 #[must_use]
147 pub fn per_version_binary_dir(mut self, dir: impl Into<String>) -> Self {
148 self.definition.per_version_binary_dir = dir.into();
149 self
150 }
151
152 #[must_use]
154 pub fn min_version(mut self, version: impl Into<String>) -> Self {
155 self.definition.min_version = version.into();
156 self
157 }
158
159 #[must_use]
161 pub fn version_allow_list<I, S>(mut self, versions: I) -> Self
162 where
163 I: IntoIterator<Item = S>,
164 S: Into<String>,
165 {
166 self.definition.version_allow_list = versions.into_iter().map(Into::into).collect();
167 self
168 }
169
170 #[must_use]
172 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
173 self.definition.labels.insert(key.into(), value.into());
174 self
175 }
176
177 #[must_use]
180 pub fn build(self) -> ServiceDefinition {
181 self.definition
182 }
183
184 pub fn install_in(self, root: &Path) -> Result<PathBuf, ServiceDefinitionError> {
191 write_service_definition_v2(root, &self.build())
192 }
193
194 pub fn install(self) -> Result<PathBuf, ServiceDefinitionError> {
201 let root = service_definition_dir_v2();
202 secure_dir::ensure_private_dir(&root)?;
208 self.install_in(&root)
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use tempfile::tempdir;
216
217 #[test]
218 fn extension_is_servicedef_v2() {
219 assert_eq!(SERVICE_DEF_V2_EXTENSION, "servicedef.v2");
220 }
221
222 #[test]
223 fn service_definition_path_v2_uses_v2_extension() {
224 let root = Path::new("/svc");
225 let path = service_definition_path_v2(root, "zccache").unwrap();
226 assert_eq!(
227 path.to_str().unwrap().replace('\\', "/"),
228 "/svc/zccache.servicedef.v2"
229 );
230 }
231
232 #[test]
233 fn service_definition_path_v2_rejects_invalid_name() {
234 let root = Path::new("/svc");
235 assert!(service_definition_path_v2(root, "ZCCACHE").is_err());
236 assert!(service_definition_path_v2(root, "").is_err());
237 assert!(service_definition_path_v2(root, "a/b").is_err());
238 }
239
240 #[test]
241 fn shared_broker_builder_sets_expected_fields() {
242 let def = ServiceDefinitionBuilder::shared_broker("zccache", "/usr/bin/zccache").build();
243 assert_eq!(def.service_name, "zccache");
244 assert_eq!(def.binary_path, "/usr/bin/zccache");
245 assert_eq!(def.isolation, BrokerIsolation::SharedBroker as i32);
246 assert!(def.explicit_instance.is_empty());
247 }
248
249 #[test]
250 fn private_broker_builder_sets_expected_fields() {
251 let def = ServiceDefinitionBuilder::private_broker("svc", "/bin/x").build();
252 assert_eq!(def.isolation, BrokerIsolation::PrivateBroker as i32);
253 }
254
255 #[test]
256 fn explicit_instance_builder_sets_expected_fields() {
257 let def =
258 ServiceDefinitionBuilder::explicit_instance("svc", "/bin/x", "ci-trusted").build();
259 assert_eq!(def.isolation, BrokerIsolation::ExplicitInstance as i32);
260 assert_eq!(def.explicit_instance, "ci-trusted");
261 }
262
263 #[test]
264 fn builder_chain_propagates_optional_fields() {
265 let def = ServiceDefinitionBuilder::shared_broker("svc", "/bin/x")
266 .per_version_binary_dir("/usr/local/bin")
267 .min_version("1.2.3")
268 .version_allow_list(["1.2.3", "1.3.0"])
269 .label("env", "prod")
270 .label("region", "us-west")
271 .build();
272 assert_eq!(def.per_version_binary_dir, "/usr/local/bin");
273 assert_eq!(def.min_version, "1.2.3");
274 assert_eq!(def.version_allow_list, vec!["1.2.3", "1.3.0"]);
275 assert_eq!(def.labels.get("env"), Some(&"prod".to_owned()));
276 assert_eq!(def.labels.get("region"), Some(&"us-west".to_owned()));
277 }
278
279 #[test]
280 fn install_in_writes_and_decodes_round_trip() {
281 let dir = tempdir().expect("tempdir");
282 let path = ServiceDefinitionBuilder::shared_broker("zccache", "/usr/bin/zccache")
283 .min_version("1.0.0")
284 .label("env", "prod")
285 .install_in(dir.path())
286 .expect("install_in");
287
288 assert_eq!(
289 path.file_name().and_then(|s| s.to_str()),
290 Some("zccache.servicedef.v2")
291 );
292
293 let bytes = std::fs::read(&path).expect("read file");
294 let decoded = ServiceDefinition::decode(bytes.as_slice()).expect("decode");
295 assert_eq!(decoded.service_name, "zccache");
296 assert_eq!(decoded.binary_path, "/usr/bin/zccache");
297 assert_eq!(decoded.isolation, BrokerIsolation::SharedBroker as i32);
298 assert_eq!(decoded.min_version, "1.0.0");
299 assert_eq!(decoded.labels.get("env"), Some(&"prod".to_owned()));
300 }
301
302 #[test]
303 fn write_service_definition_v2_rejects_invalid_name() {
304 let dir = tempdir().expect("tempdir");
305 let bad = ServiceDefinition {
306 service_name: "BAD-Caps".to_owned(),
307 ..Default::default()
308 };
309 let err = write_service_definition_v2(dir.path(), &bad).expect_err("must reject");
310 let _ = err;
311 }
312
313 #[test]
314 fn write_service_definition_v2_creates_parent_dir() {
315 let dir = tempdir().expect("tempdir");
316 let nested = dir.path().join("nested");
317 let path = ServiceDefinitionBuilder::shared_broker("svc", "/bin/x")
318 .install_in(&nested)
319 .expect("install_in into nested");
320 assert!(path.exists());
321 assert!(nested.exists());
322 }
323
324 #[test]
329 fn builder_install_round_trip_preserves_every_field() {
330 let dir = tempdir().expect("tempdir");
331 let path = ServiceDefinitionBuilder::explicit_instance("svc", "/bin/x", "ci-trusted")
332 .per_version_binary_dir("/usr/local/bin")
333 .min_version("1.0.0")
334 .version_allow_list(["1.0.0", "1.1.0"])
335 .label("env", "prod")
336 .label("rollout", "blue")
337 .install_in(dir.path())
338 .expect("install_in");
339
340 let bytes = std::fs::read(&path).expect("read");
341 let decoded = ServiceDefinition::decode(bytes.as_slice()).expect("decode");
342 assert_eq!(decoded.service_name, "svc");
343 assert_eq!(decoded.binary_path, "/bin/x");
344 assert_eq!(decoded.isolation, BrokerIsolation::ExplicitInstance as i32);
345 assert_eq!(decoded.explicit_instance, "ci-trusted");
346 assert_eq!(decoded.per_version_binary_dir, "/usr/local/bin");
347 assert_eq!(decoded.min_version, "1.0.0");
348 assert_eq!(decoded.version_allow_list, vec!["1.0.0", "1.1.0"]);
349 assert_eq!(decoded.labels.len(), 2);
350 assert_eq!(decoded.labels.get("env"), Some(&"prod".to_owned()));
351 assert_eq!(decoded.labels.get("rollout"), Some(&"blue".to_owned()));
352 }
353}