Skip to main content

alien_core/
instance_catalog.rs

1//! Instance type catalog and selection algorithm for cloud compute infrastructure.
2//!
3//! This module provides:
4//! - A static catalog of known instance types across AWS, GCP, and Azure
5//! - Resource quantity parsing (CPU strings, Kubernetes-style memory/storage quantities)
6//! - An algorithm to select the optimal instance type for a given workload
7//!
8//! The catalog is the single source of truth for instance type specifications.
9//! It is used by the preflights system to automatically populate `CapacityGroup.instance_type`
10//! and `CapacityGroup.profile` based on the containers in a stack.
11
12use crate::{GpuSpec, MachineProfile, Platform};
13use serde::{Deserialize, Serialize};
14
15// ---------------------------------------------------------------------------
16// Resource quantity parsing
17// ---------------------------------------------------------------------------
18
19/// Parse a CPU quantity string to f64.
20///
21/// Accepts plain numbers ("1", "0.5", "2.0") and millicore suffixes ("500m" = 0.5).
22pub 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
38/// Parse a memory or storage quantity string to bytes.
39///
40/// Supports Kubernetes-style binary suffixes (Ki, Mi, Gi, Ti) and
41/// decimal suffixes (k, M, G, T). Plain numbers are interpreted as bytes.
42pub 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    for (suffix, multiplier) in [
49        ("Ti", 1024_f64.powi(4)),
50        ("Gi", 1024_f64.powi(3)),
51        ("Mi", 1024_f64.powi(2)),
52        ("Ki", 1024_f64),
53        ("T", 1_000_000_000_000_f64),
54        ("G", 1_000_000_000_f64),
55        ("M", 1_000_000_f64),
56        ("k", 1_000_f64),
57    ] {
58        if let Some(number) = s.strip_suffix(suffix) {
59            let value: f64 = number
60                .parse()
61                .map_err(|_| format!("invalid memory value: '{s}'"))?;
62            let bytes = value * multiplier;
63            // Float-to-integer casts saturate negatives, infinity and NaN instead of
64            // returning an error. Reject them before a sizing request reaches a provider.
65            if !bytes.is_finite() || bytes < 0.0 || bytes >= u64::MAX as f64 {
66                return Err(format!("memory value is out of range: '{s}'"));
67            }
68            return Ok(bytes as u64);
69        }
70    }
71    s.parse()
72        .map_err(|_| format!("invalid memory value: '{s}'"))
73}
74
75// ---------------------------------------------------------------------------
76// Instance type catalog
77// ---------------------------------------------------------------------------
78
79/// Instance family classification.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum InstanceFamily {
82    Burstable,
83    GeneralPurpose,
84    ComputeOptimized,
85    MemoryOptimized,
86    StorageOptimized,
87    GpuCompute,
88}
89
90/// CPU architecture.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
92#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
93#[serde(rename_all = "snake_case")]
94pub enum Architecture {
95    Arm64,
96    X86_64,
97}
98
99/// Default machine architecture for images built for a managed cloud.
100pub fn default_architecture(platform: Platform) -> Option<Architecture> {
101    match platform {
102        Platform::Aws => Some(Architecture::Arm64),
103        Platform::Gcp | Platform::Azure => Some(Architecture::X86_64),
104        Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => None,
105    }
106}
107
108/// Static GPU specification for catalog entries (no heap allocation).
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct CatalogGpu {
111    pub gpu_type: &'static str,
112    pub count: u32,
113}
114
115/// A known instance type with its hardware specifications.
116///
117/// All fields are compile-time constants. The catalog is a flat array of these.
118#[derive(Debug, Clone)]
119pub struct InstanceTypeSpec {
120    pub name: &'static str,
121    pub platform: Platform,
122    pub family: InstanceFamily,
123    pub architecture: Architecture,
124    /// vCPU count (hardware total)
125    pub vcpu: u32,
126    /// Memory in bytes (hardware total)
127    pub memory_bytes: u64,
128    /// Ephemeral storage in bytes (hardware total, NVMe for storage-optimized)
129    pub ephemeral_storage_bytes: u64,
130    /// GPU specification (for GPU instances)
131    pub gpu: Option<CatalogGpu>,
132}
133
134impl InstanceTypeSpec {
135    /// Whether ephemeral storage is a provider disk that can be sized at
136    /// deployment time instead of fixed local instance storage.
137    pub fn has_configurable_ephemeral_storage(&self) -> bool {
138        matches!(
139            self.family,
140            InstanceFamily::Burstable
141                | InstanceFamily::GeneralPurpose
142                | InstanceFamily::ComputeOptimized
143                | InstanceFamily::MemoryOptimized
144        )
145    }
146
147    /// Whether this instance type supports nested virtualization.
148    ///
149    /// Classify by documented provider families rather than adding a flag to
150    /// every catalog row. GCP still requires the instance template to opt in;
151    /// Azure exposes the capability automatically on supported VM sizes.
152    pub fn is_nested_virt_capable(&self) -> bool {
153        match self.platform {
154            Platform::Aws => {
155                let name = self.name;
156                name.starts_with("m8i.")
157                    || name.starts_with("c8i.")
158                    || name.starts_with("r8i.")
159                    || name.starts_with("m8i-flex.")
160                    || name.starts_with("c8i-flex.")
161                    || name.starts_with("r8i-flex.")
162            }
163            Platform::Gcp => self.name.starts_with("n2-standard-"),
164            Platform::Azure => {
165                let name = self.name;
166                (name.starts_with("Standard_D") && name.ends_with("s_v5"))
167                    || (name.starts_with("Standard_E") && name.ends_with("s_v5"))
168                    || (name.starts_with("Standard_F") && name.ends_with("s_v2"))
169            }
170            _ => false,
171        }
172    }
173
174    /// Convert this catalog entry into a `MachineProfile` for use in `CapacityGroup`.
175    pub fn to_machine_profile(&self) -> MachineProfile {
176        MachineProfile {
177            cpu: format!("{}.0", self.vcpu),
178            memory_bytes: self.memory_bytes,
179            ephemeral_storage_bytes: self.ephemeral_storage_bytes,
180            architecture: Some(self.architecture),
181            gpu: self.gpu.map(|g| GpuSpec {
182                gpu_type: g.gpu_type.to_string(),
183                count: g.count,
184            }),
185            data_disk: None,
186        }
187    }
188
189    /// Convert this entry to the profile controllers should provision for a
190    /// workload. Configurable cloud disks grow to the requested capacity;
191    /// fixed local disks retain their catalog capacity.
192    pub fn to_machine_profile_for_storage(&self, requested_bytes: u64) -> MachineProfile {
193        let mut profile = self.to_machine_profile();
194        if self.has_configurable_ephemeral_storage() {
195            profile.ephemeral_storage_bytes = profile.ephemeral_storage_bytes.max(requested_bytes);
196        }
197        profile
198    }
199}
200
201// Helpers for readable byte constants
202const KI: u64 = 1024;
203const MI: u64 = KI * 1024;
204const GI: u64 = MI * 1024;
205const TI: u64 = GI * 1024;
206
207/// Maximum workload storage that the current cloud controllers can turn into
208/// their provider-backed root disk after adding 25% filesystem headroom and
209/// 12 GiB of runtime overhead. Keep these bounds in sync with the provider
210/// controller contract; rejecting here is preferable to failing after cloud
211/// resources have already been created.
212pub fn max_configurable_ephemeral_storage_bytes(platform: Platform) -> Option<u64> {
213    let disk_limit_gib = match platform {
214        // AWS gp3 and GCP persistent disks currently top out at 64 TiB.
215        Platform::Aws | Platform::Gcp => 64 * 1024,
216        // Azure managed OS disks currently top out at 4,095 GiB.
217        Platform::Azure => 4_095,
218        Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => {
219            return None;
220        }
221    };
222
223    // Controllers first round the requested bytes up to a whole GiB, then
224    // round the 25% headroom up again. Returning a fractional-GiB bound would
225    // therefore admit `bound + 1 byte` and materialize a disk one GiB over the
226    // provider limit.
227    let max_requested_gib = (disk_limit_gib - 12) * 4 / 5;
228    Some(max_requested_gib * GI)
229}
230
231/// The complete instance type catalog.
232///
233/// This is the single source of truth for instance type specifications.
234/// Update this array when adding support for new instance types.
235///
236/// NOTE: Ephemeral storage values for non-NVMe instances are conservative defaults
237/// (EBS-backed root volumes). Storage-optimized instances list their NVMe capacity.
238static CATALOG: &[InstanceTypeSpec] = &[
239    // =========================================================================
240    // AWS — ARM (Graviton) preferred for cost efficiency
241    // =========================================================================
242
243    // Burstable (t4g — ARM Graviton2)
244    InstanceTypeSpec {
245        name: "t4g.micro",
246        platform: Platform::Aws,
247        family: InstanceFamily::Burstable,
248        architecture: Architecture::Arm64,
249        vcpu: 2,
250        memory_bytes: 1 * GI,
251        ephemeral_storage_bytes: 20 * GI,
252        gpu: None,
253    },
254    InstanceTypeSpec {
255        name: "t4g.small",
256        platform: Platform::Aws,
257        family: InstanceFamily::Burstable,
258        architecture: Architecture::Arm64,
259        vcpu: 2,
260        memory_bytes: 2 * GI,
261        ephemeral_storage_bytes: 20 * GI,
262        gpu: None,
263    },
264    InstanceTypeSpec {
265        name: "t4g.medium",
266        platform: Platform::Aws,
267        family: InstanceFamily::Burstable,
268        architecture: Architecture::Arm64,
269        vcpu: 2,
270        memory_bytes: 4 * GI,
271        ephemeral_storage_bytes: 20 * GI,
272        gpu: None,
273    },
274    InstanceTypeSpec {
275        name: "t4g.large",
276        platform: Platform::Aws,
277        family: InstanceFamily::Burstable,
278        architecture: Architecture::Arm64,
279        vcpu: 2,
280        memory_bytes: 8 * GI,
281        ephemeral_storage_bytes: 20 * GI,
282        gpu: None,
283    },
284    InstanceTypeSpec {
285        name: "t3.xlarge",
286        platform: Platform::Aws,
287        family: InstanceFamily::Burstable,
288        architecture: Architecture::X86_64,
289        vcpu: 4,
290        memory_bytes: 16 * GI,
291        ephemeral_storage_bytes: 20 * GI,
292        gpu: None,
293    },
294    InstanceTypeSpec {
295        name: "t4g.xlarge",
296        platform: Platform::Aws,
297        family: InstanceFamily::Burstable,
298        architecture: Architecture::Arm64,
299        vcpu: 4,
300        memory_bytes: 16 * GI,
301        ephemeral_storage_bytes: 20 * GI,
302        gpu: None,
303    },
304    // General Purpose (m7g — ARM Graviton3, up to 2xlarge / 8 vCPU)
305    InstanceTypeSpec {
306        name: "m7g.medium",
307        platform: Platform::Aws,
308        family: InstanceFamily::GeneralPurpose,
309        architecture: Architecture::Arm64,
310        vcpu: 1,
311        memory_bytes: 4 * GI,
312        ephemeral_storage_bytes: 20 * GI,
313        gpu: None,
314    },
315    InstanceTypeSpec {
316        name: "m7i.large",
317        platform: Platform::Aws,
318        family: InstanceFamily::GeneralPurpose,
319        architecture: Architecture::X86_64,
320        vcpu: 2,
321        memory_bytes: 8 * GI,
322        ephemeral_storage_bytes: 20 * GI,
323        gpu: None,
324    },
325    InstanceTypeSpec {
326        name: "m7g.large",
327        platform: Platform::Aws,
328        family: InstanceFamily::GeneralPurpose,
329        architecture: Architecture::Arm64,
330        vcpu: 2,
331        memory_bytes: 8 * GI,
332        ephemeral_storage_bytes: 20 * GI,
333        gpu: None,
334    },
335    // 8th-gen Intel AWS families accept
336    // `CpuOptions.NestedVirtualization=enabled`. The catalog filter in
337    // `select_instance_type` includes these entries only when the
338    // workload requests nested virt, so ordinary workloads continue to
339    // pick the cost-efficient Graviton (m7g) above. The pairwise
340    // interleave keeps the per-family vCPU-non-decreasing invariant
341    // (see `test_catalog_instance_types_sorted_by_vcpu_within_family`).
342    InstanceTypeSpec {
343        name: "m8i.large",
344        platform: Platform::Aws,
345        family: InstanceFamily::GeneralPurpose,
346        architecture: Architecture::X86_64,
347        vcpu: 2,
348        memory_bytes: 8 * GI,
349        ephemeral_storage_bytes: 20 * GI,
350        gpu: None,
351    },
352    InstanceTypeSpec {
353        name: "m7i.xlarge",
354        platform: Platform::Aws,
355        family: InstanceFamily::GeneralPurpose,
356        architecture: Architecture::X86_64,
357        vcpu: 4,
358        memory_bytes: 16 * GI,
359        ephemeral_storage_bytes: 20 * GI,
360        gpu: None,
361    },
362    InstanceTypeSpec {
363        name: "m7g.xlarge",
364        platform: Platform::Aws,
365        family: InstanceFamily::GeneralPurpose,
366        architecture: Architecture::Arm64,
367        vcpu: 4,
368        memory_bytes: 16 * GI,
369        ephemeral_storage_bytes: 20 * GI,
370        gpu: None,
371    },
372    InstanceTypeSpec {
373        name: "m8i.xlarge",
374        platform: Platform::Aws,
375        family: InstanceFamily::GeneralPurpose,
376        architecture: Architecture::X86_64,
377        vcpu: 4,
378        memory_bytes: 16 * GI,
379        ephemeral_storage_bytes: 20 * GI,
380        gpu: None,
381    },
382    InstanceTypeSpec {
383        name: "m7i.2xlarge",
384        platform: Platform::Aws,
385        family: InstanceFamily::GeneralPurpose,
386        architecture: Architecture::X86_64,
387        vcpu: 8,
388        memory_bytes: 32 * GI,
389        ephemeral_storage_bytes: 20 * GI,
390        gpu: None,
391    },
392    InstanceTypeSpec {
393        name: "m7g.2xlarge",
394        platform: Platform::Aws,
395        family: InstanceFamily::GeneralPurpose,
396        architecture: Architecture::Arm64,
397        vcpu: 8,
398        memory_bytes: 32 * GI,
399        ephemeral_storage_bytes: 20 * GI,
400        gpu: None,
401    },
402    InstanceTypeSpec {
403        name: "m8i.2xlarge",
404        platform: Platform::Aws,
405        family: InstanceFamily::GeneralPurpose,
406        architecture: Architecture::X86_64,
407        vcpu: 8,
408        memory_bytes: 32 * GI,
409        ephemeral_storage_bytes: 20 * GI,
410        gpu: None,
411    },
412    InstanceTypeSpec {
413        name: "m7i.4xlarge",
414        platform: Platform::Aws,
415        family: InstanceFamily::GeneralPurpose,
416        architecture: Architecture::X86_64,
417        vcpu: 16,
418        memory_bytes: 64 * GI,
419        ephemeral_storage_bytes: 20 * GI,
420        gpu: None,
421    },
422    InstanceTypeSpec {
423        name: "m7g.4xlarge",
424        platform: Platform::Aws,
425        family: InstanceFamily::GeneralPurpose,
426        architecture: Architecture::Arm64,
427        vcpu: 16,
428        memory_bytes: 64 * GI,
429        ephemeral_storage_bytes: 20 * GI,
430        gpu: None,
431    },
432    InstanceTypeSpec {
433        name: "m8i.4xlarge",
434        platform: Platform::Aws,
435        family: InstanceFamily::GeneralPurpose,
436        architecture: Architecture::X86_64,
437        vcpu: 16,
438        memory_bytes: 64 * GI,
439        ephemeral_storage_bytes: 20 * GI,
440        gpu: None,
441    },
442    // Compute Optimized (c7g — ARM Graviton3, c8g — ARM Graviton4)
443    InstanceTypeSpec {
444        name: "c7g.medium",
445        platform: Platform::Aws,
446        family: InstanceFamily::ComputeOptimized,
447        architecture: Architecture::Arm64,
448        vcpu: 1,
449        memory_bytes: 2 * GI,
450        ephemeral_storage_bytes: 20 * GI,
451        gpu: None,
452    },
453    InstanceTypeSpec {
454        name: "c8g.medium",
455        platform: Platform::Aws,
456        family: InstanceFamily::ComputeOptimized,
457        architecture: Architecture::Arm64,
458        vcpu: 1,
459        memory_bytes: 2 * GI,
460        ephemeral_storage_bytes: 20 * GI,
461        gpu: None,
462    },
463    InstanceTypeSpec {
464        name: "c7g.large",
465        platform: Platform::Aws,
466        family: InstanceFamily::ComputeOptimized,
467        architecture: Architecture::Arm64,
468        vcpu: 2,
469        memory_bytes: 4 * GI,
470        ephemeral_storage_bytes: 20 * GI,
471        gpu: None,
472    },
473    InstanceTypeSpec {
474        name: "c8g.large",
475        platform: Platform::Aws,
476        family: InstanceFamily::ComputeOptimized,
477        architecture: Architecture::Arm64,
478        vcpu: 2,
479        memory_bytes: 4 * GI,
480        ephemeral_storage_bytes: 20 * GI,
481        gpu: None,
482    },
483    InstanceTypeSpec {
484        name: "c8i.large",
485        platform: Platform::Aws,
486        family: InstanceFamily::ComputeOptimized,
487        architecture: Architecture::X86_64,
488        vcpu: 2,
489        memory_bytes: 4 * GI,
490        ephemeral_storage_bytes: 20 * GI,
491        gpu: None,
492    },
493    InstanceTypeSpec {
494        name: "c7g.xlarge",
495        platform: Platform::Aws,
496        family: InstanceFamily::ComputeOptimized,
497        architecture: Architecture::Arm64,
498        vcpu: 4,
499        memory_bytes: 8 * GI,
500        ephemeral_storage_bytes: 20 * GI,
501        gpu: None,
502    },
503    InstanceTypeSpec {
504        name: "c8g.xlarge",
505        platform: Platform::Aws,
506        family: InstanceFamily::ComputeOptimized,
507        architecture: Architecture::Arm64,
508        vcpu: 4,
509        memory_bytes: 8 * GI,
510        ephemeral_storage_bytes: 20 * GI,
511        gpu: None,
512    },
513    InstanceTypeSpec {
514        name: "c8i.xlarge",
515        platform: Platform::Aws,
516        family: InstanceFamily::ComputeOptimized,
517        architecture: Architecture::X86_64,
518        vcpu: 4,
519        memory_bytes: 8 * GI,
520        ephemeral_storage_bytes: 20 * GI,
521        gpu: None,
522    },
523    InstanceTypeSpec {
524        name: "c7g.2xlarge",
525        platform: Platform::Aws,
526        family: InstanceFamily::ComputeOptimized,
527        architecture: Architecture::Arm64,
528        vcpu: 8,
529        memory_bytes: 16 * GI,
530        ephemeral_storage_bytes: 20 * GI,
531        gpu: None,
532    },
533    InstanceTypeSpec {
534        name: "c8g.2xlarge",
535        platform: Platform::Aws,
536        family: InstanceFamily::ComputeOptimized,
537        architecture: Architecture::Arm64,
538        vcpu: 8,
539        memory_bytes: 16 * GI,
540        ephemeral_storage_bytes: 20 * GI,
541        gpu: None,
542    },
543    InstanceTypeSpec {
544        name: "c8i.2xlarge",
545        platform: Platform::Aws,
546        family: InstanceFamily::ComputeOptimized,
547        architecture: Architecture::X86_64,
548        vcpu: 8,
549        memory_bytes: 16 * GI,
550        ephemeral_storage_bytes: 20 * GI,
551        gpu: None,
552    },
553    InstanceTypeSpec {
554        name: "c7g.4xlarge",
555        platform: Platform::Aws,
556        family: InstanceFamily::ComputeOptimized,
557        architecture: Architecture::Arm64,
558        vcpu: 16,
559        memory_bytes: 32 * GI,
560        ephemeral_storage_bytes: 20 * GI,
561        gpu: None,
562    },
563    InstanceTypeSpec {
564        name: "c8g.4xlarge",
565        platform: Platform::Aws,
566        family: InstanceFamily::ComputeOptimized,
567        architecture: Architecture::Arm64,
568        vcpu: 16,
569        memory_bytes: 32 * GI,
570        ephemeral_storage_bytes: 20 * GI,
571        gpu: None,
572    },
573    InstanceTypeSpec {
574        name: "c8i.4xlarge",
575        platform: Platform::Aws,
576        family: InstanceFamily::ComputeOptimized,
577        architecture: Architecture::X86_64,
578        vcpu: 16,
579        memory_bytes: 32 * GI,
580        ephemeral_storage_bytes: 20 * GI,
581        gpu: None,
582    },
583    // Memory optimized — r7g (ARM), r6a and r7i (x86_64).
584    InstanceTypeSpec {
585        name: "r7g.medium",
586        platform: Platform::Aws,
587        family: InstanceFamily::MemoryOptimized,
588        architecture: Architecture::Arm64,
589        vcpu: 1,
590        memory_bytes: 8 * GI,
591        ephemeral_storage_bytes: 20 * GI,
592        gpu: None,
593    },
594    InstanceTypeSpec {
595        name: "r7g.large",
596        platform: Platform::Aws,
597        family: InstanceFamily::MemoryOptimized,
598        architecture: Architecture::Arm64,
599        vcpu: 2,
600        memory_bytes: 16 * GI,
601        ephemeral_storage_bytes: 20 * GI,
602        gpu: None,
603    },
604    InstanceTypeSpec {
605        name: "r6a.large",
606        platform: Platform::Aws,
607        family: InstanceFamily::MemoryOptimized,
608        architecture: Architecture::X86_64,
609        vcpu: 2,
610        memory_bytes: 16 * GI,
611        ephemeral_storage_bytes: 20 * GI,
612        gpu: None,
613    },
614    InstanceTypeSpec {
615        name: "r7i.large",
616        platform: Platform::Aws,
617        family: InstanceFamily::MemoryOptimized,
618        architecture: Architecture::X86_64,
619        vcpu: 2,
620        memory_bytes: 16 * GI,
621        ephemeral_storage_bytes: 20 * GI,
622        gpu: None,
623    },
624    InstanceTypeSpec {
625        name: "r7g.xlarge",
626        platform: Platform::Aws,
627        family: InstanceFamily::MemoryOptimized,
628        architecture: Architecture::Arm64,
629        vcpu: 4,
630        memory_bytes: 32 * GI,
631        ephemeral_storage_bytes: 20 * GI,
632        gpu: None,
633    },
634    InstanceTypeSpec {
635        name: "r6a.xlarge",
636        platform: Platform::Aws,
637        family: InstanceFamily::MemoryOptimized,
638        architecture: Architecture::X86_64,
639        vcpu: 4,
640        memory_bytes: 32 * GI,
641        ephemeral_storage_bytes: 20 * GI,
642        gpu: None,
643    },
644    InstanceTypeSpec {
645        name: "r7i.xlarge",
646        platform: Platform::Aws,
647        family: InstanceFamily::MemoryOptimized,
648        architecture: Architecture::X86_64,
649        vcpu: 4,
650        memory_bytes: 32 * GI,
651        ephemeral_storage_bytes: 20 * GI,
652        gpu: None,
653    },
654    InstanceTypeSpec {
655        name: "r7g.2xlarge",
656        platform: Platform::Aws,
657        family: InstanceFamily::MemoryOptimized,
658        architecture: Architecture::Arm64,
659        vcpu: 8,
660        memory_bytes: 64 * GI,
661        ephemeral_storage_bytes: 20 * GI,
662        gpu: None,
663    },
664    InstanceTypeSpec {
665        name: "r6a.2xlarge",
666        platform: Platform::Aws,
667        family: InstanceFamily::MemoryOptimized,
668        architecture: Architecture::X86_64,
669        vcpu: 8,
670        memory_bytes: 64 * GI,
671        ephemeral_storage_bytes: 20 * GI,
672        gpu: None,
673    },
674    InstanceTypeSpec {
675        name: "r7i.2xlarge",
676        platform: Platform::Aws,
677        family: InstanceFamily::MemoryOptimized,
678        architecture: Architecture::X86_64,
679        vcpu: 8,
680        memory_bytes: 64 * GI,
681        ephemeral_storage_bytes: 20 * GI,
682        gpu: None,
683    },
684    InstanceTypeSpec {
685        name: "r7g.4xlarge",
686        platform: Platform::Aws,
687        family: InstanceFamily::MemoryOptimized,
688        architecture: Architecture::Arm64,
689        vcpu: 16,
690        memory_bytes: 128 * GI,
691        ephemeral_storage_bytes: 20 * GI,
692        gpu: None,
693    },
694    InstanceTypeSpec {
695        name: "r6a.4xlarge",
696        platform: Platform::Aws,
697        family: InstanceFamily::MemoryOptimized,
698        architecture: Architecture::X86_64,
699        vcpu: 16,
700        memory_bytes: 128 * GI,
701        ephemeral_storage_bytes: 20 * GI,
702        gpu: None,
703    },
704    InstanceTypeSpec {
705        name: "r7i.4xlarge",
706        platform: Platform::Aws,
707        family: InstanceFamily::MemoryOptimized,
708        architecture: Architecture::X86_64,
709        vcpu: 16,
710        memory_bytes: 128 * GI,
711        ephemeral_storage_bytes: 20 * GI,
712        gpu: None,
713    },
714    // Storage Optimized (i4i — x86_64, NVMe)
715    InstanceTypeSpec {
716        name: "i4i.xlarge",
717        platform: Platform::Aws,
718        family: InstanceFamily::StorageOptimized,
719        architecture: Architecture::X86_64,
720        vcpu: 4,
721        memory_bytes: 32 * GI,
722        ephemeral_storage_bytes: 937 * GI,
723        gpu: None,
724    },
725    InstanceTypeSpec {
726        name: "i4i.2xlarge",
727        platform: Platform::Aws,
728        family: InstanceFamily::StorageOptimized,
729        architecture: Architecture::X86_64,
730        vcpu: 8,
731        memory_bytes: 64 * GI,
732        ephemeral_storage_bytes: 1875 * GI,
733        gpu: None,
734    },
735    InstanceTypeSpec {
736        name: "i4i.4xlarge",
737        platform: Platform::Aws,
738        family: InstanceFamily::StorageOptimized,
739        architecture: Architecture::X86_64,
740        vcpu: 16,
741        memory_bytes: 128 * GI,
742        ephemeral_storage_bytes: 3750 * GI,
743        gpu: None,
744    },
745    InstanceTypeSpec {
746        name: "i4i.8xlarge",
747        platform: Platform::Aws,
748        family: InstanceFamily::StorageOptimized,
749        architecture: Architecture::X86_64,
750        vcpu: 32,
751        memory_bytes: 256 * GI,
752        ephemeral_storage_bytes: 7500 * GI,
753        gpu: None,
754    },
755    // GPU — NVIDIA T4 (g5 — x86_64)
756    InstanceTypeSpec {
757        name: "g5.xlarge",
758        platform: Platform::Aws,
759        family: InstanceFamily::GpuCompute,
760        architecture: Architecture::X86_64,
761        vcpu: 4,
762        memory_bytes: 16 * GI,
763        ephemeral_storage_bytes: 250 * GI,
764        gpu: Some(CatalogGpu {
765            gpu_type: "nvidia-t4",
766            count: 1,
767        }),
768    },
769    InstanceTypeSpec {
770        name: "g5.2xlarge",
771        platform: Platform::Aws,
772        family: InstanceFamily::GpuCompute,
773        architecture: Architecture::X86_64,
774        vcpu: 8,
775        memory_bytes: 32 * GI,
776        ephemeral_storage_bytes: 450 * GI,
777        gpu: Some(CatalogGpu {
778            gpu_type: "nvidia-t4",
779            count: 1,
780        }),
781    },
782    // GPU — NVIDIA A100 (p4d — x86_64)
783    InstanceTypeSpec {
784        name: "p4d.24xlarge",
785        platform: Platform::Aws,
786        family: InstanceFamily::GpuCompute,
787        architecture: Architecture::X86_64,
788        vcpu: 96,
789        memory_bytes: 1152 * GI,
790        ephemeral_storage_bytes: 8000 * GI,
791        gpu: Some(CatalogGpu {
792            gpu_type: "nvidia-a100",
793            count: 8,
794        }),
795    },
796    // GPU — NVIDIA H100 (p5 — x86_64)
797    InstanceTypeSpec {
798        name: "p5.48xlarge",
799        platform: Platform::Aws,
800        family: InstanceFamily::GpuCompute,
801        architecture: Architecture::X86_64,
802        vcpu: 192,
803        memory_bytes: 2048 * GI,
804        ephemeral_storage_bytes: 8000 * GI,
805        gpu: Some(CatalogGpu {
806            gpu_type: "nvidia-h100",
807            count: 8,
808        }),
809    },
810    // =========================================================================
811    // GCP
812    // =========================================================================
813
814    // Burstable (e2)
815    InstanceTypeSpec {
816        name: "e2-micro",
817        platform: Platform::Gcp,
818        family: InstanceFamily::Burstable,
819        architecture: Architecture::X86_64,
820        vcpu: 2,
821        memory_bytes: 1 * GI,
822        ephemeral_storage_bytes: 20 * GI,
823        gpu: None,
824    },
825    InstanceTypeSpec {
826        name: "e2-small",
827        platform: Platform::Gcp,
828        family: InstanceFamily::Burstable,
829        architecture: Architecture::X86_64,
830        vcpu: 2,
831        memory_bytes: 2 * GI,
832        ephemeral_storage_bytes: 20 * GI,
833        gpu: None,
834    },
835    InstanceTypeSpec {
836        name: "e2-medium",
837        platform: Platform::Gcp,
838        family: InstanceFamily::Burstable,
839        architecture: Architecture::X86_64,
840        vcpu: 2,
841        memory_bytes: 4 * GI,
842        ephemeral_storage_bytes: 20 * GI,
843        gpu: None,
844    },
845    // General Purpose (n2-standard, up to 16 vCPU)
846    InstanceTypeSpec {
847        name: "n2-standard-2",
848        platform: Platform::Gcp,
849        family: InstanceFamily::GeneralPurpose,
850        architecture: Architecture::X86_64,
851        vcpu: 2,
852        memory_bytes: 8 * GI,
853        ephemeral_storage_bytes: 20 * GI,
854        gpu: None,
855    },
856    InstanceTypeSpec {
857        name: "n2-standard-4",
858        platform: Platform::Gcp,
859        family: InstanceFamily::GeneralPurpose,
860        architecture: Architecture::X86_64,
861        vcpu: 4,
862        memory_bytes: 16 * GI,
863        ephemeral_storage_bytes: 20 * GI,
864        gpu: None,
865    },
866    InstanceTypeSpec {
867        name: "n2-standard-8",
868        platform: Platform::Gcp,
869        family: InstanceFamily::GeneralPurpose,
870        architecture: Architecture::X86_64,
871        vcpu: 8,
872        memory_bytes: 32 * GI,
873        ephemeral_storage_bytes: 20 * GI,
874        gpu: None,
875    },
876    InstanceTypeSpec {
877        name: "n2-standard-16",
878        platform: Platform::Gcp,
879        family: InstanceFamily::GeneralPurpose,
880        architecture: Architecture::X86_64,
881        vcpu: 16,
882        memory_bytes: 64 * GI,
883        ephemeral_storage_bytes: 20 * GI,
884        gpu: None,
885    },
886    // Compute Optimized (c3-standard, up to 8 vCPU)
887    InstanceTypeSpec {
888        name: "c3-standard-4",
889        platform: Platform::Gcp,
890        family: InstanceFamily::ComputeOptimized,
891        architecture: Architecture::X86_64,
892        vcpu: 4,
893        memory_bytes: 8 * GI,
894        ephemeral_storage_bytes: 20 * GI,
895        gpu: None,
896    },
897    InstanceTypeSpec {
898        name: "c3-standard-8",
899        platform: Platform::Gcp,
900        family: InstanceFamily::ComputeOptimized,
901        architecture: Architecture::X86_64,
902        vcpu: 8,
903        memory_bytes: 16 * GI,
904        ephemeral_storage_bytes: 20 * GI,
905        gpu: None,
906    },
907    // Memory Optimized (n2-highmem, up to 8 vCPU)
908    InstanceTypeSpec {
909        name: "n2-highmem-2",
910        platform: Platform::Gcp,
911        family: InstanceFamily::MemoryOptimized,
912        architecture: Architecture::X86_64,
913        vcpu: 2,
914        memory_bytes: 16 * GI,
915        ephemeral_storage_bytes: 20 * GI,
916        gpu: None,
917    },
918    InstanceTypeSpec {
919        name: "n2-highmem-4",
920        platform: Platform::Gcp,
921        family: InstanceFamily::MemoryOptimized,
922        architecture: Architecture::X86_64,
923        vcpu: 4,
924        memory_bytes: 32 * GI,
925        ephemeral_storage_bytes: 20 * GI,
926        gpu: None,
927    },
928    InstanceTypeSpec {
929        name: "n2-highmem-8",
930        platform: Platform::Gcp,
931        family: InstanceFamily::MemoryOptimized,
932        architecture: Architecture::X86_64,
933        vcpu: 8,
934        memory_bytes: 64 * GI,
935        ephemeral_storage_bytes: 20 * GI,
936        gpu: None,
937    },
938    InstanceTypeSpec {
939        name: "n2-highmem-16",
940        platform: Platform::Gcp,
941        family: InstanceFamily::MemoryOptimized,
942        architecture: Architecture::X86_64,
943        vcpu: 16,
944        memory_bytes: 128 * GI,
945        ephemeral_storage_bytes: 20 * GI,
946        gpu: None,
947    },
948    InstanceTypeSpec {
949        name: "n2-highmem-32",
950        platform: Platform::Gcp,
951        family: InstanceFamily::MemoryOptimized,
952        architecture: Architecture::X86_64,
953        vcpu: 32,
954        memory_bytes: 256 * GI,
955        ephemeral_storage_bytes: 20 * GI,
956        gpu: None,
957    },
958    // Storage Optimized (c3d-standard with local SSD)
959    InstanceTypeSpec {
960        name: "c3d-standard-8",
961        platform: Platform::Gcp,
962        family: InstanceFamily::StorageOptimized,
963        architecture: Architecture::X86_64,
964        vcpu: 8,
965        memory_bytes: 32 * GI,
966        ephemeral_storage_bytes: 480 * GI,
967        gpu: None,
968    },
969    InstanceTypeSpec {
970        name: "c3d-standard-16",
971        platform: Platform::Gcp,
972        family: InstanceFamily::StorageOptimized,
973        architecture: Architecture::X86_64,
974        vcpu: 16,
975        memory_bytes: 64 * GI,
976        ephemeral_storage_bytes: 960 * GI,
977        gpu: None,
978    },
979    InstanceTypeSpec {
980        name: "c3d-standard-30",
981        platform: Platform::Gcp,
982        family: InstanceFamily::StorageOptimized,
983        architecture: Architecture::X86_64,
984        vcpu: 30,
985        memory_bytes: 120 * GI,
986        ephemeral_storage_bytes: 1920 * GI,
987        gpu: None,
988    },
989    // GPU — NVIDIA T4 (n1-standard + T4)
990    InstanceTypeSpec {
991        name: "n1-standard-4-t4",
992        platform: Platform::Gcp,
993        family: InstanceFamily::GpuCompute,
994        architecture: Architecture::X86_64,
995        vcpu: 4,
996        memory_bytes: 15 * GI,
997        ephemeral_storage_bytes: 100 * GI,
998        gpu: Some(CatalogGpu {
999            gpu_type: "nvidia-t4",
1000            count: 1,
1001        }),
1002    },
1003    // GPU — NVIDIA A100 (a2-highgpu)
1004    InstanceTypeSpec {
1005        name: "a2-highgpu-1g",
1006        platform: Platform::Gcp,
1007        family: InstanceFamily::GpuCompute,
1008        architecture: Architecture::X86_64,
1009        vcpu: 12,
1010        memory_bytes: 85 * GI,
1011        ephemeral_storage_bytes: 100 * GI,
1012        gpu: Some(CatalogGpu {
1013            gpu_type: "nvidia-a100",
1014            count: 1,
1015        }),
1016    },
1017    InstanceTypeSpec {
1018        name: "a2-highgpu-8g",
1019        platform: Platform::Gcp,
1020        family: InstanceFamily::GpuCompute,
1021        architecture: Architecture::X86_64,
1022        vcpu: 96,
1023        memory_bytes: 1360 * GI,
1024        ephemeral_storage_bytes: 100 * GI,
1025        gpu: Some(CatalogGpu {
1026            gpu_type: "nvidia-a100",
1027            count: 8,
1028        }),
1029    },
1030    // GPU — NVIDIA H100 (a3-highgpu)
1031    InstanceTypeSpec {
1032        name: "a3-highgpu-8g",
1033        platform: Platform::Gcp,
1034        family: InstanceFamily::GpuCompute,
1035        architecture: Architecture::X86_64,
1036        vcpu: 208,
1037        memory_bytes: 1872 * GI,
1038        ephemeral_storage_bytes: 100 * GI,
1039        gpu: Some(CatalogGpu {
1040            gpu_type: "nvidia-h100",
1041            count: 8,
1042        }),
1043    },
1044    // =========================================================================
1045    // Azure
1046    // =========================================================================
1047
1048    // Burstable (B-series v2)
1049    InstanceTypeSpec {
1050        name: "Standard_B1s",
1051        platform: Platform::Azure,
1052        family: InstanceFamily::Burstable,
1053        architecture: Architecture::X86_64,
1054        vcpu: 1,
1055        memory_bytes: 1 * GI,
1056        ephemeral_storage_bytes: 20 * GI,
1057        gpu: None,
1058    },
1059    InstanceTypeSpec {
1060        name: "Standard_B2s",
1061        platform: Platform::Azure,
1062        family: InstanceFamily::Burstable,
1063        architecture: Architecture::X86_64,
1064        vcpu: 2,
1065        memory_bytes: 4 * GI,
1066        ephemeral_storage_bytes: 20 * GI,
1067        gpu: None,
1068    },
1069    InstanceTypeSpec {
1070        name: "Standard_B2ms",
1071        platform: Platform::Azure,
1072        family: InstanceFamily::Burstable,
1073        architecture: Architecture::X86_64,
1074        vcpu: 2,
1075        memory_bytes: 8 * GI,
1076        ephemeral_storage_bytes: 20 * GI,
1077        gpu: None,
1078    },
1079    InstanceTypeSpec {
1080        name: "Standard_B4ms",
1081        platform: Platform::Azure,
1082        family: InstanceFamily::Burstable,
1083        architecture: Architecture::X86_64,
1084        vcpu: 4,
1085        memory_bytes: 16 * GI,
1086        ephemeral_storage_bytes: 20 * GI,
1087        gpu: None,
1088    },
1089    // General Purpose (Dsv4/Dsv5-series, up to 16 vCPU)
1090    InstanceTypeSpec {
1091        name: "Standard_D2s_v4",
1092        platform: Platform::Azure,
1093        family: InstanceFamily::GeneralPurpose,
1094        architecture: Architecture::X86_64,
1095        vcpu: 2,
1096        memory_bytes: 8 * GI,
1097        ephemeral_storage_bytes: 20 * GI,
1098        gpu: None,
1099    },
1100    InstanceTypeSpec {
1101        name: "Standard_D2s_v5",
1102        platform: Platform::Azure,
1103        family: InstanceFamily::GeneralPurpose,
1104        architecture: Architecture::X86_64,
1105        vcpu: 2,
1106        memory_bytes: 8 * GI,
1107        ephemeral_storage_bytes: 20 * GI,
1108        gpu: None,
1109    },
1110    InstanceTypeSpec {
1111        name: "Standard_D4s_v5",
1112        platform: Platform::Azure,
1113        family: InstanceFamily::GeneralPurpose,
1114        architecture: Architecture::X86_64,
1115        vcpu: 4,
1116        memory_bytes: 16 * GI,
1117        ephemeral_storage_bytes: 20 * GI,
1118        gpu: None,
1119    },
1120    InstanceTypeSpec {
1121        name: "Standard_D8s_v5",
1122        platform: Platform::Azure,
1123        family: InstanceFamily::GeneralPurpose,
1124        architecture: Architecture::X86_64,
1125        vcpu: 8,
1126        memory_bytes: 32 * GI,
1127        ephemeral_storage_bytes: 20 * GI,
1128        gpu: None,
1129    },
1130    InstanceTypeSpec {
1131        name: "Standard_D16s_v5",
1132        platform: Platform::Azure,
1133        family: InstanceFamily::GeneralPurpose,
1134        architecture: Architecture::X86_64,
1135        vcpu: 16,
1136        memory_bytes: 64 * GI,
1137        ephemeral_storage_bytes: 20 * GI,
1138        gpu: None,
1139    },
1140    // Compute Optimized (Fv2-series, up to 16 vCPU)
1141    InstanceTypeSpec {
1142        name: "Standard_F2s_v2",
1143        platform: Platform::Azure,
1144        family: InstanceFamily::ComputeOptimized,
1145        architecture: Architecture::X86_64,
1146        vcpu: 2,
1147        memory_bytes: 4 * GI,
1148        ephemeral_storage_bytes: 20 * GI,
1149        gpu: None,
1150    },
1151    InstanceTypeSpec {
1152        name: "Standard_F4s_v2",
1153        platform: Platform::Azure,
1154        family: InstanceFamily::ComputeOptimized,
1155        architecture: Architecture::X86_64,
1156        vcpu: 4,
1157        memory_bytes: 8 * GI,
1158        ephemeral_storage_bytes: 20 * GI,
1159        gpu: None,
1160    },
1161    InstanceTypeSpec {
1162        name: "Standard_F8s_v2",
1163        platform: Platform::Azure,
1164        family: InstanceFamily::ComputeOptimized,
1165        architecture: Architecture::X86_64,
1166        vcpu: 8,
1167        memory_bytes: 16 * GI,
1168        ephemeral_storage_bytes: 20 * GI,
1169        gpu: None,
1170    },
1171    InstanceTypeSpec {
1172        name: "Standard_F16s_v2",
1173        platform: Platform::Azure,
1174        family: InstanceFamily::ComputeOptimized,
1175        architecture: Architecture::X86_64,
1176        vcpu: 16,
1177        memory_bytes: 32 * GI,
1178        ephemeral_storage_bytes: 20 * GI,
1179        gpu: None,
1180    },
1181    // Memory Optimized (Ev5-series, up to 16 vCPU)
1182    InstanceTypeSpec {
1183        name: "Standard_E2s_v5",
1184        platform: Platform::Azure,
1185        family: InstanceFamily::MemoryOptimized,
1186        architecture: Architecture::X86_64,
1187        vcpu: 2,
1188        memory_bytes: 16 * GI,
1189        ephemeral_storage_bytes: 20 * GI,
1190        gpu: None,
1191    },
1192    InstanceTypeSpec {
1193        name: "Standard_E4s_v5",
1194        platform: Platform::Azure,
1195        family: InstanceFamily::MemoryOptimized,
1196        architecture: Architecture::X86_64,
1197        vcpu: 4,
1198        memory_bytes: 32 * GI,
1199        ephemeral_storage_bytes: 20 * GI,
1200        gpu: None,
1201    },
1202    InstanceTypeSpec {
1203        name: "Standard_E8s_v5",
1204        platform: Platform::Azure,
1205        family: InstanceFamily::MemoryOptimized,
1206        architecture: Architecture::X86_64,
1207        vcpu: 8,
1208        memory_bytes: 64 * GI,
1209        ephemeral_storage_bytes: 20 * GI,
1210        gpu: None,
1211    },
1212    InstanceTypeSpec {
1213        name: "Standard_E16s_v5",
1214        platform: Platform::Azure,
1215        family: InstanceFamily::MemoryOptimized,
1216        architecture: Architecture::X86_64,
1217        vcpu: 16,
1218        memory_bytes: 128 * GI,
1219        ephemeral_storage_bytes: 20 * GI,
1220        gpu: None,
1221    },
1222    // Storage Optimized (Lsv3-series with NVMe)
1223    InstanceTypeSpec {
1224        name: "Standard_L8s_v3",
1225        platform: Platform::Azure,
1226        family: InstanceFamily::StorageOptimized,
1227        architecture: Architecture::X86_64,
1228        vcpu: 8,
1229        memory_bytes: 64 * GI,
1230        ephemeral_storage_bytes: 1788 * GI,
1231        gpu: None,
1232    },
1233    InstanceTypeSpec {
1234        name: "Standard_L16s_v3",
1235        platform: Platform::Azure,
1236        family: InstanceFamily::StorageOptimized,
1237        architecture: Architecture::X86_64,
1238        vcpu: 16,
1239        memory_bytes: 128 * GI,
1240        ephemeral_storage_bytes: 3576 * GI,
1241        gpu: None,
1242    },
1243    InstanceTypeSpec {
1244        name: "Standard_L32s_v3",
1245        platform: Platform::Azure,
1246        family: InstanceFamily::StorageOptimized,
1247        architecture: Architecture::X86_64,
1248        vcpu: 32,
1249        memory_bytes: 256 * GI,
1250        ephemeral_storage_bytes: 7154 * GI,
1251        gpu: None,
1252    },
1253    // GPU — NVIDIA T4 (NCasT4_v3-series)
1254    InstanceTypeSpec {
1255        name: "Standard_NC4as_T4_v3",
1256        platform: Platform::Azure,
1257        family: InstanceFamily::GpuCompute,
1258        architecture: Architecture::X86_64,
1259        vcpu: 4,
1260        memory_bytes: 28 * GI,
1261        ephemeral_storage_bytes: 176 * GI,
1262        gpu: Some(CatalogGpu {
1263            gpu_type: "nvidia-t4",
1264            count: 1,
1265        }),
1266    },
1267    // GPU — NVIDIA A100 (NC A100 v4-series)
1268    InstanceTypeSpec {
1269        name: "Standard_NC24ads_A100_v4",
1270        platform: Platform::Azure,
1271        family: InstanceFamily::GpuCompute,
1272        architecture: Architecture::X86_64,
1273        vcpu: 24,
1274        memory_bytes: 220 * GI,
1275        ephemeral_storage_bytes: 958 * GI,
1276        gpu: Some(CatalogGpu {
1277            gpu_type: "nvidia-a100",
1278            count: 1,
1279        }),
1280    },
1281    InstanceTypeSpec {
1282        name: "Standard_NC96ads_A100_v4",
1283        platform: Platform::Azure,
1284        family: InstanceFamily::GpuCompute,
1285        architecture: Architecture::X86_64,
1286        vcpu: 96,
1287        memory_bytes: 880 * GI,
1288        ephemeral_storage_bytes: 3916 * GI,
1289        gpu: Some(CatalogGpu {
1290            gpu_type: "nvidia-a100",
1291            count: 4,
1292        }),
1293    },
1294    // GPU — NVIDIA H100 (ND H100 v5-series)
1295    InstanceTypeSpec {
1296        name: "Standard_ND96isr_H100_v5",
1297        platform: Platform::Azure,
1298        family: InstanceFamily::GpuCompute,
1299        architecture: Architecture::X86_64,
1300        vcpu: 96,
1301        memory_bytes: 1900 * GI,
1302        ephemeral_storage_bytes: 1000 * GI,
1303        gpu: Some(CatalogGpu {
1304            gpu_type: "nvidia-h100",
1305            count: 8,
1306        }),
1307    },
1308];
1309
1310// ---------------------------------------------------------------------------
1311// Catalog lookup
1312// ---------------------------------------------------------------------------
1313
1314/// Get all instance types for a given platform.
1315pub fn catalog_for_platform(platform: Platform) -> Vec<&'static InstanceTypeSpec> {
1316    CATALOG
1317        .iter()
1318        .filter(|spec| spec.platform == platform)
1319        .collect()
1320}
1321
1322/// Find a specific instance type by name and platform.
1323pub fn find_instance_type(platform: Platform, name: &str) -> Option<&'static InstanceTypeSpec> {
1324    CATALOG
1325        .iter()
1326        .find(|spec| spec.platform == platform && spec.name == name)
1327}
1328
1329/// Whether a capacity group may move from AWS machine `old` to `new` without setup: both are
1330/// catalog machines of one CPU architecture, so the stack's images still run on the new one.
1331pub fn is_same_architecture_aws_machine(old: &str, new: &str) -> bool {
1332    is_same_architecture_machine(Platform::Aws, old, new)
1333}
1334
1335/// Whether two catalog machines belong to the given cloud and share a CPU architecture.
1336pub fn is_same_architecture_machine(platform: Platform, old: &str, new: &str) -> bool {
1337    matches!(platform, Platform::Aws | Platform::Gcp | Platform::Azure)
1338        && find_instance_type(platform, old)
1339            .zip(find_instance_type(platform, new))
1340            .is_some_and(|(old, new)| old.architecture == new.architecture)
1341}
1342
1343// ---------------------------------------------------------------------------
1344// Instance type selection
1345// ---------------------------------------------------------------------------
1346
1347/// Aggregated resource requirements from all containers in a capacity group.
1348#[derive(Debug, Clone)]
1349pub struct WorkloadRequirements {
1350    /// Total CPU needed at desired scale (sum of desired CPU * desired_replicas per container)
1351    pub total_cpu_at_desired: f64,
1352    /// Total memory needed at desired scale (sum of desired memory * desired_replicas per container)
1353    pub total_memory_bytes_at_desired: u64,
1354    /// Total CPU needed at maximum scale (sum of desired CPU * max_replicas per container)
1355    pub total_cpu_at_max: f64,
1356    /// Total memory needed at maximum scale (sum of desired memory * max_replicas per container)
1357    pub total_memory_bytes_at_max: u64,
1358    /// Largest CPU request among all individual containers (single replica)
1359    pub max_cpu_per_container: f64,
1360    /// Largest memory request among all individual containers (single replica)
1361    pub max_memory_per_container: u64,
1362    /// Maximum ephemeral storage any single container requires
1363    pub max_ephemeral_storage_bytes: u64,
1364    /// GPU requirement (if any container needs GPU)
1365    pub gpu: Option<GpuSpec>,
1366    /// Required CPU architecture, when source explicitly constrains it.
1367    pub architecture: Option<Architecture>,
1368    /// If true, only instance types that expose nested virtualization (VT-x/EPT)
1369    /// to guest VMs are eligible. Required by workloads that run QEMU/KVM
1370    /// inside a container.
1371    pub nested_virt: bool,
1372}
1373
1374/// Result of instance type selection.
1375#[derive(Debug, Clone)]
1376pub struct InstanceSelection {
1377    /// Selected instance type name (e.g., "m7g.2xlarge")
1378    pub instance_type: &'static str,
1379    /// Machine profile derived from the instance type
1380    pub profile: MachineProfile,
1381    /// Recommended minimum number of machines
1382    pub min_machines: u32,
1383    /// Recommended maximum number of machines
1384    pub max_machines: u32,
1385}
1386
1387/// Ephemeral storage threshold above which storage-optimized instances are selected.
1388const STORAGE_OPTIMIZED_THRESHOLD: u64 = 200 * GI;
1389
1390/// Maximum number of machines per cluster.
1391const MAX_MACHINES_PER_CLUSTER: u32 = 10;
1392
1393/// Preferred vCPU cap for non-GPU/non-storage workloads. A larger indivisible
1394/// allocation or bounded fleet can require a bigger machine.
1395const MAX_STANDARD_VCPU: u32 = 8;
1396
1397/// Runtime CPU reserved for system processes on each managed container machine.
1398const SYSTEM_RESERVE_CPU: f64 = 0.5;
1399
1400/// Runtime planning headroom for total desired/max workload.
1401const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1402
1403/// Select the best instance type for a workload on a given platform.
1404///
1405/// The algorithm:
1406/// 1. GPU workloads: Match by GPU type, find smallest instance with enough GPUs.
1407/// 2. Storage-heavy workloads (>200Gi ephemeral): Use storage-optimized instances.
1408/// 3. All other workloads: Fit the allocation with system reserve and headroom,
1409///    preferring at most 8 vCPUs unless the allocation requires more. Use the
1410///    workload's preferred family, with general-purpose fallback when needed.
1411///
1412/// Returns an error if no suitable instance type is found.
1413pub fn select_instance_type(
1414    platform: Platform,
1415    requirements: &WorkloadRequirements,
1416) -> Result<InstanceSelection, String> {
1417    // An unset architecture accepts either one. Nested virtualization and GPU
1418    // models can exist on only one architecture of a platform (AWS nested
1419    // virtualization is x86_64 only), so try the platform default first and
1420    // then the other architecture.
1421    if requirements.architecture.is_none()
1422        && (requirements.nested_virt || requirements.gpu.is_some())
1423    {
1424        if let Some(default) = default_architecture(platform) {
1425            let other = match default {
1426                Architecture::Arm64 => Architecture::X86_64,
1427                Architecture::X86_64 => Architecture::Arm64,
1428            };
1429            let with_architecture = |architecture| WorkloadRequirements {
1430                architecture: Some(architecture),
1431                ..requirements.clone()
1432            };
1433            let selection = select_instance_type_for_architecture(
1434                platform,
1435                &with_architecture(default),
1436            );
1437            if selection.is_ok() {
1438                return selection;
1439            }
1440            // Report both reasons: the default architecture often lacks the
1441            // feature entirely, while the other one says what to change.
1442            return select_instance_type_for_architecture(platform, &with_architecture(other))
1443                .map_err(|other_error| {
1444                    let default_error = selection.err().unwrap_or_default();
1445                    format!("{default:?}: {default_error}; {other:?}: {other_error}")
1446                });
1447        }
1448    }
1449    select_instance_type_for_architecture(platform, requirements)
1450}
1451
1452fn select_instance_type_for_architecture(
1453    platform: Platform,
1454    requirements: &WorkloadRequirements,
1455) -> Result<InstanceSelection, String> {
1456    let family = select_family(requirements);
1457    let family = if requirements.nested_virt && family == InstanceFamily::Burstable {
1458        InstanceFamily::GeneralPurpose
1459    } else {
1460        family
1461    };
1462    let selection = select_instance_type_in_family(platform, requirements, family);
1463    if selection.is_err() && family == InstanceFamily::Burstable {
1464        return select_instance_type_in_family(
1465            platform,
1466            requirements,
1467            InstanceFamily::GeneralPurpose,
1468        );
1469    }
1470    selection
1471}
1472
1473fn select_instance_type_in_family(
1474    platform: Platform,
1475    requirements: &WorkloadRequirements,
1476    family: InstanceFamily,
1477) -> Result<InstanceSelection, String> {
1478    let architecture = requirements
1479        .architecture
1480        .or_else(|| default_architecture(platform))
1481        .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1482
1483    let candidates: Vec<&InstanceTypeSpec> = CATALOG
1484        .iter()
1485        .filter(|spec| spec.platform == platform && spec.family == family)
1486        .filter(|spec| {
1487            if requirements.nested_virt {
1488                spec.is_nested_virt_capable()
1489            } else {
1490                platform != Platform::Aws || !spec.is_nested_virt_capable()
1491            }
1492        })
1493        .filter(|spec| spec.architecture == architecture)
1494        .collect();
1495
1496    // A storage-heavy workload may have no fixed-local candidate for the
1497    // requested architecture/nested-virtualization contract. That is exactly
1498    // when we must consider a provider-backed disk on a general-purpose VM;
1499    // rejecting here made the fallback below unreachable.
1500    if candidates.is_empty() && family != InstanceFamily::StorageOptimized {
1501        let family_has_other_architecture = CATALOG.iter().any(|spec| {
1502            spec.platform == platform
1503                && spec.family == family
1504                && if requirements.nested_virt {
1505                    spec.is_nested_virt_capable()
1506                } else {
1507                    platform != Platform::Aws || !spec.is_nested_virt_capable()
1508                }
1509        });
1510        if family_has_other_architecture {
1511            return Err(format!(
1512                "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1513            ));
1514        }
1515        return Err(if requirements.nested_virt {
1516            format!(
1517                "no nested-virt-capable {family:?} instance types in catalog for platform {platform}"
1518            )
1519        } else {
1520            format!("no {family:?} instance types in catalog for platform {platform}")
1521        });
1522    }
1523
1524    // For GPU workloads, filter by GPU type
1525    let candidates = if let Some(ref gpu) = requirements.gpu {
1526        let filtered: Vec<&InstanceTypeSpec> = candidates
1527            .into_iter()
1528            .filter(|spec| {
1529                spec.gpu.as_ref().map_or(false, |g| {
1530                    g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1531                })
1532            })
1533            .collect();
1534        if filtered.is_empty() {
1535            return Err(format!(
1536                "no instance type for GPU type '{}' x{} on platform {platform}",
1537                gpu.gpu_type, gpu.count
1538            ));
1539        }
1540        filtered
1541    } else {
1542        candidates
1543    };
1544
1545    // For storage workloads, filter by ephemeral storage capacity
1546    let candidates = if family == InstanceFamily::StorageOptimized {
1547        let filtered: Vec<&InstanceTypeSpec> = candidates
1548            .into_iter()
1549            .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1550            .collect();
1551        if filtered.is_empty() {
1552            // Fixed-local NVMe is preferred for large requests, but provider
1553            // disks on ordinary cloud machines remain configurable. Fall back
1554            // when no fixed-local catalog entry can satisfy the request.
1555            CATALOG
1556                .iter()
1557                .filter(|spec| {
1558                    spec.platform == platform
1559                        && spec.family == InstanceFamily::GeneralPurpose
1560                        && spec.has_configurable_ephemeral_storage()
1561                        && max_configurable_ephemeral_storage_bytes(platform)
1562                            .is_some_and(|max| requirements.max_ephemeral_storage_bytes <= max)
1563                })
1564                .filter(|spec| {
1565                    if requirements.nested_virt {
1566                        spec.is_nested_virt_capable()
1567                    } else {
1568                        platform != Platform::Aws || !spec.is_nested_virt_capable()
1569                    }
1570                })
1571                .filter(|spec| spec.architecture == architecture)
1572                .collect()
1573        } else {
1574            filtered
1575        }
1576    } else {
1577        candidates
1578    };
1579
1580    if candidates.is_empty() {
1581        return Err(format!(
1582            "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1583        ));
1584    }
1585
1586    // More machines cannot split one replica. Every candidate must fit its
1587    // largest allocation after host reserve and workload headroom.
1588    let candidates: Vec<_> = candidates
1589        .into_iter()
1590        .filter(|spec| {
1591            allocatable_cpu(spec) >= requirements.max_cpu_per_container * WORKLOAD_HEADROOM_FACTOR
1592                && allocatable_memory_bytes(spec) as f64
1593                    >= requirements.max_memory_per_container as f64 * WORKLOAD_HEADROOM_FACTOR
1594        })
1595        .collect();
1596    let smallest_replica_machine = candidates.iter().map(|spec| spec.vcpu).min()
1597        .ok_or_else(|| format!("no {platform} machine can fit one replica with system reserve and workload headroom"))?;
1598
1599    // Apply the policy for the candidates we will actually select from. A
1600    // storage-heavy request can fall back from fixed-local storage machines to
1601    // general-purpose machines with provider-backed disks; those machines must
1602    // retain the normal horizontal-scaling cap.
1603    let effective_family = candidates[0].family;
1604    let vcpu_cap = if effective_family == InstanceFamily::GpuCompute
1605        || effective_family == InstanceFamily::StorageOptimized
1606    {
1607        u32::MAX
1608    } else {
1609        MAX_STANDARD_VCPU.max(smallest_replica_machine)
1610    };
1611
1612    let desired_target_machines = desired_target_machines(requirements);
1613    let target_cpu = requirements
1614        .max_cpu_per_container
1615        .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1616        * WORKLOAD_HEADROOM_FACTOR;
1617    let target_memory = (requirements.max_memory_per_container as f64)
1618        .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1619        * WORKLOAD_HEADROOM_FACTOR;
1620
1621    // Find the smallest instance whose allocatable capacity meets the workload
1622    // target after host reserve and workload headroom. Machine count already
1623    // accounts for multiple replicas; requiring space for an arbitrary second
1624    // copy here would size the same demand twice.
1625    let selected = candidates
1626        .iter()
1627        .filter(|spec| {
1628            spec.vcpu <= vcpu_cap
1629                && allocatable_cpu(spec) >= target_cpu
1630                && allocatable_memory_bytes(spec) as f64 >= target_memory
1631        })
1632        .min_by_key(|spec| spec.vcpu)
1633        .or_else(|| {
1634            // If nothing fits within the cap, pick the largest instance under the cap
1635            candidates
1636                .iter()
1637                .filter(|spec| spec.vcpu <= vcpu_cap)
1638                .max_by_key(|spec| spec.vcpu)
1639        })
1640        .or_else(|| {
1641            // Last resort: pick the smallest available instance (for GPU/storage)
1642            candidates.iter().min_by_key(|spec| spec.vcpu)
1643        })
1644        .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1645
1646    // Calculate machine counts
1647    let max_machines = compute_max_machines(requirements, selected);
1648    let min_machines = compute_min_machines(requirements, selected, max_machines);
1649
1650    Ok(InstanceSelection {
1651        instance_type: selected.name,
1652        profile: selected.to_machine_profile_for_storage(requirements.max_ephemeral_storage_bytes),
1653        min_machines,
1654        max_machines,
1655    })
1656}
1657
1658/// Select instance family based on workload characteristics.
1659///
1660/// Uses GeneralPurpose for all standard workloads — widely available across
1661/// regions and cost-effective. Only specialized workloads (GPU, large ephemeral
1662/// storage) get specialized families. Very small workloads get burstable.
1663pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1664    // GPU workloads always get GPU instances
1665    if requirements.gpu.is_some() {
1666        return InstanceFamily::GpuCompute;
1667    }
1668
1669    // Large ephemeral storage needs NVMe (storage-optimized)
1670    if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1671        return InstanceFamily::StorageOptimized;
1672    }
1673
1674    // Very small workloads use burstable instances
1675    if requirements.total_cpu_at_max < 2.0 {
1676        return InstanceFamily::Burstable;
1677    }
1678
1679    // All other workloads use GeneralPurpose — available everywhere, good pricing
1680    InstanceFamily::GeneralPurpose
1681}
1682
1683/// Calculate maximum machines needed to fit the workload with headroom.
1684fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1685    let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1686    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1687
1688    let mem_with_headroom =
1689        requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1690    let mem_machines =
1691        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1692
1693    // Take the larger of CPU-based and memory-based, clamped to cluster limit
1694    cpu_machines
1695        .max(mem_machines)
1696        .max(1)
1697        .min(MAX_MACHINES_PER_CLUSTER)
1698}
1699
1700/// Calculate minimum machines for HA.
1701fn compute_min_machines(
1702    requirements: &WorkloadRequirements,
1703    instance: &InstanceTypeSpec,
1704    max_machines: u32,
1705) -> u32 {
1706    let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1707    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1708
1709    let mem_with_headroom =
1710        requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1711    let mem_machines =
1712        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1713
1714    cpu_machines
1715        .max(mem_machines)
1716        .max(1)
1717        .min(2)
1718        .min(max_machines)
1719}
1720
1721fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1722    if requirements.total_cpu_at_desired >= 2.0
1723        || requirements.total_memory_bytes_at_desired >= 4 * GI
1724    {
1725        2
1726    } else {
1727        1
1728    }
1729}
1730
1731pub(crate) fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1732    (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1733}
1734
1735pub(crate) fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1736    instance
1737        .memory_bytes
1738        .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1739        .max(256 * MI)
1740}
1741
1742fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1743    if memory_bytes < 4 * GI {
1744        256 * MI
1745    } else if memory_bytes < 16 * GI {
1746        512 * MI
1747    } else {
1748        GI
1749    }
1750}
1751
1752// ---------------------------------------------------------------------------
1753// Tests
1754// ---------------------------------------------------------------------------
1755
1756#[cfg(test)]
1757mod tests {
1758    use super::*;
1759    use crate::BinaryTarget;
1760
1761    // -- Parsing tests --
1762
1763    #[test]
1764    fn test_parse_cpu_plain() {
1765        assert_eq!(parse_cpu("1").unwrap(), 1.0);
1766        assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1767        assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1768        assert_eq!(parse_cpu("16").unwrap(), 16.0);
1769    }
1770
1771    #[test]
1772    fn test_parse_cpu_millicore() {
1773        assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1774        assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1775        assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1776        assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1777    }
1778
1779    #[test]
1780    fn test_parse_cpu_invalid() {
1781        assert!(parse_cpu("").is_err());
1782        assert!(parse_cpu("abc").is_err());
1783        assert!(parse_cpu("m").is_err());
1784    }
1785
1786    #[test]
1787    fn test_parse_memory_binary_suffixes() {
1788        assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1789        assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1790        assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1791        assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1792        assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1793        assert_eq!(
1794            parse_memory_bytes("1Ti").unwrap(),
1795            1024u64 * 1024 * 1024 * 1024
1796        );
1797    }
1798
1799    #[test]
1800    fn test_parse_memory_decimal_suffixes() {
1801        assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1802        assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1803        assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1804        assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1805    }
1806
1807    #[test]
1808    fn test_parse_memory_plain_bytes() {
1809        assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1810        assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1811    }
1812
1813    #[test]
1814    fn test_parse_memory_invalid() {
1815        assert!(parse_memory_bytes("").is_err());
1816        assert!(parse_memory_bytes("abc").is_err());
1817        assert!(parse_memory_bytes("Gi").is_err());
1818    }
1819
1820    #[test]
1821    fn test_parse_memory_fractional() {
1822        assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1823        assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1824    }
1825
1826    // -- Catalog lookup tests --
1827
1828    #[test]
1829    fn test_catalog_has_entries_for_all_cloud_platforms() {
1830        assert!(!catalog_for_platform(Platform::Aws).is_empty());
1831        assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1832        assert!(!catalog_for_platform(Platform::Azure).is_empty());
1833    }
1834
1835    #[test]
1836    fn test_catalog_no_entries_for_non_cloud_platforms() {
1837        assert!(catalog_for_platform(Platform::Local).is_empty());
1838        assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1839    }
1840
1841    #[test]
1842    fn test_find_known_instance_type() {
1843        let spec =
1844            find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1845        assert_eq!(spec.vcpu, 8);
1846        assert_eq!(spec.memory_bytes, 32 * GI);
1847        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1848    }
1849
1850    #[test]
1851    fn test_find_aws_c8i_nested_virt_instance_type() {
1852        let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1853        assert_eq!(spec.vcpu, 2);
1854        assert_eq!(spec.memory_bytes, 4 * GI);
1855        assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1856        assert_eq!(spec.architecture, Architecture::X86_64);
1857        assert!(spec.is_nested_virt_capable());
1858    }
1859
1860    #[test]
1861    fn test_find_unknown_instance_type() {
1862        assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1863    }
1864
1865    #[test]
1866    fn test_find_wrong_platform() {
1867        assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1868    }
1869
1870    #[test]
1871    fn test_to_machine_profile() {
1872        let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1873        let profile = spec.to_machine_profile();
1874        assert_eq!(profile.cpu, "8.0");
1875        assert_eq!(profile.memory_bytes, 32 * GI);
1876        assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1877        assert!(profile.gpu.is_none());
1878    }
1879
1880    #[test]
1881    fn test_to_machine_profile_with_gpu() {
1882        let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1883        let profile = spec.to_machine_profile();
1884        let gpu = profile.gpu.as_ref().expect("should have GPU");
1885        assert_eq!(gpu.gpu_type, "nvidia-a100");
1886        assert_eq!(gpu.count, 8);
1887    }
1888
1889    // -- Selection algorithm tests --
1890
1891    #[test]
1892    fn small_cpu_with_large_memory_falls_back_from_burstable() {
1893        let requirements = WorkloadRequirements {
1894            total_cpu_at_desired: 0.5,
1895            total_memory_bytes_at_desired: 8 * GI,
1896            total_cpu_at_max: 0.5,
1897            total_memory_bytes_at_max: 8 * GI,
1898            max_cpu_per_container: 0.5,
1899            max_memory_per_container: 8 * GI,
1900            max_ephemeral_storage_bytes: 0,
1901            gpu: None,
1902            architecture: Some(Architecture::X86_64),
1903            nested_virt: false,
1904        };
1905        let selected = select_instance_type(Platform::Gcp, &requirements).unwrap();
1906        let machine = find_instance_type(Platform::Gcp, selected.instance_type).unwrap();
1907        assert!(
1908            allocatable_memory_bytes(machine) as f64 >= 8.0 * GI as f64 * WORKLOAD_HEADROOM_FACTOR
1909        );
1910    }
1911
1912    #[test]
1913    fn test_select_burstable_for_small_workload() {
1914        let req = WorkloadRequirements {
1915            total_cpu_at_desired: 1.0,
1916            total_memory_bytes_at_desired: 2 * GI,
1917            total_cpu_at_max: 1.0,
1918            total_memory_bytes_at_max: 2 * GI,
1919            max_cpu_per_container: 0.5,
1920            max_memory_per_container: 1 * GI,
1921            max_ephemeral_storage_bytes: 10 * GI,
1922            gpu: None,
1923            architecture: None,
1924            nested_virt: false,
1925        };
1926        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1927        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1928        assert_eq!(spec.family, InstanceFamily::Burstable);
1929    }
1930
1931    #[test]
1932    fn test_selects_smallest_burstable_machine_with_real_headroom() {
1933        let req = WorkloadRequirements {
1934            total_cpu_at_desired: 1.0,
1935            total_memory_bytes_at_desired: 2 * GI,
1936            total_cpu_at_max: 1.0,
1937            total_memory_bytes_at_max: 2 * GI,
1938            max_cpu_per_container: 1.0,
1939            max_memory_per_container: 2 * GI,
1940            max_ephemeral_storage_bytes: 10 * GI,
1941            gpu: None,
1942            architecture: None,
1943            nested_virt: false,
1944        };
1945
1946        let selection = select_instance_type(Platform::Aws, &req).unwrap();
1947
1948        assert_eq!(selection.instance_type, "t4g.medium");
1949        assert_eq!(selection.min_machines, 1);
1950        assert_eq!(selection.max_machines, 1);
1951    }
1952
1953    #[test]
1954    fn test_select_general_purpose_for_standard_workload() {
1955        // Standard workloads always get GeneralPurpose regardless of CPU:memory ratio
1956        let req = WorkloadRequirements {
1957            total_cpu_at_desired: 20.0,
1958            total_memory_bytes_at_desired: 80 * GI,
1959            total_cpu_at_max: 20.0,
1960            total_memory_bytes_at_max: 80 * GI,
1961            max_cpu_per_container: 2.0,
1962            max_memory_per_container: 8 * GI,
1963            max_ephemeral_storage_bytes: 10 * GI,
1964            gpu: None,
1965            architecture: None,
1966            nested_virt: false,
1967        };
1968        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1969        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1970        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1971    }
1972
1973    #[test]
1974    fn test_select_general_purpose_even_for_cpu_heavy() {
1975        // CPU-heavy workloads still get GeneralPurpose (no more ComputeOptimized auto-select)
1976        let req = WorkloadRequirements {
1977            total_cpu_at_desired: 20.0,
1978            total_memory_bytes_at_desired: 20 * GI,
1979            total_cpu_at_max: 20.0,
1980            total_memory_bytes_at_max: 20 * GI,
1981            max_cpu_per_container: 2.0,
1982            max_memory_per_container: 2 * GI,
1983            max_ephemeral_storage_bytes: 10 * GI,
1984            gpu: None,
1985            architecture: None,
1986            nested_virt: false,
1987        };
1988        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1989        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1990        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1991    }
1992
1993    #[test]
1994    fn test_select_storage_optimized_for_large_ephemeral() {
1995        let req = WorkloadRequirements {
1996            total_cpu_at_desired: 8.0,
1997            total_memory_bytes_at_desired: 32 * GI,
1998            total_cpu_at_max: 8.0,
1999            total_memory_bytes_at_max: 32 * GI,
2000            max_cpu_per_container: 2.0,
2001            max_memory_per_container: 8 * GI,
2002            max_ephemeral_storage_bytes: 500 * GI,
2003            gpu: None,
2004            architecture: Some(Architecture::X86_64),
2005            nested_virt: false,
2006        };
2007        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2008        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2009        assert_eq!(spec.family, InstanceFamily::StorageOptimized);
2010    }
2011
2012    #[test]
2013    fn test_select_configurable_storage_above_fixed_local_catalog() {
2014        let req = WorkloadRequirements {
2015            total_cpu_at_desired: 8.0,
2016            total_memory_bytes_at_desired: 32 * GI,
2017            total_cpu_at_max: 8.0,
2018            total_memory_bytes_at_max: 32 * GI,
2019            max_cpu_per_container: 2.0,
2020            max_memory_per_container: 8 * GI,
2021            max_ephemeral_storage_bytes: 8_000 * GI,
2022            gpu: None,
2023            architecture: Some(Architecture::X86_64),
2024            nested_virt: false,
2025        };
2026        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2027        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2028        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2029        assert_eq!(sel.profile.ephemeral_storage_bytes, 8_000 * GI);
2030    }
2031
2032    #[test]
2033    fn test_configurable_storage_fallback_retains_standard_vcpu_cap() {
2034        let req = WorkloadRequirements {
2035            total_cpu_at_desired: 70.0,
2036            total_memory_bytes_at_desired: 140 * GI,
2037            total_cpu_at_max: 70.0,
2038            total_memory_bytes_at_max: 140 * GI,
2039            max_cpu_per_container: 2.0,
2040            max_memory_per_container: 4 * GI,
2041            max_ephemeral_storage_bytes: 8_000 * GI,
2042            gpu: None,
2043            architecture: Some(Architecture::X86_64),
2044            nested_virt: false,
2045        };
2046
2047        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2048        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2049        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2050        assert!(spec.vcpu <= MAX_STANDARD_VCPU);
2051        assert!(sel.max_machines > 1);
2052    }
2053
2054    #[test]
2055    fn test_nested_virtualization_storage_falls_back_to_configurable_disk() {
2056        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2057            let req = WorkloadRequirements {
2058                total_cpu_at_desired: 2.0,
2059                total_memory_bytes_at_desired: 8 * GI,
2060                total_cpu_at_max: 2.0,
2061                total_memory_bytes_at_max: 8 * GI,
2062                max_cpu_per_container: 2.0,
2063                max_memory_per_container: 8 * GI,
2064                max_ephemeral_storage_bytes: 500 * GI,
2065                gpu: None,
2066                architecture: Some(Architecture::X86_64),
2067                nested_virt: true,
2068            };
2069
2070            let sel = select_instance_type(platform, &req)
2071                .unwrap_or_else(|error| panic!("{platform} should support fallback: {error}"));
2072            let spec = find_instance_type(platform, sel.instance_type).unwrap();
2073            assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2074            assert!(spec.is_nested_virt_capable());
2075            assert_eq!(sel.profile.ephemeral_storage_bytes, 500 * GI);
2076        }
2077    }
2078
2079    #[test]
2080    fn unset_architecture_resolves_to_the_one_offering_nested_virtualization() {
2081        let req = WorkloadRequirements {
2082            total_cpu_at_desired: 2.0,
2083            total_memory_bytes_at_desired: 4 * GI,
2084            total_cpu_at_max: 2.0,
2085            total_memory_bytes_at_max: 4 * GI,
2086            max_cpu_per_container: 2.0,
2087            max_memory_per_container: 4 * GI,
2088            max_ephemeral_storage_bytes: 20 * GI,
2089            gpu: None,
2090            architecture: None,
2091            nested_virt: true,
2092        };
2093
2094        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2095            let selection = select_instance_type(platform, &req)
2096                .unwrap_or_else(|message| panic!("{platform}: {message}"));
2097            let spec = find_instance_type(platform, selection.instance_type)
2098                .expect("selected machine should exist in the catalog");
2099            assert!(spec.is_nested_virt_capable(), "{platform}: {}", spec.name);
2100            assert_eq!(spec.architecture, Architecture::X86_64, "{platform}");
2101            assert_eq!(selection.profile.architecture, Some(Architecture::X86_64));
2102        }
2103
2104        // When neither architecture fits, the error says why for each, so a
2105        // sizing problem is not reported as a missing architecture.
2106        let oversized = WorkloadRequirements {
2107            max_cpu_per_container: 1000.0,
2108            total_cpu_at_desired: 1000.0,
2109            total_cpu_at_max: 1000.0,
2110            ..req.clone()
2111        };
2112        let message = select_instance_type(Platform::Aws, &oversized)
2113            .expect_err("no AWS machine fits 1000 vCPU per replica");
2114        assert!(message.starts_with("Arm64: architecture Arm64 is unavailable"), "{message}");
2115        assert!(
2116            message.contains("; X86_64: no aws machine can fit one replica"),
2117            "{message}"
2118        );
2119
2120        // An explicit architecture is a constraint, not a preference.
2121        let arm = WorkloadRequirements {
2122            architecture: Some(Architecture::Arm64),
2123            ..req
2124        };
2125        assert!(select_instance_type(Platform::Aws, &arm).is_err());
2126    }
2127
2128    #[test]
2129    fn test_configurable_disk_rejects_capacity_above_provider_limit() {
2130        for (platform, requested) in [
2131            (Platform::Aws, 100 * TI),
2132            (Platform::Gcp, 100 * TI),
2133            (Platform::Azure, 8_000 * GI),
2134        ] {
2135            let req = WorkloadRequirements {
2136                total_cpu_at_desired: 2.0,
2137                total_memory_bytes_at_desired: 8 * GI,
2138                total_cpu_at_max: 2.0,
2139                total_memory_bytes_at_max: 8 * GI,
2140                max_cpu_per_container: 2.0,
2141                max_memory_per_container: 8 * GI,
2142                max_ephemeral_storage_bytes: requested,
2143                gpu: None,
2144                architecture: Some(Architecture::X86_64),
2145                nested_virt: true,
2146            };
2147
2148            assert!(select_instance_type(platform, &req).is_err());
2149        }
2150    }
2151
2152    #[test]
2153    fn test_configurable_disk_limits_account_for_controller_rounding() {
2154        for (platform, disk_limit_gib) in [
2155            (Platform::Aws, 64 * 1024),
2156            (Platform::Gcp, 64 * 1024),
2157            (Platform::Azure, 4_095),
2158        ] {
2159            let max = max_configurable_ephemeral_storage_bytes(platform).unwrap();
2160            let materialized_disk_gib = |requested_bytes: u64| {
2161                let requested_gib = requested_bytes.div_ceil(GI);
2162                (requested_gib * 5).div_ceil(4) + 12
2163            };
2164
2165            assert!(materialized_disk_gib(max) <= disk_limit_gib);
2166            assert!(materialized_disk_gib(max + 1) > disk_limit_gib);
2167        }
2168    }
2169
2170    #[test]
2171    fn test_gpu_local_storage_is_not_treated_as_resizable() {
2172        let spec = find_instance_type(Platform::Aws, "g5.xlarge").unwrap();
2173        assert_eq!(spec.family, InstanceFamily::GpuCompute);
2174        assert!(!spec.has_configurable_ephemeral_storage());
2175    }
2176
2177    #[test]
2178    fn test_select_gpu_instance() {
2179        let req = WorkloadRequirements {
2180            total_cpu_at_desired: 8.0,
2181            total_memory_bytes_at_desired: 32 * GI,
2182            total_cpu_at_max: 8.0,
2183            total_memory_bytes_at_max: 32 * GI,
2184            max_cpu_per_container: 4.0,
2185            max_memory_per_container: 16 * GI,
2186            max_ephemeral_storage_bytes: 10 * GI,
2187            gpu: Some(GpuSpec {
2188                gpu_type: "nvidia-a100".to_string(),
2189                count: 1,
2190            }),
2191            architecture: Some(Architecture::X86_64),
2192            nested_virt: false,
2193        };
2194        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2195        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2196        assert_eq!(spec.family, InstanceFamily::GpuCompute);
2197        assert!(spec.gpu.is_some());
2198    }
2199
2200    #[test]
2201    fn test_select_uses_each_cloud_image_target_architecture() {
2202        let req = WorkloadRequirements {
2203            total_cpu_at_desired: 4.0,
2204            total_memory_bytes_at_desired: 16 * GI,
2205            total_cpu_at_max: 4.0,
2206            total_memory_bytes_at_max: 16 * GI,
2207            max_cpu_per_container: 1.0,
2208            max_memory_per_container: 4 * GI,
2209            max_ephemeral_storage_bytes: 10 * GI,
2210            gpu: None,
2211            architecture: None,
2212            nested_virt: false,
2213        };
2214        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2215            let sel = select_instance_type(platform, &req)
2216                .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
2217            let spec = find_instance_type(platform, sel.instance_type)
2218                .expect("selected machine should exist in the catalog");
2219            assert_eq!(
2220                Some(spec.architecture),
2221                default_architecture(platform),
2222                "machine architecture must match the image target for {platform}"
2223            );
2224        }
2225    }
2226
2227    #[test]
2228    fn test_machine_count_reasonable() {
2229        // Single container: 1 CPU, 2Gi, maxReplicas=20
2230        let req = WorkloadRequirements {
2231            total_cpu_at_desired: 20.0,
2232            total_memory_bytes_at_desired: 40 * GI,
2233            total_cpu_at_max: 20.0,
2234            total_memory_bytes_at_max: 40 * GI,
2235            max_cpu_per_container: 1.0,
2236            max_memory_per_container: 2 * GI,
2237            max_ephemeral_storage_bytes: 10 * GI,
2238            gpu: None,
2239            architecture: None,
2240            nested_virt: false,
2241        };
2242        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2243        assert!(sel.min_machines >= 1);
2244        assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
2245        assert!(sel.max_machines >= sel.min_machines);
2246    }
2247
2248    #[test]
2249    fn test_instance_size_capped_at_8_vcpu() {
2250        // Many small replicas prefer more machines over larger instances.
2251        let req = WorkloadRequirements {
2252            total_cpu_at_desired: 70.0,
2253            total_memory_bytes_at_desired: 140 * GI,
2254            total_cpu_at_max: 70.0,
2255            total_memory_bytes_at_max: 140 * GI,
2256            max_cpu_per_container: 2.0,
2257            max_memory_per_container: 4 * GI,
2258            max_ephemeral_storage_bytes: 10 * GI,
2259            gpu: None,
2260            architecture: None,
2261            nested_virt: false,
2262        };
2263        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2264        let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
2265        assert!(
2266            spec.vcpu <= MAX_STANDARD_VCPU,
2267            "selected {} with {} vCPUs, expected <= {}",
2268            spec.name,
2269            spec.vcpu,
2270            MAX_STANDARD_VCPU
2271        );
2272        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
2273        // Should scale horizontally instead
2274        assert!(sel.max_machines > 1);
2275    }
2276
2277    #[test]
2278    fn test_larger_autoscaled_workload_gets_reasonable_instance() {
2279        // Simulates a larger autoscaled workload: 4 containers, each 2 CPU / 4 GiB
2280        // maxReplicas: 10, 10, 10, 5
2281        let req = WorkloadRequirements {
2282            total_cpu_at_desired: 70.0,
2283            total_memory_bytes_at_desired: 140 * GI,
2284            total_cpu_at_max: 70.0,              // 2*10 + 2*10 + 2*10 + 2*5
2285            total_memory_bytes_at_max: 140 * GI, // 4*10 + 4*10 + 4*10 + 4*5
2286            max_cpu_per_container: 2.0,
2287            max_memory_per_container: 4 * GI,
2288            max_ephemeral_storage_bytes: 20 * GI,
2289            gpu: None,
2290            architecture: None,
2291            nested_virt: false,
2292        };
2293        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
2294        // Should pick n2-standard-8 (8 vCPU, 32 GiB) — NOT c3-standard-44
2295        assert_eq!(sel.instance_type, "n2-standard-8");
2296        assert!(sel.max_machines >= 2);
2297    }
2298
2299    /// When `nested_virt` is set on the workload, the selector must
2300    /// restrict to nested-virt-capable families. On AWS that means an m8i
2301    /// (or other 8th-gen Intel) entry, never a Graviton (`*7g`, `t4g`) or
2302    /// burstable. Without this filter the launch template gets created
2303    /// with `CpuOptions.NestedVirtualization=enabled` paired with an
2304    /// instance type AWS rejects at RunInstances.
2305    #[test]
2306    fn test_select_aws_picks_m8i_when_nested_virt_required() {
2307        let req = WorkloadRequirements {
2308            total_cpu_at_desired: 4.0,
2309            total_memory_bytes_at_desired: 8 * GI,
2310            total_cpu_at_max: 4.0,
2311            total_memory_bytes_at_max: 8 * GI,
2312            max_cpu_per_container: 4.0,
2313            max_memory_per_container: 8 * GI,
2314            max_ephemeral_storage_bytes: 10 * GI,
2315            gpu: None,
2316            architecture: Some(Architecture::X86_64),
2317            nested_virt: true,
2318        };
2319        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2320        assert!(
2321            sel.instance_type.starts_with("m8i.")
2322                || sel.instance_type.starts_with("c8i.")
2323                || sel.instance_type.starts_with("r8i."),
2324            "expected an m8i/c8i/r8i instance, got {}",
2325            sel.instance_type
2326        );
2327        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2328        assert!(spec.is_nested_virt_capable());
2329    }
2330
2331    #[test]
2332    fn test_select_gcp_picks_n2_when_nested_virt_required() {
2333        let req = WorkloadRequirements {
2334            total_cpu_at_desired: 4.0,
2335            total_memory_bytes_at_desired: 8 * GI,
2336            total_cpu_at_max: 4.0,
2337            total_memory_bytes_at_max: 8 * GI,
2338            max_cpu_per_container: 4.0,
2339            max_memory_per_container: 8 * GI,
2340            max_ephemeral_storage_bytes: 0,
2341            architecture: Some(Architecture::X86_64),
2342            gpu: None,
2343            nested_virt: true,
2344        };
2345
2346        let selection = select_instance_type(Platform::Gcp, &req).unwrap();
2347        assert_eq!(selection.instance_type, "n2-standard-8");
2348        assert!(find_instance_type(Platform::Gcp, selection.instance_type)
2349            .unwrap()
2350            .is_nested_virt_capable());
2351    }
2352
2353    #[test]
2354    fn test_select_azure_picks_dsv5_when_nested_virt_required() {
2355        let req = WorkloadRequirements {
2356            total_cpu_at_desired: 4.0,
2357            total_memory_bytes_at_desired: 8 * GI,
2358            total_cpu_at_max: 4.0,
2359            total_memory_bytes_at_max: 8 * GI,
2360            max_cpu_per_container: 4.0,
2361            max_memory_per_container: 8 * GI,
2362            max_ephemeral_storage_bytes: 0,
2363            architecture: Some(Architecture::X86_64),
2364            gpu: None,
2365            nested_virt: true,
2366        };
2367
2368        let selection = select_instance_type(Platform::Azure, &req).unwrap();
2369        assert_eq!(selection.instance_type, "Standard_D8s_v5");
2370        assert!(find_instance_type(Platform::Azure, selection.instance_type)
2371            .unwrap()
2372            .is_nested_virt_capable());
2373    }
2374
2375    #[test]
2376    fn test_select_aws_defaults_to_image_target_architecture() {
2377        let req = WorkloadRequirements {
2378            total_cpu_at_desired: 4.0,
2379            total_memory_bytes_at_desired: 8 * GI,
2380            total_cpu_at_max: 4.0,
2381            total_memory_bytes_at_max: 8 * GI,
2382            max_cpu_per_container: 4.0,
2383            max_memory_per_container: 8 * GI,
2384            max_ephemeral_storage_bytes: 10 * GI,
2385            gpu: None,
2386            architecture: None,
2387            nested_virt: false,
2388        };
2389        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2390        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2391        assert_eq!(spec.architecture, Architecture::Arm64);
2392    }
2393
2394    #[test]
2395    fn test_cloud_defaults_match_image_target_architectures() {
2396        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2397            let target = BinaryTarget::defaults_for_platform(platform)
2398                .into_iter()
2399                .next()
2400                .expect("managed cloud should have a default image target");
2401            let image_architecture = match target.oci_arch() {
2402                "arm64" => Architecture::Arm64,
2403                "amd64" => Architecture::X86_64,
2404                architecture => {
2405                    panic!("unsupported managed-cloud image architecture {architecture}")
2406                }
2407            };
2408
2409            assert_eq!(default_architecture(platform), Some(image_architecture));
2410        }
2411    }
2412
2413    /// ARM remains available when the workload or capacity profile declares it.
2414    #[test]
2415    fn test_select_aws_uses_graviton_for_explicit_arm64() {
2416        let req = WorkloadRequirements {
2417            total_cpu_at_desired: 4.0,
2418            total_memory_bytes_at_desired: 8 * GI,
2419            total_cpu_at_max: 4.0,
2420            total_memory_bytes_at_max: 8 * GI,
2421            max_cpu_per_container: 4.0,
2422            max_memory_per_container: 8 * GI,
2423            max_ephemeral_storage_bytes: 10 * GI,
2424            gpu: None,
2425            architecture: Some(Architecture::Arm64),
2426            nested_virt: false,
2427        };
2428        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2429        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
2430        assert_eq!(spec.architecture, Architecture::Arm64);
2431    }
2432
2433    #[test]
2434    fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
2435        let req = WorkloadRequirements {
2436            total_cpu_at_desired: 1.0,
2437            total_memory_bytes_at_desired: 2 * GI,
2438            total_cpu_at_max: 1.0,
2439            total_memory_bytes_at_max: 2 * GI,
2440            max_cpu_per_container: 1.0,
2441            max_memory_per_container: 2 * GI,
2442            max_ephemeral_storage_bytes: 10 * GI,
2443            gpu: None,
2444            architecture: Some(Architecture::Arm64),
2445            nested_virt: false,
2446        };
2447
2448        let error = select_instance_type(Platform::Gcp, &req)
2449            .expect_err("GCP catalog has no ARM64 machine");
2450
2451        assert!(error.contains("architecture Arm64 is unavailable"));
2452    }
2453
2454    #[test]
2455    fn test_profile_has_required_fields() {
2456        let req = WorkloadRequirements {
2457            total_cpu_at_desired: 4.0,
2458            total_memory_bytes_at_desired: 16 * GI,
2459            total_cpu_at_max: 4.0,
2460            total_memory_bytes_at_max: 16 * GI,
2461            max_cpu_per_container: 1.0,
2462            max_memory_per_container: 4 * GI,
2463            max_ephemeral_storage_bytes: 10 * GI,
2464            gpu: None,
2465            architecture: None,
2466            nested_virt: false,
2467        };
2468        let sel = select_instance_type(Platform::Aws, &req).unwrap();
2469        assert!(!sel.profile.cpu.is_empty());
2470        assert!(sel.profile.memory_bytes > 0);
2471        assert!(sel.profile.ephemeral_storage_bytes > 0);
2472    }
2473
2474    #[test]
2475    fn test_error_for_unsupported_gpu_type() {
2476        let req = WorkloadRequirements {
2477            total_cpu_at_desired: 8.0,
2478            total_memory_bytes_at_desired: 32 * GI,
2479            total_cpu_at_max: 8.0,
2480            total_memory_bytes_at_max: 32 * GI,
2481            max_cpu_per_container: 4.0,
2482            max_memory_per_container: 16 * GI,
2483            max_ephemeral_storage_bytes: 10 * GI,
2484            gpu: Some(GpuSpec {
2485                gpu_type: "amd-mi300".to_string(),
2486                count: 1,
2487            }),
2488            architecture: None,
2489            nested_virt: false,
2490        };
2491        let result = select_instance_type(Platform::Aws, &req);
2492        assert!(result.is_err());
2493    }
2494
2495    #[test]
2496    fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
2497        // Verify that within each (platform, family) group, vcpu is non-decreasing.
2498        // This ensures our "min_by_key(vcpu)" logic works correctly.
2499        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
2500            let entries = catalog_for_platform(platform);
2501            let mut by_family: std::collections::HashMap<_, Vec<_>> =
2502                std::collections::HashMap::new();
2503            for entry in entries {
2504                by_family
2505                    .entry(format!("{:?}", entry.family))
2506                    .or_default()
2507                    .push(entry);
2508            }
2509            for (family, instances) in &by_family {
2510                for window in instances.windows(2) {
2511                    assert!(
2512                        window[0].vcpu <= window[1].vcpu,
2513                        "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2514                        window[0].name,
2515                        window[0].vcpu,
2516                        window[1].name,
2517                        window[1].vcpu
2518                    );
2519                }
2520            }
2521        }
2522    }
2523}