use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonClusterConfig {
pub cluster_id: String,
pub management_token: String,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub enum HorizonMachineArchitecture {
#[serde(rename = "arm64")]
Arm64,
#[serde(rename = "amd64")]
Amd64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonAwsMachineImages {
pub amis: HashMap<HorizonMachineArchitecture, HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonGcpMachineImage {
pub source_image: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonGcpMachineImages {
pub images: HashMap<HorizonMachineArchitecture, HorizonGcpMachineImage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonAzureMachineImage {
pub image_version_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonMachineBaseImage {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonAzureMachineImages {
pub images: HashMap<HorizonMachineArchitecture, HorizonAzureMachineImage>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizondArtifact {
#[serde(default)]
pub runtime_isolation_generation: u32,
pub url: String,
pub sha256: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonMachineImage {
#[serde(default)]
pub runtime_isolation_generation: u32,
pub channel: String,
pub machine_image_version: String,
pub horizond_version: String,
pub git_sha: String,
pub created_at: String,
pub base_image: HorizonMachineBaseImage,
pub horizond_artifacts: HashMap<String, HorizondArtifact>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub aws: Option<HorizonAwsMachineImages>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub gcp: Option<HorizonGcpMachineImages>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub azure: Option<HorizonAzureMachineImages>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "camelCase")]
pub struct HorizonConfig {
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub horizon_machine_image: Option<HorizonMachineImage>,
pub clusters: HashMap<String, HorizonClusterConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ComputeBackend {
Horizon(HorizonConfig),
}
#[cfg(test)]
mod tests {
use serde_json::{json, Value};
use super::{HorizonMachineImage, HorizondArtifact};
fn legacy_catalog() -> Value {
json!({
"channel": "stable",
"machineImageVersion": "1",
"horizondVersion": "1",
"gitSha": "abc123",
"createdAt": "2026-01-01T00:00:00Z",
"baseImage": { "name": "linux", "version": "1" },
"horizondArtifacts": {
"linux-arm64": { "url": "https://example.com/arm64", "sha256": "a".repeat(64) },
"linux-amd64": { "url": "https://example.com/amd64", "sha256": "b".repeat(64) }
},
"aws": { "amis": { "arm64": { "us-east-1": "ami-example" } } },
"gcp": { "images": { "arm64": { "sourceImage": "example-image" } } },
"azure": { "images": { "arm64": { "imageVersionId": "example-version" } } }
})
}
#[test]
fn runtime_isolation_generation_legacy_catalog_defaults_to_zero() {
let mut expected = legacy_catalog();
let catalog: HorizonMachineImage = serde_json::from_value(expected.clone()).unwrap();
assert_eq!(catalog.runtime_isolation_generation, 0);
expected["runtimeIsolationGeneration"] = json!(0);
for (arch, artifact) in &catalog.horizond_artifacts {
assert_eq!(artifact.runtime_isolation_generation, 0);
let decoded: HorizondArtifact =
serde_json::from_value(expected["horizondArtifacts"][arch].clone()).unwrap();
assert_eq!(&decoded, artifact);
expected["horizondArtifacts"][arch]["runtimeIsolationGeneration"] = json!(0);
assert_eq!(
serde_json::to_value(decoded).unwrap(),
expected["horizondArtifacts"][arch]
);
}
assert_eq!(serde_json::to_value(catalog).unwrap(), expected);
}
#[test]
fn runtime_isolation_generation_roundtrips_catalog_and_each_artifact() {
let mut expected = legacy_catalog();
expected["runtimeIsolationGeneration"] = json!(1);
for artifact in expected["horizondArtifacts"]
.as_object_mut()
.unwrap()
.values_mut()
{
artifact["runtimeIsolationGeneration"] = json!(1);
}
let catalog: HorizonMachineImage = serde_json::from_value(expected.clone()).unwrap();
assert_eq!(catalog.runtime_isolation_generation, 1);
for (arch, artifact) in &catalog.horizond_artifacts {
assert_eq!(artifact.runtime_isolation_generation, 1);
let encoded = serde_json::to_value(artifact).unwrap();
assert_eq!(encoded, expected["horizondArtifacts"][arch]);
assert_eq!(
serde_json::from_value::<HorizondArtifact>(encoded).unwrap(),
*artifact
);
}
assert_eq!(serde_json::to_value(catalog).unwrap(), expected);
}
#[test]
fn runtime_isolation_generation_rejects_invalid_numbers_and_types() {
for invalid in [
json!(-1),
json!(1.5),
json!("1"),
json!(null),
json!(4294967296_u64),
] {
let mut catalog = legacy_catalog();
catalog["runtimeIsolationGeneration"] = invalid.clone();
assert!(serde_json::from_value::<HorizonMachineImage>(catalog).is_err());
for arch in ["linux-arm64", "linux-amd64"] {
let mut catalog = legacy_catalog();
catalog["horizondArtifacts"][arch]["runtimeIsolationGeneration"] = invalid.clone();
assert!(serde_json::from_value::<HorizondArtifact>(
catalog["horizondArtifacts"][arch].clone()
)
.is_err());
assert!(serde_json::from_value::<HorizonMachineImage>(catalog).is_err());
}
}
}
}