hyperi_rustlib/deployment/registry.rs
1// Project: hyperi-rustlib
2// File: src/deployment/registry.rs
3// Purpose: Config-cascade-driven container registry resolution
4// Language: Rust
5//
6// License: BUSL-1.1
7// Copyright: (c) 2026 HYPERI PTY LIMITED
8
9//! Container registry resolution for deployment contracts.
10//!
11//! The publish-target registry (where the built image is pushed) and the
12//! base image (the `FROM` line) are org-wide decisions, not per-app. This
13//! module reads them from the config cascade so they live in YAML config
14//! rather than being hardcoded in each app's contract source.
15//!
16//! # Cascade keys
17//!
18//! ```yaml
19//! deployment:
20//! image_registry: ghcr.io/hyperi-io # default: ghcr.io/hyperi-io
21//! base_image: ubuntu:24.04 # default: ubuntu:24.04
22//! ```
23//!
24//! # Defaults
25//!
26//! - [`DEFAULT_IMAGE_REGISTRY`] = `ghcr.io/hyperi-io` -- where built images go
27//! - [`DEFAULT_BASE_IMAGE`] = `ubuntu:24.04` -- what the runtime stage builds on
28//!
29//! When (eventually) a curated GHCR base image lands at
30//! `ghcr.io/hyperi-io/dfe-base:ubuntu-24.04`, ops can override
31//! `deployment.base_image` in the cascade without rebuilding the apps.
32
33/// Default publish-target registry for HyperI org.
34///
35/// Combined with the contract's `app_name` to produce
36/// `<DEFAULT_IMAGE_REGISTRY>/<app_name>:<version>`.
37pub const DEFAULT_IMAGE_REGISTRY: &str = "ghcr.io/hyperi-io";
38
39/// Default base image for the runtime stage.
40///
41/// Pulled from Docker Hub (no registry prefix). To use a curated GHCR base,
42/// set `deployment.base_image` in the YAML cascade to e.g.
43/// `ghcr.io/hyperi-io/dfe-base:ubuntu-24.04` once that image exists.
44pub const DEFAULT_BASE_IMAGE: &str = "ubuntu:24.04";
45
46/// Read the publish-target image registry from the config cascade.
47///
48/// Reads `deployment.image_registry` from the YAML cascade. Falls back to
49/// [`DEFAULT_IMAGE_REGISTRY`] when not set, when config isn't loaded, or
50/// when the `config` feature is disabled.
51///
52/// # Example
53///
54/// ```rust,no_run
55/// use hyperi_rustlib::deployment::{DeploymentContract, image_registry_from_cascade};
56/// # fn dummy() -> DeploymentContract { unimplemented!() }
57/// let mut contract = dummy();
58/// contract.image_registry = image_registry_from_cascade();
59/// ```
60#[must_use]
61pub fn image_registry_from_cascade() -> String {
62 #[cfg(feature = "config")]
63 {
64 if let Some(cfg) = crate::config::try_get()
65 && let Some(s) = cfg.get_string("deployment.image_registry")
66 && !s.is_empty()
67 {
68 return s;
69 }
70 }
71 DEFAULT_IMAGE_REGISTRY.to_string()
72}
73
74/// Read the runtime base image from the config cascade.
75///
76/// Reads `deployment.base_image` from the YAML cascade. Falls back to
77/// [`DEFAULT_BASE_IMAGE`] when not set.
78#[must_use]
79pub fn base_image_from_cascade() -> String {
80 #[cfg(feature = "config")]
81 {
82 if let Some(cfg) = crate::config::try_get()
83 && let Some(s) = cfg.get_string("deployment.base_image")
84 && !s.is_empty()
85 {
86 return s;
87 }
88 }
89 DEFAULT_BASE_IMAGE.to_string()
90}
91
92/// Read the git repo URL for ArgoCD generation from the config cascade.
93///
94/// Reads `deployment.argocd.repo_url` from the YAML cascade. Falls back to
95/// `https://github.com/hyperi-io/{app_name}` if not set -- matches the org
96/// convention.
97#[must_use]
98pub fn argocd_repo_url_from_cascade(app_name: &str) -> String {
99 #[cfg(feature = "config")]
100 {
101 if let Some(cfg) = crate::config::try_get()
102 && let Some(s) = cfg.get_string("deployment.argocd.repo_url")
103 && !s.is_empty()
104 {
105 return s;
106 }
107 }
108 format!("https://github.com/hyperi-io/{app_name}")
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn defaults_are_ghcr_friendly() {
117 assert_eq!(DEFAULT_IMAGE_REGISTRY, "ghcr.io/hyperi-io");
118 assert_eq!(DEFAULT_BASE_IMAGE, "ubuntu:24.04");
119 }
120
121 #[test]
122 fn cascade_falls_back_to_defaults_when_no_config() {
123 // No config setup → returns defaults.
124 assert_eq!(image_registry_from_cascade(), DEFAULT_IMAGE_REGISTRY);
125 assert_eq!(base_image_from_cascade(), DEFAULT_BASE_IMAGE);
126 }
127}