Skip to main content

canic_core/config/component_group_deployment/
mod.rs

1//! Module: config::component_group_deployment
2//!
3//! Responsibility: compile independent Component Group deployments before planning.
4//! Does not own: root selection, persistence, or effects.
5//! Boundary: strict source deployments become bounded exact flattened Component occurrences.
6
7mod canonical;
8mod member_limit;
9#[cfg(test)]
10mod tests;
11
12use crate::{
13    config::{
14        ComponentDeploymentLabel, ComponentDeploymentLabelKey, ComponentGroupLeafKind,
15        ComponentGroupTopology, ComponentGroupTopologyError, ComponentTopology,
16        ComponentTopologyError, FlattenedComponentGroup, FlattenedComponentGroupMember,
17        FleetServiceMemberPurpose, MAX_COMPONENT_DEPLOYMENT_LABELS,
18        component_group::source_labels,
19        schema::{ComponentGroupDeploymentConfig, ConfigModel},
20    },
21    ids::{
22        ComponentGroupDeploymentId, ComponentGroupMemberPath, ComponentGroupSpecId,
23        ComponentSpecId, FleetServiceId,
24    },
25};
26use std::collections::BTreeMap;
27
28use candid::CandidType;
29use serde::{Deserialize, Serialize};
30use thiserror::Error as ThisError;
31
32pub use canonical::MAX_COMPONENT_GROUP_DEPLOYMENT_TOPOLOGY_CANONICAL_BYTES;
33pub use member_limit::{
34    ComponentDeploymentLimits, ComponentDeploymentMemberLimit, ComponentDeploymentMemberLimitError,
35    ComponentDeploymentSpawnGrantLimit, MAX_COMPONENT_DEPLOYMENT_MEMBER_LIMITS,
36    MAX_COMPONENT_DEPLOYMENT_SPAWN_GRANT_REDUCTIONS,
37};
38
39/// Maximum independent Component Group deployments in one App configuration.
40pub const MAX_COMPONENT_GROUP_DEPLOYMENTS: usize = 4_096;
41/// Maximum flattened Component occurrences across every deployment selection.
42pub const MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS: usize = 4_096;
43
44impl ConfigModel {
45    /// Compile every checked-in deployment to exact Component member occurrences.
46    pub fn compile_component_group_deployment_topology(
47        &self,
48    ) -> Result<ComponentGroupDeploymentTopology, ComponentGroupDeploymentTopologyError> {
49        ComponentGroupDeploymentTopology::compile(self)
50    }
51}
52
53/// Canonical independent Component Group deployments in raw deployment-ID order.
54#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
55#[serde(deny_unknown_fields)]
56pub struct ComponentGroupDeploymentTopology {
57    pub component_group_deployments: Vec<ComponentGroupDeploymentSpec>,
58}
59
60impl ComponentGroupDeploymentTopology {
61    /// Compile source deployment selections and prove their exact group projection and demand.
62    pub fn compile(config: &ConfigModel) -> Result<Self, ComponentGroupDeploymentTopologyError> {
63        let component_topology = config.compile_component_topology()?;
64        let component_group_topology = config.compile_component_group_topology()?;
65        Self::compile_from_topologies(config, &component_group_topology, &component_topology)
66    }
67
68    /// Compile from the exact topologies already validated by the config boundary.
69    pub(super) fn compile_from_topologies(
70        config: &ConfigModel,
71        component_group_topology: &ComponentGroupTopology,
72        component_topology: &ComponentTopology,
73    ) -> Result<Self, ComponentGroupDeploymentTopologyError> {
74        validate_deployment_count(config.component_group_deployments.len())?;
75        let mut component_group_deployments =
76            Vec::with_capacity(config.component_group_deployments.len());
77        let mut flattened_member_count = 0_usize;
78
79        for (deployment, source) in &config.component_group_deployments {
80            validate_placement_envelope(deployment, source)?;
81            let deployment_labels = source_labels(&source.labels);
82            validate_deployment_labels(deployment, &deployment_labels)?;
83            let flattened = component_group_topology
84                .flatten(&source.component_group)
85                .map_err(ComponentGroupDeploymentTopologyError::ComponentGroupTopology)?;
86            validate_deployment_service_purpose(deployment, source.service_purpose, &flattened)?;
87            let member_limits = member_limit::compile_member_limits(
88                deployment,
89                &source.member_limits,
90                &flattened.components,
91                component_topology,
92            )?;
93            flattened_member_count = flattened_member_count
94                .checked_add(flattened.components.len())
95                .ok_or(
96                    ComponentGroupDeploymentTopologyError::DeploymentMemberBoundExceeded {
97                        actual: usize::MAX,
98                        maximum: MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS,
99                    },
100                )?;
101            if flattened_member_count > MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS {
102                return Err(
103                    ComponentGroupDeploymentTopologyError::DeploymentMemberBoundExceeded {
104                        actual: flattened_member_count,
105                        maximum: MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS,
106                    },
107                );
108            }
109            let members = flattened
110                .components
111                .into_iter()
112                .map(|member| {
113                    let purpose =
114                        resolve_member_purpose(deployment, source.service_purpose, &member)?;
115                    let labels = resolve_effective_labels(
116                        deployment,
117                        &member.member_path,
118                        &deployment_labels,
119                        &member.labels,
120                    )?;
121                    let component_spec = component_topology
122                        .get(&member.component_spec)
123                        .ok_or_else(|| {
124                            ComponentGroupDeploymentTopologyError::UnknownComponentSpec {
125                                deployment: deployment.clone(),
126                                component_spec: member.component_spec.clone(),
127                            }
128                        })?;
129                    let limits = member_limit::effective_limits(
130                        component_spec,
131                        &member.member_path,
132                        &member_limits,
133                    );
134                    Ok(FlattenedComponentGroupDeploymentMember {
135                        member_path: member.member_path,
136                        component_spec: member.component_spec,
137                        component_spec_hash: component_spec.spec_hash,
138                        purpose,
139                        labels,
140                        limits,
141                    })
142                })
143                .collect::<Result<Vec<_>, ComponentGroupDeploymentTopologyError>>()?;
144            component_group_deployments.push(ComponentGroupDeploymentSpec {
145                deployment: deployment.clone(),
146                component_group: source.component_group.clone(),
147                service_purpose: source.service_purpose,
148                labels: deployment_labels,
149                member_limits,
150                initial_placements: source.initial_placements,
151                maximum_placements: source.maximum_placements,
152                placement: ComponentGroupPlacementPolicy {
153                    maximum_per_root: source.placement.maximum_per_root,
154                    minimum_distinct_roots: source.placement.minimum_distinct_roots,
155                },
156                members,
157            });
158        }
159
160        let topology = Self {
161            component_group_deployments,
162        };
163        topology.validate(component_group_topology, component_topology)?;
164        Ok(topology)
165    }
166
167    /// Return one exact canonical deployment projection.
168    #[must_use]
169    pub fn get(
170        &self,
171        deployment: &ComponentGroupDeploymentId,
172    ) -> Option<&ComponentGroupDeploymentSpec> {
173        self.component_group_deployments
174            .binary_search_by(|candidate| candidate.deployment.cmp(deployment))
175            .ok()
176            .map(|index| &self.component_group_deployments[index])
177    }
178
179    /// Revalidate a decoded deployment projection against both source authority graphs.
180    pub fn validate(
181        &self,
182        component_group_topology: &ComponentGroupTopology,
183        component_topology: &ComponentTopology,
184    ) -> Result<(), ComponentGroupDeploymentTopologyError> {
185        component_group_topology.canonical_bytes()?;
186        component_topology.canonical_bytes()?;
187        validate_deployment_count(self.component_group_deployments.len())?;
188        let mut previous_deployment: Option<&ComponentGroupDeploymentId> = None;
189        let mut validation = DeploymentValidationLedger::new(component_topology);
190
191        for deployment in &self.component_group_deployments {
192            if previous_deployment.is_some_and(|previous| previous >= &deployment.deployment) {
193                return Err(
194                    ComponentGroupDeploymentTopologyError::NonCanonicalDeploymentOrder {
195                        deployment: deployment.deployment.clone(),
196                    },
197                );
198            }
199            previous_deployment = Some(&deployment.deployment);
200            validate_deployment_projection(
201                deployment,
202                component_group_topology,
203                component_topology,
204                &mut validation,
205            )?;
206        }
207
208        validation.validate_spec_maxima(component_topology)
209    }
210
211    /// Return the exact canonical flattened-deployment section for semantic hashing.
212    pub fn canonical_bytes(
213        &self,
214        component_group_topology: &ComponentGroupTopology,
215        component_topology: &ComponentTopology,
216    ) -> Result<Vec<u8>, ComponentGroupDeploymentTopologyError> {
217        self.validate(component_group_topology, component_topology)?;
218        let bytes = canonical::encode(self);
219        if bytes.len() > MAX_COMPONENT_GROUP_DEPLOYMENT_TOPOLOGY_CANONICAL_BYTES {
220            return Err(
221                ComponentGroupDeploymentTopologyError::CanonicalBytesBoundExceeded {
222                    actual: bytes.len(),
223                    maximum: MAX_COMPONENT_GROUP_DEPLOYMENT_TOPOLOGY_CANONICAL_BYTES,
224                },
225            );
226        }
227        Ok(bytes)
228    }
229}
230
231/// One canonical independently scalable Component Group selection.
232#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
233#[serde(deny_unknown_fields)]
234pub struct ComponentGroupDeploymentSpec {
235    pub deployment: ComponentGroupDeploymentId,
236    pub component_group: ComponentGroupSpecId,
237    pub service_purpose: Option<FleetServiceMemberPurpose>,
238    pub labels: Vec<ComponentDeploymentLabel>,
239    pub member_limits: Vec<ComponentDeploymentMemberLimit>,
240    pub initial_placements: u32,
241    pub maximum_placements: u32,
242    pub placement: ComponentGroupPlacementPolicy,
243    pub members: Vec<FlattenedComponentGroupDeploymentMember>,
244}
245
246/// Protected density and spread envelope before concrete roots are selected.
247#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
248#[serde(deny_unknown_fields)]
249pub struct ComponentGroupPlacementPolicy {
250    pub maximum_per_root: u32,
251    pub minimum_distinct_roots: u32,
252}
253
254/// One exact flattened Component occurrence within an independent deployment.
255#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
256#[serde(deny_unknown_fields)]
257pub struct FlattenedComponentGroupDeploymentMember {
258    pub member_path: ComponentGroupMemberPath,
259    pub component_spec: ComponentSpecId,
260    pub component_spec_hash: [u8; 32],
261    pub purpose: ComponentDeploymentPurpose,
262    pub labels: Vec<ComponentDeploymentLabel>,
263    pub limits: ComponentDeploymentLimits,
264}
265
266/// Exact typed purpose resolved for one flattened deployment occurrence.
267#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
268pub enum ComponentDeploymentPurpose {
269    Ordinary,
270    FleetServiceMember {
271        service: FleetServiceId,
272        member_purpose: FleetServiceMemberPurpose,
273    },
274}
275
276/// Typed rejection for invalid Component Group deployment compilation.
277#[derive(Debug, ThisError)]
278pub enum ComponentGroupDeploymentTopologyError {
279    #[error(transparent)]
280    ComponentGroupTopology(#[from] ComponentGroupTopologyError),
281
282    #[error(transparent)]
283    ComponentTopology(#[from] ComponentTopologyError),
284
285    #[error(transparent)]
286    MemberLimit(#[from] ComponentDeploymentMemberLimitError),
287
288    #[error("canonical Component Group deployment topology bytes {actual} exceed bound {maximum}")]
289    CanonicalBytesBoundExceeded { actual: usize, maximum: usize },
290
291    #[error("Component Group deployment count {actual} exceeds bound {maximum}")]
292    DeploymentBoundExceeded { actual: usize, maximum: usize },
293
294    #[error("flattened Component Group deployment member count {actual} exceeds bound {maximum}")]
295    DeploymentMemberBoundExceeded { actual: usize, maximum: usize },
296
297    #[error("Component Group deployment '{deployment}' has zero maximum placements")]
298    ZeroMaximumPlacements {
299        deployment: ComponentGroupDeploymentId,
300    },
301
302    #[error(
303        "Component Group deployment '{deployment}' initial placements {initial} exceed maximum {maximum}"
304    )]
305    InitialPlacementsExceedMaximum {
306        deployment: ComponentGroupDeploymentId,
307        initial: u32,
308        maximum: u32,
309    },
310
311    #[error("Component Group deployment '{deployment}' has zero maximum placements per root")]
312    ZeroMaximumPerRoot {
313        deployment: ComponentGroupDeploymentId,
314    },
315
316    #[error(
317        "Component Group deployment '{deployment}' maximum placements per root {maximum_per_root} exceed deployment maximum {maximum_placements}"
318    )]
319    MaximumPerRootExceedsMaximumPlacements {
320        deployment: ComponentGroupDeploymentId,
321        maximum_per_root: u32,
322        maximum_placements: u32,
323    },
324
325    #[error("Component Group deployment '{deployment}' has zero minimum distinct roots")]
326    ZeroMinimumDistinctRoots {
327        deployment: ComponentGroupDeploymentId,
328    },
329
330    #[error(
331        "Component Group deployment '{deployment}' minimum distinct roots {minimum_distinct_roots} exceed deployment maximum {maximum_placements}"
332    )]
333    MinimumDistinctRootsExceedMaximumPlacements {
334        deployment: ComponentGroupDeploymentId,
335        minimum_distinct_roots: u32,
336        maximum_placements: u32,
337    },
338
339    #[error("Component Group deployment '{deployment}' is not in canonical order")]
340    NonCanonicalDeploymentOrder {
341        deployment: ComponentGroupDeploymentId,
342    },
343
344    #[error("Component Group deployment '{deployment}' does not exactly match its flattened group")]
345    MemberProjectionMismatch {
346        deployment: ComponentGroupDeploymentId,
347    },
348
349    #[error(
350        "Component Group deployment '{deployment}' references unknown Component Spec '{component_spec}'"
351    )]
352    UnknownComponentSpec {
353        deployment: ComponentGroupDeploymentId,
354        component_spec: ComponentSpecId,
355    },
356
357    #[error(
358        "Component Group deployment '{deployment}' has the wrong hash for Component Spec '{component_spec}'"
359    )]
360    ComponentSpecHashMismatch {
361        deployment: ComponentGroupDeploymentId,
362        component_spec: ComponentSpecId,
363        expected: [u8; 32],
364        received: [u8; 32],
365    },
366
367    #[error(
368        "Component Group deployment '{deployment}' assigns Fleet-service purpose without a service-bearing member"
369    )]
370    InapplicableServicePurposeAssignment {
371        deployment: ComponentGroupDeploymentId,
372    },
373
374    #[error(
375        "Component Group deployment '{deployment}' member '{member_path:?}' for service '{service}' has no Fleet-service purpose assignment"
376    )]
377    MissingServicePurposeAssignment {
378        deployment: ComponentGroupDeploymentId,
379        member_path: ComponentGroupMemberPath,
380        service: FleetServiceId,
381    },
382
383    #[error(
384        "Component Group deployment '{deployment}' member '{member_path:?}' for service '{service}' has {actual} Fleet-service purpose assignments; expected exactly one"
385    )]
386    MultipleServicePurposeAssignments {
387        deployment: ComponentGroupDeploymentId,
388        member_path: ComponentGroupMemberPath,
389        service: FleetServiceId,
390        actual: usize,
391    },
392
393    #[error("Component Group deployment '{deployment}' has {actual} labels; maximum is {maximum}")]
394    LabelBoundExceeded {
395        deployment: ComponentGroupDeploymentId,
396        actual: usize,
397        maximum: usize,
398    },
399
400    #[error(
401        "Component Group deployment '{deployment}' label '{label}' is duplicated or not in canonical order"
402    )]
403    NonCanonicalLabelOrder {
404        deployment: ComponentGroupDeploymentId,
405        label: ComponentDeploymentLabelKey,
406    },
407
408    #[error(
409        "Component Group deployment '{deployment}' member '{member_path:?}' repeats label key '{label}'"
410    )]
411    DuplicateEffectiveLabel {
412        deployment: ComponentGroupDeploymentId,
413        member_path: ComponentGroupMemberPath,
414        label: ComponentDeploymentLabelKey,
415    },
416
417    #[error(
418        "Component Group deployment '{deployment}' member '{member_path:?}' has {actual} effective labels; maximum is {maximum}"
419    )]
420    EffectiveLabelBoundExceeded {
421        deployment: ComponentGroupDeploymentId,
422        member_path: ComponentGroupMemberPath,
423        actual: usize,
424        maximum: usize,
425    },
426
427    #[error(
428        "Component Group deployment '{deployment}' member '{member_path:?}' has mismatched effective labels"
429    )]
430    MemberLabelProjectionMismatch {
431        deployment: ComponentGroupDeploymentId,
432        member_path: ComponentGroupMemberPath,
433    },
434
435    #[error("Component Group deployment demand overflowed for Component Spec '{component_spec}'")]
436    ComponentSpecDemandOverflow { component_spec: ComponentSpecId },
437
438    #[error(
439        "Component Group deployment demand {required} exceeds Component Spec '{component_spec}' Fleet maximum {maximum_fleet_instances}"
440    )]
441    ComponentSpecDemandExceedsMaximum {
442        component_spec: ComponentSpecId,
443        required: u32,
444        maximum_fleet_instances: u32,
445    },
446}
447
448struct DeploymentValidationLedger {
449    flattened_member_count: usize,
450    spec_demand: BTreeMap<ComponentSpecId, u32>,
451}
452
453impl DeploymentValidationLedger {
454    fn new(component_topology: &ComponentTopology) -> Self {
455        Self {
456            flattened_member_count: 0,
457            spec_demand: component_topology
458                .component_specs
459                .iter()
460                .map(|spec| (spec.component_spec.clone(), 0_u32))
461                .collect(),
462        }
463    }
464
465    fn record_member_count(
466        &mut self,
467        member_count: usize,
468    ) -> Result<(), ComponentGroupDeploymentTopologyError> {
469        self.flattened_member_count = self
470            .flattened_member_count
471            .checked_add(member_count)
472            .ok_or(
473                ComponentGroupDeploymentTopologyError::DeploymentMemberBoundExceeded {
474                    actual: usize::MAX,
475                    maximum: MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS,
476                },
477            )?;
478        if self.flattened_member_count > MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS {
479            return Err(
480                ComponentGroupDeploymentTopologyError::DeploymentMemberBoundExceeded {
481                    actual: self.flattened_member_count,
482                    maximum: MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS,
483                },
484            );
485        }
486        Ok(())
487    }
488
489    fn record_spec_demand(
490        &mut self,
491        deployment: &ComponentGroupDeploymentSpec,
492        component_spec: &ComponentSpecId,
493    ) -> Result<(), ComponentGroupDeploymentTopologyError> {
494        let demand = self.spec_demand.get_mut(component_spec).ok_or_else(|| {
495            ComponentGroupDeploymentTopologyError::UnknownComponentSpec {
496                deployment: deployment.deployment.clone(),
497                component_spec: component_spec.clone(),
498            }
499        })?;
500        *demand = demand
501            .checked_add(deployment.maximum_placements)
502            .ok_or_else(
503                || ComponentGroupDeploymentTopologyError::ComponentSpecDemandOverflow {
504                    component_spec: component_spec.clone(),
505                },
506            )?;
507        Ok(())
508    }
509
510    fn validate_spec_maxima(
511        &self,
512        component_topology: &ComponentTopology,
513    ) -> Result<(), ComponentGroupDeploymentTopologyError> {
514        for component_spec in &component_topology.component_specs {
515            let required = self.spec_demand[&component_spec.component_spec];
516            if required > component_spec.maximum_fleet_instances {
517                return Err(
518                    ComponentGroupDeploymentTopologyError::ComponentSpecDemandExceedsMaximum {
519                        component_spec: component_spec.component_spec.clone(),
520                        required,
521                        maximum_fleet_instances: component_spec.maximum_fleet_instances,
522                    },
523                );
524            }
525        }
526        Ok(())
527    }
528}
529
530fn validate_deployment_projection(
531    deployment: &ComponentGroupDeploymentSpec,
532    component_group_topology: &ComponentGroupTopology,
533    component_topology: &ComponentTopology,
534    validation: &mut DeploymentValidationLedger,
535) -> Result<(), ComponentGroupDeploymentTopologyError> {
536    validate_compiled_placement_envelope(deployment)?;
537    validate_deployment_labels(&deployment.deployment, &deployment.labels)?;
538    let expected = component_group_topology
539        .flatten(&deployment.component_group)
540        .map_err(ComponentGroupDeploymentTopologyError::ComponentGroupTopology)?;
541    validate_deployment_service_purpose(
542        &deployment.deployment,
543        deployment.service_purpose,
544        &expected,
545    )?;
546    member_limit::validate_member_limits(
547        &deployment.deployment,
548        &deployment.member_limits,
549        &expected.components,
550        component_topology,
551    )?;
552    if expected.components.len() != deployment.members.len() {
553        return Err(
554            ComponentGroupDeploymentTopologyError::MemberProjectionMismatch {
555                deployment: deployment.deployment.clone(),
556            },
557        );
558    }
559    validation.record_member_count(deployment.members.len())?;
560
561    for (member, expected_member) in deployment.members.iter().zip(expected.components) {
562        let expected_purpose = resolve_member_purpose(
563            &deployment.deployment,
564            deployment.service_purpose,
565            &expected_member,
566        )?;
567        if !member_projection_matches(member, &expected_member, &expected_purpose) {
568            return Err(
569                ComponentGroupDeploymentTopologyError::MemberProjectionMismatch {
570                    deployment: deployment.deployment.clone(),
571                },
572            );
573        }
574        let expected_labels = resolve_effective_labels(
575            &deployment.deployment,
576            &member.member_path,
577            &deployment.labels,
578            &expected_member.labels,
579        )?;
580        if member.labels != expected_labels {
581            return Err(
582                ComponentGroupDeploymentTopologyError::MemberLabelProjectionMismatch {
583                    deployment: deployment.deployment.clone(),
584                    member_path: member.member_path.clone(),
585                },
586            );
587        }
588        let component_spec = component_topology
589            .get(&member.component_spec)
590            .ok_or_else(
591                || ComponentGroupDeploymentTopologyError::UnknownComponentSpec {
592                    deployment: deployment.deployment.clone(),
593                    component_spec: member.component_spec.clone(),
594                },
595            )?;
596        if component_spec.spec_hash != member.component_spec_hash {
597            return Err(
598                ComponentGroupDeploymentTopologyError::ComponentSpecHashMismatch {
599                    deployment: deployment.deployment.clone(),
600                    component_spec: member.component_spec.clone(),
601                    expected: component_spec.spec_hash,
602                    received: member.component_spec_hash,
603                },
604            );
605        }
606        let expected_limits = member_limit::effective_limits(
607            component_spec,
608            &member.member_path,
609            &deployment.member_limits,
610        );
611        if member.limits != expected_limits {
612            return Err(
613                ComponentDeploymentMemberLimitError::EffectiveLimitProjectionMismatch {
614                    deployment: deployment.deployment.clone(),
615                    member: member.member_path.clone(),
616                }
617                .into(),
618            );
619        }
620        validation.record_spec_demand(deployment, &member.component_spec)?;
621    }
622    Ok(())
623}
624
625fn member_projection_matches(
626    member: &FlattenedComponentGroupDeploymentMember,
627    expected: &FlattenedComponentGroupMember,
628    expected_purpose: &ComponentDeploymentPurpose,
629) -> bool {
630    member.member_path == expected.member_path
631        && member.component_spec == expected.component_spec
632        && member.purpose == *expected_purpose
633}
634
635fn validate_deployment_labels(
636    deployment: &ComponentGroupDeploymentId,
637    labels: &[ComponentDeploymentLabel],
638) -> Result<(), ComponentGroupDeploymentTopologyError> {
639    if labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
640        return Err(ComponentGroupDeploymentTopologyError::LabelBoundExceeded {
641            deployment: deployment.clone(),
642            actual: labels.len(),
643            maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
644        });
645    }
646    let mut previous: Option<&ComponentDeploymentLabelKey> = None;
647    for label in labels {
648        if previous.is_some_and(|key| key >= &label.key) {
649            return Err(
650                ComponentGroupDeploymentTopologyError::NonCanonicalLabelOrder {
651                    deployment: deployment.clone(),
652                    label: label.key.clone(),
653                },
654            );
655        }
656        previous = Some(&label.key);
657    }
658    Ok(())
659}
660
661fn resolve_effective_labels(
662    deployment: &ComponentGroupDeploymentId,
663    member_path: &ComponentGroupMemberPath,
664    deployment_labels: &[ComponentDeploymentLabel],
665    member_labels: &[ComponentDeploymentLabel],
666) -> Result<Vec<ComponentDeploymentLabel>, ComponentGroupDeploymentTopologyError> {
667    let mut labels = BTreeMap::new();
668    for label in deployment_labels.iter().chain(member_labels) {
669        if labels.insert(&label.key, &label.value).is_some() {
670            return Err(
671                ComponentGroupDeploymentTopologyError::DuplicateEffectiveLabel {
672                    deployment: deployment.clone(),
673                    member_path: member_path.clone(),
674                    label: label.key.clone(),
675                },
676            );
677        }
678    }
679    if labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
680        return Err(
681            ComponentGroupDeploymentTopologyError::EffectiveLabelBoundExceeded {
682                deployment: deployment.clone(),
683                member_path: member_path.clone(),
684                actual: labels.len(),
685                maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
686            },
687        );
688    }
689    Ok(labels
690        .into_iter()
691        .map(|(key, value)| ComponentDeploymentLabel {
692            key: key.clone(),
693            value: value.clone(),
694        })
695        .collect())
696}
697
698fn validate_deployment_service_purpose(
699    deployment: &ComponentGroupDeploymentId,
700    service_purpose: Option<FleetServiceMemberPurpose>,
701    flattened: &FlattenedComponentGroup,
702) -> Result<(), ComponentGroupDeploymentTopologyError> {
703    if service_purpose.is_some()
704        && !flattened
705            .components
706            .iter()
707            .any(FlattenedComponentGroupMember::is_fleet_service)
708    {
709        return Err(
710            ComponentGroupDeploymentTopologyError::InapplicableServicePurposeAssignment {
711                deployment: deployment.clone(),
712            },
713        );
714    }
715    Ok(())
716}
717
718fn resolve_member_purpose(
719    deployment: &ComponentGroupDeploymentId,
720    deployment_purpose: Option<FleetServiceMemberPurpose>,
721    member: &FlattenedComponentGroupMember,
722) -> Result<ComponentDeploymentPurpose, ComponentGroupDeploymentTopologyError> {
723    let ComponentGroupLeafKind::FleetService { service } = &member.kind else {
724        return Ok(ComponentDeploymentPurpose::Ordinary);
725    };
726    let assignment_count =
727        member.service_purpose_assignments.len() + usize::from(deployment_purpose.is_some());
728    if assignment_count == 0 {
729        return Err(
730            ComponentGroupDeploymentTopologyError::MissingServicePurposeAssignment {
731                deployment: deployment.clone(),
732                member_path: member.member_path.clone(),
733                service: service.clone(),
734            },
735        );
736    }
737    if assignment_count > 1 {
738        return Err(
739            ComponentGroupDeploymentTopologyError::MultipleServicePurposeAssignments {
740                deployment: deployment.clone(),
741                member_path: member.member_path.clone(),
742                service: service.clone(),
743                actual: assignment_count,
744            },
745        );
746    }
747    let member_purpose = deployment_purpose
748        .or_else(|| member.service_purpose_assignments.first().copied())
749        .ok_or_else(
750            || ComponentGroupDeploymentTopologyError::MissingServicePurposeAssignment {
751                deployment: deployment.clone(),
752                member_path: member.member_path.clone(),
753                service: service.clone(),
754            },
755        )?;
756    Ok(ComponentDeploymentPurpose::FleetServiceMember {
757        service: service.clone(),
758        member_purpose,
759    })
760}
761
762const fn validate_deployment_count(
763    deployment_count: usize,
764) -> Result<(), ComponentGroupDeploymentTopologyError> {
765    if deployment_count > MAX_COMPONENT_GROUP_DEPLOYMENTS {
766        return Err(
767            ComponentGroupDeploymentTopologyError::DeploymentBoundExceeded {
768                actual: deployment_count,
769                maximum: MAX_COMPONENT_GROUP_DEPLOYMENTS,
770            },
771        );
772    }
773    Ok(())
774}
775
776fn validate_placement_envelope(
777    deployment: &ComponentGroupDeploymentId,
778    source: &ComponentGroupDeploymentConfig,
779) -> Result<(), ComponentGroupDeploymentTopologyError> {
780    validate_placement_values(
781        deployment,
782        source.initial_placements,
783        source.maximum_placements,
784        source.placement.maximum_per_root,
785        source.placement.minimum_distinct_roots,
786    )
787}
788
789fn validate_compiled_placement_envelope(
790    deployment: &ComponentGroupDeploymentSpec,
791) -> Result<(), ComponentGroupDeploymentTopologyError> {
792    validate_placement_values(
793        &deployment.deployment,
794        deployment.initial_placements,
795        deployment.maximum_placements,
796        deployment.placement.maximum_per_root,
797        deployment.placement.minimum_distinct_roots,
798    )
799}
800
801fn validate_placement_values(
802    deployment: &ComponentGroupDeploymentId,
803    initial_placements: u32,
804    maximum_placements: u32,
805    maximum_per_root: u32,
806    minimum_distinct_roots: u32,
807) -> Result<(), ComponentGroupDeploymentTopologyError> {
808    if maximum_placements == 0 {
809        return Err(
810            ComponentGroupDeploymentTopologyError::ZeroMaximumPlacements {
811                deployment: deployment.clone(),
812            },
813        );
814    }
815    if initial_placements > maximum_placements {
816        return Err(
817            ComponentGroupDeploymentTopologyError::InitialPlacementsExceedMaximum {
818                deployment: deployment.clone(),
819                initial: initial_placements,
820                maximum: maximum_placements,
821            },
822        );
823    }
824    if maximum_per_root == 0 {
825        return Err(ComponentGroupDeploymentTopologyError::ZeroMaximumPerRoot {
826            deployment: deployment.clone(),
827        });
828    }
829    if maximum_per_root > maximum_placements {
830        return Err(
831            ComponentGroupDeploymentTopologyError::MaximumPerRootExceedsMaximumPlacements {
832                deployment: deployment.clone(),
833                maximum_per_root,
834                maximum_placements,
835            },
836        );
837    }
838    if minimum_distinct_roots == 0 {
839        return Err(
840            ComponentGroupDeploymentTopologyError::ZeroMinimumDistinctRoots {
841                deployment: deployment.clone(),
842            },
843        );
844    }
845    if minimum_distinct_roots > maximum_placements {
846        return Err(
847            ComponentGroupDeploymentTopologyError::MinimumDistinctRootsExceedMaximumPlacements {
848                deployment: deployment.clone(),
849                minimum_distinct_roots,
850                maximum_placements,
851            },
852        );
853    }
854    Ok(())
855}