1mod 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
39pub const MAX_COMPONENT_GROUP_DEPLOYMENTS: usize = 4_096;
41pub const MAX_COMPONENT_GROUP_DEPLOYMENT_MEMBERS: usize = 4_096;
43
44impl ConfigModel {
45 pub fn compile_component_group_deployment_topology(
47 &self,
48 ) -> Result<ComponentGroupDeploymentTopology, ComponentGroupDeploymentTopologyError> {
49 ComponentGroupDeploymentTopology::compile(self)
50 }
51}
52
53#[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 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 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 #[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 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 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#[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#[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#[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#[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#[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}