Skip to main content

alien_core/
instance_catalog.rs

1//! Instance type catalog and selection algorithm for cloud compute infrastructure.
2//!
3//! This module provides:
4//! - A static catalog of known instance types across AWS, GCP, and Azure
5//! - Resource quantity parsing (CPU strings, Kubernetes-style memory/storage quantities)
6//! - An algorithm to select the optimal instance type for a given workload
7//!
8//! The catalog is the single source of truth for instance type specifications.
9//! It is used by the preflights system to automatically populate `CapacityGroup.instance_type`
10//! and `CapacityGroup.profile` based on the containers in a stack.
11
12use crate::{GpuSpec, MachineProfile, Platform};
13use serde::{Deserialize, Serialize};
14
15// ---------------------------------------------------------------------------
16// Resource quantity parsing
17// ---------------------------------------------------------------------------
18
19/// Parse a CPU quantity string to f64.
20///
21/// Accepts plain numbers ("1", "0.5", "2.0") and millicore suffixes ("500m" = 0.5).
22pub fn parse_cpu(s: &str) -> Result<f64, String> {
23    let s = s.trim();
24    if s.is_empty() {
25        return Err("empty CPU string".to_string());
26    }
27
28    if let Some(millis) = s.strip_suffix('m') {
29        let v: f64 = millis
30            .parse()
31            .map_err(|_| format!("invalid CPU millicore value: '{s}'"))?;
32        Ok(v / 1000.0)
33    } else {
34        s.parse().map_err(|_| format!("invalid CPU value: '{s}'"))
35    }
36}
37
38/// Parse a memory or storage quantity string to bytes.
39///
40/// Supports Kubernetes-style binary suffixes (Ki, Mi, Gi, Ti) and
41/// decimal suffixes (k, M, G, T). Plain numbers are interpreted as bytes.
42pub fn parse_memory_bytes(s: &str) -> Result<u64, String> {
43    let s = s.trim();
44    if s.is_empty() {
45        return Err("empty memory/storage string".to_string());
46    }
47
48    // Binary suffixes (powers of 1024)
49    if let Some(num) = s.strip_suffix("Ti") {
50        let v: f64 = num
51            .parse()
52            .map_err(|_| format!("invalid memory value: '{s}'"))?;
53        return Ok((v * 1024.0 * 1024.0 * 1024.0 * 1024.0) as u64);
54    }
55    if let Some(num) = s.strip_suffix("Gi") {
56        let v: f64 = num
57            .parse()
58            .map_err(|_| format!("invalid memory value: '{s}'"))?;
59        return Ok((v * 1024.0 * 1024.0 * 1024.0) as u64);
60    }
61    if let Some(num) = s.strip_suffix("Mi") {
62        let v: f64 = num
63            .parse()
64            .map_err(|_| format!("invalid memory value: '{s}'"))?;
65        return Ok((v * 1024.0 * 1024.0) as u64);
66    }
67    if let Some(num) = s.strip_suffix("Ki") {
68        let v: f64 = num
69            .parse()
70            .map_err(|_| format!("invalid memory value: '{s}'"))?;
71        return Ok((v * 1024.0) as u64);
72    }
73
74    // Decimal suffixes (powers of 1000)
75    if let Some(num) = s.strip_suffix('T') {
76        let v: f64 = num
77            .parse()
78            .map_err(|_| format!("invalid memory value: '{s}'"))?;
79        return Ok((v * 1_000_000_000_000.0) as u64);
80    }
81    if let Some(num) = s.strip_suffix('G') {
82        let v: f64 = num
83            .parse()
84            .map_err(|_| format!("invalid memory value: '{s}'"))?;
85        return Ok((v * 1_000_000_000.0) as u64);
86    }
87    if let Some(num) = s.strip_suffix('M') {
88        let v: f64 = num
89            .parse()
90            .map_err(|_| format!("invalid memory value: '{s}'"))?;
91        return Ok((v * 1_000_000.0) as u64);
92    }
93    if let Some(num) = s.strip_suffix('k') {
94        let v: f64 = num
95            .parse()
96            .map_err(|_| format!("invalid memory value: '{s}'"))?;
97        return Ok((v * 1000.0) as u64);
98    }
99
100    // Plain bytes
101    s.parse()
102        .map_err(|_| format!("invalid memory value: '{s}'"))
103}
104
105// ---------------------------------------------------------------------------
106// Instance type catalog
107// ---------------------------------------------------------------------------
108
109/// Instance family classification.
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum InstanceFamily {
112    Burstable,
113    GeneralPurpose,
114    ComputeOptimized,
115    MemoryOptimized,
116    StorageOptimized,
117    GpuCompute,
118}
119
120/// CPU architecture.
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
122#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
123#[serde(rename_all = "snake_case")]
124pub enum Architecture {
125    Arm64,
126    X86_64,
127}
128
129/// Default machine architecture for images built for a managed cloud.
130pub fn default_architecture(platform: Platform) -> Option<Architecture> {
131    match platform {
132        Platform::Aws => Some(Architecture::Arm64),
133        Platform::Gcp | Platform::Azure => Some(Architecture::X86_64),
134        Platform::Kubernetes | Platform::Machines | Platform::Local | Platform::Test => None,
135    }
136}
137
138/// Static GPU specification for catalog entries (no heap allocation).
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct CatalogGpu {
141    pub gpu_type: &'static str,
142    pub count: u32,
143}
144
145/// A known instance type with its hardware specifications.
146///
147/// All fields are compile-time constants. The catalog is a flat array of these.
148#[derive(Debug, Clone)]
149pub struct InstanceTypeSpec {
150    pub name: &'static str,
151    pub platform: Platform,
152    pub family: InstanceFamily,
153    pub architecture: Architecture,
154    /// vCPU count (hardware total)
155    pub vcpu: u32,
156    /// Memory in bytes (hardware total)
157    pub memory_bytes: u64,
158    /// Ephemeral storage in bytes (hardware total, NVMe for storage-optimized)
159    pub ephemeral_storage_bytes: u64,
160    /// GPU specification (for GPU instances)
161    pub gpu: Option<CatalogGpu>,
162}
163
164impl InstanceTypeSpec {
165    /// Whether this instance type supports
166    /// `CpuOptions.NestedVirtualization=enabled` on AWS launch.
167    ///
168    /// Per AWS docs (`aws ec2 create-launch-template help`), nested
169    /// virtualization is only supported on 8th-generation Intel instance
170    /// types: c8i, m8i, r8i, and their `-flex` variants. We classify by
171    /// family-name prefix rather than a per-row bool so the existing 70+
172    /// catalog rows don't need an extra field.
173    pub fn is_nested_virt_capable(&self) -> bool {
174        if self.platform != Platform::Aws {
175            // GCP/Azure equivalents would need their own family lists.
176            // Today nested virt is wired through only for AWS.
177            return false;
178        }
179        let name = self.name;
180        name.starts_with("m8i.")
181            || name.starts_with("c8i.")
182            || name.starts_with("r8i.")
183            || name.starts_with("m8i-flex.")
184            || name.starts_with("c8i-flex.")
185            || name.starts_with("r8i-flex.")
186    }
187
188    /// Convert this catalog entry into a `MachineProfile` for use in `CapacityGroup`.
189    pub fn to_machine_profile(&self) -> MachineProfile {
190        MachineProfile {
191            cpu: format!("{}.0", self.vcpu),
192            memory_bytes: self.memory_bytes,
193            ephemeral_storage_bytes: self.ephemeral_storage_bytes,
194            architecture: Some(self.architecture),
195            gpu: self.gpu.map(|g| GpuSpec {
196                gpu_type: g.gpu_type.to_string(),
197                count: g.count,
198            }),
199        }
200    }
201}
202
203// Helpers for readable byte constants
204const KI: u64 = 1024;
205const MI: u64 = KI * 1024;
206const GI: u64 = MI * 1024;
207
208/// The complete instance type catalog.
209///
210/// This is the single source of truth for instance type specifications.
211/// Update this array when adding support for new instance types.
212///
213/// NOTE: Ephemeral storage values for non-NVMe instances are conservative defaults
214/// (EBS-backed root volumes). Storage-optimized instances list their NVMe capacity.
215static CATALOG: &[InstanceTypeSpec] = &[
216    // =========================================================================
217    // AWS — ARM (Graviton) preferred for cost efficiency
218    // =========================================================================
219
220    // Burstable (t4g — ARM Graviton2)
221    InstanceTypeSpec {
222        name: "t4g.micro",
223        platform: Platform::Aws,
224        family: InstanceFamily::Burstable,
225        architecture: Architecture::Arm64,
226        vcpu: 2,
227        memory_bytes: 1 * GI,
228        ephemeral_storage_bytes: 20 * GI,
229        gpu: None,
230    },
231    InstanceTypeSpec {
232        name: "t4g.small",
233        platform: Platform::Aws,
234        family: InstanceFamily::Burstable,
235        architecture: Architecture::Arm64,
236        vcpu: 2,
237        memory_bytes: 2 * GI,
238        ephemeral_storage_bytes: 20 * GI,
239        gpu: None,
240    },
241    InstanceTypeSpec {
242        name: "t4g.medium",
243        platform: Platform::Aws,
244        family: InstanceFamily::Burstable,
245        architecture: Architecture::Arm64,
246        vcpu: 2,
247        memory_bytes: 4 * GI,
248        ephemeral_storage_bytes: 20 * GI,
249        gpu: None,
250    },
251    InstanceTypeSpec {
252        name: "t4g.large",
253        platform: Platform::Aws,
254        family: InstanceFamily::Burstable,
255        architecture: Architecture::Arm64,
256        vcpu: 2,
257        memory_bytes: 8 * GI,
258        ephemeral_storage_bytes: 20 * GI,
259        gpu: None,
260    },
261    InstanceTypeSpec {
262        name: "t3.xlarge",
263        platform: Platform::Aws,
264        family: InstanceFamily::Burstable,
265        architecture: Architecture::X86_64,
266        vcpu: 4,
267        memory_bytes: 16 * GI,
268        ephemeral_storage_bytes: 20 * GI,
269        gpu: None,
270    },
271    InstanceTypeSpec {
272        name: "t4g.xlarge",
273        platform: Platform::Aws,
274        family: InstanceFamily::Burstable,
275        architecture: Architecture::Arm64,
276        vcpu: 4,
277        memory_bytes: 16 * GI,
278        ephemeral_storage_bytes: 20 * GI,
279        gpu: None,
280    },
281    // General Purpose (m7g — ARM Graviton3, up to 2xlarge / 8 vCPU)
282    InstanceTypeSpec {
283        name: "m7g.medium",
284        platform: Platform::Aws,
285        family: InstanceFamily::GeneralPurpose,
286        architecture: Architecture::Arm64,
287        vcpu: 1,
288        memory_bytes: 4 * GI,
289        ephemeral_storage_bytes: 20 * GI,
290        gpu: None,
291    },
292    InstanceTypeSpec {
293        name: "m7i.large",
294        platform: Platform::Aws,
295        family: InstanceFamily::GeneralPurpose,
296        architecture: Architecture::X86_64,
297        vcpu: 2,
298        memory_bytes: 8 * GI,
299        ephemeral_storage_bytes: 20 * GI,
300        gpu: None,
301    },
302    InstanceTypeSpec {
303        name: "m7g.large",
304        platform: Platform::Aws,
305        family: InstanceFamily::GeneralPurpose,
306        architecture: Architecture::Arm64,
307        vcpu: 2,
308        memory_bytes: 8 * GI,
309        ephemeral_storage_bytes: 20 * GI,
310        gpu: None,
311    },
312    // 8th-gen Intel AWS families accept
313    // `CpuOptions.NestedVirtualization=enabled`. The catalog filter in
314    // `select_instance_type` includes these entries only when the
315    // workload requests nested virt, so ordinary workloads continue to
316    // pick the cost-efficient Graviton (m7g) above. The pairwise
317    // interleave keeps the per-family vCPU-non-decreasing invariant
318    // (see `test_catalog_instance_types_sorted_by_vcpu_within_family`).
319    InstanceTypeSpec {
320        name: "m8i.large",
321        platform: Platform::Aws,
322        family: InstanceFamily::GeneralPurpose,
323        architecture: Architecture::X86_64,
324        vcpu: 2,
325        memory_bytes: 8 * GI,
326        ephemeral_storage_bytes: 20 * GI,
327        gpu: None,
328    },
329    InstanceTypeSpec {
330        name: "m7i.xlarge",
331        platform: Platform::Aws,
332        family: InstanceFamily::GeneralPurpose,
333        architecture: Architecture::X86_64,
334        vcpu: 4,
335        memory_bytes: 16 * GI,
336        ephemeral_storage_bytes: 20 * GI,
337        gpu: None,
338    },
339    InstanceTypeSpec {
340        name: "m7g.xlarge",
341        platform: Platform::Aws,
342        family: InstanceFamily::GeneralPurpose,
343        architecture: Architecture::Arm64,
344        vcpu: 4,
345        memory_bytes: 16 * GI,
346        ephemeral_storage_bytes: 20 * GI,
347        gpu: None,
348    },
349    InstanceTypeSpec {
350        name: "m8i.xlarge",
351        platform: Platform::Aws,
352        family: InstanceFamily::GeneralPurpose,
353        architecture: Architecture::X86_64,
354        vcpu: 4,
355        memory_bytes: 16 * GI,
356        ephemeral_storage_bytes: 20 * GI,
357        gpu: None,
358    },
359    InstanceTypeSpec {
360        name: "m7i.2xlarge",
361        platform: Platform::Aws,
362        family: InstanceFamily::GeneralPurpose,
363        architecture: Architecture::X86_64,
364        vcpu: 8,
365        memory_bytes: 32 * GI,
366        ephemeral_storage_bytes: 20 * GI,
367        gpu: None,
368    },
369    InstanceTypeSpec {
370        name: "m7g.2xlarge",
371        platform: Platform::Aws,
372        family: InstanceFamily::GeneralPurpose,
373        architecture: Architecture::Arm64,
374        vcpu: 8,
375        memory_bytes: 32 * GI,
376        ephemeral_storage_bytes: 20 * GI,
377        gpu: None,
378    },
379    InstanceTypeSpec {
380        name: "m8i.2xlarge",
381        platform: Platform::Aws,
382        family: InstanceFamily::GeneralPurpose,
383        architecture: Architecture::X86_64,
384        vcpu: 8,
385        memory_bytes: 32 * GI,
386        ephemeral_storage_bytes: 20 * GI,
387        gpu: None,
388    },
389    InstanceTypeSpec {
390        name: "m7i.4xlarge",
391        platform: Platform::Aws,
392        family: InstanceFamily::GeneralPurpose,
393        architecture: Architecture::X86_64,
394        vcpu: 16,
395        memory_bytes: 64 * GI,
396        ephemeral_storage_bytes: 20 * GI,
397        gpu: None,
398    },
399    InstanceTypeSpec {
400        name: "m7g.4xlarge",
401        platform: Platform::Aws,
402        family: InstanceFamily::GeneralPurpose,
403        architecture: Architecture::Arm64,
404        vcpu: 16,
405        memory_bytes: 64 * GI,
406        ephemeral_storage_bytes: 20 * GI,
407        gpu: None,
408    },
409    InstanceTypeSpec {
410        name: "m8i.4xlarge",
411        platform: Platform::Aws,
412        family: InstanceFamily::GeneralPurpose,
413        architecture: Architecture::X86_64,
414        vcpu: 16,
415        memory_bytes: 64 * GI,
416        ephemeral_storage_bytes: 20 * GI,
417        gpu: None,
418    },
419    // Compute Optimized (c7g — ARM Graviton3, up to 2xlarge / 8 vCPU)
420    InstanceTypeSpec {
421        name: "c7g.medium",
422        platform: Platform::Aws,
423        family: InstanceFamily::ComputeOptimized,
424        architecture: Architecture::Arm64,
425        vcpu: 1,
426        memory_bytes: 2 * GI,
427        ephemeral_storage_bytes: 20 * GI,
428        gpu: None,
429    },
430    InstanceTypeSpec {
431        name: "c7g.large",
432        platform: Platform::Aws,
433        family: InstanceFamily::ComputeOptimized,
434        architecture: Architecture::Arm64,
435        vcpu: 2,
436        memory_bytes: 4 * GI,
437        ephemeral_storage_bytes: 20 * GI,
438        gpu: None,
439    },
440    InstanceTypeSpec {
441        name: "c8i.large",
442        platform: Platform::Aws,
443        family: InstanceFamily::ComputeOptimized,
444        architecture: Architecture::X86_64,
445        vcpu: 2,
446        memory_bytes: 4 * GI,
447        ephemeral_storage_bytes: 20 * GI,
448        gpu: None,
449    },
450    InstanceTypeSpec {
451        name: "c7g.xlarge",
452        platform: Platform::Aws,
453        family: InstanceFamily::ComputeOptimized,
454        architecture: Architecture::Arm64,
455        vcpu: 4,
456        memory_bytes: 8 * GI,
457        ephemeral_storage_bytes: 20 * GI,
458        gpu: None,
459    },
460    InstanceTypeSpec {
461        name: "c8i.xlarge",
462        platform: Platform::Aws,
463        family: InstanceFamily::ComputeOptimized,
464        architecture: Architecture::X86_64,
465        vcpu: 4,
466        memory_bytes: 8 * GI,
467        ephemeral_storage_bytes: 20 * GI,
468        gpu: None,
469    },
470    InstanceTypeSpec {
471        name: "c7g.2xlarge",
472        platform: Platform::Aws,
473        family: InstanceFamily::ComputeOptimized,
474        architecture: Architecture::Arm64,
475        vcpu: 8,
476        memory_bytes: 16 * GI,
477        ephemeral_storage_bytes: 20 * GI,
478        gpu: None,
479    },
480    InstanceTypeSpec {
481        name: "c8i.2xlarge",
482        platform: Platform::Aws,
483        family: InstanceFamily::ComputeOptimized,
484        architecture: Architecture::X86_64,
485        vcpu: 8,
486        memory_bytes: 16 * GI,
487        ephemeral_storage_bytes: 20 * GI,
488        gpu: None,
489    },
490    InstanceTypeSpec {
491        name: "c7g.4xlarge",
492        platform: Platform::Aws,
493        family: InstanceFamily::ComputeOptimized,
494        architecture: Architecture::Arm64,
495        vcpu: 16,
496        memory_bytes: 32 * GI,
497        ephemeral_storage_bytes: 20 * GI,
498        gpu: None,
499    },
500    InstanceTypeSpec {
501        name: "c8i.4xlarge",
502        platform: Platform::Aws,
503        family: InstanceFamily::ComputeOptimized,
504        architecture: Architecture::X86_64,
505        vcpu: 16,
506        memory_bytes: 32 * GI,
507        ephemeral_storage_bytes: 20 * GI,
508        gpu: None,
509    },
510    // Memory Optimized (r7g — ARM Graviton3, up to 2xlarge / 8 vCPU)
511    InstanceTypeSpec {
512        name: "r7g.medium",
513        platform: Platform::Aws,
514        family: InstanceFamily::MemoryOptimized,
515        architecture: Architecture::Arm64,
516        vcpu: 1,
517        memory_bytes: 8 * GI,
518        ephemeral_storage_bytes: 20 * GI,
519        gpu: None,
520    },
521    InstanceTypeSpec {
522        name: "r7g.large",
523        platform: Platform::Aws,
524        family: InstanceFamily::MemoryOptimized,
525        architecture: Architecture::Arm64,
526        vcpu: 2,
527        memory_bytes: 16 * GI,
528        ephemeral_storage_bytes: 20 * GI,
529        gpu: None,
530    },
531    InstanceTypeSpec {
532        name: "r7g.xlarge",
533        platform: Platform::Aws,
534        family: InstanceFamily::MemoryOptimized,
535        architecture: Architecture::Arm64,
536        vcpu: 4,
537        memory_bytes: 32 * GI,
538        ephemeral_storage_bytes: 20 * GI,
539        gpu: None,
540    },
541    InstanceTypeSpec {
542        name: "r7g.2xlarge",
543        platform: Platform::Aws,
544        family: InstanceFamily::MemoryOptimized,
545        architecture: Architecture::Arm64,
546        vcpu: 8,
547        memory_bytes: 64 * GI,
548        ephemeral_storage_bytes: 20 * GI,
549        gpu: None,
550    },
551    InstanceTypeSpec {
552        name: "r7g.4xlarge",
553        platform: Platform::Aws,
554        family: InstanceFamily::MemoryOptimized,
555        architecture: Architecture::Arm64,
556        vcpu: 16,
557        memory_bytes: 128 * GI,
558        ephemeral_storage_bytes: 20 * GI,
559        gpu: None,
560    },
561    // Storage Optimized (i4i — x86_64, NVMe)
562    InstanceTypeSpec {
563        name: "i4i.xlarge",
564        platform: Platform::Aws,
565        family: InstanceFamily::StorageOptimized,
566        architecture: Architecture::X86_64,
567        vcpu: 4,
568        memory_bytes: 32 * GI,
569        ephemeral_storage_bytes: 937 * GI,
570        gpu: None,
571    },
572    InstanceTypeSpec {
573        name: "i4i.2xlarge",
574        platform: Platform::Aws,
575        family: InstanceFamily::StorageOptimized,
576        architecture: Architecture::X86_64,
577        vcpu: 8,
578        memory_bytes: 64 * GI,
579        ephemeral_storage_bytes: 1875 * GI,
580        gpu: None,
581    },
582    InstanceTypeSpec {
583        name: "i4i.4xlarge",
584        platform: Platform::Aws,
585        family: InstanceFamily::StorageOptimized,
586        architecture: Architecture::X86_64,
587        vcpu: 16,
588        memory_bytes: 128 * GI,
589        ephemeral_storage_bytes: 3750 * GI,
590        gpu: None,
591    },
592    InstanceTypeSpec {
593        name: "i4i.8xlarge",
594        platform: Platform::Aws,
595        family: InstanceFamily::StorageOptimized,
596        architecture: Architecture::X86_64,
597        vcpu: 32,
598        memory_bytes: 256 * GI,
599        ephemeral_storage_bytes: 7500 * GI,
600        gpu: None,
601    },
602    // GPU — NVIDIA T4 (g5 — x86_64)
603    InstanceTypeSpec {
604        name: "g5.xlarge",
605        platform: Platform::Aws,
606        family: InstanceFamily::GpuCompute,
607        architecture: Architecture::X86_64,
608        vcpu: 4,
609        memory_bytes: 16 * GI,
610        ephemeral_storage_bytes: 250 * GI,
611        gpu: Some(CatalogGpu {
612            gpu_type: "nvidia-t4",
613            count: 1,
614        }),
615    },
616    InstanceTypeSpec {
617        name: "g5.2xlarge",
618        platform: Platform::Aws,
619        family: InstanceFamily::GpuCompute,
620        architecture: Architecture::X86_64,
621        vcpu: 8,
622        memory_bytes: 32 * GI,
623        ephemeral_storage_bytes: 450 * GI,
624        gpu: Some(CatalogGpu {
625            gpu_type: "nvidia-t4",
626            count: 1,
627        }),
628    },
629    // GPU — NVIDIA A100 (p4d — x86_64)
630    InstanceTypeSpec {
631        name: "p4d.24xlarge",
632        platform: Platform::Aws,
633        family: InstanceFamily::GpuCompute,
634        architecture: Architecture::X86_64,
635        vcpu: 96,
636        memory_bytes: 1152 * GI,
637        ephemeral_storage_bytes: 8000 * GI,
638        gpu: Some(CatalogGpu {
639            gpu_type: "nvidia-a100",
640            count: 8,
641        }),
642    },
643    // GPU — NVIDIA H100 (p5 — x86_64)
644    InstanceTypeSpec {
645        name: "p5.48xlarge",
646        platform: Platform::Aws,
647        family: InstanceFamily::GpuCompute,
648        architecture: Architecture::X86_64,
649        vcpu: 192,
650        memory_bytes: 2048 * GI,
651        ephemeral_storage_bytes: 8000 * GI,
652        gpu: Some(CatalogGpu {
653            gpu_type: "nvidia-h100",
654            count: 8,
655        }),
656    },
657    // =========================================================================
658    // GCP
659    // =========================================================================
660
661    // Burstable (e2)
662    InstanceTypeSpec {
663        name: "e2-micro",
664        platform: Platform::Gcp,
665        family: InstanceFamily::Burstable,
666        architecture: Architecture::X86_64,
667        vcpu: 2,
668        memory_bytes: 1 * GI,
669        ephemeral_storage_bytes: 20 * GI,
670        gpu: None,
671    },
672    InstanceTypeSpec {
673        name: "e2-small",
674        platform: Platform::Gcp,
675        family: InstanceFamily::Burstable,
676        architecture: Architecture::X86_64,
677        vcpu: 2,
678        memory_bytes: 2 * GI,
679        ephemeral_storage_bytes: 20 * GI,
680        gpu: None,
681    },
682    InstanceTypeSpec {
683        name: "e2-medium",
684        platform: Platform::Gcp,
685        family: InstanceFamily::Burstable,
686        architecture: Architecture::X86_64,
687        vcpu: 2,
688        memory_bytes: 4 * GI,
689        ephemeral_storage_bytes: 20 * GI,
690        gpu: None,
691    },
692    // General Purpose (n2-standard, up to 16 vCPU)
693    InstanceTypeSpec {
694        name: "n2-standard-2",
695        platform: Platform::Gcp,
696        family: InstanceFamily::GeneralPurpose,
697        architecture: Architecture::X86_64,
698        vcpu: 2,
699        memory_bytes: 8 * GI,
700        ephemeral_storage_bytes: 20 * GI,
701        gpu: None,
702    },
703    InstanceTypeSpec {
704        name: "n2-standard-4",
705        platform: Platform::Gcp,
706        family: InstanceFamily::GeneralPurpose,
707        architecture: Architecture::X86_64,
708        vcpu: 4,
709        memory_bytes: 16 * GI,
710        ephemeral_storage_bytes: 20 * GI,
711        gpu: None,
712    },
713    InstanceTypeSpec {
714        name: "n2-standard-8",
715        platform: Platform::Gcp,
716        family: InstanceFamily::GeneralPurpose,
717        architecture: Architecture::X86_64,
718        vcpu: 8,
719        memory_bytes: 32 * GI,
720        ephemeral_storage_bytes: 20 * GI,
721        gpu: None,
722    },
723    InstanceTypeSpec {
724        name: "n2-standard-16",
725        platform: Platform::Gcp,
726        family: InstanceFamily::GeneralPurpose,
727        architecture: Architecture::X86_64,
728        vcpu: 16,
729        memory_bytes: 64 * GI,
730        ephemeral_storage_bytes: 20 * GI,
731        gpu: None,
732    },
733    // Compute Optimized (c3-standard, up to 8 vCPU)
734    InstanceTypeSpec {
735        name: "c3-standard-4",
736        platform: Platform::Gcp,
737        family: InstanceFamily::ComputeOptimized,
738        architecture: Architecture::X86_64,
739        vcpu: 4,
740        memory_bytes: 8 * GI,
741        ephemeral_storage_bytes: 20 * GI,
742        gpu: None,
743    },
744    InstanceTypeSpec {
745        name: "c3-standard-8",
746        platform: Platform::Gcp,
747        family: InstanceFamily::ComputeOptimized,
748        architecture: Architecture::X86_64,
749        vcpu: 8,
750        memory_bytes: 16 * GI,
751        ephemeral_storage_bytes: 20 * GI,
752        gpu: None,
753    },
754    // Memory Optimized (n2-highmem, up to 8 vCPU)
755    InstanceTypeSpec {
756        name: "n2-highmem-2",
757        platform: Platform::Gcp,
758        family: InstanceFamily::MemoryOptimized,
759        architecture: Architecture::X86_64,
760        vcpu: 2,
761        memory_bytes: 16 * GI,
762        ephemeral_storage_bytes: 20 * GI,
763        gpu: None,
764    },
765    InstanceTypeSpec {
766        name: "n2-highmem-4",
767        platform: Platform::Gcp,
768        family: InstanceFamily::MemoryOptimized,
769        architecture: Architecture::X86_64,
770        vcpu: 4,
771        memory_bytes: 32 * GI,
772        ephemeral_storage_bytes: 20 * GI,
773        gpu: None,
774    },
775    InstanceTypeSpec {
776        name: "n2-highmem-8",
777        platform: Platform::Gcp,
778        family: InstanceFamily::MemoryOptimized,
779        architecture: Architecture::X86_64,
780        vcpu: 8,
781        memory_bytes: 64 * GI,
782        ephemeral_storage_bytes: 20 * GI,
783        gpu: None,
784    },
785    InstanceTypeSpec {
786        name: "n2-highmem-16",
787        platform: Platform::Gcp,
788        family: InstanceFamily::MemoryOptimized,
789        architecture: Architecture::X86_64,
790        vcpu: 16,
791        memory_bytes: 128 * GI,
792        ephemeral_storage_bytes: 20 * GI,
793        gpu: None,
794    },
795    InstanceTypeSpec {
796        name: "n2-highmem-32",
797        platform: Platform::Gcp,
798        family: InstanceFamily::MemoryOptimized,
799        architecture: Architecture::X86_64,
800        vcpu: 32,
801        memory_bytes: 256 * GI,
802        ephemeral_storage_bytes: 20 * GI,
803        gpu: None,
804    },
805    // Storage Optimized (c3d-standard with local SSD)
806    InstanceTypeSpec {
807        name: "c3d-standard-8",
808        platform: Platform::Gcp,
809        family: InstanceFamily::StorageOptimized,
810        architecture: Architecture::X86_64,
811        vcpu: 8,
812        memory_bytes: 32 * GI,
813        ephemeral_storage_bytes: 480 * GI,
814        gpu: None,
815    },
816    InstanceTypeSpec {
817        name: "c3d-standard-16",
818        platform: Platform::Gcp,
819        family: InstanceFamily::StorageOptimized,
820        architecture: Architecture::X86_64,
821        vcpu: 16,
822        memory_bytes: 64 * GI,
823        ephemeral_storage_bytes: 960 * GI,
824        gpu: None,
825    },
826    InstanceTypeSpec {
827        name: "c3d-standard-30",
828        platform: Platform::Gcp,
829        family: InstanceFamily::StorageOptimized,
830        architecture: Architecture::X86_64,
831        vcpu: 30,
832        memory_bytes: 120 * GI,
833        ephemeral_storage_bytes: 1920 * GI,
834        gpu: None,
835    },
836    // GPU — NVIDIA T4 (n1-standard + T4)
837    InstanceTypeSpec {
838        name: "n1-standard-4-t4",
839        platform: Platform::Gcp,
840        family: InstanceFamily::GpuCompute,
841        architecture: Architecture::X86_64,
842        vcpu: 4,
843        memory_bytes: 15 * GI,
844        ephemeral_storage_bytes: 100 * GI,
845        gpu: Some(CatalogGpu {
846            gpu_type: "nvidia-t4",
847            count: 1,
848        }),
849    },
850    // GPU — NVIDIA A100 (a2-highgpu)
851    InstanceTypeSpec {
852        name: "a2-highgpu-1g",
853        platform: Platform::Gcp,
854        family: InstanceFamily::GpuCompute,
855        architecture: Architecture::X86_64,
856        vcpu: 12,
857        memory_bytes: 85 * GI,
858        ephemeral_storage_bytes: 100 * GI,
859        gpu: Some(CatalogGpu {
860            gpu_type: "nvidia-a100",
861            count: 1,
862        }),
863    },
864    InstanceTypeSpec {
865        name: "a2-highgpu-8g",
866        platform: Platform::Gcp,
867        family: InstanceFamily::GpuCompute,
868        architecture: Architecture::X86_64,
869        vcpu: 96,
870        memory_bytes: 1360 * GI,
871        ephemeral_storage_bytes: 100 * GI,
872        gpu: Some(CatalogGpu {
873            gpu_type: "nvidia-a100",
874            count: 8,
875        }),
876    },
877    // GPU — NVIDIA H100 (a3-highgpu)
878    InstanceTypeSpec {
879        name: "a3-highgpu-8g",
880        platform: Platform::Gcp,
881        family: InstanceFamily::GpuCompute,
882        architecture: Architecture::X86_64,
883        vcpu: 208,
884        memory_bytes: 1872 * GI,
885        ephemeral_storage_bytes: 100 * GI,
886        gpu: Some(CatalogGpu {
887            gpu_type: "nvidia-h100",
888            count: 8,
889        }),
890    },
891    // =========================================================================
892    // Azure
893    // =========================================================================
894
895    // Burstable (B-series v2)
896    InstanceTypeSpec {
897        name: "Standard_B1s",
898        platform: Platform::Azure,
899        family: InstanceFamily::Burstable,
900        architecture: Architecture::X86_64,
901        vcpu: 1,
902        memory_bytes: 1 * GI,
903        ephemeral_storage_bytes: 20 * GI,
904        gpu: None,
905    },
906    InstanceTypeSpec {
907        name: "Standard_B2s",
908        platform: Platform::Azure,
909        family: InstanceFamily::Burstable,
910        architecture: Architecture::X86_64,
911        vcpu: 2,
912        memory_bytes: 4 * GI,
913        ephemeral_storage_bytes: 20 * GI,
914        gpu: None,
915    },
916    InstanceTypeSpec {
917        name: "Standard_B2ms",
918        platform: Platform::Azure,
919        family: InstanceFamily::Burstable,
920        architecture: Architecture::X86_64,
921        vcpu: 2,
922        memory_bytes: 8 * GI,
923        ephemeral_storage_bytes: 20 * GI,
924        gpu: None,
925    },
926    InstanceTypeSpec {
927        name: "Standard_B4ms",
928        platform: Platform::Azure,
929        family: InstanceFamily::Burstable,
930        architecture: Architecture::X86_64,
931        vcpu: 4,
932        memory_bytes: 16 * GI,
933        ephemeral_storage_bytes: 20 * GI,
934        gpu: None,
935    },
936    // General Purpose (Dv5-series, up to 16 vCPU)
937    InstanceTypeSpec {
938        name: "Standard_D2s_v5",
939        platform: Platform::Azure,
940        family: InstanceFamily::GeneralPurpose,
941        architecture: Architecture::X86_64,
942        vcpu: 2,
943        memory_bytes: 8 * GI,
944        ephemeral_storage_bytes: 20 * GI,
945        gpu: None,
946    },
947    InstanceTypeSpec {
948        name: "Standard_D4s_v5",
949        platform: Platform::Azure,
950        family: InstanceFamily::GeneralPurpose,
951        architecture: Architecture::X86_64,
952        vcpu: 4,
953        memory_bytes: 16 * GI,
954        ephemeral_storage_bytes: 20 * GI,
955        gpu: None,
956    },
957    InstanceTypeSpec {
958        name: "Standard_D8s_v5",
959        platform: Platform::Azure,
960        family: InstanceFamily::GeneralPurpose,
961        architecture: Architecture::X86_64,
962        vcpu: 8,
963        memory_bytes: 32 * GI,
964        ephemeral_storage_bytes: 20 * GI,
965        gpu: None,
966    },
967    InstanceTypeSpec {
968        name: "Standard_D16s_v5",
969        platform: Platform::Azure,
970        family: InstanceFamily::GeneralPurpose,
971        architecture: Architecture::X86_64,
972        vcpu: 16,
973        memory_bytes: 64 * GI,
974        ephemeral_storage_bytes: 20 * GI,
975        gpu: None,
976    },
977    // Compute Optimized (Fv2-series, up to 16 vCPU)
978    InstanceTypeSpec {
979        name: "Standard_F2s_v2",
980        platform: Platform::Azure,
981        family: InstanceFamily::ComputeOptimized,
982        architecture: Architecture::X86_64,
983        vcpu: 2,
984        memory_bytes: 4 * GI,
985        ephemeral_storage_bytes: 20 * GI,
986        gpu: None,
987    },
988    InstanceTypeSpec {
989        name: "Standard_F4s_v2",
990        platform: Platform::Azure,
991        family: InstanceFamily::ComputeOptimized,
992        architecture: Architecture::X86_64,
993        vcpu: 4,
994        memory_bytes: 8 * GI,
995        ephemeral_storage_bytes: 20 * GI,
996        gpu: None,
997    },
998    InstanceTypeSpec {
999        name: "Standard_F8s_v2",
1000        platform: Platform::Azure,
1001        family: InstanceFamily::ComputeOptimized,
1002        architecture: Architecture::X86_64,
1003        vcpu: 8,
1004        memory_bytes: 16 * GI,
1005        ephemeral_storage_bytes: 20 * GI,
1006        gpu: None,
1007    },
1008    InstanceTypeSpec {
1009        name: "Standard_F16s_v2",
1010        platform: Platform::Azure,
1011        family: InstanceFamily::ComputeOptimized,
1012        architecture: Architecture::X86_64,
1013        vcpu: 16,
1014        memory_bytes: 32 * GI,
1015        ephemeral_storage_bytes: 20 * GI,
1016        gpu: None,
1017    },
1018    // Memory Optimized (Ev5-series, up to 16 vCPU)
1019    InstanceTypeSpec {
1020        name: "Standard_E2s_v5",
1021        platform: Platform::Azure,
1022        family: InstanceFamily::MemoryOptimized,
1023        architecture: Architecture::X86_64,
1024        vcpu: 2,
1025        memory_bytes: 16 * GI,
1026        ephemeral_storage_bytes: 20 * GI,
1027        gpu: None,
1028    },
1029    InstanceTypeSpec {
1030        name: "Standard_E4s_v5",
1031        platform: Platform::Azure,
1032        family: InstanceFamily::MemoryOptimized,
1033        architecture: Architecture::X86_64,
1034        vcpu: 4,
1035        memory_bytes: 32 * GI,
1036        ephemeral_storage_bytes: 20 * GI,
1037        gpu: None,
1038    },
1039    InstanceTypeSpec {
1040        name: "Standard_E8s_v5",
1041        platform: Platform::Azure,
1042        family: InstanceFamily::MemoryOptimized,
1043        architecture: Architecture::X86_64,
1044        vcpu: 8,
1045        memory_bytes: 64 * GI,
1046        ephemeral_storage_bytes: 20 * GI,
1047        gpu: None,
1048    },
1049    InstanceTypeSpec {
1050        name: "Standard_E16s_v5",
1051        platform: Platform::Azure,
1052        family: InstanceFamily::MemoryOptimized,
1053        architecture: Architecture::X86_64,
1054        vcpu: 16,
1055        memory_bytes: 128 * GI,
1056        ephemeral_storage_bytes: 20 * GI,
1057        gpu: None,
1058    },
1059    // Storage Optimized (Lsv3-series with NVMe)
1060    InstanceTypeSpec {
1061        name: "Standard_L8s_v3",
1062        platform: Platform::Azure,
1063        family: InstanceFamily::StorageOptimized,
1064        architecture: Architecture::X86_64,
1065        vcpu: 8,
1066        memory_bytes: 64 * GI,
1067        ephemeral_storage_bytes: 1788 * GI,
1068        gpu: None,
1069    },
1070    InstanceTypeSpec {
1071        name: "Standard_L16s_v3",
1072        platform: Platform::Azure,
1073        family: InstanceFamily::StorageOptimized,
1074        architecture: Architecture::X86_64,
1075        vcpu: 16,
1076        memory_bytes: 128 * GI,
1077        ephemeral_storage_bytes: 3576 * GI,
1078        gpu: None,
1079    },
1080    InstanceTypeSpec {
1081        name: "Standard_L32s_v3",
1082        platform: Platform::Azure,
1083        family: InstanceFamily::StorageOptimized,
1084        architecture: Architecture::X86_64,
1085        vcpu: 32,
1086        memory_bytes: 256 * GI,
1087        ephemeral_storage_bytes: 7154 * GI,
1088        gpu: None,
1089    },
1090    // GPU — NVIDIA T4 (NCasT4_v3-series)
1091    InstanceTypeSpec {
1092        name: "Standard_NC4as_T4_v3",
1093        platform: Platform::Azure,
1094        family: InstanceFamily::GpuCompute,
1095        architecture: Architecture::X86_64,
1096        vcpu: 4,
1097        memory_bytes: 28 * GI,
1098        ephemeral_storage_bytes: 176 * GI,
1099        gpu: Some(CatalogGpu {
1100            gpu_type: "nvidia-t4",
1101            count: 1,
1102        }),
1103    },
1104    // GPU — NVIDIA A100 (NC A100 v4-series)
1105    InstanceTypeSpec {
1106        name: "Standard_NC24ads_A100_v4",
1107        platform: Platform::Azure,
1108        family: InstanceFamily::GpuCompute,
1109        architecture: Architecture::X86_64,
1110        vcpu: 24,
1111        memory_bytes: 220 * GI,
1112        ephemeral_storage_bytes: 958 * GI,
1113        gpu: Some(CatalogGpu {
1114            gpu_type: "nvidia-a100",
1115            count: 1,
1116        }),
1117    },
1118    InstanceTypeSpec {
1119        name: "Standard_NC96ads_A100_v4",
1120        platform: Platform::Azure,
1121        family: InstanceFamily::GpuCompute,
1122        architecture: Architecture::X86_64,
1123        vcpu: 96,
1124        memory_bytes: 880 * GI,
1125        ephemeral_storage_bytes: 3916 * GI,
1126        gpu: Some(CatalogGpu {
1127            gpu_type: "nvidia-a100",
1128            count: 4,
1129        }),
1130    },
1131    // GPU — NVIDIA H100 (ND H100 v5-series)
1132    InstanceTypeSpec {
1133        name: "Standard_ND96isr_H100_v5",
1134        platform: Platform::Azure,
1135        family: InstanceFamily::GpuCompute,
1136        architecture: Architecture::X86_64,
1137        vcpu: 96,
1138        memory_bytes: 1900 * GI,
1139        ephemeral_storage_bytes: 1000 * GI,
1140        gpu: Some(CatalogGpu {
1141            gpu_type: "nvidia-h100",
1142            count: 8,
1143        }),
1144    },
1145];
1146
1147// ---------------------------------------------------------------------------
1148// Catalog lookup
1149// ---------------------------------------------------------------------------
1150
1151/// Get all instance types for a given platform.
1152pub fn catalog_for_platform(platform: Platform) -> Vec<&'static InstanceTypeSpec> {
1153    CATALOG
1154        .iter()
1155        .filter(|spec| spec.platform == platform)
1156        .collect()
1157}
1158
1159/// Find a specific instance type by name and platform.
1160pub fn find_instance_type(platform: Platform, name: &str) -> Option<&'static InstanceTypeSpec> {
1161    CATALOG
1162        .iter()
1163        .find(|spec| spec.platform == platform && spec.name == name)
1164}
1165
1166// ---------------------------------------------------------------------------
1167// Instance type selection
1168// ---------------------------------------------------------------------------
1169
1170/// Aggregated resource requirements from all containers in a capacity group.
1171#[derive(Debug, Clone)]
1172pub struct WorkloadRequirements {
1173    /// Total CPU needed at desired scale (sum of desired CPU * desired_replicas per container)
1174    pub total_cpu_at_desired: f64,
1175    /// Total memory needed at desired scale (sum of desired memory * desired_replicas per container)
1176    pub total_memory_bytes_at_desired: u64,
1177    /// Total CPU needed at maximum scale (sum of desired CPU * max_replicas per container)
1178    pub total_cpu_at_max: f64,
1179    /// Total memory needed at maximum scale (sum of desired memory * max_replicas per container)
1180    pub total_memory_bytes_at_max: u64,
1181    /// Largest CPU request among all individual containers (single replica)
1182    pub max_cpu_per_container: f64,
1183    /// Largest memory request among all individual containers (single replica)
1184    pub max_memory_per_container: u64,
1185    /// Maximum ephemeral storage any single container requires
1186    pub max_ephemeral_storage_bytes: u64,
1187    /// GPU requirement (if any container needs GPU)
1188    pub gpu: Option<GpuSpec>,
1189    /// Required CPU architecture, when source explicitly constrains it.
1190    pub architecture: Option<Architecture>,
1191    /// If true, only instance types that expose nested virtualization (VT-x/EPT)
1192    /// to guest VMs are eligible. Required by workloads that run QEMU/KVM
1193    /// inside a container.
1194    pub nested_virt: bool,
1195}
1196
1197/// Result of instance type selection.
1198#[derive(Debug, Clone)]
1199pub struct InstanceSelection {
1200    /// Selected instance type name (e.g., "m7g.2xlarge")
1201    pub instance_type: &'static str,
1202    /// Machine profile derived from the instance type
1203    pub profile: MachineProfile,
1204    /// Recommended minimum number of machines
1205    pub min_machines: u32,
1206    /// Recommended maximum number of machines
1207    pub max_machines: u32,
1208}
1209
1210/// Ephemeral storage threshold above which storage-optimized instances are selected.
1211const STORAGE_OPTIMIZED_THRESHOLD: u64 = 200 * GI;
1212
1213/// Maximum number of machines per cluster.
1214const MAX_MACHINES_PER_CLUSTER: u32 = 10;
1215
1216/// Hard cap on vCPUs for non-GPU/non-storage workloads. Equivalent to AWS 2xlarge.
1217/// Beyond this, horizontal scaling is always preferred over bigger machines.
1218const MAX_STANDARD_VCPU: u32 = 8;
1219
1220/// How many of the largest standard container we want to fit per machine.
1221const STANDARD_CONTAINERS_PER_MACHINE: f64 = 2.0;
1222
1223/// Overhead factor for system processes and bin-packing inefficiency.
1224const OVERHEAD_FACTOR: f64 = 1.25;
1225
1226/// Runtime CPU reserved for system processes on each managed container machine.
1227const SYSTEM_RESERVE_CPU: f64 = 0.5;
1228
1229/// Runtime planning headroom for total desired/max workload.
1230const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1231
1232/// Select the best instance type for a workload on a given platform.
1233///
1234/// The algorithm:
1235/// 1. GPU workloads: Match by GPU type, find smallest instance with enough GPUs.
1236/// 2. Storage-heavy workloads (>200Gi ephemeral): Use storage-optimized instances.
1237/// 3. All other workloads: Size the machine to fit a small HA-friendly baseline,
1238///    capped at 8 vCPUs. Use GeneralPurpose family for broad availability and
1239///    reasonable cost. Scale horizontally for more capacity.
1240///
1241/// Returns an error if no suitable instance type is found.
1242pub fn select_instance_type(
1243    platform: Platform,
1244    requirements: &WorkloadRequirements,
1245) -> Result<InstanceSelection, String> {
1246    // Determine which family to use. Nested virt isn't available on
1247    // burstable hardware on any cloud, so a workload that classifies as
1248    // Burstable but needs nested virt must be upgraded to GeneralPurpose
1249    // (the family that actually has nested-virt-capable entries).
1250    let raw_family = select_family(requirements);
1251    let family = if requirements.nested_virt && raw_family == InstanceFamily::Burstable {
1252        InstanceFamily::GeneralPurpose
1253    } else {
1254        raw_family
1255    };
1256
1257    let candidates: Vec<&InstanceTypeSpec> = CATALOG
1258        .iter()
1259        .filter(|spec| spec.platform == platform && spec.family == family)
1260        .filter(|spec| {
1261            if requirements.nested_virt {
1262                spec.is_nested_virt_capable()
1263            } else {
1264                !spec.is_nested_virt_capable()
1265            }
1266        })
1267        .collect();
1268
1269    if candidates.is_empty() {
1270        return Err(if requirements.nested_virt {
1271            format!(
1272                "no nested-virt-capable {family:?} instance types in catalog for platform {platform}; \
1273                 only 8th-gen Intel families (m8i/c8i/r8i) support nested virtualization on AWS"
1274            )
1275        } else {
1276            format!("no {family:?} instance types in catalog for platform {platform}")
1277        });
1278    }
1279
1280    // For GPU workloads, filter by GPU type
1281    let candidates = if let Some(ref gpu) = requirements.gpu {
1282        let filtered: Vec<&InstanceTypeSpec> = candidates
1283            .into_iter()
1284            .filter(|spec| {
1285                spec.gpu.as_ref().map_or(false, |g| {
1286                    g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1287                })
1288            })
1289            .collect();
1290        if filtered.is_empty() {
1291            return Err(format!(
1292                "no instance type for GPU type '{}' x{} on platform {platform}",
1293                gpu.gpu_type, gpu.count
1294            ));
1295        }
1296        filtered
1297    } else {
1298        candidates
1299    };
1300
1301    // For storage workloads, filter by ephemeral storage capacity
1302    let candidates = if family == InstanceFamily::StorageOptimized {
1303        let filtered: Vec<&InstanceTypeSpec> = candidates
1304            .into_iter()
1305            .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1306            .collect();
1307        if filtered.is_empty() {
1308            return Err(format!(
1309                "no storage-optimized instance with >= {} bytes ephemeral storage on platform {platform}",
1310                requirements.max_ephemeral_storage_bytes
1311            ));
1312        }
1313        filtered
1314    } else {
1315        candidates
1316    };
1317
1318    let architecture = requirements
1319        .architecture
1320        .or_else(|| default_architecture(platform))
1321        .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1322    let candidates: Vec<&InstanceTypeSpec> = candidates
1323        .into_iter()
1324        .filter(|spec| spec.architecture == architecture)
1325        .collect();
1326    if candidates.is_empty() {
1327        return Err(format!(
1328            "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1329        ));
1330    }
1331
1332    // Cap at MAX_STANDARD_VCPU for non-GPU/non-storage workloads
1333    let vcpu_cap =
1334        if family == InstanceFamily::GpuCompute || family == InstanceFamily::StorageOptimized {
1335            u32::MAX
1336        } else {
1337            MAX_STANDARD_VCPU
1338        };
1339
1340    let desired_target_machines = desired_target_machines(requirements);
1341    let target_cpu =
1342        (requirements.max_cpu_per_container * STANDARD_CONTAINERS_PER_MACHINE * OVERHEAD_FACTOR)
1343            .max(
1344                requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR
1345                    / desired_target_machines as f64,
1346            )
1347            .max(0.25);
1348    let target_memory = (requirements.max_memory_per_container as f64
1349        * STANDARD_CONTAINERS_PER_MACHINE
1350        * OVERHEAD_FACTOR)
1351        .max(
1352            requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR
1353                / desired_target_machines as f64,
1354        )
1355        .max(256.0 * MI as f64);
1356
1357    // Find the smallest instance that meets per-container targets within the cap.
1358    let selected = candidates
1359        .iter()
1360        .filter(|spec| {
1361            spec.vcpu <= vcpu_cap
1362                && spec.vcpu as f64 >= target_cpu
1363                && spec.memory_bytes as f64 >= target_memory
1364        })
1365        .min_by_key(|spec| spec.vcpu)
1366        .or_else(|| {
1367            // If nothing fits within the cap, pick the largest instance under the cap
1368            candidates
1369                .iter()
1370                .filter(|spec| spec.vcpu <= vcpu_cap)
1371                .max_by_key(|spec| spec.vcpu)
1372        })
1373        .or_else(|| {
1374            // Last resort: pick the smallest available instance (for GPU/storage)
1375            candidates.iter().min_by_key(|spec| spec.vcpu)
1376        })
1377        .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1378
1379    // Calculate machine counts
1380    let max_machines = compute_max_machines(requirements, selected);
1381    let min_machines = compute_min_machines(requirements, selected, max_machines);
1382
1383    Ok(InstanceSelection {
1384        instance_type: selected.name,
1385        profile: selected.to_machine_profile(),
1386        min_machines,
1387        max_machines,
1388    })
1389}
1390
1391/// Select instance family based on workload characteristics.
1392///
1393/// Uses GeneralPurpose for all standard workloads — widely available across
1394/// regions and cost-effective. Only specialized workloads (GPU, large ephemeral
1395/// storage) get specialized families. Very small workloads get burstable.
1396pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1397    // GPU workloads always get GPU instances
1398    if requirements.gpu.is_some() {
1399        return InstanceFamily::GpuCompute;
1400    }
1401
1402    // Large ephemeral storage needs NVMe (storage-optimized)
1403    if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1404        return InstanceFamily::StorageOptimized;
1405    }
1406
1407    // Very small workloads use burstable instances
1408    if requirements.total_cpu_at_max < 2.0 {
1409        return InstanceFamily::Burstable;
1410    }
1411
1412    // All other workloads use GeneralPurpose — available everywhere, good pricing
1413    InstanceFamily::GeneralPurpose
1414}
1415
1416/// Calculate maximum machines needed to fit the workload with headroom.
1417fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1418    let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1419    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1420
1421    let mem_with_headroom =
1422        requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1423    let mem_machines =
1424        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1425
1426    // Take the larger of CPU-based and memory-based, clamped to cluster limit
1427    cpu_machines
1428        .max(mem_machines)
1429        .max(1)
1430        .min(MAX_MACHINES_PER_CLUSTER)
1431}
1432
1433/// Calculate minimum machines for HA.
1434fn compute_min_machines(
1435    requirements: &WorkloadRequirements,
1436    instance: &InstanceTypeSpec,
1437    max_machines: u32,
1438) -> u32 {
1439    let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1440    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1441
1442    let mem_with_headroom =
1443        requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1444    let mem_machines =
1445        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1446
1447    cpu_machines
1448        .max(mem_machines)
1449        .max(1)
1450        .min(2)
1451        .min(max_machines)
1452}
1453
1454fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1455    if requirements.total_cpu_at_desired >= 2.0
1456        || requirements.total_memory_bytes_at_desired >= 4 * GI
1457    {
1458        2
1459    } else {
1460        1
1461    }
1462}
1463
1464fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1465    (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1466}
1467
1468fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1469    instance
1470        .memory_bytes
1471        .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1472        .max(256 * MI)
1473}
1474
1475fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1476    if memory_bytes < 4 * GI {
1477        256 * MI
1478    } else if memory_bytes < 16 * GI {
1479        512 * MI
1480    } else {
1481        GI
1482    }
1483}
1484
1485// ---------------------------------------------------------------------------
1486// Tests
1487// ---------------------------------------------------------------------------
1488
1489#[cfg(test)]
1490mod tests {
1491    use super::*;
1492    use crate::BinaryTarget;
1493
1494    // -- Parsing tests --
1495
1496    #[test]
1497    fn test_parse_cpu_plain() {
1498        assert_eq!(parse_cpu("1").unwrap(), 1.0);
1499        assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1500        assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1501        assert_eq!(parse_cpu("16").unwrap(), 16.0);
1502    }
1503
1504    #[test]
1505    fn test_parse_cpu_millicore() {
1506        assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1507        assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1508        assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1509        assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1510    }
1511
1512    #[test]
1513    fn test_parse_cpu_invalid() {
1514        assert!(parse_cpu("").is_err());
1515        assert!(parse_cpu("abc").is_err());
1516        assert!(parse_cpu("m").is_err());
1517    }
1518
1519    #[test]
1520    fn test_parse_memory_binary_suffixes() {
1521        assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1522        assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1523        assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1524        assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1525        assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1526        assert_eq!(
1527            parse_memory_bytes("1Ti").unwrap(),
1528            1024u64 * 1024 * 1024 * 1024
1529        );
1530    }
1531
1532    #[test]
1533    fn test_parse_memory_decimal_suffixes() {
1534        assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1535        assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1536        assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1537        assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1538    }
1539
1540    #[test]
1541    fn test_parse_memory_plain_bytes() {
1542        assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1543        assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1544    }
1545
1546    #[test]
1547    fn test_parse_memory_invalid() {
1548        assert!(parse_memory_bytes("").is_err());
1549        assert!(parse_memory_bytes("abc").is_err());
1550        assert!(parse_memory_bytes("Gi").is_err());
1551    }
1552
1553    #[test]
1554    fn test_parse_memory_fractional() {
1555        assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1556        assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1557    }
1558
1559    // -- Catalog lookup tests --
1560
1561    #[test]
1562    fn test_catalog_has_entries_for_all_cloud_platforms() {
1563        assert!(!catalog_for_platform(Platform::Aws).is_empty());
1564        assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1565        assert!(!catalog_for_platform(Platform::Azure).is_empty());
1566    }
1567
1568    #[test]
1569    fn test_catalog_no_entries_for_non_cloud_platforms() {
1570        assert!(catalog_for_platform(Platform::Local).is_empty());
1571        assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1572    }
1573
1574    #[test]
1575    fn test_find_known_instance_type() {
1576        let spec =
1577            find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1578        assert_eq!(spec.vcpu, 8);
1579        assert_eq!(spec.memory_bytes, 32 * GI);
1580        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1581    }
1582
1583    #[test]
1584    fn test_find_aws_c8i_nested_virt_instance_type() {
1585        let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1586        assert_eq!(spec.vcpu, 2);
1587        assert_eq!(spec.memory_bytes, 4 * GI);
1588        assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1589        assert_eq!(spec.architecture, Architecture::X86_64);
1590        assert!(spec.is_nested_virt_capable());
1591    }
1592
1593    #[test]
1594    fn test_find_unknown_instance_type() {
1595        assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1596    }
1597
1598    #[test]
1599    fn test_find_wrong_platform() {
1600        assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1601    }
1602
1603    #[test]
1604    fn test_to_machine_profile() {
1605        let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1606        let profile = spec.to_machine_profile();
1607        assert_eq!(profile.cpu, "8.0");
1608        assert_eq!(profile.memory_bytes, 32 * GI);
1609        assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1610        assert!(profile.gpu.is_none());
1611    }
1612
1613    #[test]
1614    fn test_to_machine_profile_with_gpu() {
1615        let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1616        let profile = spec.to_machine_profile();
1617        let gpu = profile.gpu.as_ref().expect("should have GPU");
1618        assert_eq!(gpu.gpu_type, "nvidia-a100");
1619        assert_eq!(gpu.count, 8);
1620    }
1621
1622    // -- Selection algorithm tests --
1623
1624    #[test]
1625    fn test_select_burstable_for_small_workload() {
1626        let req = WorkloadRequirements {
1627            total_cpu_at_desired: 1.0,
1628            total_memory_bytes_at_desired: 2 * GI,
1629            total_cpu_at_max: 1.0,
1630            total_memory_bytes_at_max: 2 * GI,
1631            max_cpu_per_container: 0.5,
1632            max_memory_per_container: 1 * GI,
1633            max_ephemeral_storage_bytes: 10 * GI,
1634            gpu: None,
1635            architecture: None,
1636            nested_virt: false,
1637        };
1638        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1639        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1640        assert_eq!(spec.family, InstanceFamily::Burstable);
1641    }
1642
1643    #[test]
1644    fn test_select_general_purpose_for_standard_workload() {
1645        // Standard workloads always get GeneralPurpose regardless of CPU:memory ratio
1646        let req = WorkloadRequirements {
1647            total_cpu_at_desired: 20.0,
1648            total_memory_bytes_at_desired: 80 * GI,
1649            total_cpu_at_max: 20.0,
1650            total_memory_bytes_at_max: 80 * GI,
1651            max_cpu_per_container: 2.0,
1652            max_memory_per_container: 8 * GI,
1653            max_ephemeral_storage_bytes: 10 * GI,
1654            gpu: None,
1655            architecture: None,
1656            nested_virt: false,
1657        };
1658        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1659        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1660        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1661    }
1662
1663    #[test]
1664    fn test_select_general_purpose_even_for_cpu_heavy() {
1665        // CPU-heavy workloads still get GeneralPurpose (no more ComputeOptimized auto-select)
1666        let req = WorkloadRequirements {
1667            total_cpu_at_desired: 20.0,
1668            total_memory_bytes_at_desired: 20 * GI,
1669            total_cpu_at_max: 20.0,
1670            total_memory_bytes_at_max: 20 * GI,
1671            max_cpu_per_container: 2.0,
1672            max_memory_per_container: 2 * GI,
1673            max_ephemeral_storage_bytes: 10 * GI,
1674            gpu: None,
1675            architecture: None,
1676            nested_virt: false,
1677        };
1678        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1679        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1680        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1681    }
1682
1683    #[test]
1684    fn test_select_storage_optimized_for_large_ephemeral() {
1685        let req = WorkloadRequirements {
1686            total_cpu_at_desired: 8.0,
1687            total_memory_bytes_at_desired: 32 * GI,
1688            total_cpu_at_max: 8.0,
1689            total_memory_bytes_at_max: 32 * GI,
1690            max_cpu_per_container: 2.0,
1691            max_memory_per_container: 8 * GI,
1692            max_ephemeral_storage_bytes: 500 * GI,
1693            gpu: None,
1694            architecture: Some(Architecture::X86_64),
1695            nested_virt: false,
1696        };
1697        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1698        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1699        assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1700    }
1701
1702    #[test]
1703    fn test_select_gpu_instance() {
1704        let req = WorkloadRequirements {
1705            total_cpu_at_desired: 8.0,
1706            total_memory_bytes_at_desired: 32 * GI,
1707            total_cpu_at_max: 8.0,
1708            total_memory_bytes_at_max: 32 * GI,
1709            max_cpu_per_container: 4.0,
1710            max_memory_per_container: 16 * GI,
1711            max_ephemeral_storage_bytes: 10 * GI,
1712            gpu: Some(GpuSpec {
1713                gpu_type: "nvidia-a100".to_string(),
1714                count: 1,
1715            }),
1716            architecture: Some(Architecture::X86_64),
1717            nested_virt: false,
1718        };
1719        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1720        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1721        assert_eq!(spec.family, InstanceFamily::GpuCompute);
1722        assert!(spec.gpu.is_some());
1723    }
1724
1725    #[test]
1726    fn test_select_uses_each_cloud_image_target_architecture() {
1727        let req = WorkloadRequirements {
1728            total_cpu_at_desired: 4.0,
1729            total_memory_bytes_at_desired: 16 * GI,
1730            total_cpu_at_max: 4.0,
1731            total_memory_bytes_at_max: 16 * GI,
1732            max_cpu_per_container: 1.0,
1733            max_memory_per_container: 4 * GI,
1734            max_ephemeral_storage_bytes: 10 * GI,
1735            gpu: None,
1736            architecture: None,
1737            nested_virt: false,
1738        };
1739        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1740            let sel = select_instance_type(platform, &req)
1741                .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
1742            let spec = find_instance_type(platform, sel.instance_type)
1743                .expect("selected machine should exist in the catalog");
1744            assert_eq!(
1745                Some(spec.architecture),
1746                default_architecture(platform),
1747                "machine architecture must match the image target for {platform}"
1748            );
1749        }
1750    }
1751
1752    #[test]
1753    fn test_machine_count_reasonable() {
1754        // Single container: 1 CPU, 2Gi, maxReplicas=20
1755        let req = WorkloadRequirements {
1756            total_cpu_at_desired: 20.0,
1757            total_memory_bytes_at_desired: 40 * GI,
1758            total_cpu_at_max: 20.0,
1759            total_memory_bytes_at_max: 40 * GI,
1760            max_cpu_per_container: 1.0,
1761            max_memory_per_container: 2 * GI,
1762            max_ephemeral_storage_bytes: 10 * GI,
1763            gpu: None,
1764            architecture: None,
1765            nested_virt: false,
1766        };
1767        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1768        assert!(sel.min_machines >= 1);
1769        assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
1770        assert!(sel.max_machines >= sel.min_machines);
1771    }
1772
1773    #[test]
1774    fn test_instance_size_capped_at_8_vcpu() {
1775        // Even with very large containers, instance size is capped at 8 vCPUs
1776        let req = WorkloadRequirements {
1777            total_cpu_at_desired: 70.0,
1778            total_memory_bytes_at_desired: 140 * GI,
1779            total_cpu_at_max: 70.0,
1780            total_memory_bytes_at_max: 140 * GI,
1781            max_cpu_per_container: 2.0,
1782            max_memory_per_container: 4 * GI,
1783            max_ephemeral_storage_bytes: 10 * GI,
1784            gpu: None,
1785            architecture: None,
1786            nested_virt: false,
1787        };
1788        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1789        let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
1790        assert!(
1791            spec.vcpu <= MAX_STANDARD_VCPU,
1792            "selected {} with {} vCPUs, expected <= {}",
1793            spec.name,
1794            spec.vcpu,
1795            MAX_STANDARD_VCPU
1796        );
1797        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1798        // Should scale horizontally instead
1799        assert!(sel.max_machines > 1);
1800    }
1801
1802    #[test]
1803    fn test_larger_autoscaled_workload_gets_reasonable_instance() {
1804        // Simulates a larger autoscaled workload: 4 containers, each 2 CPU / 4 GiB
1805        // maxReplicas: 10, 10, 10, 5
1806        let req = WorkloadRequirements {
1807            total_cpu_at_desired: 70.0,
1808            total_memory_bytes_at_desired: 140 * GI,
1809            total_cpu_at_max: 70.0,              // 2*10 + 2*10 + 2*10 + 2*5
1810            total_memory_bytes_at_max: 140 * GI, // 4*10 + 4*10 + 4*10 + 4*5
1811            max_cpu_per_container: 2.0,
1812            max_memory_per_container: 4 * GI,
1813            max_ephemeral_storage_bytes: 20 * GI,
1814            gpu: None,
1815            architecture: None,
1816            nested_virt: false,
1817        };
1818        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1819        // Should pick n2-standard-8 (8 vCPU, 32 GiB) — NOT c3-standard-44
1820        assert_eq!(sel.instance_type, "n2-standard-8");
1821        assert!(sel.max_machines >= 2);
1822    }
1823
1824    /// When `nested_virt` is set on the workload, the selector must
1825    /// restrict to nested-virt-capable families. On AWS that means an m8i
1826    /// (or other 8th-gen Intel) entry, never a Graviton (`*7g`, `t4g`) or
1827    /// burstable. Without this filter the launch template gets created
1828    /// with `CpuOptions.NestedVirtualization=enabled` paired with an
1829    /// instance type AWS rejects at RunInstances.
1830    #[test]
1831    fn test_select_aws_picks_m8i_when_nested_virt_required() {
1832        let req = WorkloadRequirements {
1833            total_cpu_at_desired: 4.0,
1834            total_memory_bytes_at_desired: 8 * GI,
1835            total_cpu_at_max: 4.0,
1836            total_memory_bytes_at_max: 8 * GI,
1837            max_cpu_per_container: 4.0,
1838            max_memory_per_container: 8 * GI,
1839            max_ephemeral_storage_bytes: 10 * GI,
1840            gpu: None,
1841            architecture: Some(Architecture::X86_64),
1842            nested_virt: true,
1843        };
1844        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1845        assert!(
1846            sel.instance_type.starts_with("m8i.")
1847                || sel.instance_type.starts_with("c8i.")
1848                || sel.instance_type.starts_with("r8i."),
1849            "expected an m8i/c8i/r8i instance, got {}",
1850            sel.instance_type
1851        );
1852        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1853        assert!(spec.is_nested_virt_capable());
1854    }
1855
1856    #[test]
1857    fn test_select_aws_defaults_to_image_target_architecture() {
1858        let req = WorkloadRequirements {
1859            total_cpu_at_desired: 4.0,
1860            total_memory_bytes_at_desired: 8 * GI,
1861            total_cpu_at_max: 4.0,
1862            total_memory_bytes_at_max: 8 * GI,
1863            max_cpu_per_container: 4.0,
1864            max_memory_per_container: 8 * GI,
1865            max_ephemeral_storage_bytes: 10 * GI,
1866            gpu: None,
1867            architecture: None,
1868            nested_virt: false,
1869        };
1870        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1871        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1872        assert_eq!(spec.architecture, Architecture::Arm64);
1873    }
1874
1875    #[test]
1876    fn test_cloud_defaults_match_image_target_architectures() {
1877        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1878            let target = BinaryTarget::defaults_for_platform(platform)
1879                .into_iter()
1880                .next()
1881                .expect("managed cloud should have a default image target");
1882            let image_architecture = match target.oci_arch() {
1883                "arm64" => Architecture::Arm64,
1884                "amd64" => Architecture::X86_64,
1885                architecture => {
1886                    panic!("unsupported managed-cloud image architecture {architecture}")
1887                }
1888            };
1889
1890            assert_eq!(default_architecture(platform), Some(image_architecture));
1891        }
1892    }
1893
1894    /// ARM remains available when the workload or capacity profile declares it.
1895    #[test]
1896    fn test_select_aws_uses_graviton_for_explicit_arm64() {
1897        let req = WorkloadRequirements {
1898            total_cpu_at_desired: 4.0,
1899            total_memory_bytes_at_desired: 8 * GI,
1900            total_cpu_at_max: 4.0,
1901            total_memory_bytes_at_max: 8 * GI,
1902            max_cpu_per_container: 4.0,
1903            max_memory_per_container: 8 * GI,
1904            max_ephemeral_storage_bytes: 10 * GI,
1905            gpu: None,
1906            architecture: Some(Architecture::Arm64),
1907            nested_virt: false,
1908        };
1909        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1910        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1911        assert_eq!(spec.architecture, Architecture::Arm64);
1912    }
1913
1914    #[test]
1915    fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
1916        let req = WorkloadRequirements {
1917            total_cpu_at_desired: 1.0,
1918            total_memory_bytes_at_desired: 2 * GI,
1919            total_cpu_at_max: 1.0,
1920            total_memory_bytes_at_max: 2 * GI,
1921            max_cpu_per_container: 1.0,
1922            max_memory_per_container: 2 * GI,
1923            max_ephemeral_storage_bytes: 10 * GI,
1924            gpu: None,
1925            architecture: Some(Architecture::Arm64),
1926            nested_virt: false,
1927        };
1928
1929        let error = select_instance_type(Platform::Gcp, &req)
1930            .expect_err("GCP catalog has no ARM64 machine");
1931
1932        assert!(error.contains("architecture Arm64 is unavailable"));
1933    }
1934
1935    #[test]
1936    fn test_profile_has_required_fields() {
1937        let req = WorkloadRequirements {
1938            total_cpu_at_desired: 4.0,
1939            total_memory_bytes_at_desired: 16 * GI,
1940            total_cpu_at_max: 4.0,
1941            total_memory_bytes_at_max: 16 * GI,
1942            max_cpu_per_container: 1.0,
1943            max_memory_per_container: 4 * 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        assert!(!sel.profile.cpu.is_empty());
1951        assert!(sel.profile.memory_bytes > 0);
1952        assert!(sel.profile.ephemeral_storage_bytes > 0);
1953    }
1954
1955    #[test]
1956    fn test_error_for_unsupported_gpu_type() {
1957        let req = WorkloadRequirements {
1958            total_cpu_at_desired: 8.0,
1959            total_memory_bytes_at_desired: 32 * GI,
1960            total_cpu_at_max: 8.0,
1961            total_memory_bytes_at_max: 32 * GI,
1962            max_cpu_per_container: 4.0,
1963            max_memory_per_container: 16 * GI,
1964            max_ephemeral_storage_bytes: 10 * GI,
1965            gpu: Some(GpuSpec {
1966                gpu_type: "amd-mi300".to_string(),
1967                count: 1,
1968            }),
1969            architecture: None,
1970            nested_virt: false,
1971        };
1972        let result = select_instance_type(Platform::Aws, &req);
1973        assert!(result.is_err());
1974    }
1975
1976    #[test]
1977    fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
1978        // Verify that within each (platform, family) group, vcpu is non-decreasing.
1979        // This ensures our "min_by_key(vcpu)" logic works correctly.
1980        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1981            let entries = catalog_for_platform(platform);
1982            let mut by_family: std::collections::HashMap<_, Vec<_>> =
1983                std::collections::HashMap::new();
1984            for entry in entries {
1985                by_family
1986                    .entry(format!("{:?}", entry.family))
1987                    .or_default()
1988                    .push(entry);
1989            }
1990            for (family, instances) in &by_family {
1991                for window in instances.windows(2) {
1992                    assert!(
1993                        window[0].vcpu <= window[1].vcpu,
1994                        "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
1995                        window[0].name,
1996                        window[0].vcpu,
1997                        window[1].name,
1998                        window[1].vcpu
1999                    );
2000                }
2001            }
2002        }
2003    }
2004}