use crate::{
config::schema::{
ComponentChildConfig, ComponentChildKind, ComponentSpecConfig, ConfigSchemaError,
CyclesFundingPolicyConfig, MAX_COMPONENT_CHILD_ROLES, NAME_MAX_BYTES, TopupPolicy,
Validate,
},
config::validation::validate_canister_role,
ids::CanisterRole,
};
use std::collections::BTreeMap;
impl Validate for ComponentSpecConfig {
fn validate(&self) -> Result<(), ConfigSchemaError> {
validate_canister_role(&self.component_role, "Component role")?;
if self.component_role.is_root() || self.component_role.is_wasm_store() {
return Err(ConfigSchemaError::ValidationError(format!(
"Component role '{}' is reserved infrastructure",
self.component_role,
)));
}
if self.maximum_instances == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' maximum_instances must be > 0",
self.component_role,
)));
}
if self.children.len() > MAX_COMPONENT_CHILD_ROLES {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' declares {} child roles, exceeding bound {MAX_COMPONENT_CHILD_ROLES}",
self.component_role,
self.children.len(),
)));
}
if !self.children.is_empty() && self.limits.maximum_children == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' limits.maximum_children must be > 0 when children are declared",
self.component_role,
)));
}
if self.limits.maximum_registry_bytes == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' limits.maximum_registry_bytes must be > 0",
self.component_role,
)));
}
if self.limits.cycles_funding.window_secs == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' limits.cycles_funding.window_secs must be > 0",
self.component_role,
)));
}
if self.limits.cycles_funding.maximum_cycles.to_u128() == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' limits.cycles_funding.maximum_cycles must be > 0",
self.component_role,
)));
}
validate_cycles_funding(&self.cycles_funding, &self.component_role)?;
validate_topup(self.topup.as_ref(), &self.component_role)?;
for (role, child) in &self.children {
validate_canister_role(role, "Component Child")?;
if role == &self.component_role {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{}' cannot also be its own child",
self.component_role,
)));
}
if role.is_root() || role.is_wasm_store() {
return Err(ConfigSchemaError::ValidationError(format!(
"Component Child '{role}' is reserved infrastructure",
)));
}
if child.maximum_instances == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component Child '{role}' maximum_instances must be > 0",
)));
}
if child.kind == ComponentChildKind::Singleton && child.maximum_instances != 1 {
return Err(ConfigSchemaError::ValidationError(format!(
"singleton Component Child '{role}' maximum_instances must equal 1",
)));
}
validate_cycles_funding(&child.cycles_funding, role)?;
validate_topup(child.topup.as_ref(), role)?;
}
validate_scaling(self, &self.component_role, &self.children)?;
validate_sharding(self, &self.component_role, &self.children)?;
validate_binding(self, &self.component_role, &self.children)?;
Ok(())
}
}
fn validate_cycles_funding(
policy: &CyclesFundingPolicyConfig,
canister: &CanisterRole,
) -> Result<(), ConfigSchemaError> {
let max_per_request = policy.max_per_request.to_u128();
let max_per_child = policy.max_per_child.to_u128();
if max_per_request == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{canister}' cycles_funding.max_per_request must be > 0",
)));
}
if max_per_child == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{canister}' cycles_funding.max_per_child must be > 0",
)));
}
if policy.cooldown_secs == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{canister}' cycles_funding.cooldown_secs must be > 0",
)));
}
if max_per_request > max_per_child {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{canister}' cycles_funding.max_per_request must be <= cycles_funding.max_per_child",
)));
}
Ok(())
}
fn validate_topup(
topup: Option<&TopupPolicy>,
canister: &CanisterRole,
) -> Result<(), ConfigSchemaError> {
let Some(topup) = topup else {
return Ok(());
};
let threshold = topup.threshold.to_u128();
let amount = topup.amount.to_u128();
if amount.saturating_mul(2) > threshold {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{canister}' topup.amount must be <= 50% of topup.threshold (got amount={amount}, threshold={threshold})",
)));
}
Ok(())
}
fn validate_sharding(
cfg: &ComponentSpecConfig,
role: &CanisterRole,
children: &BTreeMap<CanisterRole, ComponentChildConfig>,
) -> Result<(), ConfigSchemaError> {
let Some(sharding) = &cfg.sharding else {
return Ok(());
};
for (pool_name, pool) in &sharding.pools {
if pool_name.len() > NAME_MAX_BYTES {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' sharding pool '{pool_name}' name exceeds {NAME_MAX_BYTES} bytes",
)));
}
if !children.contains_key(&pool.canister_role) {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' sharding pool '{pool_name}' references undeclared direct child '{}'",
pool.canister_role
)));
}
let target = &children[&pool.canister_role];
if target.kind != ComponentChildKind::Shard {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' sharding pool '{pool_name}' references child '{}' which is not kind = \"shard\"",
pool.canister_role
)));
}
if pool.policy.capacity == 0 || pool.policy.max_shards == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' sharding pool '{pool_name}' must have positive capacity and max_shards",
)));
}
if pool.policy.initial_shards > pool.policy.max_shards {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' sharding pool '{pool_name}' has initial_shards > max_shards",
)));
}
if pool.policy.max_shards > target.maximum_instances {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' sharding pool '{pool_name}' max_shards exceeds child '{}' maximum_instances",
pool.canister_role,
)));
}
}
Ok(())
}
fn validate_scaling(
cfg: &ComponentSpecConfig,
role: &CanisterRole,
children: &BTreeMap<CanisterRole, ComponentChildConfig>,
) -> Result<(), ConfigSchemaError> {
let Some(scaling) = &cfg.scaling else {
return Ok(());
};
for (pool_name, pool) in &scaling.pools {
if pool_name.len() > NAME_MAX_BYTES {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' scaling pool '{pool_name}' name exceeds {NAME_MAX_BYTES} bytes",
)));
}
if !children.contains_key(&pool.canister_role) {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' scaling pool '{pool_name}' references undeclared direct child '{}'",
pool.canister_role
)));
}
let target = &children[&pool.canister_role];
if target.kind != ComponentChildKind::Replica {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' scaling pool '{pool_name}' references child '{}' which is not kind = \"replica\"",
pool.canister_role
)));
}
if pool.policy.max_workers != 0 && pool.policy.max_workers < pool.policy.min_workers {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' scaling pool '{pool_name}' has max_workers < min_workers",
)));
}
if pool.policy.max_workers != 0 && pool.policy.max_workers < pool.policy.initial_workers {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' scaling pool '{pool_name}' has max_workers < initial_workers",
)));
}
if pool.policy.max_workers == 0 {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' scaling pool '{pool_name}' max_workers must be > 0",
)));
}
if pool.policy.max_workers > target.maximum_instances {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' scaling pool '{pool_name}' max_workers exceeds child '{}' maximum_instances",
pool.canister_role,
)));
}
}
Ok(())
}
fn validate_binding(
cfg: &ComponentSpecConfig,
role: &CanisterRole,
children: &BTreeMap<CanisterRole, ComponentChildConfig>,
) -> Result<(), ConfigSchemaError> {
let Some(binding) = &cfg.binding else {
return Ok(());
};
for (pool_name, pool) in &binding.pools {
if pool_name.len() > NAME_MAX_BYTES {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' binding pool '{pool_name}' name exceeds {NAME_MAX_BYTES} bytes",
)));
}
if pool.key_name.is_empty() {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' binding pool '{pool_name}' must define a non-empty key_name",
)));
}
if pool.key_name.len() > NAME_MAX_BYTES {
return Err(ConfigSchemaError::ValidationError(format!(
"canister '{role}' binding pool '{pool_name}' key_name '{}' exceeds {NAME_MAX_BYTES} bytes",
pool.key_name
)));
}
if !children.contains_key(&pool.canister_role) {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' binding pool '{pool_name}' references undeclared direct child '{}'",
pool.canister_role
)));
}
let target = &children[&pool.canister_role];
if target.kind != ComponentChildKind::Instance {
return Err(ConfigSchemaError::ValidationError(format!(
"Component '{role}' binding pool '{pool_name}' references child '{}' which is not kind = \"instance\"",
pool.canister_role
)));
}
}
Ok(())
}