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