Skip to main content

hyperi_rustlib/deployment/generate/
manifest.rs

1// Project:   hyperi-rustlib
2// File:      src/deployment/generate/manifest.rs
3// Purpose:   Container manifest (CI-consumable JSON) generation
4// Language:  Rust
5//
6// License:   BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9use crate::deployment::contract::{DeploymentContract, ImageProfile};
10
11// ============================================================================
12// Container Manifest (CI-consumable JSON)
13// ============================================================================
14
15/// Generate a container manifest JSON for CI consumption.
16///
17/// This is the minimal subset of the deployment contract that CI needs to
18/// build the container image. No secrets, no K8s-specific config.
19///
20/// # Errors
21///
22/// Returns an error string if JSON serialisation fails.
23pub fn generate_container_manifest(contract: &DeploymentContract) -> Result<String, String> {
24    let binary = contract.binary();
25
26    let apt_repos: Vec<serde_json::Value> = contract
27        .native_deps
28        .apt_repos
29        .iter()
30        .map(|r| {
31            serde_json::json!({
32                "key_url": r.key_url,
33                "keyring": r.keyring,
34                "url": r.url,
35                "codename": r.codename,
36                "packages": r.packages,
37            })
38        })
39        .collect();
40
41    let mut expose_ports: Vec<u16> = vec![contract.metrics_port];
42    expose_ports.extend(contract.extra_ports.iter().map(|p| p.port));
43
44    let profile_str = match contract.image_profile {
45        ImageProfile::Production => "production",
46        ImageProfile::Development => "development",
47    };
48
49    let title = if contract.oci_labels.title.is_empty() {
50        &contract.app_name
51    } else {
52        &contract.oci_labels.title
53    };
54
55    let manifest = serde_json::json!({
56        "schema_version": "1",
57        "app_name": contract.app_name,
58        "binary_name": binary,
59        "base_image": contract.base_image,
60        "image_registry": contract.image_registry,
61        "image_profile": profile_str,
62        "runtime_packages": {
63            "apt_repos": apt_repos,
64            "apt_packages": contract.native_deps.apt_packages,
65        },
66        "expose_ports": expose_ports,
67        "healthcheck": {
68            "path": contract.health.liveness_path,
69            "port": contract.metrics_port,
70            "interval": "30s",
71            "timeout": "3s",
72            "start_period": "5s",
73            "retries": 3,
74        },
75        "entrypoint": [binary],
76        "cmd": contract.entrypoint_args,
77        "user": "appuser",
78        "uid": 1000,
79        "labels": {
80            "io.hyperi.profile": profile_str,
81            "io.hyperi.app": contract.app_name,
82            "io.hyperi.metrics_port": contract.metrics_port.to_string(),
83            "org.opencontainers.image.title": title,
84            "org.opencontainers.image.description": contract.oci_labels.description,
85            "org.opencontainers.image.vendor": contract.oci_labels.vendor,
86            "org.opencontainers.image.licenses": contract.oci_labels.licenses,
87        },
88    });
89
90    serde_json::to_string_pretty(&manifest)
91        .map_err(|e| format!("container manifest JSON failed: {e}"))
92}