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 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_pool_expectations_from_config, configured_role_auto_create_from_config,
36 configured_role_details_from_config, configured_role_kinds_from_config,
37 configured_role_lifecycle_from_config, configured_role_metrics_profiles_from_config,
38 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 #[must_use]
103 pub fn controllers(&self) -> Vec<String> {
104 configured_controllers_from_config(&self.config)
105 }
106
107 #[must_use]
108 pub fn pool_expectations(&self) -> Vec<ConfiguredPoolExpectation> {
109 configured_pool_expectations_from_config(&self.config)
110 }
111
112 #[must_use]
113 pub fn role_lifecycle(&self) -> Vec<ConfiguredRoleLifecycle> {
114 configured_role_lifecycle_from_config(&self.config)
115 }
116
117 #[must_use]
118 pub fn role_kinds(&self) -> BTreeMap<String, String> {
119 configured_role_kinds_from_config(&self.config)
120 }
121
122 pub fn role_capabilities(&self) -> Result<BTreeMap<String, Vec<String>>, AppConfigError> {
123 let mut projected = BTreeMap::new();
124
125 for role in self.config.deployable_roles() {
126 let contract = match crate::role_contract::resolve_declared_role_contract(
127 &self.path,
128 &self.config,
129 &role,
130 crate::role_contract::PackageValidationMode::Passive,
131 ) {
132 canic_core::role_contract::RoleContractResolution::Resolved { contract } => {
133 contract
134 }
135 canic_core::role_contract::RoleContractResolution::Rejected { errors } => {
136 return Err(AppConfigError::RoleContractRejected { errors });
137 }
138 };
139 let labels = project_role_capabilities(&contract.capabilities);
140 if !labels.is_empty() {
141 projected.insert(role.as_str().to_string(), labels);
142 }
143 }
144
145 Ok(projected)
146 }
147
148 #[must_use]
149 pub fn role_auto_create(&self) -> BTreeSet<String> {
150 configured_role_auto_create_from_config(&self.config)
151 }
152
153 #[must_use]
154 pub fn role_topups(&self) -> BTreeMap<String, String> {
155 configured_role_topups_from_config(&self.config)
156 }
157
158 #[must_use]
159 pub fn role_metrics_profiles(&self) -> BTreeMap<String, String> {
160 configured_role_metrics_profiles_from_config(&self.config)
161 }
162
163 #[must_use]
164 pub fn role_details(&self) -> BTreeMap<String, Vec<String>> {
165 configured_role_details_from_config(&self.config)
166 }
167}
168
169pub fn read_app_config_identity(path: &Path) -> Result<String, AppConfigError> {
173 let source = read_config_source(path)?;
174 app_identity_from_source(&source).map_err(|error| error.at_config_path(path))
175}
176
177pub fn plan_declare_app_role(
179 config_path: &Path,
180 expected_app: &str,
181 role: &str,
182 package: &str,
183) -> Result<DeclaredAppRole, AppConfigError> {
184 let source = read_config_source(config_path)?;
185 let updated = declare_app_role_source(&source, expected_app, role, package)
186 .map_err(|error| error.at_config_path(config_path))?;
187 Ok(updated.role)
188}
189
190pub fn plan_attach_app_role(
192 config_path: &Path,
193 expected_app: &str,
194 role: &str,
195 component_spec: &str,
196 kind: &str,
197) -> Result<AttachedAppRole, AppConfigError> {
198 let source = read_config_source(config_path)?;
199 let updated = attach_app_role_source(&source, expected_app, role, component_spec, kind)
200 .map_err(|error| error.at_config_path(config_path))?;
201 Ok(updated.role)
202}
203
204pub fn plan_rename_app_role(
206 config_path: &Path,
207 expected_app: &str,
208 old_role: &str,
209 new_role: &str,
210) -> Result<RenamedAppRole, AppConfigError> {
211 let source = read_config_source(config_path)?;
212 let updated = rename_app_role_source(&source, config_path, expected_app, old_role, new_role)
213 .map_err(|error| error.at_config_path(config_path))?;
214 Ok(updated.role)
215}
216
217pub fn declare_app_role(
219 config_path: &Path,
220 expected_app: &str,
221 role: &str,
222 package: &str,
223) -> Result<DeclaredAppRole, AppConfigError> {
224 let source = read_config_source(config_path)?;
225 let updated = declare_app_role_source(&source, expected_app, role, package)
226 .map_err(|error| error.at_config_path(config_path))?;
227 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
228 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
229 })?;
230 Ok(updated.role)
231}
232
233pub fn attach_app_role(
235 config_path: &Path,
236 expected_app: &str,
237 role: &str,
238 component_spec: &str,
239 kind: &str,
240) -> Result<AttachedAppRole, AppConfigError> {
241 let source = read_config_source(config_path)?;
242 let updated = attach_app_role_source(&source, expected_app, role, component_spec, kind)
243 .map_err(|error| error.at_config_path(config_path))?;
244 write_bytes(config_path, updated.source.as_bytes()).map_err(|source| {
245 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
246 })?;
247 Ok(updated.role)
248}
249
250pub fn rename_app_role(
252 config_path: &Path,
253 expected_app: &str,
254 old_role: &str,
255 new_role: &str,
256) -> Result<RenamedAppRole, AppConfigError> {
257 let source = read_config_source(config_path)?;
258 let updated = rename_app_role_source(&source, config_path, expected_app, old_role, new_role)
259 .map_err(|error| error.at_config_path(config_path))?;
260 commit_role_rename_sources(
261 config_path,
262 &source,
263 &updated.source,
264 updated
265 .package_manifest
266 .as_deref()
267 .zip(updated.package_source.as_deref()),
268 )?;
269 Ok(updated.role)
270}
271
272fn commit_role_rename_sources(
273 config_path: &Path,
274 original_config: &str,
275 updated_config: &str,
276 package_update: Option<(&Path, &str)>,
277) -> Result<(), AppConfigError> {
278 commit_role_rename_sources_with_writer(
279 config_path,
280 original_config,
281 updated_config,
282 package_update,
283 write_bytes,
284 )
285}
286
287fn commit_role_rename_sources_with_writer(
288 config_path: &Path,
289 original_config: &str,
290 updated_config: &str,
291 package_update: Option<(&Path, &str)>,
292 mut write: impl FnMut(&Path, &[u8]) -> io::Result<()>,
293) -> Result<(), AppConfigError> {
294 write(config_path, updated_config.as_bytes()).map_err(|source| {
295 AppConfigError::io(AppConfigIoOperation::WriteConfig, config_path, source)
296 })?;
297 let Some((package_path, package_source)) = package_update else {
298 return Ok(());
299 };
300
301 if let Err(source) = write(package_path, package_source.as_bytes()) {
302 let mutation = AppConfigError::io(
303 AppConfigIoOperation::WritePackageManifest,
304 package_path,
305 source,
306 );
307 if let Err(source) = write(config_path, original_config.as_bytes()) {
308 let rollback =
309 AppConfigError::io(AppConfigIoOperation::RestoreConfig, config_path, source);
310 return Err(AppConfigError::RollbackFailed {
311 mutation: Box::new(mutation),
312 rollback: Box::new(rollback),
313 });
314 }
315 return Err(mutation);
316 }
317
318 Ok(())
319}
320
321pub(in crate::release_set) fn project_role_capabilities(
322 capabilities: &BTreeSet<canic_core::role_contract::RoleCapabilityKey>,
323) -> Vec<String> {
324 use canic_core::role_contract::RoleCapabilityKey;
325
326 let mut labels = BTreeSet::new();
327 for capability in capabilities {
328 match capability {
329 RoleCapabilityKey::DelegatedTokenIssuer
330 | RoleCapabilityKey::DelegatedTokenVerifier
331 | RoleCapabilityKey::RoleAttestationSigner
332 | RoleCapabilityKey::RoleAttestationVerifier => {
333 labels.insert("auth");
334 }
335 RoleCapabilityKey::Index => {
336 labels.insert("index");
337 }
338 RoleCapabilityKey::Icrc21 => {
339 labels.insert("icrc21");
340 }
341 RoleCapabilityKey::Scaling => {
342 labels.insert("scaling");
343 }
344 RoleCapabilityKey::Sharding => {
345 labels.insert("sharding");
346 }
347 RoleCapabilityKey::FleetCoordinator
348 | RoleCapabilityKey::Root
349 | RoleCapabilityKey::RootControlPlane
350 | RoleCapabilityKey::Runtime
351 | RoleCapabilityKey::WasmStore => {}
352 }
353 }
354 labels.into_iter().map(str::to_string).collect()
355}
356
357fn read_config_source(config_path: &Path) -> Result<String, AppConfigError> {
358 fs::read_to_string(config_path)
359 .map_err(|source| AppConfigError::io(AppConfigIoOperation::ReadConfig, config_path, source))
360}