1mod label;
8#[cfg(test)]
9mod tests;
10
11use crate::{
12 config::schema::{ComponentGroupSpecConfig, ConfigModel},
13 ids::{
14 ComponentGroupMemberId, ComponentGroupMemberPath, ComponentGroupMemberPathError,
15 ComponentGroupSpecId, ComponentSpecId, FleetServiceId,
16 },
17};
18use std::collections::{BTreeMap, BTreeSet};
19
20use candid::CandidType;
21use serde::{Deserialize, Serialize};
22use thiserror::Error as ThisError;
23
24pub use label::{
25 ComponentDeploymentLabel, ComponentDeploymentLabelKey, ComponentDeploymentLabelParseError,
26 ComponentDeploymentLabelValue, MAX_COMPONENT_DEPLOYMENT_LABEL_KEY_BYTES,
27 MAX_COMPONENT_DEPLOYMENT_LABEL_VALUE_BYTES, MAX_COMPONENT_DEPLOYMENT_LABELS,
28};
29
30const COMPONENT_GROUP_GRAPH_DOMAIN: &[u8] = b"canic/component-group-graph/v3";
31const COMPONENT_GROUP_GRAPH_SCHEMA_VERSION: u32 = 3;
32
33pub const MAX_COMPONENT_GROUP_SPECS: usize = 256;
35pub const MAX_COMPONENT_GROUP_MEMBERS: usize = 256;
37pub const MAX_COMPONENT_GROUP_DECLARED_MEMBERS: usize = 16_384;
39pub const MAX_COMPONENT_GROUP_INCLUSIONS: usize = 4_096;
41pub const MAX_COMPONENT_GROUP_FLATTENED_MEMBERS: usize = 4_096;
43pub const MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES: usize = 2_097_152;
45
46impl ConfigModel {
47 pub fn compile_component_group_topology(
49 &self,
50 ) -> Result<ComponentGroupTopology, ComponentGroupTopologyError> {
51 ComponentGroupTopology::compile(self)
52 }
53}
54
55#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
57#[serde(deny_unknown_fields)]
58pub struct ComponentGroupTopology {
59 pub component_groups: Vec<ComponentGroupSpec>,
60}
61
62impl ComponentGroupTopology {
63 pub fn compile(config: &ConfigModel) -> Result<Self, ComponentGroupTopologyError> {
65 if config.component_groups.len() > MAX_COMPONENT_GROUP_SPECS {
66 return Err(ComponentGroupTopologyError::GroupBoundExceeded {
67 actual: config.component_groups.len(),
68 maximum: MAX_COMPONENT_GROUP_SPECS,
69 });
70 }
71
72 let mut declared_members = 0_usize;
73 let mut inclusions = 0_usize;
74 let mut component_groups = Vec::with_capacity(config.component_groups.len());
75 for (component_group, source) in &config.component_groups {
76 let member_count = checked_member_count(component_group, source)?;
77 declared_members = declared_members.checked_add(member_count).ok_or(
78 ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
79 actual: usize::MAX,
80 maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
81 },
82 )?;
83 if declared_members > MAX_COMPONENT_GROUP_DECLARED_MEMBERS {
84 return Err(ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
85 actual: declared_members,
86 maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
87 });
88 }
89 inclusions = inclusions.checked_add(source.groups.len()).ok_or(
90 ComponentGroupTopologyError::InclusionBoundExceeded {
91 actual: usize::MAX,
92 maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
93 },
94 )?;
95 if inclusions > MAX_COMPONENT_GROUP_INCLUSIONS {
96 return Err(ComponentGroupTopologyError::InclusionBoundExceeded {
97 actual: inclusions,
98 maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
99 });
100 }
101 component_groups.push(compile_group(config, component_group, source)?);
102 }
103
104 let topology = Self { component_groups };
105 topology.canonical_bytes()?;
106 Ok(topology)
107 }
108
109 #[must_use]
111 pub fn get(&self, component_group: &ComponentGroupSpecId) -> Option<&ComponentGroupSpec> {
112 self.component_groups
113 .binary_search_by(|candidate| candidate.component_group.cmp(component_group))
114 .ok()
115 .map(|index| &self.component_groups[index])
116 }
117
118 pub fn flatten(
120 &self,
121 component_group: &ComponentGroupSpecId,
122 ) -> Result<FlattenedComponentGroup, ComponentGroupTopologyError> {
123 self.validate_canonical_projection()?;
124 self.flatten_canonical(component_group)
125 }
126
127 fn flatten_canonical(
128 &self,
129 component_group: &ComponentGroupSpecId,
130 ) -> Result<FlattenedComponentGroup, ComponentGroupTopologyError> {
131 if self.get(component_group).is_none() {
132 return Err(ComponentGroupTopologyError::UnknownGroup {
133 component_group: component_group.clone(),
134 });
135 }
136
137 let mut components = Vec::new();
138 let mut member_path = Vec::new();
139 let mut active_groups = Vec::new();
140 let mut service_purposes = Vec::new();
141 let mut labels = BTreeMap::new();
142 self.flatten_into(
143 component_group,
144 &mut member_path,
145 &mut active_groups,
146 &mut service_purposes,
147 &mut labels,
148 &mut components,
149 )?;
150 Ok(FlattenedComponentGroup {
151 component_group: component_group.clone(),
152 components,
153 })
154 }
155
156 pub fn canonical_bytes(&self) -> Result<Vec<u8>, ComponentGroupTopologyError> {
158 self.validate_canonical_projection()?;
159 for group in &self.component_groups {
160 self.flatten_canonical(&group.component_group)?;
161 }
162 let mut bytes = Vec::new();
163 encode_bytes(&mut bytes, COMPONENT_GROUP_GRAPH_DOMAIN);
164 bytes.extend_from_slice(&COMPONENT_GROUP_GRAPH_SCHEMA_VERSION.to_be_bytes());
165 encode_u64(&mut bytes, self.component_groups.len());
166 for group in &self.component_groups {
167 encode_text(&mut bytes, group.component_group.as_str());
168 encode_u64(&mut bytes, group.members.len());
169 for member in &group.members {
170 match member {
171 ComponentGroupMember::Component {
172 member,
173 component_spec,
174 kind,
175 service_purpose,
176 labels,
177 } => {
178 bytes.push(0);
179 encode_text(&mut bytes, member.as_str());
180 encode_text(&mut bytes, component_spec.as_str());
181 encode_leaf_kind(&mut bytes, kind);
182 encode_service_purpose(&mut bytes, service_purpose.as_ref());
183 encode_labels(&mut bytes, labels);
184 }
185 ComponentGroupMember::Group {
186 member,
187 component_group,
188 service_purpose,
189 labels,
190 } => {
191 bytes.push(1);
192 encode_text(&mut bytes, member.as_str());
193 encode_text(&mut bytes, component_group.as_str());
194 encode_service_purpose(&mut bytes, service_purpose.as_ref());
195 encode_labels(&mut bytes, labels);
196 }
197 }
198 }
199 }
200 if bytes.len() > MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES {
201 return Err(ComponentGroupTopologyError::CanonicalBytesBoundExceeded {
202 actual: bytes.len(),
203 maximum: MAX_COMPONENT_GROUP_GRAPH_CANONICAL_BYTES,
204 });
205 }
206 Ok(bytes)
207 }
208
209 fn validate_canonical_projection(&self) -> Result<(), ComponentGroupTopologyError> {
210 if self.component_groups.len() > MAX_COMPONENT_GROUP_SPECS {
211 return Err(ComponentGroupTopologyError::GroupBoundExceeded {
212 actual: self.component_groups.len(),
213 maximum: MAX_COMPONENT_GROUP_SPECS,
214 });
215 }
216
217 let mut declared_members = 0_usize;
218 let mut inclusions = 0_usize;
219 let mut previous_group: Option<&ComponentGroupSpecId> = None;
220 for group in &self.component_groups {
221 if previous_group.is_some_and(|previous| previous >= &group.component_group) {
222 return Err(ComponentGroupTopologyError::NonCanonicalGroupOrder {
223 component_group: group.component_group.clone(),
224 });
225 }
226 previous_group = Some(&group.component_group);
227
228 if group.members.is_empty() {
229 return Err(ComponentGroupTopologyError::EmptyGroup {
230 component_group: group.component_group.clone(),
231 });
232 }
233 if group.members.len() > MAX_COMPONENT_GROUP_MEMBERS {
234 return Err(ComponentGroupTopologyError::MemberBoundExceeded {
235 component_group: group.component_group.clone(),
236 actual: group.members.len(),
237 maximum: MAX_COMPONENT_GROUP_MEMBERS,
238 });
239 }
240 declared_members = declared_members.checked_add(group.members.len()).ok_or(
241 ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
242 actual: usize::MAX,
243 maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
244 },
245 )?;
246 if declared_members > MAX_COMPONENT_GROUP_DECLARED_MEMBERS {
247 return Err(ComponentGroupTopologyError::DeclaredMemberBoundExceeded {
248 actual: declared_members,
249 maximum: MAX_COMPONENT_GROUP_DECLARED_MEMBERS,
250 });
251 }
252 let mut previous_member: Option<&ComponentGroupMemberId> = None;
253 for member in &group.members {
254 let member_id = member.member();
255 if previous_member.is_some_and(|previous| previous >= member_id) {
256 return Err(ComponentGroupTopologyError::NonCanonicalMemberOrder {
257 component_group: group.component_group.clone(),
258 member: member_id.clone(),
259 });
260 }
261 previous_member = Some(member_id);
262 validate_member_labels(&group.component_group, member)?;
263 if let ComponentGroupMember::Group {
264 component_group: included,
265 ..
266 } = member
267 {
268 inclusions = inclusions.checked_add(1).ok_or(
269 ComponentGroupTopologyError::InclusionBoundExceeded {
270 actual: usize::MAX,
271 maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
272 },
273 )?;
274 if inclusions > MAX_COMPONENT_GROUP_INCLUSIONS {
275 return Err(ComponentGroupTopologyError::InclusionBoundExceeded {
276 actual: inclusions,
277 maximum: MAX_COMPONENT_GROUP_INCLUSIONS,
278 });
279 }
280 if self.get(included).is_none() {
281 return Err(ComponentGroupTopologyError::UnknownIncludedGroup {
282 component_group: group.component_group.clone(),
283 included: included.clone(),
284 });
285 }
286 }
287 }
288 }
289 Ok(())
290 }
291
292 fn flatten_into(
293 &self,
294 component_group: &ComponentGroupSpecId,
295 member_path: &mut Vec<ComponentGroupMemberId>,
296 active_groups: &mut Vec<ComponentGroupSpecId>,
297 service_purposes: &mut Vec<FleetServiceMemberPurpose>,
298 effective_labels: &mut BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
299 output: &mut Vec<FlattenedComponentGroupMember>,
300 ) -> Result<(), ComponentGroupTopologyError> {
301 if active_groups.contains(component_group) {
302 return Err(ComponentGroupTopologyError::InclusionCycle {
303 component_group: component_group.clone(),
304 });
305 }
306 let group =
307 self.get(component_group)
308 .ok_or_else(|| ComponentGroupTopologyError::UnknownGroup {
309 component_group: component_group.clone(),
310 })?;
311 active_groups.push(component_group.clone());
312
313 for member in &group.members {
314 member_path.push(member.member().clone());
315 let output_start = output.len();
316 if let Some(purpose) = member.service_purpose() {
317 service_purposes.push(purpose);
318 }
319 let current_path =
320 ComponentGroupMemberPath::try_from(member_path.clone()).map_err(|source| {
321 ComponentGroupTopologyError::InvalidMemberPath {
322 component_group: active_groups[0].clone(),
323 source,
324 }
325 })?;
326 let added_label_keys = extend_effective_labels(
327 &active_groups[0],
328 ¤t_path,
329 member.labels(),
330 effective_labels,
331 )?;
332 match member {
333 ComponentGroupMember::Component {
334 component_spec,
335 kind,
336 ..
337 } => {
338 if output.len() >= MAX_COMPONENT_GROUP_FLATTENED_MEMBERS {
339 return Err(ComponentGroupTopologyError::FlattenedMemberBoundExceeded {
340 component_group: active_groups[0].clone(),
341 actual: output.len() + 1,
342 maximum: MAX_COMPONENT_GROUP_FLATTENED_MEMBERS,
343 });
344 }
345 output.push(FlattenedComponentGroupMember {
346 member_path: current_path,
347 component_spec: component_spec.clone(),
348 kind: kind.clone(),
349 service_purpose_assignments: match kind {
350 ComponentGroupLeafKind::Ordinary => Vec::new(),
351 ComponentGroupLeafKind::FleetService { .. } => service_purposes.clone(),
352 },
353 labels: effective_labels
354 .iter()
355 .map(|(key, value)| ComponentDeploymentLabel {
356 key: key.clone(),
357 value: value.clone(),
358 })
359 .collect(),
360 });
361 }
362 ComponentGroupMember::Group {
363 component_group: included,
364 ..
365 } => self.flatten_into(
366 included,
367 member_path,
368 active_groups,
369 service_purposes,
370 effective_labels,
371 output,
372 )?,
373 }
374 if member.service_purpose().is_some()
375 && !output[output_start..]
376 .iter()
377 .any(FlattenedComponentGroupMember::is_fleet_service)
378 {
379 return Err(
380 ComponentGroupTopologyError::InapplicableServicePurposeAssignment {
381 component_group: component_group.clone(),
382 member: member.member().clone(),
383 },
384 );
385 }
386 if member.service_purpose().is_some() {
387 service_purposes.pop();
388 }
389 for key in added_label_keys {
390 effective_labels.remove(&key);
391 }
392 member_path.pop();
393 }
394
395 active_groups.pop();
396 Ok(())
397 }
398}
399
400#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
402#[serde(deny_unknown_fields)]
403pub struct ComponentGroupSpec {
404 pub component_group: ComponentGroupSpecId,
405 pub members: Vec<ComponentGroupMember>,
406}
407
408#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
410pub enum ComponentGroupMember {
411 Component {
412 member: ComponentGroupMemberId,
413 component_spec: ComponentSpecId,
414 kind: ComponentGroupLeafKind,
415 service_purpose: Option<FleetServiceMemberPurpose>,
416 labels: Vec<ComponentDeploymentLabel>,
417 },
418 Group {
419 member: ComponentGroupMemberId,
420 component_group: ComponentGroupSpecId,
421 service_purpose: Option<FleetServiceMemberPurpose>,
422 labels: Vec<ComponentDeploymentLabel>,
423 },
424}
425
426impl ComponentGroupMember {
427 #[must_use]
428 pub const fn member(&self) -> &ComponentGroupMemberId {
429 match self {
430 Self::Component { member, .. } | Self::Group { member, .. } => member,
431 }
432 }
433
434 #[must_use]
435 pub const fn service_purpose(&self) -> Option<FleetServiceMemberPurpose> {
436 match self {
437 Self::Component {
438 service_purpose, ..
439 }
440 | Self::Group {
441 service_purpose, ..
442 } => *service_purpose,
443 }
444 }
445
446 #[must_use]
447 pub fn labels(&self) -> &[ComponentDeploymentLabel] {
448 match self {
449 Self::Component { labels, .. } | Self::Group { labels, .. } => labels,
450 }
451 }
452}
453
454#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
456pub enum ComponentGroupLeafKind {
457 Ordinary,
458 FleetService { service: FleetServiceId },
459}
460
461#[derive(CandidType, Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
463pub enum FleetServiceMemberPurpose {
464 #[serde(rename = "authority")]
465 Authority,
466 #[serde(rename = "replica")]
467 Replica,
468 #[serde(rename = "pool_member")]
469 PoolMember,
470}
471
472#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
474#[serde(deny_unknown_fields)]
475pub struct FlattenedComponentGroup {
476 pub component_group: ComponentGroupSpecId,
477 pub components: Vec<FlattenedComponentGroupMember>,
478}
479
480#[derive(CandidType, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
482#[serde(deny_unknown_fields)]
483pub struct FlattenedComponentGroupMember {
484 pub member_path: ComponentGroupMemberPath,
485 pub component_spec: ComponentSpecId,
486 pub kind: ComponentGroupLeafKind,
487 pub service_purpose_assignments: Vec<FleetServiceMemberPurpose>,
488 pub labels: Vec<ComponentDeploymentLabel>,
489}
490
491impl FlattenedComponentGroupMember {
492 pub(super) const fn is_fleet_service(&self) -> bool {
493 matches!(self.kind, ComponentGroupLeafKind::FleetService { .. })
494 }
495}
496
497#[derive(Debug, ThisError)]
499pub enum ComponentGroupTopologyError {
500 #[error("Component Group count {actual} exceeds bound {maximum}")]
501 GroupBoundExceeded { actual: usize, maximum: usize },
502
503 #[error("Component Group '{component_group}' must declare at least one member")]
504 EmptyGroup {
505 component_group: ComponentGroupSpecId,
506 },
507
508 #[error("Component Group '{component_group}' member count {actual} exceeds bound {maximum}")]
509 MemberBoundExceeded {
510 component_group: ComponentGroupSpecId,
511 actual: usize,
512 maximum: usize,
513 },
514
515 #[error("Component Group declared-member count {actual} exceeds bound {maximum}")]
516 DeclaredMemberBoundExceeded { actual: usize, maximum: usize },
517
518 #[error("Component Group inclusion count {actual} exceeds bound {maximum}")]
519 InclusionBoundExceeded { actual: usize, maximum: usize },
520
521 #[error("Component Group '{component_group}' declares member '{member}' more than once")]
522 DuplicateMember {
523 component_group: ComponentGroupSpecId,
524 member: ComponentGroupMemberId,
525 },
526
527 #[error(
528 "Component Group '{component_group}' member '{member}' references unknown Component Spec '{component_spec}'"
529 )]
530 UnknownComponentSpec {
531 component_group: ComponentGroupSpecId,
532 member: ComponentGroupMemberId,
533 component_spec: ComponentSpecId,
534 },
535
536 #[error("Component Group '{component_group}' includes unknown Component Group '{included}'")]
537 UnknownIncludedGroup {
538 component_group: ComponentGroupSpecId,
539 included: ComponentGroupSpecId,
540 },
541
542 #[error("unknown Component Group '{component_group}'")]
543 UnknownGroup {
544 component_group: ComponentGroupSpecId,
545 },
546
547 #[error("Component Group inclusion cycle involves '{component_group}'")]
548 InclusionCycle {
549 component_group: ComponentGroupSpecId,
550 },
551
552 #[error(
553 "Component Group '{component_group}' flattened member count {actual} exceeds bound {maximum}"
554 )]
555 FlattenedMemberBoundExceeded {
556 component_group: ComponentGroupSpecId,
557 actual: usize,
558 maximum: usize,
559 },
560
561 #[error("Component Group '{component_group}' has an invalid flattened member path: {source}")]
562 InvalidMemberPath {
563 component_group: ComponentGroupSpecId,
564 #[source]
565 source: ComponentGroupMemberPathError,
566 },
567
568 #[error(
569 "Component Group '{component_group}' member '{member}' assigns Fleet-service purpose without a service-bearing leaf"
570 )]
571 InapplicableServicePurposeAssignment {
572 component_group: ComponentGroupSpecId,
573 member: ComponentGroupMemberId,
574 },
575
576 #[error(
577 "Component Group '{component_group}' member '{member}' has {actual} labels; maximum is {maximum}"
578 )]
579 LabelBoundExceeded {
580 component_group: ComponentGroupSpecId,
581 member: ComponentGroupMemberId,
582 actual: usize,
583 maximum: usize,
584 },
585
586 #[error(
587 "Component Group '{component_group}' member '{member}' label '{label}' is duplicated or not in canonical order"
588 )]
589 NonCanonicalLabelOrder {
590 component_group: ComponentGroupSpecId,
591 member: ComponentGroupMemberId,
592 label: ComponentDeploymentLabelKey,
593 },
594
595 #[error(
596 "Component Group '{component_group}' flattened member '{member_path:?}' repeats label key '{label}'"
597 )]
598 DuplicateEffectiveLabel {
599 component_group: ComponentGroupSpecId,
600 member_path: ComponentGroupMemberPath,
601 label: ComponentDeploymentLabelKey,
602 },
603
604 #[error(
605 "Component Group '{component_group}' flattened member '{member_path:?}' has {actual} effective labels; maximum is {maximum}"
606 )]
607 EffectiveLabelBoundExceeded {
608 component_group: ComponentGroupSpecId,
609 member_path: ComponentGroupMemberPath,
610 actual: usize,
611 maximum: usize,
612 },
613
614 #[error("Component Group graph canonical bytes {actual} exceed bound {maximum}")]
615 CanonicalBytesBoundExceeded { actual: usize, maximum: usize },
616
617 #[error("Component Group '{component_group}' is not in canonical order")]
618 NonCanonicalGroupOrder {
619 component_group: ComponentGroupSpecId,
620 },
621
622 #[error("Component Group '{component_group}' member '{member}' is not in canonical order")]
623 NonCanonicalMemberOrder {
624 component_group: ComponentGroupSpecId,
625 member: ComponentGroupMemberId,
626 },
627}
628
629fn checked_member_count(
630 component_group: &ComponentGroupSpecId,
631 source: &ComponentGroupSpecConfig,
632) -> Result<usize, ComponentGroupTopologyError> {
633 let count = source
634 .components
635 .len()
636 .checked_add(source.groups.len())
637 .ok_or_else(|| ComponentGroupTopologyError::MemberBoundExceeded {
638 component_group: component_group.clone(),
639 actual: usize::MAX,
640 maximum: MAX_COMPONENT_GROUP_MEMBERS,
641 })?;
642 if count == 0 {
643 return Err(ComponentGroupTopologyError::EmptyGroup {
644 component_group: component_group.clone(),
645 });
646 }
647 if count > MAX_COMPONENT_GROUP_MEMBERS {
648 return Err(ComponentGroupTopologyError::MemberBoundExceeded {
649 component_group: component_group.clone(),
650 actual: count,
651 maximum: MAX_COMPONENT_GROUP_MEMBERS,
652 });
653 }
654 Ok(count)
655}
656
657fn compile_group(
658 config: &ConfigModel,
659 component_group: &ComponentGroupSpecId,
660 source: &ComponentGroupSpecConfig,
661) -> Result<ComponentGroupSpec, ComponentGroupTopologyError> {
662 let mut seen = BTreeSet::new();
663 let mut members = Vec::with_capacity(source.components.len() + source.groups.len());
664 for (member, component) in &source.components {
665 if !seen.insert(member.clone()) {
666 return Err(ComponentGroupTopologyError::DuplicateMember {
667 component_group: component_group.clone(),
668 member: member.clone(),
669 });
670 }
671 if !config
672 .component_specs
673 .contains_key(&component.component_spec)
674 {
675 return Err(ComponentGroupTopologyError::UnknownComponentSpec {
676 component_group: component_group.clone(),
677 member: member.clone(),
678 component_spec: component.component_spec.clone(),
679 });
680 }
681 members.push(ComponentGroupMember::Component {
682 member: member.clone(),
683 component_spec: component.component_spec.clone(),
684 kind: component
685 .service
686 .clone()
687 .map_or(ComponentGroupLeafKind::Ordinary, |service| {
688 ComponentGroupLeafKind::FleetService { service }
689 }),
690 service_purpose: component.service_purpose,
691 labels: source_labels(&component.labels),
692 });
693 }
694 for (member, included) in &source.groups {
695 if !seen.insert(member.clone()) {
696 return Err(ComponentGroupTopologyError::DuplicateMember {
697 component_group: component_group.clone(),
698 member: member.clone(),
699 });
700 }
701 if !config
702 .component_groups
703 .contains_key(&included.component_group)
704 {
705 return Err(ComponentGroupTopologyError::UnknownIncludedGroup {
706 component_group: component_group.clone(),
707 included: included.component_group.clone(),
708 });
709 }
710 members.push(ComponentGroupMember::Group {
711 member: member.clone(),
712 component_group: included.component_group.clone(),
713 service_purpose: included.service_purpose,
714 labels: source_labels(&included.labels),
715 });
716 }
717 members.sort_by(|left, right| left.member().cmp(right.member()));
718 Ok(ComponentGroupSpec {
719 component_group: component_group.clone(),
720 members,
721 })
722}
723
724fn encode_u64(bytes: &mut Vec<u8>, value: usize) {
725 let value = u64::try_from(value).expect("bounded Component Group length fits in u64");
726 bytes.extend_from_slice(&value.to_be_bytes());
727}
728
729fn encode_bytes(output: &mut Vec<u8>, value: &[u8]) {
730 encode_u64(output, value.len());
731 output.extend_from_slice(value);
732}
733
734fn encode_text(output: &mut Vec<u8>, value: &str) {
735 encode_bytes(output, value.as_bytes());
736}
737
738fn encode_leaf_kind(output: &mut Vec<u8>, kind: &ComponentGroupLeafKind) {
739 match kind {
740 ComponentGroupLeafKind::Ordinary => output.push(0),
741 ComponentGroupLeafKind::FleetService { service } => {
742 output.push(1);
743 encode_text(output, service.as_str());
744 }
745 }
746}
747
748fn encode_service_purpose(output: &mut Vec<u8>, purpose: Option<&FleetServiceMemberPurpose>) {
749 match purpose {
750 None => output.push(0),
751 Some(FleetServiceMemberPurpose::Authority) => output.extend_from_slice(&[1, 0]),
752 Some(FleetServiceMemberPurpose::Replica) => output.extend_from_slice(&[1, 1]),
753 Some(FleetServiceMemberPurpose::PoolMember) => output.extend_from_slice(&[1, 2]),
754 }
755}
756
757fn encode_labels(output: &mut Vec<u8>, labels: &[ComponentDeploymentLabel]) {
758 encode_u64(output, labels.len());
759 for label in labels {
760 encode_text(output, label.key.as_str());
761 encode_text(output, label.value.as_str());
762 }
763}
764
765pub(super) fn source_labels(
766 labels: &BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
767) -> Vec<ComponentDeploymentLabel> {
768 labels
769 .iter()
770 .map(|(key, value)| ComponentDeploymentLabel {
771 key: key.clone(),
772 value: value.clone(),
773 })
774 .collect()
775}
776
777fn validate_member_labels(
778 component_group: &ComponentGroupSpecId,
779 member: &ComponentGroupMember,
780) -> Result<(), ComponentGroupTopologyError> {
781 let labels = member.labels();
782 if labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
783 return Err(ComponentGroupTopologyError::LabelBoundExceeded {
784 component_group: component_group.clone(),
785 member: member.member().clone(),
786 actual: labels.len(),
787 maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
788 });
789 }
790 let mut previous: Option<&ComponentDeploymentLabelKey> = None;
791 for label in labels {
792 if previous.is_some_and(|key| key >= &label.key) {
793 return Err(ComponentGroupTopologyError::NonCanonicalLabelOrder {
794 component_group: component_group.clone(),
795 member: member.member().clone(),
796 label: label.key.clone(),
797 });
798 }
799 previous = Some(&label.key);
800 }
801 Ok(())
802}
803
804fn extend_effective_labels(
805 component_group: &ComponentGroupSpecId,
806 member_path: &ComponentGroupMemberPath,
807 member_labels: &[ComponentDeploymentLabel],
808 effective_labels: &mut BTreeMap<ComponentDeploymentLabelKey, ComponentDeploymentLabelValue>,
809) -> Result<Vec<ComponentDeploymentLabelKey>, ComponentGroupTopologyError> {
810 let mut added_label_keys = Vec::with_capacity(member_labels.len());
811 for label in member_labels {
812 match effective_labels.entry(label.key.clone()) {
813 std::collections::btree_map::Entry::Vacant(entry) => {
814 entry.insert(label.value.clone());
815 added_label_keys.push(label.key.clone());
816 }
817 std::collections::btree_map::Entry::Occupied(_) => {
818 return Err(ComponentGroupTopologyError::DuplicateEffectiveLabel {
819 component_group: component_group.clone(),
820 member_path: member_path.clone(),
821 label: label.key.clone(),
822 });
823 }
824 }
825 }
826 if effective_labels.len() > MAX_COMPONENT_DEPLOYMENT_LABELS {
827 return Err(ComponentGroupTopologyError::EffectiveLabelBoundExceeded {
828 component_group: component_group.clone(),
829 member_path: member_path.clone(),
830 actual: effective_labels.len(),
831 maximum: MAX_COMPONENT_DEPLOYMENT_LABELS,
832 });
833 }
834 Ok(added_label_keys)
835}