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::{
10 compiled::{ComponentTopology, ConfigModel},
11 parse_config_model,
12};
13use std::{
14 collections::{BTreeMap, BTreeSet},
15 fs, io,
16 path::{Path, PathBuf},
17};
18
19pub use error::{
20 AppConfigDeclaration, AppConfigError, AppConfigIoOperation, AppConfigMutationConflict,
21 AppConfigNameField, AppConfigNameIssue, AppConfigOperation, AppConfigPackageIssue,
22 AppConfigTomlOperation,
23};
24pub use model::{
25 AttachedAppRole, ConfiguredPoolExpectation, ConfiguredRoleLifecycle, DeclaredAppRole,
26 RenamedAppRole,
27};
28pub(super) use mutation::{
29 attach_app_role_source, declare_app_role_source, rename_app_role_source,
30};
31pub(super) use projection::{
32 app_identity_from_source, configured_bootstrap_roles_from_config,
33 configured_deployable_roles_from_config, configured_pool_expectations_from_config,
34 configured_role_auto_create_from_config, configured_role_details_from_config,
35 configured_role_kinds_from_config, configured_role_lifecycle_from_config,
36 configured_role_metrics_profiles_from_config, configured_role_topups_from_config,
37};
38
39#[derive(Debug)]
44pub struct AppConfigSnapshot {
45 path: PathBuf,
46 config: ConfigModel,
47 component_topology: ComponentTopology,
48}
49
50impl AppConfigSnapshot {
51 pub fn load(path: &Path) -> Result<Self, AppConfigError> {
52 let source = read_config_source(path)?;
53 let config = parse_config_model(&source)
54 .map_err(|source| AppConfigError::CoreConfig {
55 operation: AppConfigOperation::Project,
56 source: Box::new(source),
57 })
58 .map_err(|error| error.at_config_path(path))?;
59 let component_topology = config
60 .compile_component_topology()
61 .map_err(canic_core::bootstrap::ConfigError::from)
62 .map_err(|source| AppConfigError::CoreConfig {
63 operation: AppConfigOperation::Project,
64 source: Box::new(source),
65 })
66 .map_err(|error| error.at_config_path(path))?;
67 Ok(Self {
68 path: path.to_path_buf(),
69 config,
70 component_topology,
71 })
72 }
73
74 #[must_use]
75 pub const fn model(&self) -> &ConfigModel {
76 &self.config
77 }
78
79 #[must_use]
81 pub const fn component_topology(&self) -> &ComponentTopology {
82 &self.component_topology
83 }
84
85 #[must_use]
86 pub const fn app_id(&self) -> &str {
87 self.config.app_id().as_str()
88 }
89
90 #[must_use]
91 pub fn deployable_roles(&self) -> Vec<String> {
92 configured_deployable_roles_from_config(&self.config)
93 }
94
95 #[must_use]
96 pub fn bootstrap_roles(&self) -> Vec<String> {
97 configured_bootstrap_roles_from_config(&self.config)
98 }
99
100 #[must_use]
101 pub fn pool_expectations(&self) -> Vec<ConfiguredPoolExpectation> {
102 configured_pool_expectations_from_config(&self.config)
103 }
104
105 #[must_use]
106 pub fn role_lifecycle(&self) -> Vec<ConfiguredRoleLifecycle> {
107 configured_role_lifecycle_from_config(&self.config)
108 }
109
110 #[must_use]
111 pub fn role_kinds(&self) -> BTreeMap<String, String> {
112 configured_role_kinds_from_config(&self.config)
113 }
114
115 pub fn role_capabilities(&self) -> Result<BTreeMap<String, Vec<String>>, AppConfigError> {
116 let mut projected = BTreeMap::new();
117
118 let roles = self
119 .config
120 .deployable_roles()
121 .into_iter()
122 .collect::<Vec<_>>();
123 let contracts = crate::role_contract::resolve_declared_role_contracts(
124 &self.path,
125 &self.config,
126 &roles,
127 crate::role_contract::PackageValidationMode::Passive,
128 );
129 for (role, resolution) in roles.iter().zip(contracts) {
130 let contract = match resolution {
131 canic_core::role_contract::RoleContractResolution::Resolved { contract } => {
132 contract
133 }
134 canic_core::role_contract::RoleContractResolution::Rejected { errors } => {
135 return Err(AppConfigError::RoleContractRejected { errors });
136 }
137 };
138 let labels = project_role_capabilities(&contract.capabilities);
139 if !labels.is_empty() {
140 projected.insert(role.as_str().to_string(), labels);
141 }
142 }
143
144 Ok(projected)
145 }
146
147 #[must_use]
148 pub fn role_auto_create(&self) -> BTreeSet<String> {
149 configured_role_auto_create_from_config(&self.config)
150 }
151
152 #[must_use]
153 pub fn role_topups(&self) -> BTreeMap<String, String> {
154 configured_role_topups_from_config(&self.config)
155 }
156
157 #[must_use]
158 pub fn role_metrics_profiles(&self) -> BTreeMap<String, String> {
159 configured_role_metrics_profiles_from_config(&self.config)
160 }
161
162 #[must_use]
163 pub fn role_details(&self) -> BTreeMap<String, Vec<String>> {
164 configured_role_details_from_config(&self.config)
165 }
166}
167
168pub fn read_app_config_identity(path: &Path) -> Result<String, AppConfigError> {
172 let source = read_config_source(path)?;
173 app_identity_from_source(&source).map_err(|error| error.at_config_path(path))
174}
175
176pub fn plan_declare_app_role(
178 config_path: &Path,
179 expected_app: &str,
180 role: &str,
181 package: &str,
182) -> Result<DeclaredAppRole, AppConfigError> {
183 let source = read_config_source(config_path)?;
184 let updated = declare_app_role_source(&source, expected_app, role, package)
185 .map_err(|error| error.at_config_path(config_path))?;
186 Ok(updated.role)
187}
188
189pub fn plan_attach_app_role(
191 config_path: &Path,
192 expected_app: &str,
193 role: &str,
194 component_spec: &str,
195 kind: &str,
196) -> Result<AttachedAppRole, AppConfigError> {
197 let source = read_config_source(config_path)?;
198 let updated = attach_app_role_source(&source, expected_app, role, component_spec, kind)
199 .map_err(|error| error.at_config_path(config_path))?;
200 Ok(updated.role)
201}
202
203pub fn plan_rename_app_role(
205 config_path: &Path,
206 expected_app: &str,
207 old_role: &str,
208 new_role: &str,
209) -> Result<RenamedAppRole, AppConfigError> {
210 let source = read_config_source(config_path)?;
211 let updated = rename_app_role_source(&source, config_path, expected_app, old_role, new_role)
212 .map_err(|error| error.at_config_path(config_path))?;
213 Ok(updated.role)
214}
215
216pub fn declare_app_role(
218 config_path: &Path,
219 expected_app: &str,
220 role: &str,
221 package: &str,
222) -> Result<DeclaredAppRole, AppConfigError> {
223 let source = read_config_source(config_path)?;
224 let updated = declare_app_role_source(&source, expected_app, role, package)
225 .map_err(|error| error.at_config_path(config_path))?;
226 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
227 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
228 })?;
229 Ok(updated.role)
230}
231
232pub fn attach_app_role(
234 config_path: &Path,
235 expected_app: &str,
236 role: &str,
237 component_spec: &str,
238 kind: &str,
239) -> Result<AttachedAppRole, AppConfigError> {
240 let source = read_config_source(config_path)?;
241 let updated = attach_app_role_source(&source, expected_app, role, component_spec, kind)
242 .map_err(|error| error.at_config_path(config_path))?;
243 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
244 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
245 })?;
246 Ok(updated.role)
247}
248
249pub fn rename_app_role(
251 config_path: &Path,
252 expected_app: &str,
253 old_role: &str,
254 new_role: &str,
255) -> Result<RenamedAppRole, AppConfigError> {
256 let source = read_config_source(config_path)?;
257 let updated = rename_app_role_source(&source, config_path, expected_app, old_role, new_role)
258 .map_err(|error| error.at_config_path(config_path))?;
259 commit_role_rename_sources(
260 config_path,
261 &source,
262 &updated.source,
263 updated
264 .package_manifest
265 .as_deref()
266 .zip(updated.package_source.as_deref()),
267 )?;
268 Ok(updated.role)
269}
270
271fn commit_role_rename_sources(
272 config_path: &Path,
273 original_config: &str,
274 updated_config: &str,
275 package_update: Option<(&Path, &str)>,
276) -> Result<(), AppConfigError> {
277 commit_role_rename_sources_with_writer(
278 config_path,
279 original_config,
280 updated_config,
281 package_update,
282 write_bytes,
283 )
284}
285
286fn commit_role_rename_sources_with_writer(
287 config_path: &Path,
288 original_config: &str,
289 updated_config: &str,
290 package_update: Option<(&Path, &str)>,
291 mut write: impl FnMut(&Path, &[u8]) -> io::Result<()>,
292) -> Result<(), AppConfigError> {
293 write(config_path, updated_config.as_bytes()).map_err(|source| {
294 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
295 })?;
296 let Some((package_path, package_source)) = package_update else {
297 return Ok(());
298 };
299
300 if let Err(source) = write(package_path, package_source.as_bytes()) {
301 let mutation = AppConfigError::io(
302 AppConfigIoOperation::WritePackageManifest,
303 package_path,
304 source,
305 );
306 if let Err(source) = write(config_path, original_config.as_bytes()) {
307 let rollback =
308 AppConfigError::io(AppConfigIoOperation::RestoreConfig, config_path, source);
309 return Err(AppConfigError::RollbackFailed {
310 mutation: Box::new(mutation),
311 rollback: Box::new(rollback),
312 });
313 }
314 return Err(mutation);
315 }
316
317 Ok(())
318}
319
320pub(in crate::release_set) fn project_role_capabilities(
321 capabilities: &BTreeSet<canic_core::role_contract::RoleCapabilityKey>,
322) -> Vec<String> {
323 use canic_core::role_contract::RoleCapabilityKey;
324
325 let mut labels = BTreeSet::new();
326 for capability in capabilities {
327 match capability {
328 RoleCapabilityKey::AutomaticTopup => {
329 labels.insert("automatic_topup");
330 }
331 RoleCapabilityKey::DelegatedTokenIssuer
332 | RoleCapabilityKey::DelegatedTokenVerifier
333 | RoleCapabilityKey::LocalApplicationAuthorization
334 | RoleCapabilityKey::RoleAttestationSigner
335 | RoleCapabilityKey::RoleAttestationVerifier => {
336 labels.insert("auth");
337 }
338 RoleCapabilityKey::Index => {
339 labels.insert("index");
340 }
341 RoleCapabilityKey::Icrc21 => {
342 labels.insert("icrc21");
343 }
344 RoleCapabilityKey::Scaling => {
345 labels.insert("scaling");
346 }
347 RoleCapabilityKey::Sharding => {
348 labels.insert("sharding");
349 }
350 RoleCapabilityKey::FleetCoordinator
351 | RoleCapabilityKey::FleetAdmissionProjection
352 | RoleCapabilityKey::ChildProvisioning
353 | RoleCapabilityKey::Root
354 | RoleCapabilityKey::RootControlPlane
355 | RoleCapabilityKey::RootDelegation
356 | RoleCapabilityKey::Runtime
357 | RoleCapabilityKey::WasmStore => {}
358 }
359 }
360 labels.into_iter().map(str::to_string).collect()
361}
362
363fn read_config_source(config_path: &Path) -> Result<String, AppConfigError> {
364 fs::read_to_string(config_path)
365 .map_err(|source| AppConfigError::io(AppConfigIoOperation::ReadConfig, config_path, source))
366}