canic_host/release_set/config/
mod.rs1mod error;
2mod model;
3mod mutation;
4mod projection;
5#[cfg(test)]
6mod tests;
7
8use crate::durable_io::write_bytes;
9use canic_core::bootstrap::{compiled::ConfigModel, parse_config_model};
10use std::{
11 collections::{BTreeMap, BTreeSet},
12 fs, io,
13 path::{Path, PathBuf},
14};
15
16pub use error::{
17 FleetConfigDeclaration, FleetConfigError, FleetConfigIoOperation, FleetConfigMutationConflict,
18 FleetConfigNameField, FleetConfigNameIssue, FleetConfigOperation, FleetConfigPackageIssue,
19 FleetConfigTomlOperation,
20};
21pub use model::{
22 AttachedFleetRole, ConfiguredPoolExpectation, ConfiguredRoleLifecycle, DeclaredFleetRole,
23 LOCAL_ROOT_MIN_READY_CYCLES, RenamedFleetRole,
24};
25pub(super) use mutation::{
26 attach_fleet_role_source, declare_fleet_role_source, rename_fleet_role_source,
27};
28pub use projection::configured_release_roles_from_config;
29pub(super) use projection::{
30 configured_bootstrap_roles_from_config, configured_controllers_from_config,
31 configured_deployable_roles_from_config, configured_local_root_create_cycles_from_config,
32 configured_pool_expectations_from_config, configured_role_auto_create_from_config,
33 configured_role_details_from_config, configured_role_kinds_from_config,
34 configured_role_lifecycle_from_config, configured_role_metrics_profiles_from_config,
35 configured_role_topups_from_config, fleet_identity_from_source,
36};
37
38#[derive(Debug)]
43pub struct FleetConfigSnapshot {
44 path: PathBuf,
45 config: ConfigModel,
46 fleet_name: String,
47}
48
49impl FleetConfigSnapshot {
50 pub fn load(path: &Path) -> Result<Self, FleetConfigError> {
51 let source = read_config_source(path)?;
52 let config = parse_config_model(&source)
53 .map_err(|source| FleetConfigError::CoreConfig {
54 operation: FleetConfigOperation::Project,
55 source,
56 })
57 .map_err(|error| error.at_config_path(path))?;
58 let fleet_name = config
59 .fleet_name()
60 .ok_or(FleetConfigError::DeclarationMissing {
61 declaration: FleetConfigDeclaration::FleetName,
62 })
63 .map_err(|error| error.at_config_path(path))?
64 .to_string();
65 Ok(Self {
66 path: path.to_path_buf(),
67 config,
68 fleet_name,
69 })
70 }
71
72 #[must_use]
73 pub const fn model(&self) -> &ConfigModel {
74 &self.config
75 }
76
77 #[must_use]
78 pub const fn fleet_name(&self) -> &str {
79 self.fleet_name.as_str()
80 }
81
82 #[must_use]
83 pub fn deployable_roles(&self) -> Vec<String> {
84 configured_deployable_roles_from_config(&self.config)
85 }
86
87 #[must_use]
88 pub fn bootstrap_roles(&self) -> Vec<String> {
89 configured_bootstrap_roles_from_config(&self.config)
90 }
91
92 #[must_use]
93 pub fn local_root_create_cycles(&self) -> u128 {
94 configured_local_root_create_cycles_from_config(&self.config)
95 }
96
97 #[must_use]
98 pub fn controllers(&self) -> Vec<String> {
99 configured_controllers_from_config(&self.config)
100 }
101
102 #[must_use]
103 pub fn pool_expectations(&self) -> Vec<ConfiguredPoolExpectation> {
104 configured_pool_expectations_from_config(&self.config)
105 }
106
107 #[must_use]
108 pub fn role_lifecycle(&self) -> Vec<ConfiguredRoleLifecycle> {
109 configured_role_lifecycle_from_config(&self.config)
110 }
111
112 #[must_use]
113 pub fn role_kinds(&self) -> BTreeMap<String, String> {
114 configured_role_kinds_from_config(&self.config)
115 }
116
117 pub fn role_capabilities(&self) -> Result<BTreeMap<String, Vec<String>>, FleetConfigError> {
118 let mut projected = BTreeMap::new();
119
120 for role in self.config.attached_roles() {
121 let contract = match crate::role_contract::resolve_declared_role_contract(
122 &self.path,
123 &self.config,
124 &role,
125 crate::role_contract::PackageValidationMode::Passive,
126 ) {
127 canic_core::role_contract::RoleContractResolution::Resolved { contract } => {
128 contract
129 }
130 canic_core::role_contract::RoleContractResolution::Rejected { errors } => {
131 return Err(FleetConfigError::RoleContractRejected { errors });
132 }
133 };
134 let labels = project_role_capabilities(&contract.capabilities);
135 if !labels.is_empty() {
136 projected.insert(role.as_str().to_string(), labels);
137 }
138 }
139
140 Ok(projected)
141 }
142
143 #[must_use]
144 pub fn role_auto_create(&self) -> BTreeSet<String> {
145 configured_role_auto_create_from_config(&self.config)
146 }
147
148 #[must_use]
149 pub fn role_topups(&self) -> BTreeMap<String, String> {
150 configured_role_topups_from_config(&self.config)
151 }
152
153 #[must_use]
154 pub fn role_metrics_profiles(&self) -> BTreeMap<String, String> {
155 configured_role_metrics_profiles_from_config(&self.config)
156 }
157
158 #[must_use]
159 pub fn role_details(&self) -> BTreeMap<String, Vec<String>> {
160 configured_role_details_from_config(&self.config)
161 }
162}
163
164pub fn read_fleet_config_identity(path: &Path) -> Result<String, FleetConfigError> {
168 let source = read_config_source(path)?;
169 fleet_identity_from_source(&source).map_err(|error| error.at_config_path(path))
170}
171
172pub fn plan_declare_fleet_role(
174 config_path: &Path,
175 expected_fleet: &str,
176 role: &str,
177 package: &str,
178) -> Result<DeclaredFleetRole, FleetConfigError> {
179 let source = read_config_source(config_path)?;
180 let updated = declare_fleet_role_source(&source, expected_fleet, role, package)
181 .map_err(|error| error.at_config_path(config_path))?;
182 Ok(updated.role)
183}
184
185pub fn plan_attach_fleet_role(
187 config_path: &Path,
188 expected_fleet: &str,
189 role: &str,
190 subnet: &str,
191 kind: &str,
192) -> Result<AttachedFleetRole, FleetConfigError> {
193 let source = read_config_source(config_path)?;
194 let updated = attach_fleet_role_source(&source, expected_fleet, role, subnet, kind)
195 .map_err(|error| error.at_config_path(config_path))?;
196 Ok(updated.role)
197}
198
199pub fn plan_rename_fleet_role(
201 config_path: &Path,
202 expected_fleet: &str,
203 old_role: &str,
204 new_role: &str,
205) -> Result<RenamedFleetRole, FleetConfigError> {
206 let source = read_config_source(config_path)?;
207 let updated =
208 rename_fleet_role_source(&source, config_path, expected_fleet, old_role, new_role)
209 .map_err(|error| error.at_config_path(config_path))?;
210 Ok(updated.role)
211}
212
213pub fn declare_fleet_role(
215 config_path: &Path,
216 expected_fleet: &str,
217 role: &str,
218 package: &str,
219) -> Result<DeclaredFleetRole, FleetConfigError> {
220 let source = read_config_source(config_path)?;
221 let updated = declare_fleet_role_source(&source, expected_fleet, role, package)
222 .map_err(|error| error.at_config_path(config_path))?;
223 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
224 FleetConfigError::io(FleetConfigIoOperation::WriteConfig, config_path, source)
225 })?;
226 Ok(updated.role)
227}
228
229pub fn attach_fleet_role(
231 config_path: &Path,
232 expected_fleet: &str,
233 role: &str,
234 subnet: &str,
235 kind: &str,
236) -> Result<AttachedFleetRole, FleetConfigError> {
237 let source = read_config_source(config_path)?;
238 let updated = attach_fleet_role_source(&source, expected_fleet, role, subnet, kind)
239 .map_err(|error| error.at_config_path(config_path))?;
240 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
241 FleetConfigError::io(FleetConfigIoOperation::WriteConfig, config_path, source)
242 })?;
243 Ok(updated.role)
244}
245
246pub fn rename_fleet_role(
248 config_path: &Path,
249 expected_fleet: &str,
250 old_role: &str,
251 new_role: &str,
252) -> Result<RenamedFleetRole, FleetConfigError> {
253 let source = read_config_source(config_path)?;
254 let updated =
255 rename_fleet_role_source(&source, config_path, expected_fleet, old_role, new_role)
256 .map_err(|error| error.at_config_path(config_path))?;
257 commit_role_rename_sources(
258 config_path,
259 &source,
260 &updated.source,
261 updated
262 .package_manifest
263 .as_deref()
264 .zip(updated.package_source.as_deref()),
265 )?;
266 Ok(updated.role)
267}
268
269fn commit_role_rename_sources(
270 config_path: &Path,
271 original_config: &str,
272 updated_config: &str,
273 package_update: Option<(&Path, &str)>,
274) -> Result<(), FleetConfigError> {
275 commit_role_rename_sources_with_writer(
276 config_path,
277 original_config,
278 updated_config,
279 package_update,
280 write_bytes,
281 )
282}
283
284fn commit_role_rename_sources_with_writer(
285 config_path: &Path,
286 original_config: &str,
287 updated_config: &str,
288 package_update: Option<(&Path, &str)>,
289 mut write: impl FnMut(&Path, &[u8]) -> io::Result<()>,
290) -> Result<(), FleetConfigError> {
291 write(config_path, updated_config.as_bytes()).map_err(|source| {
292 FleetConfigError::io(FleetConfigIoOperation::WriteConfig, config_path, source)
293 })?;
294 let Some((package_path, package_source)) = package_update else {
295 return Ok(());
296 };
297
298 if let Err(source) = write(package_path, package_source.as_bytes()) {
299 let mutation = FleetConfigError::io(
300 FleetConfigIoOperation::WritePackageManifest,
301 package_path,
302 source,
303 );
304 if let Err(source) = write(config_path, original_config.as_bytes()) {
305 let rollback =
306 FleetConfigError::io(FleetConfigIoOperation::RestoreConfig, config_path, source);
307 return Err(FleetConfigError::RollbackFailed {
308 mutation: Box::new(mutation),
309 rollback: Box::new(rollback),
310 });
311 }
312 return Err(mutation);
313 }
314
315 Ok(())
316}
317
318pub(in crate::release_set) fn project_role_capabilities(
319 capabilities: &BTreeSet<canic_core::role_contract::RoleCapabilityKey>,
320) -> Vec<String> {
321 use canic_core::role_contract::RoleCapabilityKey;
322
323 let mut labels = BTreeSet::new();
324 for capability in capabilities {
325 match capability {
326 RoleCapabilityKey::DelegatedTokenIssuer
327 | RoleCapabilityKey::DelegatedTokenVerifier
328 | RoleCapabilityKey::RoleAttestationSigner
329 | RoleCapabilityKey::RoleAttestationVerifier => {
330 labels.insert("auth");
331 }
332 RoleCapabilityKey::Directory => {
333 labels.insert("directory");
334 }
335 RoleCapabilityKey::Icrc21 => {
336 labels.insert("icrc21");
337 }
338 RoleCapabilityKey::Scaling => {
339 labels.insert("scaling");
340 }
341 RoleCapabilityKey::Sharding => {
342 labels.insert("sharding");
343 }
344 RoleCapabilityKey::Root
345 | RoleCapabilityKey::RootControlPlane
346 | RoleCapabilityKey::Runtime
347 | RoleCapabilityKey::WasmStore => {}
348 }
349 }
350 labels.into_iter().map(str::to_string).collect()
351}
352
353fn read_config_source(config_path: &Path) -> Result<String, FleetConfigError> {
354 fs::read_to_string(config_path).map_err(|source| {
355 FleetConfigError::io(FleetConfigIoOperation::ReadConfig, config_path, source)
356 })
357}