Skip to main content

alien_core/
instance_catalog.rs

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