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/// Runtime CPU reserved for system processes on each managed container machine.
1221const SYSTEM_RESERVE_CPU: f64 = 0.5;
1222
1223/// Runtime planning headroom for total desired/max workload.
1224const WORKLOAD_HEADROOM_FACTOR: f64 = 1.15;
1225
1226/// Select the best instance type for a workload on a given platform.
1227///
1228/// The algorithm:
1229/// 1. GPU workloads: Match by GPU type, find smallest instance with enough GPUs.
1230/// 2. Storage-heavy workloads (>200Gi ephemeral): Use storage-optimized instances.
1231/// 3. All other workloads: Size the machine to fit a small HA-friendly baseline,
1232///    capped at 8 vCPUs. Use GeneralPurpose family for broad availability and
1233///    reasonable cost. Scale horizontally for more capacity.
1234///
1235/// Returns an error if no suitable instance type is found.
1236pub fn select_instance_type(
1237    platform: Platform,
1238    requirements: &WorkloadRequirements,
1239) -> Result<InstanceSelection, String> {
1240    // Determine which family to use. Nested virt isn't available on
1241    // burstable hardware on any cloud, so a workload that classifies as
1242    // Burstable but needs nested virt must be upgraded to GeneralPurpose
1243    // (the family that actually has nested-virt-capable entries).
1244    let raw_family = select_family(requirements);
1245    let family = if requirements.nested_virt && raw_family == InstanceFamily::Burstable {
1246        InstanceFamily::GeneralPurpose
1247    } else {
1248        raw_family
1249    };
1250
1251    let candidates: Vec<&InstanceTypeSpec> = CATALOG
1252        .iter()
1253        .filter(|spec| spec.platform == platform && spec.family == family)
1254        .filter(|spec| {
1255            if requirements.nested_virt {
1256                spec.is_nested_virt_capable()
1257            } else {
1258                !spec.is_nested_virt_capable()
1259            }
1260        })
1261        .collect();
1262
1263    if candidates.is_empty() {
1264        return Err(if requirements.nested_virt {
1265            format!(
1266                "no nested-virt-capable {family:?} instance types in catalog for platform {platform}; \
1267                 only 8th-gen Intel families (m8i/c8i/r8i) support nested virtualization on AWS"
1268            )
1269        } else {
1270            format!("no {family:?} instance types in catalog for platform {platform}")
1271        });
1272    }
1273
1274    // For GPU workloads, filter by GPU type
1275    let candidates = if let Some(ref gpu) = requirements.gpu {
1276        let filtered: Vec<&InstanceTypeSpec> = candidates
1277            .into_iter()
1278            .filter(|spec| {
1279                spec.gpu.as_ref().map_or(false, |g| {
1280                    g.gpu_type == gpu.gpu_type && g.count >= gpu.count
1281                })
1282            })
1283            .collect();
1284        if filtered.is_empty() {
1285            return Err(format!(
1286                "no instance type for GPU type '{}' x{} on platform {platform}",
1287                gpu.gpu_type, gpu.count
1288            ));
1289        }
1290        filtered
1291    } else {
1292        candidates
1293    };
1294
1295    // For storage workloads, filter by ephemeral storage capacity
1296    let candidates = if family == InstanceFamily::StorageOptimized {
1297        let filtered: Vec<&InstanceTypeSpec> = candidates
1298            .into_iter()
1299            .filter(|spec| spec.ephemeral_storage_bytes >= requirements.max_ephemeral_storage_bytes)
1300            .collect();
1301        if filtered.is_empty() {
1302            return Err(format!(
1303                "no storage-optimized instance with >= {} bytes ephemeral storage on platform {platform}",
1304                requirements.max_ephemeral_storage_bytes
1305            ));
1306        }
1307        filtered
1308    } else {
1309        candidates
1310    };
1311
1312    let architecture = requirements
1313        .architecture
1314        .or_else(|| default_architecture(platform))
1315        .ok_or_else(|| format!("platform {platform} has no default compute architecture"))?;
1316    let candidates: Vec<&InstanceTypeSpec> = candidates
1317        .into_iter()
1318        .filter(|spec| spec.architecture == architecture)
1319        .collect();
1320    if candidates.is_empty() {
1321        return Err(format!(
1322            "architecture {architecture:?} is unavailable for this workload on platform {platform}"
1323        ));
1324    }
1325
1326    // Cap at MAX_STANDARD_VCPU for non-GPU/non-storage workloads
1327    let vcpu_cap =
1328        if family == InstanceFamily::GpuCompute || family == InstanceFamily::StorageOptimized {
1329            u32::MAX
1330        } else {
1331            MAX_STANDARD_VCPU
1332        };
1333
1334    let desired_target_machines = desired_target_machines(requirements);
1335    let target_cpu = requirements
1336        .max_cpu_per_container
1337        .max(requirements.total_cpu_at_desired / desired_target_machines as f64)
1338        * WORKLOAD_HEADROOM_FACTOR;
1339    let target_memory = (requirements.max_memory_per_container as f64)
1340        .max(requirements.total_memory_bytes_at_desired as f64 / desired_target_machines as f64)
1341        * WORKLOAD_HEADROOM_FACTOR;
1342
1343    // Find the smallest instance whose allocatable capacity meets the workload
1344    // target after host reserve and workload headroom. Machine count already
1345    // accounts for multiple replicas; requiring space for an arbitrary second
1346    // copy here would size the same demand twice.
1347    let selected = candidates
1348        .iter()
1349        .filter(|spec| {
1350            spec.vcpu <= vcpu_cap
1351                && allocatable_cpu(spec) >= target_cpu
1352                && allocatable_memory_bytes(spec) as f64 >= target_memory
1353        })
1354        .min_by_key(|spec| spec.vcpu)
1355        .or_else(|| {
1356            // If nothing fits within the cap, pick the largest instance under the cap
1357            candidates
1358                .iter()
1359                .filter(|spec| spec.vcpu <= vcpu_cap)
1360                .max_by_key(|spec| spec.vcpu)
1361        })
1362        .or_else(|| {
1363            // Last resort: pick the smallest available instance (for GPU/storage)
1364            candidates.iter().min_by_key(|spec| spec.vcpu)
1365        })
1366        .ok_or_else(|| format!("no instance types available for platform {platform}"))?;
1367
1368    // Calculate machine counts
1369    let max_machines = compute_max_machines(requirements, selected);
1370    let min_machines = compute_min_machines(requirements, selected, max_machines);
1371
1372    Ok(InstanceSelection {
1373        instance_type: selected.name,
1374        profile: selected.to_machine_profile(),
1375        min_machines,
1376        max_machines,
1377    })
1378}
1379
1380/// Select instance family based on workload characteristics.
1381///
1382/// Uses GeneralPurpose for all standard workloads — widely available across
1383/// regions and cost-effective. Only specialized workloads (GPU, large ephemeral
1384/// storage) get specialized families. Very small workloads get burstable.
1385pub fn select_family(requirements: &WorkloadRequirements) -> InstanceFamily {
1386    // GPU workloads always get GPU instances
1387    if requirements.gpu.is_some() {
1388        return InstanceFamily::GpuCompute;
1389    }
1390
1391    // Large ephemeral storage needs NVMe (storage-optimized)
1392    if requirements.max_ephemeral_storage_bytes > STORAGE_OPTIMIZED_THRESHOLD {
1393        return InstanceFamily::StorageOptimized;
1394    }
1395
1396    // Very small workloads use burstable instances
1397    if requirements.total_cpu_at_max < 2.0 {
1398        return InstanceFamily::Burstable;
1399    }
1400
1401    // All other workloads use GeneralPurpose — available everywhere, good pricing
1402    InstanceFamily::GeneralPurpose
1403}
1404
1405/// Calculate maximum machines needed to fit the workload with headroom.
1406fn compute_max_machines(requirements: &WorkloadRequirements, instance: &InstanceTypeSpec) -> u32 {
1407    let cpu_with_headroom = requirements.total_cpu_at_max * WORKLOAD_HEADROOM_FACTOR;
1408    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1409
1410    let mem_with_headroom =
1411        requirements.total_memory_bytes_at_max as f64 * WORKLOAD_HEADROOM_FACTOR;
1412    let mem_machines =
1413        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1414
1415    // Take the larger of CPU-based and memory-based, clamped to cluster limit
1416    cpu_machines
1417        .max(mem_machines)
1418        .max(1)
1419        .min(MAX_MACHINES_PER_CLUSTER)
1420}
1421
1422/// Calculate minimum machines for HA.
1423fn compute_min_machines(
1424    requirements: &WorkloadRequirements,
1425    instance: &InstanceTypeSpec,
1426    max_machines: u32,
1427) -> u32 {
1428    let cpu_with_headroom = requirements.total_cpu_at_desired * WORKLOAD_HEADROOM_FACTOR;
1429    let cpu_machines = (cpu_with_headroom / allocatable_cpu(instance)).ceil() as u32;
1430
1431    let mem_with_headroom =
1432        requirements.total_memory_bytes_at_desired as f64 * WORKLOAD_HEADROOM_FACTOR;
1433    let mem_machines =
1434        (mem_with_headroom / allocatable_memory_bytes(instance) as f64).ceil() as u32;
1435
1436    cpu_machines
1437        .max(mem_machines)
1438        .max(1)
1439        .min(2)
1440        .min(max_machines)
1441}
1442
1443fn desired_target_machines(requirements: &WorkloadRequirements) -> u32 {
1444    if requirements.total_cpu_at_desired >= 2.0
1445        || requirements.total_memory_bytes_at_desired >= 4 * GI
1446    {
1447        2
1448    } else {
1449        1
1450    }
1451}
1452
1453fn allocatable_cpu(instance: &InstanceTypeSpec) -> f64 {
1454    (instance.vcpu as f64 - SYSTEM_RESERVE_CPU).max(0.25)
1455}
1456
1457fn allocatable_memory_bytes(instance: &InstanceTypeSpec) -> u64 {
1458    instance
1459        .memory_bytes
1460        .saturating_sub(system_reserve_memory_bytes(instance.memory_bytes))
1461        .max(256 * MI)
1462}
1463
1464fn system_reserve_memory_bytes(memory_bytes: u64) -> u64 {
1465    if memory_bytes < 4 * GI {
1466        256 * MI
1467    } else if memory_bytes < 16 * GI {
1468        512 * MI
1469    } else {
1470        GI
1471    }
1472}
1473
1474// ---------------------------------------------------------------------------
1475// Tests
1476// ---------------------------------------------------------------------------
1477
1478#[cfg(test)]
1479mod tests {
1480    use super::*;
1481    use crate::BinaryTarget;
1482
1483    // -- Parsing tests --
1484
1485    #[test]
1486    fn test_parse_cpu_plain() {
1487        assert_eq!(parse_cpu("1").unwrap(), 1.0);
1488        assert_eq!(parse_cpu("0.5").unwrap(), 0.5);
1489        assert_eq!(parse_cpu("2.0").unwrap(), 2.0);
1490        assert_eq!(parse_cpu("16").unwrap(), 16.0);
1491    }
1492
1493    #[test]
1494    fn test_parse_cpu_millicore() {
1495        assert_eq!(parse_cpu("500m").unwrap(), 0.5);
1496        assert_eq!(parse_cpu("250m").unwrap(), 0.25);
1497        assert_eq!(parse_cpu("1000m").unwrap(), 1.0);
1498        assert_eq!(parse_cpu("100m").unwrap(), 0.1);
1499    }
1500
1501    #[test]
1502    fn test_parse_cpu_invalid() {
1503        assert!(parse_cpu("").is_err());
1504        assert!(parse_cpu("abc").is_err());
1505        assert!(parse_cpu("m").is_err());
1506    }
1507
1508    #[test]
1509    fn test_parse_memory_binary_suffixes() {
1510        assert_eq!(parse_memory_bytes("1Ki").unwrap(), 1024);
1511        assert_eq!(parse_memory_bytes("1Mi").unwrap(), 1024 * 1024);
1512        assert_eq!(parse_memory_bytes("1Gi").unwrap(), 1024 * 1024 * 1024);
1513        assert_eq!(parse_memory_bytes("4Gi").unwrap(), 4 * 1024 * 1024 * 1024);
1514        assert_eq!(parse_memory_bytes("512Mi").unwrap(), 512 * 1024 * 1024);
1515        assert_eq!(
1516            parse_memory_bytes("1Ti").unwrap(),
1517            1024u64 * 1024 * 1024 * 1024
1518        );
1519    }
1520
1521    #[test]
1522    fn test_parse_memory_decimal_suffixes() {
1523        assert_eq!(parse_memory_bytes("1k").unwrap(), 1000);
1524        assert_eq!(parse_memory_bytes("1M").unwrap(), 1_000_000);
1525        assert_eq!(parse_memory_bytes("1G").unwrap(), 1_000_000_000);
1526        assert_eq!(parse_memory_bytes("1T").unwrap(), 1_000_000_000_000);
1527    }
1528
1529    #[test]
1530    fn test_parse_memory_plain_bytes() {
1531        assert_eq!(parse_memory_bytes("1024").unwrap(), 1024);
1532        assert_eq!(parse_memory_bytes("0").unwrap(), 0);
1533    }
1534
1535    #[test]
1536    fn test_parse_memory_invalid() {
1537        assert!(parse_memory_bytes("").is_err());
1538        assert!(parse_memory_bytes("abc").is_err());
1539        assert!(parse_memory_bytes("Gi").is_err());
1540    }
1541
1542    #[test]
1543    fn test_parse_memory_fractional() {
1544        assert_eq!(parse_memory_bytes("0.5Gi").unwrap(), GI / 2);
1545        assert_eq!(parse_memory_bytes("1.5Gi").unwrap(), GI + GI / 2);
1546    }
1547
1548    // -- Catalog lookup tests --
1549
1550    #[test]
1551    fn test_catalog_has_entries_for_all_cloud_platforms() {
1552        assert!(!catalog_for_platform(Platform::Aws).is_empty());
1553        assert!(!catalog_for_platform(Platform::Gcp).is_empty());
1554        assert!(!catalog_for_platform(Platform::Azure).is_empty());
1555    }
1556
1557    #[test]
1558    fn test_catalog_no_entries_for_non_cloud_platforms() {
1559        assert!(catalog_for_platform(Platform::Local).is_empty());
1560        assert!(catalog_for_platform(Platform::Kubernetes).is_empty());
1561    }
1562
1563    #[test]
1564    fn test_find_known_instance_type() {
1565        let spec =
1566            find_instance_type(Platform::Aws, "m7g.2xlarge").expect("should find m7g.2xlarge");
1567        assert_eq!(spec.vcpu, 8);
1568        assert_eq!(spec.memory_bytes, 32 * GI);
1569        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1570    }
1571
1572    #[test]
1573    fn test_find_aws_c8i_nested_virt_instance_type() {
1574        let spec = find_instance_type(Platform::Aws, "c8i.large").expect("should find c8i.large");
1575        assert_eq!(spec.vcpu, 2);
1576        assert_eq!(spec.memory_bytes, 4 * GI);
1577        assert_eq!(spec.family, InstanceFamily::ComputeOptimized);
1578        assert_eq!(spec.architecture, Architecture::X86_64);
1579        assert!(spec.is_nested_virt_capable());
1580    }
1581
1582    #[test]
1583    fn test_find_unknown_instance_type() {
1584        assert!(find_instance_type(Platform::Aws, "nonexistent.xlarge").is_none());
1585    }
1586
1587    #[test]
1588    fn test_find_wrong_platform() {
1589        assert!(find_instance_type(Platform::Gcp, "m7g.2xlarge").is_none());
1590    }
1591
1592    #[test]
1593    fn test_to_machine_profile() {
1594        let spec = find_instance_type(Platform::Aws, "m7g.2xlarge").unwrap();
1595        let profile = spec.to_machine_profile();
1596        assert_eq!(profile.cpu, "8.0");
1597        assert_eq!(profile.memory_bytes, 32 * GI);
1598        assert_eq!(profile.ephemeral_storage_bytes, 20 * GI);
1599        assert!(profile.gpu.is_none());
1600    }
1601
1602    #[test]
1603    fn test_to_machine_profile_with_gpu() {
1604        let spec = find_instance_type(Platform::Aws, "p4d.24xlarge").unwrap();
1605        let profile = spec.to_machine_profile();
1606        let gpu = profile.gpu.as_ref().expect("should have GPU");
1607        assert_eq!(gpu.gpu_type, "nvidia-a100");
1608        assert_eq!(gpu.count, 8);
1609    }
1610
1611    // -- Selection algorithm tests --
1612
1613    #[test]
1614    fn test_select_burstable_for_small_workload() {
1615        let req = WorkloadRequirements {
1616            total_cpu_at_desired: 1.0,
1617            total_memory_bytes_at_desired: 2 * GI,
1618            total_cpu_at_max: 1.0,
1619            total_memory_bytes_at_max: 2 * GI,
1620            max_cpu_per_container: 0.5,
1621            max_memory_per_container: 1 * GI,
1622            max_ephemeral_storage_bytes: 10 * GI,
1623            gpu: None,
1624            architecture: None,
1625            nested_virt: false,
1626        };
1627        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1628        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1629        assert_eq!(spec.family, InstanceFamily::Burstable);
1630    }
1631
1632    #[test]
1633    fn test_selects_smallest_burstable_machine_with_real_headroom() {
1634        let req = WorkloadRequirements {
1635            total_cpu_at_desired: 1.0,
1636            total_memory_bytes_at_desired: 2 * GI,
1637            total_cpu_at_max: 1.0,
1638            total_memory_bytes_at_max: 2 * GI,
1639            max_cpu_per_container: 1.0,
1640            max_memory_per_container: 2 * GI,
1641            max_ephemeral_storage_bytes: 10 * GI,
1642            gpu: None,
1643            architecture: None,
1644            nested_virt: false,
1645        };
1646
1647        let selection = select_instance_type(Platform::Aws, &req).unwrap();
1648
1649        assert_eq!(selection.instance_type, "t4g.medium");
1650        assert_eq!(selection.min_machines, 1);
1651        assert_eq!(selection.max_machines, 1);
1652    }
1653
1654    #[test]
1655    fn test_select_general_purpose_for_standard_workload() {
1656        // Standard workloads always get GeneralPurpose regardless of CPU:memory ratio
1657        let req = WorkloadRequirements {
1658            total_cpu_at_desired: 20.0,
1659            total_memory_bytes_at_desired: 80 * GI,
1660            total_cpu_at_max: 20.0,
1661            total_memory_bytes_at_max: 80 * GI,
1662            max_cpu_per_container: 2.0,
1663            max_memory_per_container: 8 * GI,
1664            max_ephemeral_storage_bytes: 10 * GI,
1665            gpu: None,
1666            architecture: None,
1667            nested_virt: false,
1668        };
1669        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1670        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1671        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1672    }
1673
1674    #[test]
1675    fn test_select_general_purpose_even_for_cpu_heavy() {
1676        // CPU-heavy workloads still get GeneralPurpose (no more ComputeOptimized auto-select)
1677        let req = WorkloadRequirements {
1678            total_cpu_at_desired: 20.0,
1679            total_memory_bytes_at_desired: 20 * GI,
1680            total_cpu_at_max: 20.0,
1681            total_memory_bytes_at_max: 20 * GI,
1682            max_cpu_per_container: 2.0,
1683            max_memory_per_container: 2 * GI,
1684            max_ephemeral_storage_bytes: 10 * GI,
1685            gpu: None,
1686            architecture: None,
1687            nested_virt: false,
1688        };
1689        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1690        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1691        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1692    }
1693
1694    #[test]
1695    fn test_select_storage_optimized_for_large_ephemeral() {
1696        let req = WorkloadRequirements {
1697            total_cpu_at_desired: 8.0,
1698            total_memory_bytes_at_desired: 32 * GI,
1699            total_cpu_at_max: 8.0,
1700            total_memory_bytes_at_max: 32 * GI,
1701            max_cpu_per_container: 2.0,
1702            max_memory_per_container: 8 * GI,
1703            max_ephemeral_storage_bytes: 500 * GI,
1704            gpu: None,
1705            architecture: Some(Architecture::X86_64),
1706            nested_virt: false,
1707        };
1708        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1709        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1710        assert_eq!(spec.family, InstanceFamily::StorageOptimized);
1711    }
1712
1713    #[test]
1714    fn test_select_gpu_instance() {
1715        let req = WorkloadRequirements {
1716            total_cpu_at_desired: 8.0,
1717            total_memory_bytes_at_desired: 32 * GI,
1718            total_cpu_at_max: 8.0,
1719            total_memory_bytes_at_max: 32 * GI,
1720            max_cpu_per_container: 4.0,
1721            max_memory_per_container: 16 * GI,
1722            max_ephemeral_storage_bytes: 10 * GI,
1723            gpu: Some(GpuSpec {
1724                gpu_type: "nvidia-a100".to_string(),
1725                count: 1,
1726            }),
1727            architecture: Some(Architecture::X86_64),
1728            nested_virt: false,
1729        };
1730        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1731        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1732        assert_eq!(spec.family, InstanceFamily::GpuCompute);
1733        assert!(spec.gpu.is_some());
1734    }
1735
1736    #[test]
1737    fn test_select_uses_each_cloud_image_target_architecture() {
1738        let req = WorkloadRequirements {
1739            total_cpu_at_desired: 4.0,
1740            total_memory_bytes_at_desired: 16 * GI,
1741            total_cpu_at_max: 4.0,
1742            total_memory_bytes_at_max: 16 * GI,
1743            max_cpu_per_container: 1.0,
1744            max_memory_per_container: 4 * GI,
1745            max_ephemeral_storage_bytes: 10 * GI,
1746            gpu: None,
1747            architecture: None,
1748            nested_virt: false,
1749        };
1750        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1751            let sel = select_instance_type(platform, &req)
1752                .unwrap_or_else(|error| panic!("selection failed for {platform}: {error}"));
1753            let spec = find_instance_type(platform, sel.instance_type)
1754                .expect("selected machine should exist in the catalog");
1755            assert_eq!(
1756                Some(spec.architecture),
1757                default_architecture(platform),
1758                "machine architecture must match the image target for {platform}"
1759            );
1760        }
1761    }
1762
1763    #[test]
1764    fn test_machine_count_reasonable() {
1765        // Single container: 1 CPU, 2Gi, maxReplicas=20
1766        let req = WorkloadRequirements {
1767            total_cpu_at_desired: 20.0,
1768            total_memory_bytes_at_desired: 40 * GI,
1769            total_cpu_at_max: 20.0,
1770            total_memory_bytes_at_max: 40 * GI,
1771            max_cpu_per_container: 1.0,
1772            max_memory_per_container: 2 * GI,
1773            max_ephemeral_storage_bytes: 10 * GI,
1774            gpu: None,
1775            architecture: None,
1776            nested_virt: false,
1777        };
1778        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1779        assert!(sel.min_machines >= 1);
1780        assert!(sel.max_machines <= MAX_MACHINES_PER_CLUSTER);
1781        assert!(sel.max_machines >= sel.min_machines);
1782    }
1783
1784    #[test]
1785    fn test_instance_size_capped_at_8_vcpu() {
1786        // Even with very large containers, instance size is capped at 8 vCPUs
1787        let req = WorkloadRequirements {
1788            total_cpu_at_desired: 70.0,
1789            total_memory_bytes_at_desired: 140 * GI,
1790            total_cpu_at_max: 70.0,
1791            total_memory_bytes_at_max: 140 * GI,
1792            max_cpu_per_container: 2.0,
1793            max_memory_per_container: 4 * GI,
1794            max_ephemeral_storage_bytes: 10 * GI,
1795            gpu: None,
1796            architecture: None,
1797            nested_virt: false,
1798        };
1799        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1800        let spec = find_instance_type(Platform::Gcp, sel.instance_type).unwrap();
1801        assert!(
1802            spec.vcpu <= MAX_STANDARD_VCPU,
1803            "selected {} with {} vCPUs, expected <= {}",
1804            spec.name,
1805            spec.vcpu,
1806            MAX_STANDARD_VCPU
1807        );
1808        assert_eq!(spec.family, InstanceFamily::GeneralPurpose);
1809        // Should scale horizontally instead
1810        assert!(sel.max_machines > 1);
1811    }
1812
1813    #[test]
1814    fn test_larger_autoscaled_workload_gets_reasonable_instance() {
1815        // Simulates a larger autoscaled workload: 4 containers, each 2 CPU / 4 GiB
1816        // maxReplicas: 10, 10, 10, 5
1817        let req = WorkloadRequirements {
1818            total_cpu_at_desired: 70.0,
1819            total_memory_bytes_at_desired: 140 * GI,
1820            total_cpu_at_max: 70.0,              // 2*10 + 2*10 + 2*10 + 2*5
1821            total_memory_bytes_at_max: 140 * GI, // 4*10 + 4*10 + 4*10 + 4*5
1822            max_cpu_per_container: 2.0,
1823            max_memory_per_container: 4 * GI,
1824            max_ephemeral_storage_bytes: 20 * GI,
1825            gpu: None,
1826            architecture: None,
1827            nested_virt: false,
1828        };
1829        let sel = select_instance_type(Platform::Gcp, &req).unwrap();
1830        // Should pick n2-standard-8 (8 vCPU, 32 GiB) — NOT c3-standard-44
1831        assert_eq!(sel.instance_type, "n2-standard-8");
1832        assert!(sel.max_machines >= 2);
1833    }
1834
1835    /// When `nested_virt` is set on the workload, the selector must
1836    /// restrict to nested-virt-capable families. On AWS that means an m8i
1837    /// (or other 8th-gen Intel) entry, never a Graviton (`*7g`, `t4g`) or
1838    /// burstable. Without this filter the launch template gets created
1839    /// with `CpuOptions.NestedVirtualization=enabled` paired with an
1840    /// instance type AWS rejects at RunInstances.
1841    #[test]
1842    fn test_select_aws_picks_m8i_when_nested_virt_required() {
1843        let req = WorkloadRequirements {
1844            total_cpu_at_desired: 4.0,
1845            total_memory_bytes_at_desired: 8 * GI,
1846            total_cpu_at_max: 4.0,
1847            total_memory_bytes_at_max: 8 * GI,
1848            max_cpu_per_container: 4.0,
1849            max_memory_per_container: 8 * GI,
1850            max_ephemeral_storage_bytes: 10 * GI,
1851            gpu: None,
1852            architecture: Some(Architecture::X86_64),
1853            nested_virt: true,
1854        };
1855        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1856        assert!(
1857            sel.instance_type.starts_with("m8i.")
1858                || sel.instance_type.starts_with("c8i.")
1859                || sel.instance_type.starts_with("r8i."),
1860            "expected an m8i/c8i/r8i instance, got {}",
1861            sel.instance_type
1862        );
1863        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1864        assert!(spec.is_nested_virt_capable());
1865    }
1866
1867    #[test]
1868    fn test_select_aws_defaults_to_image_target_architecture() {
1869        let req = WorkloadRequirements {
1870            total_cpu_at_desired: 4.0,
1871            total_memory_bytes_at_desired: 8 * GI,
1872            total_cpu_at_max: 4.0,
1873            total_memory_bytes_at_max: 8 * GI,
1874            max_cpu_per_container: 4.0,
1875            max_memory_per_container: 8 * GI,
1876            max_ephemeral_storage_bytes: 10 * GI,
1877            gpu: None,
1878            architecture: None,
1879            nested_virt: false,
1880        };
1881        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1882        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1883        assert_eq!(spec.architecture, Architecture::Arm64);
1884    }
1885
1886    #[test]
1887    fn test_cloud_defaults_match_image_target_architectures() {
1888        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1889            let target = BinaryTarget::defaults_for_platform(platform)
1890                .into_iter()
1891                .next()
1892                .expect("managed cloud should have a default image target");
1893            let image_architecture = match target.oci_arch() {
1894                "arm64" => Architecture::Arm64,
1895                "amd64" => Architecture::X86_64,
1896                architecture => {
1897                    panic!("unsupported managed-cloud image architecture {architecture}")
1898                }
1899            };
1900
1901            assert_eq!(default_architecture(platform), Some(image_architecture));
1902        }
1903    }
1904
1905    /// ARM remains available when the workload or capacity profile declares it.
1906    #[test]
1907    fn test_select_aws_uses_graviton_for_explicit_arm64() {
1908        let req = WorkloadRequirements {
1909            total_cpu_at_desired: 4.0,
1910            total_memory_bytes_at_desired: 8 * GI,
1911            total_cpu_at_max: 4.0,
1912            total_memory_bytes_at_max: 8 * GI,
1913            max_cpu_per_container: 4.0,
1914            max_memory_per_container: 8 * GI,
1915            max_ephemeral_storage_bytes: 10 * GI,
1916            gpu: None,
1917            architecture: Some(Architecture::Arm64),
1918            nested_virt: false,
1919        };
1920        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1921        let spec = find_instance_type(Platform::Aws, sel.instance_type).unwrap();
1922        assert_eq!(spec.architecture, Architecture::Arm64);
1923    }
1924
1925    #[test]
1926    fn test_select_rejects_explicit_architecture_missing_from_cloud_catalog() {
1927        let req = WorkloadRequirements {
1928            total_cpu_at_desired: 1.0,
1929            total_memory_bytes_at_desired: 2 * GI,
1930            total_cpu_at_max: 1.0,
1931            total_memory_bytes_at_max: 2 * GI,
1932            max_cpu_per_container: 1.0,
1933            max_memory_per_container: 2 * GI,
1934            max_ephemeral_storage_bytes: 10 * GI,
1935            gpu: None,
1936            architecture: Some(Architecture::Arm64),
1937            nested_virt: false,
1938        };
1939
1940        let error = select_instance_type(Platform::Gcp, &req)
1941            .expect_err("GCP catalog has no ARM64 machine");
1942
1943        assert!(error.contains("architecture Arm64 is unavailable"));
1944    }
1945
1946    #[test]
1947    fn test_profile_has_required_fields() {
1948        let req = WorkloadRequirements {
1949            total_cpu_at_desired: 4.0,
1950            total_memory_bytes_at_desired: 16 * GI,
1951            total_cpu_at_max: 4.0,
1952            total_memory_bytes_at_max: 16 * GI,
1953            max_cpu_per_container: 1.0,
1954            max_memory_per_container: 4 * GI,
1955            max_ephemeral_storage_bytes: 10 * GI,
1956            gpu: None,
1957            architecture: None,
1958            nested_virt: false,
1959        };
1960        let sel = select_instance_type(Platform::Aws, &req).unwrap();
1961        assert!(!sel.profile.cpu.is_empty());
1962        assert!(sel.profile.memory_bytes > 0);
1963        assert!(sel.profile.ephemeral_storage_bytes > 0);
1964    }
1965
1966    #[test]
1967    fn test_error_for_unsupported_gpu_type() {
1968        let req = WorkloadRequirements {
1969            total_cpu_at_desired: 8.0,
1970            total_memory_bytes_at_desired: 32 * GI,
1971            total_cpu_at_max: 8.0,
1972            total_memory_bytes_at_max: 32 * GI,
1973            max_cpu_per_container: 4.0,
1974            max_memory_per_container: 16 * GI,
1975            max_ephemeral_storage_bytes: 10 * GI,
1976            gpu: Some(GpuSpec {
1977                gpu_type: "amd-mi300".to_string(),
1978                count: 1,
1979            }),
1980            architecture: None,
1981            nested_virt: false,
1982        };
1983        let result = select_instance_type(Platform::Aws, &req);
1984        assert!(result.is_err());
1985    }
1986
1987    #[test]
1988    fn test_catalog_instance_types_sorted_by_vcpu_within_family() {
1989        // Verify that within each (platform, family) group, vcpu is non-decreasing.
1990        // This ensures our "min_by_key(vcpu)" logic works correctly.
1991        for platform in [Platform::Aws, Platform::Gcp, Platform::Azure] {
1992            let entries = catalog_for_platform(platform);
1993            let mut by_family: std::collections::HashMap<_, Vec<_>> =
1994                std::collections::HashMap::new();
1995            for entry in entries {
1996                by_family
1997                    .entry(format!("{:?}", entry.family))
1998                    .or_default()
1999                    .push(entry);
2000            }
2001            for (family, instances) in &by_family {
2002                for window in instances.windows(2) {
2003                    assert!(
2004                        window[0].vcpu <= window[1].vcpu,
2005                        "catalog not sorted by vcpu for {platform}/{family}: {} ({}) > {} ({})",
2006                        window[0].name,
2007                        window[0].vcpu,
2008                        window[1].name,
2009                        window[1].vcpu
2010                    );
2011                }
2012            }
2013        }
2014    }
2015}