hyperi_rustlib/deployment/
app_project.rs1use std::fmt::Write as _;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct AppProjectDestination {
23 pub server: String,
25 pub namespace: String,
27}
28
29#[derive(Debug, Clone)]
33pub struct AppProjectContract {
34 pub name: String,
36 pub argocd_namespace: String,
38 pub description: String,
40 pub source_repos: Vec<String>,
43 pub destinations: Vec<AppProjectDestination>,
45 pub cluster_resource_allow: Vec<String>,
49 pub namespace_resource_allow: Vec<String>,
52}
53
54impl Default for AppProjectContract {
55 fn default() -> Self {
56 Self {
57 name: String::new(),
58 argocd_namespace: "argocd".into(),
59 description: String::new(),
60 source_repos: vec!["*".into()],
61 destinations: vec![AppProjectDestination {
62 server: "https://kubernetes.default.svc".into(),
63 namespace: "*".into(),
64 }],
65 cluster_resource_allow: vec!["*:*".into()],
66 namespace_resource_allow: vec!["*:*".into()],
67 }
68 }
69}
70
71#[must_use]
73pub fn generate_argocd_app_project(project: &AppProjectContract) -> String {
74 let mut out = String::new();
75 let _ = writeln!(out, "# AUTOGENERATED -- do not edit by hand.");
76 let _ = writeln!(
77 out,
78 "# Generated by hyperi-rustlib::deployment::generate_argocd_app_project()"
79 );
80 let _ = writeln!(out, "apiVersion: argoproj.io/v1alpha1");
81 let _ = writeln!(out, "kind: AppProject");
82 let _ = writeln!(out, "metadata:");
83 let _ = writeln!(out, " name: {}", project.name);
84 let _ = writeln!(out, " namespace: {}", project.argocd_namespace);
85 let _ = writeln!(out, "spec:");
86 let _ = writeln!(
87 out,
88 " description: {}",
89 quote_if_needed(&project.description)
90 );
91 let _ = writeln!(out, " sourceRepos:");
92 for repo in &project.source_repos {
93 let _ = writeln!(out, " - {repo}");
94 }
95 let _ = writeln!(out, " destinations:");
96 for dest in &project.destinations {
97 let _ = writeln!(out, " - server: {}", dest.server);
98 let _ = writeln!(out, " namespace: {}", dest.namespace);
99 }
100 let _ = writeln!(out, " clusterResourceWhitelist:");
101 for entry in &project.cluster_resource_allow {
102 let (group, kind) = split_resource(entry);
103 let _ = writeln!(out, " - group: {group}");
104 let _ = writeln!(out, " kind: {kind}");
105 }
106 let _ = writeln!(out, " namespaceResourceWhitelist:");
107 for entry in &project.namespace_resource_allow {
108 let (group, kind) = split_resource(entry);
109 let _ = writeln!(out, " - group: {group}");
110 let _ = writeln!(out, " kind: {kind}");
111 }
112 out
113}
114
115fn split_resource(entry: &str) -> (&str, &str) {
116 entry.split_once(':').unwrap_or(("*", "*"))
117}
118
119fn quote_if_needed(s: &str) -> String {
120 if s.contains(':') || s.contains('#') || s.is_empty() {
122 format!("\"{s}\"")
123 } else {
124 s.to_string()
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 fn sample_project() -> AppProjectContract {
133 AppProjectContract {
134 name: "hyperi-platform".into(),
135 description: "HyperI platform team".into(),
136 source_repos: vec![
137 "https://github.com/hyperi-io/gitops".into(),
138 "oci://ghcr.io/hyperi-io/helm-charts".into(),
139 ],
140 destinations: vec![AppProjectDestination {
141 server: "https://kubernetes.default.svc".into(),
142 namespace: "hyperi-dfe".into(),
143 }],
144 cluster_resource_allow: vec!["kafka.strimzi.io:KafkaTopic".into()],
145 namespace_resource_allow: vec!["*:*".into()],
146 ..Default::default()
147 }
148 }
149
150 #[test]
151 fn generate_produces_appproject_yaml() {
152 let yaml = generate_argocd_app_project(&sample_project());
153 assert!(yaml.contains("kind: AppProject"));
154 assert!(yaml.contains("name: hyperi-platform"));
155 }
156
157 #[test]
158 fn includes_source_repos() {
159 let yaml = generate_argocd_app_project(&sample_project());
160 assert!(yaml.contains("https://github.com/hyperi-io/gitops"));
161 assert!(yaml.contains("oci://ghcr.io/hyperi-io/helm-charts"));
162 }
163
164 #[test]
165 fn includes_destinations() {
166 let yaml = generate_argocd_app_project(&sample_project());
167 assert!(yaml.contains("server: https://kubernetes.default.svc"));
168 assert!(yaml.contains("namespace: hyperi-dfe"));
169 }
170
171 #[test]
172 fn cluster_resource_allow_splits_group_kind() {
173 let yaml = generate_argocd_app_project(&sample_project());
174 assert!(yaml.contains("group: kafka.strimzi.io"));
175 assert!(yaml.contains("kind: KafkaTopic"));
176 }
177
178 #[test]
179 fn star_kind_split_handles_namespace_wildcards() {
180 let yaml = generate_argocd_app_project(&sample_project());
181 assert!(yaml.contains("group: *"));
183 assert!(yaml.contains("kind: *"));
184 }
185
186 #[test]
187 fn default_contract_yields_unrestricted_project() {
188 let default = AppProjectContract::default();
189 assert_eq!(default.source_repos, vec!["*"]);
190 assert_eq!(default.cluster_resource_allow, vec!["*:*"]);
191 }
192
193 #[test]
194 fn description_with_colon_is_quoted() {
195 let project = AppProjectContract {
196 name: "test".into(),
197 description: "Team: platform".into(),
198 ..Default::default()
199 };
200 let yaml = generate_argocd_app_project(&project);
201 assert!(yaml.contains("description: \"Team: platform\""));
202 }
203
204 #[test]
205 fn empty_description_is_quoted() {
206 let project = AppProjectContract {
207 name: "test".into(),
208 description: String::new(),
209 ..Default::default()
210 };
211 let yaml = generate_argocd_app_project(&project);
212 assert!(yaml.contains("description: \"\""));
213 }
214
215 #[test]
216 fn multiple_destinations_all_present() {
217 let project = AppProjectContract {
218 name: "multi".into(),
219 destinations: vec![
220 AppProjectDestination {
221 server: "https://prod.k8s".into(),
222 namespace: "prod".into(),
223 },
224 AppProjectDestination {
225 server: "https://staging.k8s".into(),
226 namespace: "staging".into(),
227 },
228 ],
229 ..Default::default()
230 };
231 let yaml = generate_argocd_app_project(&project);
232 assert!(yaml.contains("server: https://prod.k8s"));
233 assert!(yaml.contains("namespace: prod"));
234 assert!(yaml.contains("server: https://staging.k8s"));
235 assert!(yaml.contains("namespace: staging"));
236 }
237}