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`](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}