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