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