running_process/broker/builders.rs
1//! Ergonomic builders for the two registration messages a consumer must
2//! produce to join the broker: [`ServiceDefinition`](crate::daemon_registration::protocol::ServiceDefinition)
3//! and [`CacheManifest`](crate::daemon_registration::protocol::CacheManifest)
4//! (#433 R2).
5//!
6//! The wire types are prost-generated structs with ~10-16 fields each, most of
7//! which a consumer leaves at their defaults. Hand-constructing them means
8//! spelling out every field (and re-deriving the boilerplate the broker already
9//! owns: media type, schema version, host identity, timestamps, self-digest).
10//! These builders set the required fields, default the rest, validate on
11//! `build`, and optionally persist via the existing central-registry helpers.
12//!
13//! ```no_run
14//! use running_process::daemon_registration::builders::{CacheManifestBuilder, ServiceDefinitionBuilder};
15//! use running_process::daemon_registration::protocol::CacheRootKind;
16//!
17//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! // Register the service the broker spawns/negotiates.
19//! ServiceDefinitionBuilder::shared_broker("zccache", "/usr/local/bin/zccache")
20//! .min_version("1.10.0")
21//! .allow_version("1.11.20")
22//! .install()?;
23//!
24//! // Publish the daemon's cache manifest into the central registry.
25//! CacheManifestBuilder::new("zccache", "1.11.20")
26//! .broker_instance("shared")
27//! .root(CacheRootKind::CacheData, "/var/cache/zccache")
28//! .publish()?;
29//! # Ok(()) }
30//! ```
31
32use std::path::{Path, PathBuf};
33use std::time::{SystemTime, UNIX_EPOCH};
34
35use crate::daemon_registration::host_identity;
36use crate::daemon_registration::manifest::{
37 manifest_with_self_sha256, write_to_central, write_to_central_in_dir, ManifestError,
38 CACHE_MANIFEST_MEDIA_TYPE, SUPPORTED_MANIFEST_SCHEMA_VERSION,
39};
40use crate::daemon_registration::protocol::{
41 BrokerIsolation, CacheManifest, CacheRoot, CacheRootKind, ServiceDefinition,
42};
43use crate::daemon_registration::service_def_loader::{
44 service_definition_dir, validate_service_definition_for_service, write_service_definition,
45 ServiceDefinitionError,
46};
47
48/// Broker envelope version stamped onto every manifest this builder produces.
49const BROKER_ENVELOPE_VERSION: &str = "v1";
50
51/// Fluent builder for a [`ServiceDefinition`].
52///
53/// Construct via [`shared_broker`](Self::shared_broker) (per-user local) or
54/// [`explicit_instance`](Self::explicit_instance) (trust-grouped CI), chain the
55/// optional setters, then [`build`](Self::build) to validate or
56/// [`install`](Self::install) to validate and write the `.servicedef`.
57#[derive(Clone, Debug)]
58pub struct ServiceDefinitionBuilder {
59 definition: ServiceDefinition,
60}
61
62impl ServiceDefinitionBuilder {
63 /// Begin a `SHARED_BROKER` (per-user local) service definition.
64 ///
65 /// `binary_path` must be an absolute path — the broker validates it on
66 /// [`build`](Self::build).
67 pub fn shared_broker(service_name: impl Into<String>, binary_path: impl Into<String>) -> Self {
68 Self {
69 definition: ServiceDefinition {
70 service_name: service_name.into(),
71 binary_path: binary_path.into(),
72 isolation: BrokerIsolation::SharedBroker as i32,
73 ..Default::default()
74 },
75 }
76 }
77
78 /// Begin an `EXPLICIT_INSTANCE` (trust-grouped) service definition.
79 ///
80 /// `instance` is the trust-group label; it must be a valid service-name
81 /// token.
82 pub fn explicit_instance(
83 service_name: impl Into<String>,
84 binary_path: impl Into<String>,
85 instance: impl Into<String>,
86 ) -> Self {
87 Self {
88 definition: ServiceDefinition {
89 service_name: service_name.into(),
90 binary_path: binary_path.into(),
91 isolation: BrokerIsolation::ExplicitInstance as i32,
92 explicit_instance: instance.into(),
93 ..Default::default()
94 },
95 }
96 }
97
98 /// Set the minimum acceptable backend version.
99 pub fn min_version(mut self, version: impl Into<String>) -> Self {
100 self.definition.min_version = version.into();
101 self
102 }
103
104 /// Append one version to the allow-list.
105 pub fn allow_version(mut self, version: impl Into<String>) -> Self {
106 self.definition.version_allow_list.push(version.into());
107 self
108 }
109
110 /// Set the absolute directory holding per-version backend binaries.
111 pub fn per_version_binary_dir(mut self, dir: impl Into<String>) -> Self {
112 self.definition.per_version_binary_dir = dir.into();
113 self
114 }
115
116 /// Attach one diagnostic label.
117 pub fn label(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
118 self.definition.labels.insert(key.into(), value.into());
119 self
120 }
121
122 /// Validate and return the [`ServiceDefinition`] without persisting it.
123 pub fn build(self) -> Result<ServiceDefinition, ServiceDefinitionError> {
124 validate_service_definition_for_service(&self.definition, &self.definition.service_name)?;
125 Ok(self.definition)
126 }
127
128 /// Validate and write the `.servicedef` into the default
129 /// service-definition directory.
130 pub fn install(self) -> Result<PathBuf, ServiceDefinitionError> {
131 self.install_in(&service_definition_dir())
132 }
133
134 /// Validate and write the `.servicedef` into an explicit root (tests,
135 /// custom layouts).
136 pub fn install_in(self, root: &Path) -> Result<PathBuf, ServiceDefinitionError> {
137 let definition = self.build()?;
138 write_service_definition(root, &definition)
139 }
140}
141
142/// Fluent builder for a [`CacheManifest`].
143///
144/// [`new`](Self::new) stamps the boilerplate the broker owns — media type,
145/// schema version, host identity, created/last-active timestamps — leaving the
146/// consumer to declare only what is theirs: the cache roots and broker
147/// instance. [`build`](Self::build) seals the `self_sha256` digest;
148/// [`publish`](Self::publish) writes it into the central registry.
149#[derive(Clone, Debug)]
150pub struct CacheManifestBuilder {
151 manifest: CacheManifest,
152}
153
154impl CacheManifestBuilder {
155 /// Begin a manifest for `service_name` at `service_version`.
156 pub fn new(service_name: impl Into<String>, service_version: impl Into<String>) -> Self {
157 let now = now_unix_ms();
158 Self {
159 manifest: CacheManifest {
160 manifest_schema_version: SUPPORTED_MANIFEST_SCHEMA_VERSION,
161 media_type: CACHE_MANIFEST_MEDIA_TYPE.to_string(),
162 host: Some(host_identity::current()),
163 service_name: service_name.into(),
164 service_version: service_version.into(),
165 broker_envelope_version: BROKER_ENVELOPE_VERSION.to_string(),
166 created_at_unix_ms: now,
167 last_active_unix_ms: now,
168 ..Default::default()
169 },
170 }
171 }
172
173 /// Set the broker instance label (e.g. `"shared"` or an explicit-instance
174 /// trust group).
175 pub fn broker_instance(mut self, instance: impl Into<String>) -> Self {
176 self.manifest.broker_instance = instance.into();
177 self
178 }
179
180 /// Set the manifest bundle id.
181 pub fn bundle_id(mut self, bundle_id: impl Into<String>) -> Self {
182 self.manifest.bundle_id = bundle_id.into();
183 self
184 }
185
186 /// Append one cache root of the given kind at `path`.
187 pub fn root(mut self, kind: CacheRootKind, path: impl Into<String>) -> Self {
188 self.manifest.roots.push(CacheRoot {
189 path: path.into(),
190 kind: kind as i32,
191 ..Default::default()
192 });
193 self
194 }
195
196 /// Seal the manifest by computing its `self_sha256` digest and return it
197 /// without persisting.
198 pub fn build(self) -> Result<CacheManifest, ManifestError> {
199 manifest_with_self_sha256(&self.manifest)
200 }
201
202 /// Seal and write the manifest atomically into the central registry,
203 /// returning the written path.
204 pub fn publish(self) -> Result<PathBuf, ManifestError> {
205 let manifest = self.build()?;
206 write_to_central(&manifest.service_name, &manifest.service_version, &manifest)
207 }
208
209 /// Seal and write into an explicit registry dir (tests, custom layouts).
210 pub fn publish_in(self, registry_dir: &Path) -> Result<PathBuf, ManifestError> {
211 let manifest = self.build()?;
212 write_to_central_in_dir(
213 registry_dir,
214 &manifest.service_name,
215 &manifest.service_version,
216 &manifest,
217 )
218 }
219}
220
221fn now_unix_ms() -> u64 {
222 SystemTime::now()
223 .duration_since(UNIX_EPOCH)
224 .map(|d| d.as_millis() as u64)
225 .unwrap_or(0)
226}