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