1use crate::{GpuSpec, MachineProfile, Platform};
13use serde::{Deserialize, Serialize};
14
15pub fn parse_cpu(s: &str) -> Result<f64, String> {
23 let s = s.trim();
24 if s.is_empty() {
25 return Err("empty CPU string".to_string());
26 }
27
28 if let Some(millis) = s.strip_suffix('m') {
29 let v: f64 = millis
30 .parse()
31 .map_err(|_| format!("invalid CPU millicore value: '{s}'"))?;
32 Ok(v / 1000.0)
33 } else {
34 s.parse().map_err(|_| format!("invalid CPU value: '{s}'"))
35 }
36}
37
38pub fn parse_memory_bytes(s: &str) -> Result<u64, String> {
43 let s = s.trim();
44 if s.is_empty() {
45 return Err("empty memory/storage string".to_string());
46 }
47
48 if let Some(num) = s.strip_suffix("Ti") {
50 let v: f64 = num
51 .parse()
52 .map_err(|_| format!("invalid memory value: '{s}'"))?;
53 return Ok((v * 1024.0 * 1024.0 * 1024.0 * 1024.0) as u64);
54 }
55 if let Some(num) = s.strip_suffix("Gi") {
56 let v: f64 = num
57 .parse()
58 .map_err(|_| format!("invalid memory value: '{s}'"))?;
59 return Ok((v * 1024.0 * 1024.0 * 1024.0) as u64);
60 }
61 if let Some(num) = s.strip_suffix("Mi") {
62 let v: f64 = num
63 .parse()
64 .map_err(|_| format!("invalid memory value: '{s}'"))?;
65 return Ok((v * 1024.0 * 1024.0) as u64);
66 }
67 if let Some(num) = s.strip_suffix("Ki") {
68 let v: f64 = num
69 .parse()
70 .map_err(|_| format!("invalid memory value: '{s}'"))?;
71 return Ok((v * 1024.0) as u64);
72 }
73
74 if let Some(num) = s.strip_suffix('T') {
76 let v: f64 = num
77 .parse()
78 .map_err(|_| format!("invalid memory value: '{s}'"))?;
79 return Ok((v * 1_000_000_000_000.0) as u64);
80 }
81 if let Some(num) = s.strip_suffix('G') {
82 let v: f64 = num
83 .parse()
84 .map_err(|_| format!("invalid memory value: '{s}'"))?;
85 return Ok((v * 1_000_000_000.0) as u64);
86 }
87 if let Some(num) = s.strip_suffix('M') {
88 let v: f64 = num
89 .parse()
90 .map_err(|_| format!("invalid memory value: '{s}'"))?;
91 return Ok((v * 1_000_000.0) as u64);
92 }
93 if let Some(num) = s.strip_suffix('k') {
94 let v: f64 = num
95 .parse()
96 .map_err(|_| format!("invalid memory value: '{s}'"))?;
97 return Ok((v * 1000.0) as u64);
98 }
99
100 s.parse()
102 .map_err(|_| format!("invalid memory value: '{s}'"))
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum InstanceFamily {
112 Burstable,
113 GeneralPurpose,
114 ComputeOptimized,
115 MemoryOptimized,
116 StorageOptimized,
117 GpuCompute,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
123#[serde(rename_all = "snake_case")]
124pub enum Architecture {
125 Arm64,
126 X86_64,
127}
128
129pub fn default_architecture(platform: Platform) -> Option<Architecture> {
131 match platform {
132 Platform::Aws => Some(Architecture::Arm64),
133 Platform::Gcp | Platform::Azure => Some(Architecture::X86_64),
134 Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => None,
135 }
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct CatalogGpu {
141 pub gpu_type: &'static str,
142 pub count: u32,
143}
144
145#[derive(Debug, Clone)]
149pub struct InstanceTypeSpec {
150 pub name: &'static str,
151 pub platform: Platform,
152 pub family: InstanceFamily,
153 pub architecture: Architecture,
154 pub vcpu: u32,
156 pub memory_bytes: u64,
158 pub ephemeral_storage_bytes: u64,
160 pub gpu: Option<CatalogGpu>,
162}
163
164impl InstanceTypeSpec {
165 pub fn has_configurable_ephemeral_storage(&self) -> bool {
168 matches!(
169 self.family,
170 InstanceFamily::Burstable
171 | InstanceFamily::GeneralPurpose
172 | InstanceFamily::ComputeOptimized
173 | InstanceFamily::MemoryOptimized
174 )
175 }
176
177 pub fn is_nested_virt_capable(&self) -> bool {
183 match self.platform {
184 Platform::Aws => {
185 let name = self.name;
186 name.starts_with("m8i.")
187 || name.starts_with("c8i.")
188 || name.starts_with("r8i.")
189 || name.starts_with("m8i-flex.")
190 || name.starts_with("c8i-flex.")
191 || name.starts_with("r8i-flex.")
192 }
193 Platform::Gcp => self.name.starts_with("n2-standard-"),
194 Platform::Azure => {
195 let name = self.name;
196 (name.starts_with("Standard_D") && name.ends_with("s_v5"))
197 || (name.starts_with("Standard_E") && name.ends_with("s_v5"))
198 || (name.starts_with("Standard_F") && name.ends_with("s_v2"))
199 }
200 _ => false,
201 }
202 }
203
204 pub fn to_machine_profile(&self) -> MachineProfile {
206 MachineProfile {
207 cpu: format!("{}.0", self.vcpu),
208 memory_bytes: self.memory_bytes,
209 ephemeral_storage_bytes: self.ephemeral_storage_bytes,
210 architecture: Some(self.architecture),
211 gpu: self.gpu.map(|g| GpuSpec {
212 gpu_type: g.gpu_type.to_string(),
213 count: g.count,
214 }),
215 }
216 }
217
218 pub fn to_machine_profile_for_storage(&self, requested_bytes: u64) -> MachineProfile {
222 let mut profile = self.to_machine_profile();
223 if self.has_configurable_ephemeral_storage() {
224 profile.ephemeral_storage_bytes = profile.ephemeral_storage_bytes.max(requested_bytes);
225 }
226 profile
227 }
228}
229
230const KI: u64 = 1024;
232const MI: u64 = KI * 1024;
233const GI: u64 = MI * 1024;
234const TI: u64 = GI * 1024;
235
236pub fn max_configurable_ephemeral_storage_bytes(platform: Platform) -> Option<u64> {
242 let disk_limit_gib = match platform {
243 Platform::Aws | Platform::Gcp => 64 * 1024,
245 Platform::Azure => 4_095,
247 Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => {
248 return None;
249 }
250 };
251
252 let max_requested_gib = (disk_limit_gib - 12) * 4 / 5;
257 Some(max_requested_gib * GI)
258}
259
260static CATALOG: &[InstanceTypeSpec] = &[
268 InstanceTypeSpec {
274 name: "t4g.micro",
275 platform: Platform::Aws,
276 family: InstanceFamily::Burstable,
277 architecture: Architecture::Arm64,
278 vcpu: 2,
279 memory_bytes: 1 * GI,
280 ephemeral_storage_bytes: 20 * GI,
281 gpu: None,
282 },
283 InstanceTypeSpec {
284 name: "t4g.small",
285 platform: Platform::Aws,
286 family: InstanceFamily::Burstable,
287 architecture: Architecture::Arm64,
288 vcpu: 2,
289 memory_bytes: 2 * GI,
290 ephemeral_storage_bytes: 20 * GI,
291 gpu: None,
292 },
293 InstanceTypeSpec {
294 name: "t4g.medium",
295 platform: Platform::Aws,
296 family: InstanceFamily::Burstable,
297 architecture: Architecture::Arm64,
298 vcpu: 2,
299 memory_bytes: 4 * GI,
300 ephemeral_storage_bytes: 20 * GI,
301 gpu: None,
302 },
303 InstanceTypeSpec {
304 name: "t4g.large",
305 platform: Platform::Aws,
306 family: InstanceFamily::Burstable,
307 architecture: Architecture::Arm64,
308 vcpu: 2,
309 memory_bytes: 8 * GI,
310 ephemeral_storage_bytes: 20 * GI,
311 gpu: None,
312 },
313 InstanceTypeSpec {
314 name: "t3.xlarge",
315 platform: Platform::Aws,
316 family: InstanceFamily::Burstable,
317 architecture: Architecture::X86_64,
318 vcpu: 4,
319 memory_bytes: 16 * GI,
320 ephemeral_storage_bytes: 20 * GI,
321 gpu: None,
322 },
323 InstanceTypeSpec {
324 name: "t4g.xlarge",
325 platform: Platform::Aws,
326 family: InstanceFamily::Burstable,
327 architecture: Architecture::Arm64,
328 vcpu: 4,
329 memory_bytes: 16 * GI,
330 ephemeral_storage_bytes: 20 * GI,
331 gpu: None,
332 },
333 InstanceTypeSpec {
335 name: "m7g.medium",
336 platform: Platform::Aws,
337 family: InstanceFamily::GeneralPurpose,
338 architecture: Architecture::Arm64,
339 vcpu: 1,
340 memory_bytes: 4 * GI,
341 ephemeral_storage_bytes: 20 * GI,
342 gpu: None,
343 },
344 InstanceTypeSpec {
345 name: "m7i.large",
346 platform: Platform::Aws,
347 family: InstanceFamily::GeneralPurpose,
348 architecture: Architecture::X86_64,
349 vcpu: 2,
350 memory_bytes: 8 * GI,
351 ephemeral_storage_bytes: 20 * GI,
352 gpu: None,
353 },
354 InstanceTypeSpec {
355 name: "m7g.large",
356 platform: Platform::Aws,
357 family: InstanceFamily::GeneralPurpose,
358 architecture: Architecture::Arm64,
359 vcpu: 2,
360 memory_bytes: 8 * GI,
361 ephemeral_storage_bytes: 20 * GI,
362 gpu: None,
363 },
364 InstanceTypeSpec {
372 name: "m8i.large",
373 platform: Platform::Aws,
374 family: InstanceFamily::GeneralPurpose,
375 architecture: Architecture::X86_64,
376 vcpu: 2,
377 memory_bytes: 8 * GI,
378 ephemeral_storage_bytes: 20 * GI,
379 gpu: None,
380 },
381 InstanceTypeSpec {
382 name: "m7i.xlarge",
383 platform: Platform::Aws,
384 family: InstanceFamily::GeneralPurpose,
385 architecture: Architecture::X86_64,
386 vcpu: 4,
387 memory_bytes: 16 * GI,
388 ephemeral_storage_bytes: 20 * GI,
389 gpu: None,
390 },
391 InstanceTypeSpec {
392 name: "m7g.xlarge",
393 platform: Platform::Aws,
394 family: InstanceFamily::GeneralPurpose,
395 architecture: Architecture::Arm64,
396 vcpu: 4,
397 memory_bytes: 16 * GI,
398 ephemeral_storage_bytes: 20 * GI,
399 gpu: None,
400 },
401 InstanceTypeSpec {
402 name: "m8i.xlarge",
403 platform: Platform::Aws,
404 family: InstanceFamily::GeneralPurpose,
405 architecture: Architecture::X86_64,
406 vcpu: 4,
407 memory_bytes: 16 * GI,
408 ephemeral_storage_bytes: 20 * GI,
409 gpu: None,
410 },
411 InstanceTypeSpec {
412 name: "m7i.2xlarge",
413 platform: Platform::Aws,
414 family: InstanceFamily::GeneralPurpose,
415 architecture: Architecture::X86_64,
416 vcpu: 8,
417 memory_bytes: 32 * GI,
418 ephemeral_storage_bytes: 20 * GI,
419 gpu: None,
420 },
421 InstanceTypeSpec {
422 name: "m7g.2xlarge",
423 platform: Platform::Aws,
424 family: InstanceFamily::GeneralPurpose,
425 architecture: Architecture::Arm64,
426 vcpu: 8,
427 memory_bytes: 32 * GI,
428 ephemeral_storage_bytes: 20 * GI,
429 gpu: None,
430 },
431 InstanceTypeSpec {
432 name: "m8i.2xlarge",
433 platform: Platform::Aws,
434 family: InstanceFamily::GeneralPurpose,
435 architecture: Architecture::X86_64,
436 vcpu: 8,
437 memory_bytes: 32 * GI,
438 ephemeral_storage_bytes: 20 * GI,
439 gpu: None,
440 },
441 InstanceTypeSpec {
442 name: "m7i.4xlarge",
443 platform: Platform::Aws,
444 family: InstanceFamily::GeneralPurpose,
445 architecture: Architecture::X86_64,
446 vcpu: 16,
447 memory_bytes: 64 * GI,
448 ephemeral_storage_bytes: 20 * GI,
449 gpu: None,
450 },
451 InstanceTypeSpec {
452 name: "m7g.4xlarge",
453 platform: Platform::Aws,
454 family: InstanceFamily::GeneralPurpose,
455 architecture: Architecture::Arm64,
456 vcpu: 16,
457 memory_bytes: 64 * GI,
458 ephemeral_storage_bytes: 20 * GI,
459 gpu: None,
460 },
461 InstanceTypeSpec {
462 name: "m8i.4xlarge",
463 platform: Platform::Aws,
464 family: InstanceFamily::GeneralPurpose,
465 architecture: Architecture::X86_64,
466 vcpu: 16,
467 memory_bytes: 64 * GI,
468 ephemeral_storage_bytes: 20 * GI,
469 gpu: None,
470 },
471 InstanceTypeSpec {
473 name: "c7g.medium",
474 platform: Platform::Aws,
475 family: InstanceFamily::ComputeOptimized,
476 architecture: Architecture::Arm64,
477 vcpu: 1,
478 memory_bytes: 2 * GI,
479 ephemeral_storage_bytes: 20 * GI,
480 gpu: None,
481 },
482 InstanceTypeSpec {
483 name: "c8g.medium",
484 platform: Platform::Aws,
485 family: InstanceFamily::ComputeOptimized,
486 architecture: Architecture::Arm64,
487 vcpu: 1,
488 memory_bytes: 2 * GI,
489 ephemeral_storage_bytes: 20 * GI,
490 gpu: None,
491 },
492 InstanceTypeSpec {
493 name: "c7g.large",
494 platform: Platform::Aws,
495 family: InstanceFamily::ComputeOptimized,
496 architecture: Architecture::Arm64,
497 vcpu: 2,
498 memory_bytes: 4 * GI,
499 ephemeral_storage_bytes: 20 * GI,
500 gpu: None,
501 },
502 InstanceTypeSpec {
503 name: "c8g.large",
504 platform: Platform::Aws,
505 family: InstanceFamily::ComputeOptimized,
506 architecture: Architecture::Arm64,
507 vcpu: 2,
508 memory_bytes: 4 * GI,
509 ephemeral_storage_bytes: 20 * GI,
510 gpu: None,
511 },
512 InstanceTypeSpec {
513 name: "c8i.large",
514 platform: Platform::Aws,
515 family: InstanceFamily::ComputeOptimized,
516 architecture: Architecture::X86_64,
517 vcpu: 2,
518 memory_bytes: 4 * GI,
519 ephemeral_storage_bytes: 20 * GI,
520 gpu: None,
521 },
522 InstanceTypeSpec {
523 name: "c7g.xlarge",
524 platform: Platform::Aws,
525 family: InstanceFamily::ComputeOptimized,
526 architecture: Architecture::Arm64,
527 vcpu: 4,
528 memory_bytes: 8 * GI,
529 ephemeral_storage_bytes: 20 * GI,
530 gpu: None,
531 },
532 InstanceTypeSpec {
533 name: "c8g.xlarge",
534 platform: Platform::Aws,
535 family: InstanceFamily::ComputeOptimized,
536 architecture: Architecture::Arm64,
537 vcpu: 4,
538 memory_bytes: 8 * GI,
539 ephemeral_storage_bytes: 20 * GI,
540 gpu: None,
541 },
542 InstanceTypeSpec {
543 name: "c8i.xlarge",
544 platform: Platform::Aws,
545 family: InstanceFamily::ComputeOptimized,
546 architecture: Architecture::X86_64,
547 vcpu: 4,
548 memory_bytes: 8 * GI,
549 ephemeral_storage_bytes: 20 * GI,
550 gpu: None,
551 },
552 InstanceTypeSpec {
553 name: "c7g.2xlarge",
554 platform: Platform::Aws,
555 family: InstanceFamily::ComputeOptimized,
556 architecture: Architecture::Arm64,
557 vcpu: 8,
558 memory_bytes: 16 * GI,
559 ephemeral_storage_bytes: 20 * GI,
560 gpu: None,
561 },
562 InstanceTypeSpec {
563 name: "c8g.2xlarge",
564 platform: Platform::Aws,
565 family: InstanceFamily::ComputeOptimized,
566 architecture: Architecture::Arm64,
567 vcpu: 8,
568 memory_bytes: 16 * GI,
569 ephemeral_storage_bytes: 20 * GI,
570 gpu: None,
571 },
572 InstanceTypeSpec {
573 name: "c8i.2xlarge",
574 platform: Platform::Aws,
575 family: InstanceFamily::ComputeOptimized,
576 architecture: Architecture::X86_64,
577 vcpu: 8,
578 memory_bytes: 16 * GI,
579 ephemeral_storage_bytes: 20 * GI,
580 gpu: None,
581 },
582 InstanceTypeSpec {
583 name: "c7g.4xlarge",
584 platform: Platform::Aws,
585 family: InstanceFamily::ComputeOptimized,
586 architecture: Architecture::Arm64,
587 vcpu: 16,
588 memory_bytes: 32 * GI,
589 ephemeral_storage_bytes: 20 * GI,
590 gpu: None,
591 },
592 InstanceTypeSpec {
593 name: "c8g.4xlarge",
594 platform: Platform::Aws,
595 family: InstanceFamily::ComputeOptimized,
596 architecture: Architecture::Arm64,
597 vcpu: 16,
598 memory_bytes: 32 * GI,
599 ephemeral_storage_bytes: 20 * GI,
600 gpu: None,
601 },
602 InstanceTypeSpec {
603 name: "c8i.4xlarge",
604 platform: Platform::Aws,
605 family: InstanceFamily::ComputeOptimized,
606 architecture: Architecture::X86_64,
607 vcpu: 16,
608 memory_bytes: 32 * GI,
609 ephemeral_storage_bytes: 20 * GI,
610 gpu: None,
611 },
612 InstanceTypeSpec {
614 name: "r7g.medium",
615 platform: Platform::Aws,
616 family: InstanceFamily::MemoryOptimized,
617 architecture: Architecture::Arm64,
618 vcpu: 1,
619 memory_bytes: 8 * GI,
620 ephemeral_storage_bytes: 20 * GI,
621 gpu: None,
622 },
623 InstanceTypeSpec {
624 name: "r7g.large",
625 platform: Platform::Aws,
626 family: InstanceFamily::MemoryOptimized,
627 architecture: Architecture::Arm64,
628 vcpu: 2,
629 memory_bytes: 16 * GI,
630 ephemeral_storage_bytes: 20 * GI,
631 gpu: None,
632 },
633 InstanceTypeSpec {
634 name: "r7g.xlarge",
635 platform: Platform::Aws,
636 family: InstanceFamily::MemoryOptimized,
637 architecture: Architecture::Arm64,
638 vcpu: 4,
639 memory_bytes: 32 * GI,
640 ephemeral_storage_bytes: 20 * GI,
641 gpu: None,
642 },
643 InstanceTypeSpec {
644 name: "r7g.2xlarge",
645 platform: Platform::Aws,
646 family: InstanceFamily::MemoryOptimized,
647 architecture: Architecture::Arm64,
648 vcpu: 8,
649 memory_bytes: 64 * GI,
650 ephemeral_storage_bytes: 20 * GI,
651 gpu: None,
652 },
653 InstanceTypeSpec {
654 name: "r7g.4xlarge",
655 platform: Platform::Aws,
656 family: InstanceFamily::MemoryOptimized,
657 architecture: Architecture::Arm64,
658 vcpu: 16,
659 memory_bytes: 128 * GI,
660 ephemeral_storage_bytes: 20 * GI,
661 gpu: None,
662 },
663 InstanceTypeSpec {
665 name: "i4i.xlarge",
666 platform: Platform::Aws,
667 family: InstanceFamily::StorageOptimized,
668 architecture: Architecture::X86_64,
669 vcpu: 4,
670 memory_bytes: 32 * GI,
671 ephemeral_storage_bytes: 937 * GI,
672 gpu: None,
673 },
674 InstanceTypeSpec {
675 name: "i4i.2xlarge",
676 platform: Platform::Aws,
677 family: InstanceFamily::StorageOptimized,
678 architecture: Architecture::X86_64,
679 vcpu: 8,
680 memory_bytes: 64 * GI,
681 ephemeral_storage_bytes: 1875 * GI,
682 gpu: None,
683 },
684 InstanceTypeSpec {
685 name: "i4i.4xlarge",
686 platform: Platform::Aws,
687 family: InstanceFamily::StorageOptimized,
688 architecture: Architecture::X86_64,
689 vcpu: 16,
690 memory_bytes: 128 * GI,
691 ephemeral_storage_bytes: 3750 * GI,
692 gpu: None,
693 },
694 InstanceTypeSpec {
695 name: "i4i.8xlarge",
696 platform: Platform::Aws,
697 family: InstanceFamily::StorageOptimized,
698 architecture: Architecture::X86_64,
699 vcpu: 32,
700 memory_bytes: 256 * GI,
701 ephemeral_storage_bytes: 7500 * GI,
702 gpu: None,
703 },
704 InstanceTypeSpec {
706 name: "g5.xlarge",
707 platform: Platform::Aws,
708 family: InstanceFamily::GpuCompute,
709 architecture: Architecture::X86_64,
710 vcpu: 4,
711 memory_bytes: 16 * GI,
712 ephemeral_storage_bytes: 250 * GI,
713 gpu: Some(CatalogGpu {
714 gpu_type: "nvidia-t4",
715 count: 1,
716 }),
717 },
718 InstanceTypeSpec {
719 name: "g5.2xlarge",
720 platform: Platform::Aws,
721 family: InstanceFamily::GpuCompute,
722 architecture: Architecture::X86_64,
723 vcpu: 8,
724 memory_bytes: 32 * GI,
725 ephemeral_storage_bytes: 450 * GI,
726 gpu: Some(CatalogGpu {
727 gpu_type: "nvidia-t4",
728 count: 1,
729 }),
730 },
731 InstanceTypeSpec {
733 name: "p4d.24xlarge",
734 platform: Platform::Aws,
735 family: InstanceFamily::GpuCompute,
736 architecture: Architecture::X86_64,
737 vcpu: 96,
738 memory_bytes: 1152 * GI,
739 ephemeral_storage_bytes: 8000 * GI,
740 gpu: Some(CatalogGpu {
741 gpu_type: "nvidia-a100",
742 count: 8,
743 }),
744 },
745 InstanceTypeSpec {
747 name: "p5.48xlarge",
748 platform: Platform::Aws,
749 family: InstanceFamily::GpuCompute,
750 architecture: Architecture::X86_64,
751 vcpu: 192,
752 memory_bytes: 2048 * GI,
753 ephemeral_storage_bytes: 8000 * GI,
754 gpu: Some(CatalogGpu {
755 gpu_type: "nvidia-h100",
756 count: 8,
757 }),
758 },
759 InstanceTypeSpec {
765 name: "e2-micro",
766 platform: Platform::Gcp,
767 family: InstanceFamily::Burstable,
768 architecture: Architecture::X86_64,
769 vcpu: 2,
770 memory_bytes: 1 * GI,
771 ephemeral_storage_bytes: 20 * GI,
772 gpu: None,
773 },
774 InstanceTypeSpec {
775 name: "e2-small",
776 platform: Platform::Gcp,
777 family: InstanceFamily::Burstable,
778 architecture: Architecture::X86_64,
779 vcpu: 2,
780 memory_bytes: 2 * GI,
781 ephemeral_storage_bytes: 20 * GI,
782 gpu: None,
783 },
784 InstanceTypeSpec {
785 name: "e2-medium",
786 platform: Platform::Gcp,
787 family: InstanceFamily::Burstable,
788 architecture: Architecture::X86_64,
789 vcpu: 2,
790 memory_bytes: 4 * GI,
791 ephemeral_storage_bytes: 20 * GI,
792 gpu: None,
793 },
794 InstanceTypeSpec {
796 name: "n2-standard-2",
797 platform: Platform::Gcp,
798 family: InstanceFamily::GeneralPurpose,
799 architecture: Architecture::X86_64,
800 vcpu: 2,
801 memory_bytes: 8 * GI,
802 ephemeral_storage_bytes: 20 * GI,
803 gpu: None,
804 },
805 InstanceTypeSpec {
806 name: "n2-standard-4",
807 platform: Platform::Gcp,
808 family: InstanceFamily::GeneralPurpose,
809 architecture: Architecture::X86_64,
810 vcpu: 4,
811 memory_bytes: 16 * GI,
812 ephemeral_storage_bytes: 20 * GI,
813 gpu: None,
814 },
815 InstanceTypeSpec {
816 name: "n2-standard-8",
817 platform: Platform::Gcp,
818 family: InstanceFamily::GeneralPurpose,
819 architecture: Architecture::X86_64,
820 vcpu: 8,
821 memory_bytes: 32 * GI,
822 ephemeral_storage_bytes: 20 * GI,
823 gpu: None,
824 },
825 InstanceTypeSpec {
826 name: "n2-standard-16",
827 platform: Platform::Gcp,
828 family: InstanceFamily::GeneralPurpose,
829 architecture: Architecture::X86_64,
830 vcpu: 16,
831 memory_bytes: 64 * GI,
832 ephemeral_storage_bytes: 20 * GI,
833 gpu: None,
834 },
835 InstanceTypeSpec {
837 name: "c3-standard-4",
838 platform: Platform::Gcp,
839 family: InstanceFamily::ComputeOptimized,
840 architecture: Architecture::X86_64,
841 vcpu: 4,
842 memory_bytes: 8 * GI,
843 ephemeral_storage_bytes: 20 * GI,
844 gpu: None,
845 },
846 InstanceTypeSpec {
847 name: "c3-standard-8",
848 platform: Platform::Gcp,
849 family: InstanceFamily::ComputeOptimized,
850 architecture: Architecture::X86_64,
851 vcpu: 8,
852 memory_bytes: 16 * GI,
853 ephemeral_storage_bytes: 20 * GI,
854 gpu: None,
855 },
856 InstanceTypeSpec {
858 name: "n2-highmem-2",
859 platform: Platform::Gcp,
860 family: InstanceFamily::MemoryOptimized,
861 architecture: Architecture::X86_64,
862 vcpu: 2,
863 memory_bytes: 16 * GI,
864 ephemeral_storage_bytes: 20 * GI,
865 gpu: None,
866 },
867 InstanceTypeSpec {
868 name: "n2-highmem-4",
869 platform: Platform::Gcp,
870 family: InstanceFamily::MemoryOptimized,
871 architecture: Architecture::X86_64,
872 vcpu: 4,
873 memory_bytes: 32 * GI,
874 ephemeral_storage_bytes: 20 * GI,
875 gpu: None,
876 },
877 InstanceTypeSpec {
878 name: "n2-highmem-8",
879 platform: Platform::Gcp,
880 family: InstanceFamily::MemoryOptimized,
881 architecture: Architecture::X86_64,
882 vcpu: 8,
883 memory_bytes: 64 * GI,
884 ephemeral_storage_bytes: 20 * GI,
885 gpu: None,
886 },
887 InstanceTypeSpec {
888 name: "n2-highmem-16",
889 platform: Platform::Gcp,
890 family: InstanceFamily::MemoryOptimized,
891 architecture: Architecture::X86_64,
892 vcpu: 16,
893 memory_bytes: 128 * GI,
894 ephemeral_storage_bytes: 20 * GI,
895 gpu: None,
896 },
897 InstanceTypeSpec {
898 name: "n2-highmem-32",
899 platform: Platform::Gcp,
900 family: InstanceFamily::MemoryOptimized,
901 architecture: Architecture::X86_64,
902 vcpu: 32,
903 memory_bytes: 256 * GI,
904 ephemeral_storage_bytes: 20 * GI,
905 gpu: None,
906 },
907 InstanceTypeSpec {
909 name: "c3d-standard-8",
910 platform: Platform::Gcp,
911 family: InstanceFamily::StorageOptimized,
912 architecture: Architecture::X86_64,
913 vcpu: 8,
914 memory_bytes: 32 * GI,
915 ephemeral_storage_bytes: 480 * GI,
916 gpu: None,
917 },
918 InstanceTypeSpec {
919 name: "c3d-standard-16",
920 platform: Platform::Gcp,
921 family: InstanceFamily::StorageOptimized,
922 architecture: Architecture::X86_64,
923 vcpu: 16,
924 memory_bytes: 64 * GI,
925 ephemeral_storage_bytes: 960 * GI,
926 gpu: None,
927 },
928 InstanceTypeSpec {
929 name: "c3d-standard-30",
930 platform: Platform::Gcp,
931 family: InstanceFamily::StorageOptimized,
932 architecture: Architecture::X86_64,
933 vcpu: 30,
934 memory_bytes: 120 * GI,
935 ephemeral_storage_bytes: 1920 * GI,
936 gpu: None,
937 },
938 InstanceTypeSpec {
940 name: "n1-standard-4-t4",
941 platform: Platform::Gcp,
942 family: InstanceFamily::GpuCompute,
943 architecture: Architecture::X86_64,
944 vcpu: 4,
945 memory_bytes: 15 * GI,
946 ephemeral_storage_bytes: 100 * GI,
947 gpu: Some(CatalogGpu {
948 gpu_type: "nvidia-t4",
949 count: 1,
950 }),
951 },
952 InstanceTypeSpec {
954 name: "a2-highgpu-1g",
955 platform: Platform::Gcp,
956 family: InstanceFamily::GpuCompute,
957 architecture: Architecture::X86_64,
958 vcpu: 12,
959 memory_bytes: 85 * GI,
960 ephemeral_storage_bytes: 100 * GI,
961 gpu: Some(CatalogGpu {
962 gpu_type: "nvidia-a100",
963 count: 1,
964 }),
965 },
966 InstanceTypeSpec {
967 name: "a2-highgpu-8g",
968 platform: Platform::Gcp,
969 family: InstanceFamily::GpuCompute,
970 architecture: Architecture::X86_64,
971 vcpu: 96,
972 memory_bytes: 1360 * GI,
973 ephemeral_storage_bytes: 100 * GI,
974 gpu: Some(CatalogGpu {
975 gpu_type: "nvidia-a100",
976 count: 8,
977 }),
978 },
979 InstanceTypeSpec {
981 name: "a3-highgpu-8g",
982 platform: Platform::Gcp,
983 family: InstanceFamily::GpuCompute,
984 architecture: Architecture::X86_64,
985 vcpu: 208,
986 memory_bytes: 1872 * GI,
987 ephemeral_storage_bytes: 100 * GI,
988 gpu: Some(CatalogGpu {
989 gpu_type: "nvidia-h100",
990 count: 8,
991 }),
992 },
993 InstanceTypeSpec {
999 name: "Standard_B1s",
1000 platform: Platform::Azure,
1001 family: InstanceFamily::Burstable,
1002 architecture: Architecture::X86_64,
1003 vcpu: 1,
1004 memory_bytes: 1 * GI,
1005 ephemeral_storage_bytes: 20 * GI,
1006 gpu: None,
1007 },
1008 InstanceTypeSpec {
1009 name: "Standard_B2s",
1010 platform: Platform::Azure,
1011 family: InstanceFamily::Burstable,
1012 architecture: Architecture::X86_64,
1013 vcpu: 2,
1014 memory_bytes: 4 * GI,
1015 ephemeral_storage_bytes: 20 * GI,
1016 gpu: None,
1017 },
1018 InstanceTypeSpec {
1019 name: "Standard_B2ms",
1020 platform: Platform::Azure,
1021 family: InstanceFamily::Burstable,
1022 architecture: Architecture::X86_64,
1023 vcpu: 2,
1024 memory_bytes: 8 * GI,
1025 ephemeral_storage_bytes: 20 * GI,
1026 gpu: None,
1027 },
1028 InstanceTypeSpec {
1029 name: "Standard_B4ms",
1030 platform: Platform::Azure,
1031 family: InstanceFamily::Burstable,
1032 architecture: Architecture::X86_64,
1033 vcpu: 4,
1034 memory_bytes: 16 * GI,
1035 ephemeral_storage_bytes: 20 * GI,
1036 gpu: None,
1037 },
1038 InstanceTypeSpec {
1040 name: "Standard_D2s_v4",
1041 platform: Platform::Azure,
1042 family: InstanceFamily::GeneralPurpose,
1043 architecture: Architecture::X86_64,
1044 vcpu: 2,
1045 memory_bytes: 8 * GI,
1046 ephemeral_storage_bytes: 20 * GI,
1047 gpu: None,
1048 },
1049 InstanceTypeSpec {
1050 name: "Standard_D2s_v5",
1051 platform: Platform::Azure,
1052 family: InstanceFamily::GeneralPurpose,
1053 architecture: Architecture::X86_64,
1054 vcpu: 2,
1055 memory_bytes: 8 * GI,
1056 ephemeral_storage_bytes: 20 * GI,
1057 gpu: None,
1058 },
1059 InstanceTypeSpec {
1060 name: "Standard_D4s_v5",
1061 platform: Platform::Azure,
1062 family: InstanceFamily::GeneralPurpose,
1063 architecture: Architecture::X86_64,
1064 vcpu: 4,
1065 memory_bytes: 16 * GI,
1066 ephemeral_storage_bytes: 20 * GI,
1067 gpu: None,
1068 },
1069 InstanceTypeSpec {
1070 name: "Standard_D8s_v5",
1071 platform: Platform::Azure,
1072 family: InstanceFamily::GeneralPurpose,
1073 architecture: Architecture::X86_64,
1074 vcpu: 8,
1075 memory_bytes: 32 * GI,
1076 ephemeral_storage_bytes: 20 * GI,
1077 gpu: None,
1078 },
1079 InstanceTypeSpec {
1080 name: "Standard_D16s_v5",
1081 platform: Platform::Azure,
1082 family: InstanceFamily::GeneralPurpose,
1083 architecture: Architecture::X86_64,
1084 vcpu: 16,
1085 memory_bytes: 64 * GI,
1086 ephemeral_storage_bytes: 20 * GI,
1087 gpu: None,
1088 },
1089 InstanceTypeSpec {
1091 name: "Standard_F2s_v2",
1092 platform: Platform::Azure,
1093 family: InstanceFamily::ComputeOptimized,
1094 architecture: Architecture::X86_64,
1095 vcpu: 2,
1096 memory_bytes: 4 * GI,
1097 ephemeral_storage_bytes: 20 * GI,
1098 gpu: None,
1099 },
1100 InstanceTypeSpec {
1101 name: "Standard_F4s_v2",
1102 platform: Platform::Azure,
1103 family: InstanceFamily::ComputeOptimized,
1104 architecture: Architecture::X86_64,
1105 vcpu: 4,
1106 memory_bytes: 8 * GI,
1107 ephemeral_storage_bytes: 20 * GI,
1108 gpu: None,
1109 },
1110 InstanceTypeSpec {
1111 name: "Standard_F8s_v2",
1112 platform: Platform::Azure,
1113 family: InstanceFamily::ComputeOptimized,
1114 architecture: Architecture::X86_64,
1115 vcpu: 8,
1116 memory_bytes: 16 * GI,
1117 ephemeral_storage_bytes: 20 * GI,
1118 gpu: None,
1119 },
1120 InstanceTypeSpec {
1121 name: "Standard_F16s_v2",
1122 platform: Platform::Azure,
1123 family: InstanceFamily::ComputeOptimized,
1124 architecture: Architecture::X86_64,
1125 vcpu: 16,
1126 memory_bytes: 32 * GI,
1127 ephemeral_storage_bytes: 20 * GI,
1128 gpu: None,
1129 },
1130 InstanceTypeSpec {
1132 name: "Standard_E2s_v5",
1133 platform: Platform::Azure,
1134 family: InstanceFamily::MemoryOptimized,
1135 architecture: Architecture::X86_64,
1136 vcpu: 2,
1137 memory_bytes: 16 * GI,
1138 ephemeral_storage_bytes: 20 * GI,
1139 gpu: None,
1140 },
1141 InstanceTypeSpec {
1142 name: "Standard_E4s_v5",
1143 platform: Platform::Azure,
1144 family: InstanceFamily::MemoryOptimized,
1145 architecture: Architecture::X86_64,
1146 vcpu: 4,
1147 memory_bytes: 32 * GI,
1148 ephemeral_storage_bytes: 20 * GI,
1149 gpu: None,
1150 },
1151 InstanceTypeSpec {
1152 name: "Standard_E8s_v5",
1153 platform: Platform::Azure,
1154 family: InstanceFamily::MemoryOptimized,
1155 architecture: Architecture::X86_64,
1156 vcpu: 8,
1157 memory_bytes: 64 * GI,
1158 ephemeral_storage_bytes: 20 * GI,
1159 gpu: None,
1160 },
1161 InstanceTypeSpec {
1162 name: "Standard_E16s_v5",
1163 platform: Platform::Azure,
1164 family: InstanceFamily::MemoryOptimized,
1165 architecture: Architecture::X86_64,
1166 vcpu: 16,
1167 memory_bytes: 128 * GI,
1168 ephemeral_storage_bytes: 20 * GI,
1169 gpu: None,
1170 },
1171 InstanceTypeSpec {
1173 name: "Standard_L8s_v3",
1174 platform: Platform::Azure,
1175 family: InstanceFamily::StorageOptimized,
1176 architecture: Architecture::X86_64,
1177 vcpu: 8,
1178 memory_bytes: 64 * GI,
1179 ephemeral_storage_bytes: 1788 * GI,
1180 gpu: None,
1181 },
1182 InstanceTypeSpec {
1183 name: "Standard_L16s_v3",
1184 platform: Platform::Azure,
1185 family: InstanceFamily::StorageOptimized,
1186 architecture: Architecture::X86_64,
1187 vcpu: 16,
1188 memory_bytes: 128 * GI,
1189 ephemeral_storage_bytes: 3576 * GI,
1190 gpu: None,
1191 },
1192 InstanceTypeSpec {
1193 name: "Standard_L32s_v3",
1194 platform: Platform::Azure,
1195 family: InstanceFamily::StorageOptimized,
1196 architecture: Architecture::X86_64,
1197 vcpu: 32,
1198 memory_bytes: 256 * GI,
1199 ephemeral_storage_bytes: 7154 * GI,
1200 gpu: None,
1201 },
1202 InstanceTypeSpec {
1204 name: "Standard_NC4as_T4_v3",
1205 platform: Platform::Azure,
1206 family: InstanceFamily::GpuCompute,
1207 architecture: Architecture::X86_64,
1208 vcpu: 4,
1209 memory_bytes: 28 * GI,
1210 ephemeral_storage_bytes: 176 * GI,
1211 gpu: Some(CatalogGpu {
1212 gpu_type: "nvidia-t4",
1213 count: 1,
1214 }),
1215 },
1216 InstanceTypeSpec {
1218 name: "Standard_NC24ads_A100_v4",
1219 platform: Platform::Azure,
1220 family: InstanceFamily::GpuCompute,
1221 architecture: Architecture::X86_64,
1222 vcpu: 24,
1223 memory_bytes: 220 * GI,
1224 ephemeral_storage_bytes: 958 * GI,
1225 gpu: Some(CatalogGpu {
1226 gpu_type: "nvidia-a100",
1227 count: 1,
1228 }),
1229 },
1230 InstanceTypeSpec {
1231 name: "Standard_NC96ads_A100_v4",
1232 platform: Platform::Azure,
1233 family: InstanceFamily::GpuCompute,
1234 architecture: Architecture::X86_64,
1235 vcpu: 96,
1236 memory_bytes: 880 * GI,
1237 ephemeral_storage_bytes: 3916 * GI,
1238 gpu: Some(CatalogGpu {
1239 gpu_type: "nvidia-a100",
1240 count: 4,
1241 }),
1242 },
1243 InstanceTypeSpec {
1245 name: "Standard_ND96isr_H100_v5",
1246 platform: Platform::Azure,
1247 family: InstanceFamily::GpuCompute,
1248 architecture: Architecture::X86_64,
1249 vcpu: 96,
1250 memory_bytes: 1900 * GI,
1251 ephemeral_storage_bytes: 1000 * GI,
1252 gpu: Some(CatalogGpu {
1253 gpu_type: "nvidia-h100",
1254 count: 8,
1255 }),
1256 },
1257];
1258
1259pub fn catalog_for_platform(platform: Platform) -> Vec<&'static InstanceTypeSpec> {
1265 CATALOG
1266 .iter()
1267 .filter(|spec| spec.platform == platform)
1268 .collect()
1269}
1270
1271pub fn find_instance_type(platform: Platform, name: &str) -> Option<&'static InstanceTypeSpec> {
1273 CATALOG
1274 .iter()
1275 .find(|spec| spec.platform == platform && spec.name == name)
1276}
1277
1278pub fn is_same_architecture_aws_machine(old: &str, new: &str) -> bool {
1281 is_same_architecture_machine(Platform::Aws, old, new)
1282}
1283
1284pub fn is_same_architecture_machine(platform: Platform, old: &str, new: &str) -> bool {
1286 matches!(platform, Platform::Aws | Platform::Gcp | Platform::Azure)
1287 && find_instance_type(platform, old)
1288 .zip(find_instance_type(platform, new))
1289 .is_some_and(|(old, new)| old.architecture == new.architecture)
1290}
1291
1292#[derive(Debug, Clone)]
1298pub struct WorkloadRequirements {
1299 pub total_cpu_at_desired: f64,
1301 pub total_memory_bytes_at_desired: u64,
1303 pub total_cpu_at_max: f64,
1305 pub total_memory_bytes_at_max: u64,
1307 pub max_cpu_per_container: f64,
1309 pub max_memory_per_container: u64,
1311 pub max_ephemeral_storage_bytes: u64,
1313 pub gpu: Option<GpuSpec>,
1315 pub architecture: Option<Architecture>,
1317 pub nested_virt: bool,
1321}
1322
1323#[derive(Debug, Clone)]
1325pub struct InstanceSelection {
1326 pub instance_type: &'static str,
1328 pub profile: MachineProfile,
1330 pub min_machines: u32,
1332 pub max_machines: u32,
1334}
1335
1336const STORAGE_OPTIMIZED_THRESHOLD: u64 = 200 * GI;
1338
1339const MAX_MACHINES_PER_CLUSTER: u32 = 10;
1341
1342const MAX_STANDARD_VCPU: u32 = 8;
1345
1346const SYSTEM_RESERVE_CPU: f64 = 0.5;
1348
1349const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1351
1352pub fn select_instance_type(
1363 platform: Platform,
1364 requirements: &WorkloadRequirements,
1365) -> Result<InstanceSelection, String> {
1366 let family = select_family(requirements);
1367 let family = if requirements.nested_virt && family == InstanceFamily::Burstable {
1368 InstanceFamily::GeneralPurpose
1369 } else {
1370 family
1371 };
1372 let selection = select_instance_type_in_family(platform, requirements, family);
1373 if selection.is_err() && family == InstanceFamily::Burstable {
1374 return select_instance_type_in_family(
1375 platform,
1376 requirements,
1377 InstanceFamily::GeneralPurpose,
1378 );
1379 }
1380 selection
1381}
1382
1383fn select_instance_type_in_family(
1384 platform: Platform,
1385 requirements: &WorkloadRequirements,
1386 family: InstanceFamily,
1387) -> Result<InstanceSelection, String> {
1388 let architecture = requirements
1389 .architecture
1390 .or_else(|| default_architecture(platform))
1391 .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1392
1393 let candidates: Vec<&InstanceTypeSpec> = CATALOG
1394 .iter()
1395 .filter(|spec| spec.platform == platform && spec.family == family)
1396 .filter(|spec| {
1397 if requirements.nested_virt {
1398 spec.is_nested_virt_capable()
1399 } else {
1400 platform != Platform::Aws || !spec.is_nested_virt_capable()
1401 }
1402 })
1403 .filter(|spec| spec.architecture == architecture)
1404 .collect();
1405
1406 if candidates.is_empty() && family != InstanceFamily::StorageOptimized {
1411 let family_has_other_architecture = CATALOG.iter().any(|spec| {
1412 spec.platform == platform
1413 && spec.family == family
1414 && if requirements.nested_virt {
1415 spec.is_nested_virt_capable()
1416 } else {
1417 platform != Platform::Aws || !spec.is_nested_virt_capable()
1418 }
1419 });
1420 if family_has_other_architecture {
1421 return Err(format!(
1422 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1423 ));
1424 }
1425 return Err(if requirements.nested_virt {
1426 format!(
1427 "no nested-virt-capable {family:?} instance types in catalog for platform {platform}"
1428 )
1429 } else {
1430 format!("no {family:?} instance types in catalog for platform {platform}")
1431 });
1432 }
1433
1434 let candidates = if let Some(ref gpu) = requirements.gpu {
1436 let filtered: Vec<&InstanceTypeSpec> = candidates
1437 .into_iter()
1438 .filter(|spec| {
1439 spec.gpu.as_ref().map_or(false, |g| {
1440 g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1441 })
1442 })
1443 .collect();
1444 if filtered.is_empty() {
1445 return Err(format!(
1446 "no instance type for GPU type '{}' x{} on platform {platform}",
1447 gpu.gpu_type, gpu.count
1448 ));
1449 }
1450 filtered
1451 } else {
1452 candidates
1453 };
1454
1455 let candidates = if family == InstanceFamily::StorageOptimized {
1457 let filtered: Vec<&InstanceTypeSpec> = candidates
1458 .into_iter()
1459 .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1460 .collect();
1461 if filtered.is_empty() {
1462 CATALOG
1466 .iter()
1467 .filter(|spec| {
1468 spec.platform == platform
1469 && spec.family == InstanceFamily::GeneralPurpose
1470 && spec.has_configurable_ephemeral_storage()
1471 && max_configurable_ephemeral_storage_bytes(platform)
1472 .is_some_and(|max| requirements.max_ephemeral_storage_bytes <= max)
1473 })
1474 .filter(|spec| {
1475 if requirements.nested_virt {
1476 spec.is_nested_virt_capable()
1477 } else {
1478 platform != Platform::Aws || !spec.is_nested_virt_capable()
1479 }
1480 })
1481 .filter(|spec| spec.architecture == architecture)
1482 .collect()
1483 } else {
1484 filtered
1485 }
1486 } else {
1487 candidates
1488 };
1489
1490 if candidates.is_empty() {
1491 return Err(format!(
1492 "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1493 ));
1494 }
1495
1496 let candidates: Vec<_> = candidates
1499 .into_iter()
1500 .filter(|spec| {
1501 allocatable_cpu(spec) >= requirements.max_cpu_per_container * WORKLOAD_HEADROOM_FACTOR
1502 && allocatable_memory_bytes(spec) as f64
1503 >= requirements.max_memory_per_container as f64 * WORKLOAD_HEADROOM_FACTOR
1504 })
1505 .collect();
1506 let smallest_replica_machine = candidates.iter().map(|spec| spec.vcpu).min()
1507 .ok_or_else(|| format!("no {platform} machine can fit one replica with system reserve and workload headroom"))?;
1508
1509 let effective_family = candidates[0].family;
1514 let vcpu_cap = if effective_family == InstanceFamily::GpuCompute
1515 || effective_family == InstanceFamily::StorageOptimized
1516 {
1517 u32::MAX
1518 } else {
1519 MAX_STANDARD_VCPU.max(smallest_replica_machine)
1520 };
1521
1522 let desired_target_machines = desired_target_machines(requirements);
1523 let target_cpu = requirements
1524 .max_cpu_per_container
1525 .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1526 * WORKLOAD_HEADROOM_FACTOR;
1527 let target_memory = (requirements.max_memory_per_container as f64)
1528 .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1529 * WORKLOAD_HEADROOM_FACTOR;
1530
1531 let selected = candidates
1536 .iter()
1537 .filter(|spec| {
1538 spec.vcpu <= vcpu_cap
1539 && allocatable_cpu(spec) >= target_cpu
1540 && allocatable_memory_bytes(spec) as f64 >= target_memory
1541 })
1542 .min_by_key(|spec| spec.vcpu)
1543 .or_else(|| {
1544 candidates
1546 .iter()
1547 .filter(|spec| spec.vcpu <= vcpu_cap)
1548 .max_by_key(|spec| spec.vcpu)
1549 })
1550 .or_else(|| {
1551 candidates.iter().min_by_key(|spec| spec.vcpu)
1553 })
1554 .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1555
1556 let max_machines = compute_max_machines(requirements, selected);
1558 let min_machines = compute_min_machines(requirements, selected, max_machines);
1559
1560 Ok(InstanceSelection {
1561 instance_type: selected.name,
1562 profile: selected.to_machine_profile_for_storage(requirements.max_ephemeral_storage_bytes),
1563 min_machines,
1564 max_machines,
1565 })
1566}
1567
1568pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1574 if requirements.gpu.is_some() {
1576 return InstanceFamily::GpuCompute;
1577 }
1578
1579 if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1581 return InstanceFamily::StorageOptimized;
1582 }
1583
1584 if requirements.total_cpu_at_max < 2.0 {
1586 return InstanceFamily::Burstable;
1587 }
1588
1589 InstanceFamily::GeneralPurpose
1591}
1592
1593fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1595 let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1596 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1597
1598 let mem_with_headroom =
1599 requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1600 let mem_machines =
1601 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1602
1603 cpu_machines
1605 .max(mem_machines)
1606 .max(1)
1607 .min(MAX_MACHINES_PER_CLUSTER)
1608}
1609
1610fn compute_min_machines(
1612 requirements: &WorkloadRequirements,
1613 instance: &InstanceTypeSpec,
1614 max_machines: u32,
1615) -> u32 {
1616 let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1617 let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1618
1619 let mem_with_headroom =
1620 requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1621 let mem_machines =
1622 (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1623
1624 cpu_machines
1625 .max(mem_machines)
1626 .max(1)
1627 .min(2)
1628 .min(max_machines)
1629}
1630
1631fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1632 if requirements.total_cpu_at_desired >= 2.0
1633 || requirements.total_memory_bytes_at_desired >= 4 * GI
1634 {
1635 2
1636 } else {
1637 1
1638 }
1639}
1640
1641pub(crate) fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1642 (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1643}
1644
1645pub(crate) fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1646 instance
1647 .memory_bytes
1648 .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1649 .max(256 * MI)
1650}
1651
1652fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1653 if memory_bytes < 4 * GI {
1654 256 * MI
1655 } else if memory_bytes < 16 * GI {
1656 512 * MI
1657 } else {
1658 GI
1659 }
1660}
1661
1662#[cfg(test)]
1667mod tests {
1668 use super::*;
1669 use crate::BinaryTarget;
1670
1671 #[test]
1674 fn test_parse_cpu_plain() {
1675 assert_eq!(parse_cpu("1").unwrap(), 1.0);
1676 assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1677 assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1678 assert_eq!(parse_cpu("16").unwrap(), 16.0);
1679 }
1680
1681 #[test]
1682 fn test_parse_cpu_millicore() {
1683 assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1684 assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1685 assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1686 assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1687 }
1688
1689 #[test]
1690 fn test_parse_cpu_invalid() {
1691 assert!(parse_cpu("").is_err());
1692 assert!(parse_cpu("abc").is_err());
1693 assert!(parse_cpu("m").is_err());
1694 }
1695
1696 #[test]
1697 fn test_parse_memory_binary_suffixes() {
1698 assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1699 assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1700 assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1701 assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1702 assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1703 assert_eq!(
1704 parse_memory_bytes("1Ti").unwrap(),
1705 1024u64 * 1024 * 1024 * 1024
1706 );
1707 }
1708
1709 #[test]
1710 fn test_parse_memory_decimal_suffixes() {
1711 assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1712 assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1713 assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1714 assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1715 }
1716
1717 #[test]
1718 fn test_parse_memory_plain_bytes() {
1719 assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1720 assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1721 }
1722
1723 #[test]
1724 fn test_parse_memory_invalid() {
1725 assert!(parse_memory_bytes("").is_err());
1726 assert!(parse_memory_bytes("abc").is_err());
1727 assert!(parse_memory_bytes("Gi").is_err());
1728 }
1729
1730 #[test]
1731 fn test_parse_memory_fractional() {
1732 assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1733 assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1734 }
1735
1736 #[test]
1739 fn test_catalog_has_entries_for_all_cloud_platforms() {
1740 assert!(!catalog_for_platform(Platform::Aws).is_empty());
1741 assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1742 assert!(!catalog_for_platform(Platform::Azure).is_empty());
1743 }
1744
1745 #[test]
1746 fn test_catalog_no_entries_for_non_cloud_platforms() {
1747 assert!(catalog_for_platform(Platform::Local).is_empty());
1748 assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1749 }
1750
1751 #[test]
1752 fn test_find_known_instance_type() {
1753 let spec =
1754 find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1755 assert_eq!(spec.vcpu, 8);
1756 assert_eq!(spec.memory_bytes, 32 * GI);
1757 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1758 }
1759
1760 #[test]
1761 fn test_find_aws_c8i_nested_virt_instance_type() {
1762 let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1763 assert_eq!(spec.vcpu, 2);
1764 assert_eq!(spec.memory_bytes, 4 * GI);
1765 assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1766 assert_eq!(spec.architecture, Architecture::X86_64);
1767 assert!(spec.is_nested_virt_capable());
1768 }
1769
1770 #[test]
1771 fn test_find_unknown_instance_type() {
1772 assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1773 }
1774
1775 #[test]
1776 fn test_find_wrong_platform() {
1777 assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1778 }
1779
1780 #[test]
1781 fn test_to_machine_profile() {
1782 let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1783 let profile = spec.to_machine_profile();
1784 assert_eq!(profile.cpu, "8.0");
1785 assert_eq!(profile.memory_bytes, 32 * GI);
1786 assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1787 assert!(profile.gpu.is_none());
1788 }
1789
1790 #[test]
1791 fn test_to_machine_profile_with_gpu() {
1792 let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1793 let profile = spec.to_machine_profile();
1794 let gpu = profile.gpu.as_ref().expect("should have GPU");
1795 assert_eq!(gpu.gpu_type, "nvidia-a100");
1796 assert_eq!(gpu.count, 8);
1797 }
1798
1799 #[test]
1802 fn small_cpu_with_large_memory_falls_back_from_burstable() {
1803 let requirements = WorkloadRequirements {
1804 total_cpu_at_desired: 0.5,
1805 total_memory_bytes_at_desired: 8 * GI,
1806 total_cpu_at_max: 0.5,
1807 total_memory_bytes_at_max: 8 * GI,
1808 max_cpu_per_container: 0.5,
1809 max_memory_per_container: 8 * GI,
1810 max_ephemeral_storage_bytes: 0,
1811 gpu: None,
1812 architecture: Some(Architecture::X86_64),
1813 nested_virt: false,
1814 };
1815 let selected = select_instance_type(Platform::Gcp, &requirements).unwrap();
1816 let machine = find_instance_type(Platform::Gcp, selected.instance_type).unwrap();
1817 assert!(
1818 allocatable_memory_bytes(machine) as f64 >= 8.0 * GI as f64 * WORKLOAD_HEADROOM_FACTOR
1819 );
1820 }
1821
1822 #[test]
1823 fn test_select_burstable_for_small_workload() {
1824 let req = WorkloadRequirements {
1825 total_cpu_at_desired: 1.0,
1826 total_memory_bytes_at_desired: 2 * GI,
1827 total_cpu_at_max: 1.0,
1828 total_memory_bytes_at_max: 2 * GI,
1829 max_cpu_per_container: 0.5,
1830 max_memory_per_container: 1 * GI,
1831 max_ephemeral_storage_bytes: 10 * GI,
1832 gpu: None,
1833 architecture: None,
1834 nested_virt: false,
1835 };
1836 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1837 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1838 assert_eq!(spec.family, InstanceFamily::Burstable);
1839 }
1840
1841 #[test]
1842 fn test_selects_smallest_burstable_machine_with_real_headroom() {
1843 let req = WorkloadRequirements {
1844 total_cpu_at_desired: 1.0,
1845 total_memory_bytes_at_desired: 2 * GI,
1846 total_cpu_at_max: 1.0,
1847 total_memory_bytes_at_max: 2 * GI,
1848 max_cpu_per_container: 1.0,
1849 max_memory_per_container: 2 * GI,
1850 max_ephemeral_storage_bytes: 10 * GI,
1851 gpu: None,
1852 architecture: None,
1853 nested_virt: false,
1854 };
1855
1856 let selection = select_instance_type(Platform::Aws, &req).unwrap();
1857
1858 assert_eq!(selection.instance_type, "t4g.medium");
1859 assert_eq!(selection.min_machines, 1);
1860 assert_eq!(selection.max_machines, 1);
1861 }
1862
1863 #[test]
1864 fn test_select_general_purpose_for_standard_workload() {
1865 let req = WorkloadRequirements {
1867 total_cpu_at_desired: 20.0,
1868 total_memory_bytes_at_desired: 80 * GI,
1869 total_cpu_at_max: 20.0,
1870 total_memory_bytes_at_max: 80 * GI,
1871 max_cpu_per_container: 2.0,
1872 max_memory_per_container: 8 * GI,
1873 max_ephemeral_storage_bytes: 10 * GI,
1874 gpu: None,
1875 architecture: None,
1876 nested_virt: false,
1877 };
1878 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1879 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1880 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1881 }
1882
1883 #[test]
1884 fn test_select_general_purpose_even_for_cpu_heavy() {
1885 let req = WorkloadRequirements {
1887 total_cpu_at_desired: 20.0,
1888 total_memory_bytes_at_desired: 20 * GI,
1889 total_cpu_at_max: 20.0,
1890 total_memory_bytes_at_max: 20 * GI,
1891 max_cpu_per_container: 2.0,
1892 max_memory_per_container: 2 * GI,
1893 max_ephemeral_storage_bytes: 10 * GI,
1894 gpu: None,
1895 architecture: None,
1896 nested_virt: false,
1897 };
1898 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1899 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1900 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1901 }
1902
1903 #[test]
1904 fn test_select_storage_optimized_for_large_ephemeral() {
1905 let req = WorkloadRequirements {
1906 total_cpu_at_desired: 8.0,
1907 total_memory_bytes_at_desired: 32 * GI,
1908 total_cpu_at_max: 8.0,
1909 total_memory_bytes_at_max: 32 * GI,
1910 max_cpu_per_container: 2.0,
1911 max_memory_per_container: 8 * GI,
1912 max_ephemeral_storage_bytes: 500 * GI,
1913 gpu: None,
1914 architecture: Some(Architecture::X86_64),
1915 nested_virt: false,
1916 };
1917 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1918 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1919 assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1920 }
1921
1922 #[test]
1923 fn test_select_configurable_storage_above_fixed_local_catalog() {
1924 let req = WorkloadRequirements {
1925 total_cpu_at_desired: 8.0,
1926 total_memory_bytes_at_desired: 32 * GI,
1927 total_cpu_at_max: 8.0,
1928 total_memory_bytes_at_max: 32 * GI,
1929 max_cpu_per_container: 2.0,
1930 max_memory_per_container: 8 * GI,
1931 max_ephemeral_storage_bytes: 8_000 * GI,
1932 gpu: None,
1933 architecture: Some(Architecture::X86_64),
1934 nested_virt: false,
1935 };
1936 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1937 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1938 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1939 assert_eq!(sel.profile.ephemeral_storage_bytes, 8_000 * GI);
1940 }
1941
1942 #[test]
1943 fn test_configurable_storage_fallback_retains_standard_vcpu_cap() {
1944 let req = WorkloadRequirements {
1945 total_cpu_at_desired: 70.0,
1946 total_memory_bytes_at_desired: 140 * GI,
1947 total_cpu_at_max: 70.0,
1948 total_memory_bytes_at_max: 140 * GI,
1949 max_cpu_per_container: 2.0,
1950 max_memory_per_container: 4 * GI,
1951 max_ephemeral_storage_bytes: 8_000 * GI,
1952 gpu: None,
1953 architecture: Some(Architecture::X86_64),
1954 nested_virt: false,
1955 };
1956
1957 let sel = select_instance_type(Platform::Aws, &req).unwrap();
1958 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1959 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1960 assert!(spec.vcpu <= MAX_STANDARD_VCPU);
1961 assert!(sel.max_machines > 1);
1962 }
1963
1964 #[test]
1965 fn test_nested_virtualization_storage_falls_back_to_configurable_disk() {
1966 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1967 let req = WorkloadRequirements {
1968 total_cpu_at_desired: 2.0,
1969 total_memory_bytes_at_desired: 8 * GI,
1970 total_cpu_at_max: 2.0,
1971 total_memory_bytes_at_max: 8 * GI,
1972 max_cpu_per_container: 2.0,
1973 max_memory_per_container: 8 * GI,
1974 max_ephemeral_storage_bytes: 500 * GI,
1975 gpu: None,
1976 architecture: Some(Architecture::X86_64),
1977 nested_virt: true,
1978 };
1979
1980 let sel = select_instance_type(platform, &req)
1981 .unwrap_or_else(|error| panic!("{platform} should support fallback: {error}"));
1982 let spec = find_instance_type(platform, sel.instance_type).unwrap();
1983 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1984 assert!(spec.is_nested_virt_capable());
1985 assert_eq!(sel.profile.ephemeral_storage_bytes, 500 * GI);
1986 }
1987 }
1988
1989 #[test]
1990 fn test_configurable_disk_rejects_capacity_above_provider_limit() {
1991 for (platform, requested) in [
1992 (Platform::Aws, 100 * TI),
1993 (Platform::Gcp, 100 * TI),
1994 (Platform::Azure, 8_000 * GI),
1995 ] {
1996 let req = WorkloadRequirements {
1997 total_cpu_at_desired: 2.0,
1998 total_memory_bytes_at_desired: 8 * GI,
1999 total_cpu_at_max: 2.0,
2000 total_memory_bytes_at_max: 8 * GI,
2001 max_cpu_per_container: 2.0,
2002 max_memory_per_container: 8 * GI,
2003 max_ephemeral_storage_bytes: requested,
2004 gpu: None,
2005 architecture: Some(Architecture::X86_64),
2006 nested_virt: true,
2007 };
2008
2009 assert!(select_instance_type(platform, &req).is_err());
2010 }
2011 }
2012
2013 #[test]
2014 fn test_configurable_disk_limits_account_for_controller_rounding() {
2015 for (platform, disk_limit_gib) in [
2016 (Platform::Aws, 64 * 1024),
2017 (Platform::Gcp, 64 * 1024),
2018 (Platform::Azure, 4_095),
2019 ] {
2020 let max = max_configurable_ephemeral_storage_bytes(platform).unwrap();
2021 let materialized_disk_gib = |requested_bytes: u64| {
2022 let requested_gib = requested_bytes.div_ceil(GI);
2023 (requested_gib * 5).div_ceil(4) + 12
2024 };
2025
2026 assert!(materialized_disk_gib(max) <= disk_limit_gib);
2027 assert!(materialized_disk_gib(max + 1) > disk_limit_gib);
2028 }
2029 }
2030
2031 #[test]
2032 fn test_gpu_local_storage_is_not_treated_as_resizable() {
2033 let spec = find_instance_type(Platform::Aws, "g5.xlarge").unwrap();
2034 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2035 assert!(!spec.has_configurable_ephemeral_storage());
2036 }
2037
2038 #[test]
2039 fn test_select_gpu_instance() {
2040 let req = WorkloadRequirements {
2041 total_cpu_at_desired: 8.0,
2042 total_memory_bytes_at_desired: 32 * GI,
2043 total_cpu_at_max: 8.0,
2044 total_memory_bytes_at_max: 32 * GI,
2045 max_cpu_per_container: 4.0,
2046 max_memory_per_container: 16 * GI,
2047 max_ephemeral_storage_bytes: 10 * GI,
2048 gpu: Some(GpuSpec {
2049 gpu_type: "nvidia-a100".to_string(),
2050 count: 1,
2051 }),
2052 architecture: Some(Architecture::X86_64),
2053 nested_virt: false,
2054 };
2055 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2056 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2057 assert_eq!(spec.family, InstanceFamily::GpuCompute);
2058 assert!(spec.gpu.is_some());
2059 }
2060
2061 #[test]
2062 fn test_select_uses_each_cloud_image_target_architecture() {
2063 let req = WorkloadRequirements {
2064 total_cpu_at_desired: 4.0,
2065 total_memory_bytes_at_desired: 16 * GI,
2066 total_cpu_at_max: 4.0,
2067 total_memory_bytes_at_max: 16 * GI,
2068 max_cpu_per_container: 1.0,
2069 max_memory_per_container: 4 * GI,
2070 max_ephemeral_storage_bytes: 10 * GI,
2071 gpu: None,
2072 architecture: None,
2073 nested_virt: false,
2074 };
2075 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2076 let sel = select_instance_type(platform, &req)
2077 .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
2078 let spec = find_instance_type(platform, sel.instance_type)
2079 .expect("selected machine should exist in the catalog");
2080 assert_eq!(
2081 Some(spec.architecture),
2082 default_architecture(platform),
2083 "machine architecture must match the image target for {platform}"
2084 );
2085 }
2086 }
2087
2088 #[test]
2089 fn test_machine_count_reasonable() {
2090 let req = WorkloadRequirements {
2092 total_cpu_at_desired: 20.0,
2093 total_memory_bytes_at_desired: 40 * GI,
2094 total_cpu_at_max: 20.0,
2095 total_memory_bytes_at_max: 40 * GI,
2096 max_cpu_per_container: 1.0,
2097 max_memory_per_container: 2 * GI,
2098 max_ephemeral_storage_bytes: 10 * GI,
2099 gpu: None,
2100 architecture: None,
2101 nested_virt: false,
2102 };
2103 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2104 assert!(sel.min_machines >= 1);
2105 assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
2106 assert!(sel.max_machines >= sel.min_machines);
2107 }
2108
2109 #[test]
2110 fn test_instance_size_capped_at_8_vcpu() {
2111 let req = WorkloadRequirements {
2113 total_cpu_at_desired: 70.0,
2114 total_memory_bytes_at_desired: 140 * GI,
2115 total_cpu_at_max: 70.0,
2116 total_memory_bytes_at_max: 140 * GI,
2117 max_cpu_per_container: 2.0,
2118 max_memory_per_container: 4 * GI,
2119 max_ephemeral_storage_bytes: 10 * GI,
2120 gpu: None,
2121 architecture: None,
2122 nested_virt: false,
2123 };
2124 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2125 let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
2126 assert!(
2127 spec.vcpu <= MAX_STANDARD_VCPU,
2128 "selected {} with {} vCPUs, expected <= {}",
2129 spec.name,
2130 spec.vcpu,
2131 MAX_STANDARD_VCPU
2132 );
2133 assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2134 assert!(sel.max_machines > 1);
2136 }
2137
2138 #[test]
2139 fn test_larger_autoscaled_workload_gets_reasonable_instance() {
2140 let req = WorkloadRequirements {
2143 total_cpu_at_desired: 70.0,
2144 total_memory_bytes_at_desired: 140 * GI,
2145 total_cpu_at_max: 70.0, total_memory_bytes_at_max: 140 * GI, max_cpu_per_container: 2.0,
2148 max_memory_per_container: 4 * GI,
2149 max_ephemeral_storage_bytes: 20 * GI,
2150 gpu: None,
2151 architecture: None,
2152 nested_virt: false,
2153 };
2154 let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2155 assert_eq!(sel.instance_type, "n2-standard-8");
2157 assert!(sel.max_machines >= 2);
2158 }
2159
2160 #[test]
2167 fn test_select_aws_picks_m8i_when_nested_virt_required() {
2168 let req = WorkloadRequirements {
2169 total_cpu_at_desired: 4.0,
2170 total_memory_bytes_at_desired: 8 * GI,
2171 total_cpu_at_max: 4.0,
2172 total_memory_bytes_at_max: 8 * GI,
2173 max_cpu_per_container: 4.0,
2174 max_memory_per_container: 8 * GI,
2175 max_ephemeral_storage_bytes: 10 * GI,
2176 gpu: None,
2177 architecture: Some(Architecture::X86_64),
2178 nested_virt: true,
2179 };
2180 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2181 assert!(
2182 sel.instance_type.starts_with("m8i.")
2183 || sel.instance_type.starts_with("c8i.")
2184 || sel.instance_type.starts_with("r8i."),
2185 "expected an m8i/c8i/r8i instance, got {}",
2186 sel.instance_type
2187 );
2188 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2189 assert!(spec.is_nested_virt_capable());
2190 }
2191
2192 #[test]
2193 fn test_select_gcp_picks_n2_when_nested_virt_required() {
2194 let req = WorkloadRequirements {
2195 total_cpu_at_desired: 4.0,
2196 total_memory_bytes_at_desired: 8 * GI,
2197 total_cpu_at_max: 4.0,
2198 total_memory_bytes_at_max: 8 * GI,
2199 max_cpu_per_container: 4.0,
2200 max_memory_per_container: 8 * GI,
2201 max_ephemeral_storage_bytes: 0,
2202 architecture: Some(Architecture::X86_64),
2203 gpu: None,
2204 nested_virt: true,
2205 };
2206
2207 let selection = select_instance_type(Platform::Gcp, &req).unwrap();
2208 assert_eq!(selection.instance_type, "n2-standard-8");
2209 assert!(find_instance_type(Platform::Gcp, selection.instance_type)
2210 .unwrap()
2211 .is_nested_virt_capable());
2212 }
2213
2214 #[test]
2215 fn test_select_azure_picks_dsv5_when_nested_virt_required() {
2216 let req = WorkloadRequirements {
2217 total_cpu_at_desired: 4.0,
2218 total_memory_bytes_at_desired: 8 * GI,
2219 total_cpu_at_max: 4.0,
2220 total_memory_bytes_at_max: 8 * GI,
2221 max_cpu_per_container: 4.0,
2222 max_memory_per_container: 8 * GI,
2223 max_ephemeral_storage_bytes: 0,
2224 architecture: Some(Architecture::X86_64),
2225 gpu: None,
2226 nested_virt: true,
2227 };
2228
2229 let selection = select_instance_type(Platform::Azure, &req).unwrap();
2230 assert_eq!(selection.instance_type, "Standard_D8s_v5");
2231 assert!(find_instance_type(Platform::Azure, selection.instance_type)
2232 .unwrap()
2233 .is_nested_virt_capable());
2234 }
2235
2236 #[test]
2237 fn test_select_aws_defaults_to_image_target_architecture() {
2238 let req = WorkloadRequirements {
2239 total_cpu_at_desired: 4.0,
2240 total_memory_bytes_at_desired: 8 * GI,
2241 total_cpu_at_max: 4.0,
2242 total_memory_bytes_at_max: 8 * GI,
2243 max_cpu_per_container: 4.0,
2244 max_memory_per_container: 8 * GI,
2245 max_ephemeral_storage_bytes: 10 * GI,
2246 gpu: None,
2247 architecture: None,
2248 nested_virt: false,
2249 };
2250 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2251 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2252 assert_eq!(spec.architecture, Architecture::Arm64);
2253 }
2254
2255 #[test]
2256 fn test_cloud_defaults_match_image_target_architectures() {
2257 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2258 let target = BinaryTarget::defaults_for_platform(platform)
2259 .into_iter()
2260 .next()
2261 .expect("managed cloud should have a default image target");
2262 let image_architecture = match target.oci_arch() {
2263 "arm64" => Architecture::Arm64,
2264 "amd64" => Architecture::X86_64,
2265 architecture => {
2266 panic!("unsupported managed-cloud image architecture {architecture}")
2267 }
2268 };
2269
2270 assert_eq!(default_architecture(platform), Some(image_architecture));
2271 }
2272 }
2273
2274 #[test]
2276 fn test_select_aws_uses_graviton_for_explicit_arm64() {
2277 let req = WorkloadRequirements {
2278 total_cpu_at_desired: 4.0,
2279 total_memory_bytes_at_desired: 8 * GI,
2280 total_cpu_at_max: 4.0,
2281 total_memory_bytes_at_max: 8 * GI,
2282 max_cpu_per_container: 4.0,
2283 max_memory_per_container: 8 * GI,
2284 max_ephemeral_storage_bytes: 10 * GI,
2285 gpu: None,
2286 architecture: Some(Architecture::Arm64),
2287 nested_virt: false,
2288 };
2289 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2290 let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2291 assert_eq!(spec.architecture, Architecture::Arm64);
2292 }
2293
2294 #[test]
2295 fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
2296 let req = WorkloadRequirements {
2297 total_cpu_at_desired: 1.0,
2298 total_memory_bytes_at_desired: 2 * GI,
2299 total_cpu_at_max: 1.0,
2300 total_memory_bytes_at_max: 2 * GI,
2301 max_cpu_per_container: 1.0,
2302 max_memory_per_container: 2 * GI,
2303 max_ephemeral_storage_bytes: 10 * GI,
2304 gpu: None,
2305 architecture: Some(Architecture::Arm64),
2306 nested_virt: false,
2307 };
2308
2309 let error = select_instance_type(Platform::Gcp, &req)
2310 .expect_err("GCP catalog has no ARM64 machine");
2311
2312 assert!(error.contains("architecture Arm64 is unavailable"));
2313 }
2314
2315 #[test]
2316 fn test_profile_has_required_fields() {
2317 let req = WorkloadRequirements {
2318 total_cpu_at_desired: 4.0,
2319 total_memory_bytes_at_desired: 16 * GI,
2320 total_cpu_at_max: 4.0,
2321 total_memory_bytes_at_max: 16 * GI,
2322 max_cpu_per_container: 1.0,
2323 max_memory_per_container: 4 * GI,
2324 max_ephemeral_storage_bytes: 10 * GI,
2325 gpu: None,
2326 architecture: None,
2327 nested_virt: false,
2328 };
2329 let sel = select_instance_type(Platform::Aws, &req).unwrap();
2330 assert!(!sel.profile.cpu.is_empty());
2331 assert!(sel.profile.memory_bytes > 0);
2332 assert!(sel.profile.ephemeral_storage_bytes > 0);
2333 }
2334
2335 #[test]
2336 fn test_error_for_unsupported_gpu_type() {
2337 let req = WorkloadRequirements {
2338 total_cpu_at_desired: 8.0,
2339 total_memory_bytes_at_desired: 32 * GI,
2340 total_cpu_at_max: 8.0,
2341 total_memory_bytes_at_max: 32 * GI,
2342 max_cpu_per_container: 4.0,
2343 max_memory_per_container: 16 * GI,
2344 max_ephemeral_storage_bytes: 10 * GI,
2345 gpu: Some(GpuSpec {
2346 gpu_type: "amd-mi300".to_string(),
2347 count: 1,
2348 }),
2349 architecture: None,
2350 nested_virt: false,
2351 };
2352 let result = select_instance_type(Platform::Aws, &req);
2353 assert!(result.is_err());
2354 }
2355
2356 #[test]
2357 fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
2358 for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2361 let entries = catalog_for_platform(platform);
2362 let mut by_family: std::collections::HashMap<_, Vec<_>> =
2363 std::collections::HashMap::new();
2364 for entry in entries {
2365 by_family
2366 .entry(format!("{:?}", entry.family))
2367 .or_default()
2368 .push(entry);
2369 }
2370 for (family, instances) in &by_family {
2371 for window in instances.windows(2) {
2372 assert!(
2373 window[0].vcpu <= window[1].vcpu,
2374 "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2375 window[0].name,
2376 window[0].vcpu,
2377 window[1].name,
2378 window[1].vcpu
2379 );
2380 }
2381 }
2382 }
2383 }
2384}