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