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