Skip to main content

running_process/broker/
builders.rs

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