Skip to main content

alien_core/deployment/
compute.rs

1//! Compute backend configuration for container orchestration.
2
3use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7/// Configuration for a single container worker cluster.
8///
9/// Contains the cluster ID and management token needed to interact with
10/// the managed container control plane API for container operations.
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[serde(rename_all = "camelCase")]
14pub struct HorizonClusterConfig {
15    /// Cluster ID (deterministic: workspace/project/deployment/resourceid)
16    pub cluster_id: String,
17
18    /// Management token for API access (hm_...)
19    /// Used by alien-deployment controllers to create/update containers
20    pub management_token: String,
21}
22
23/// Horizon machine image architecture.
24#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
25#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
26#[serde(rename_all = "camelCase")]
27pub enum HorizonMachineArchitecture {
28    /// Linux arm64 / aarch64 machine image.
29    #[serde(rename = "arm64")]
30    Arm64,
31    /// Linux amd64 / x86_64 machine image.
32    #[serde(rename = "amd64")]
33    Amd64,
34}
35
36/// AWS Horizon machine image catalog.
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
39#[serde(rename_all = "camelCase")]
40pub struct HorizonAwsMachineImages {
41    /// AMI IDs by architecture, then AWS region.
42    pub amis: HashMap<HorizonMachineArchitecture, HashMap<String, String>>,
43}
44
45/// GCP Horizon machine image entry.
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
47#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
48#[serde(rename_all = "camelCase")]
49pub struct HorizonGcpMachineImage {
50    /// Source image self link or image-family URL.
51    pub source_image: String,
52}
53
54/// GCP Horizon machine image catalog.
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
56#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
57#[serde(rename_all = "camelCase")]
58pub struct HorizonGcpMachineImages {
59    /// Images by architecture.
60    pub images: HashMap<HorizonMachineArchitecture, HorizonGcpMachineImage>,
61}
62
63/// Azure Horizon machine image entry.
64#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
65#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
66#[serde(rename_all = "camelCase")]
67pub struct HorizonAzureMachineImage {
68    /// Azure Compute Gallery image version ID.
69    pub image_version_id: String,
70}
71
72/// Base image metadata for the Horizon machine image.
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
74#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
75#[serde(rename_all = "camelCase")]
76pub struct HorizonMachineBaseImage {
77    /// Base OS image name.
78    pub name: String,
79    /// Base OS image version or channel.
80    pub version: String,
81}
82
83/// Azure Horizon machine image catalog.
84#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
85#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
86#[serde(rename_all = "camelCase")]
87pub struct HorizonAzureMachineImages {
88    /// Images by architecture.
89    pub images: HashMap<HorizonMachineArchitecture, HorizonAzureMachineImage>,
90}
91
92/// Download artifact for one horizond release platform.
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
94#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
95#[serde(rename_all = "camelCase")]
96pub struct HorizondArtifact {
97    /// Runtime isolation capability generation of the immutable artifact, not live readiness.
98    #[serde(default)]
99    pub runtime_isolation_generation: u32,
100    /// HTTPS URL for the artifact.
101    pub url: String,
102    /// SHA-256 digest for the artifact payload.
103    pub sha256: String,
104}
105
106/// Horizon machine image catalog.
107///
108/// Platform resolves concrete provider images from this catalog during rollout.
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
110#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
111#[serde(rename_all = "camelCase")]
112pub struct HorizonMachineImage {
113    /// Runtime isolation capability generation of the immutable artifact, not live readiness.
114    #[serde(default)]
115    pub runtime_isolation_generation: u32,
116    /// Logical image channel, such as prod, staging, or canary.
117    pub channel: String,
118    /// Published immutable machine image version.
119    pub machine_image_version: String,
120    /// horizond daemon version baked into the image.
121    pub horizond_version: String,
122    /// Git commit SHA used to build the image.
123    pub git_sha: String,
124    /// Image manifest creation timestamp.
125    pub created_at: String,
126    /// Base OS image metadata.
127    pub base_image: HorizonMachineBaseImage,
128    /// Per-architecture horizond artifacts by release-platform key.
129    pub horizond_artifacts: HashMap<String, HorizondArtifact>,
130    /// AWS image catalog.
131    #[serde(default, skip_serializing_if = "Option::is_none")]
132    pub aws: Option<HorizonAwsMachineImages>,
133    /// GCP image catalog.
134    #[serde(default, skip_serializing_if = "Option::is_none")]
135    pub gcp: Option<HorizonGcpMachineImages>,
136    /// Azure image catalog.
137    #[serde(default, skip_serializing_if = "Option::is_none")]
138    pub azure: Option<HorizonAzureMachineImages>,
139}
140
141/// Horizon control-plane configuration for container orchestration.
142///
143/// Contains all the information needed for Alien to interact with managed
144/// container clusters during deployment. Each ComputeCluster resource gets its own
145/// entry in the clusters map.
146#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
147#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
148#[serde(rename_all = "camelCase")]
149pub struct HorizonConfig {
150    /// Horizon control-plane API base URL.
151    pub url: String,
152
153    /// Horizon machine image catalog.
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub horizon_machine_image: Option<HorizonMachineImage>,
156
157    /// Cluster configurations (one per ComputeCluster resource)
158    /// Key: ComputeCluster resource ID from stack
159    /// Value: Cluster ID and management token for that cluster
160    pub clusters: HashMap<String, HorizonClusterConfig>,
161}
162
163/// Compute backend for Container and Worker resources.
164///
165/// Determines how compute workloads are orchestrated on cloud platforms.
166/// When None, the platform default is used for cloud platforms.
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
169#[serde(tag = "type", rename_all = "camelCase")]
170pub enum ComputeBackend {
171    /// VM-backed container orchestration (default for cloud platforms)
172    Horizon(HorizonConfig),
173    // Future backends:
174    // /// Deploy to existing Kubernetes cluster (EKS/GKE/AKS)
175    // Kubernetes(KubernetesCredentials),
176    // /// AWS ECS Fargate (serverless containers)
177    // EcsFargate,
178}
179
180#[cfg(test)]
181mod tests {
182    use serde_json::{json, Value};
183
184    use super::{HorizonMachineImage, HorizondArtifact};
185
186    fn legacy_catalog() -> Value {
187        json!({
188            "channel": "stable",
189            "machineImageVersion": "1",
190            "horizondVersion": "1",
191            "gitSha": "abc123",
192            "createdAt": "2026-01-01T00:00:00Z",
193            "baseImage": { "name": "linux", "version": "1" },
194            "horizondArtifacts": {
195                "linux-arm64": { "url": "https://example.com/arm64", "sha256": "a".repeat(64) },
196                "linux-amd64": { "url": "https://example.com/amd64", "sha256": "b".repeat(64) }
197            },
198            "aws": { "amis": { "arm64": { "us-east-1": "ami-example" } } },
199            "gcp": { "images": { "arm64": { "sourceImage": "example-image" } } },
200            "azure": { "images": { "arm64": { "imageVersionId": "example-version" } } }
201        })
202    }
203
204    #[test]
205    fn runtime_isolation_generation_legacy_catalog_defaults_to_zero() {
206        let mut expected = legacy_catalog();
207        let catalog: HorizonMachineImage = serde_json::from_value(expected.clone()).unwrap();
208        assert_eq!(catalog.runtime_isolation_generation, 0);
209        expected["runtimeIsolationGeneration"] = json!(0);
210        for (arch, artifact) in &catalog.horizond_artifacts {
211            assert_eq!(artifact.runtime_isolation_generation, 0);
212            let decoded: HorizondArtifact =
213                serde_json::from_value(expected["horizondArtifacts"][arch].clone()).unwrap();
214            assert_eq!(&decoded, artifact);
215            expected["horizondArtifacts"][arch]["runtimeIsolationGeneration"] = json!(0);
216            assert_eq!(
217                serde_json::to_value(decoded).unwrap(),
218                expected["horizondArtifacts"][arch]
219            );
220        }
221        assert_eq!(serde_json::to_value(catalog).unwrap(), expected);
222    }
223
224    #[test]
225    fn runtime_isolation_generation_roundtrips_catalog_and_each_artifact() {
226        let mut expected = legacy_catalog();
227        expected["runtimeIsolationGeneration"] = json!(1);
228        for artifact in expected["horizondArtifacts"]
229            .as_object_mut()
230            .unwrap()
231            .values_mut()
232        {
233            artifact["runtimeIsolationGeneration"] = json!(1);
234        }
235        let catalog: HorizonMachineImage = serde_json::from_value(expected.clone()).unwrap();
236        assert_eq!(catalog.runtime_isolation_generation, 1);
237        for (arch, artifact) in &catalog.horizond_artifacts {
238            assert_eq!(artifact.runtime_isolation_generation, 1);
239            let encoded = serde_json::to_value(artifact).unwrap();
240            assert_eq!(encoded, expected["horizondArtifacts"][arch]);
241            assert_eq!(
242                serde_json::from_value::<HorizondArtifact>(encoded).unwrap(),
243                *artifact
244            );
245        }
246        assert_eq!(serde_json::to_value(catalog).unwrap(), expected);
247    }
248
249    #[test]
250    fn runtime_isolation_generation_rejects_invalid_numbers_and_types() {
251        for invalid in [
252            json!(-1),
253            json!(1.5),
254            json!("1"),
255            json!(null),
256            json!(4294967296_u64),
257        ] {
258            let mut catalog = legacy_catalog();
259            catalog["runtimeIsolationGeneration"] = invalid.clone();
260            assert!(serde_json::from_value::<HorizonMachineImage>(catalog).is_err());
261            for arch in ["linux-arm64", "linux-amd64"] {
262                let mut catalog = legacy_catalog();
263                catalog["horizondArtifacts"][arch]["runtimeIsolationGeneration"] = invalid.clone();
264                assert!(serde_json::from_value::<HorizondArtifact>(
265                    catalog["horizondArtifacts"][arch].clone()
266                )
267                .is_err());
268                assert!(serde_json::from_value::<HorizonMachineImage>(catalog).is_err());
269            }
270        }
271    }
272}