1mod argocd;
18mod common;
19mod compose;
20mod dockerfile;
21mod helm;
22mod manifest;
23
24pub use argocd::{ArgocdConfig, generate_argocd_application};
25pub use compose::generate_compose_fragment;
26pub use dockerfile::{generate_dockerfile, generate_runtime_stage};
27pub use helm::generate_chart;
28pub use manifest::generate_container_manifest;
29
30#[cfg(test)]
32use crate::deployment::contract::{DeploymentContract, ImageProfile};
33#[cfg(test)]
34use common::{is_go_identifier, safe_template_lookup, to_camel_suffix};
35
36#[cfg(test)]
37mod tests {
38 use super::*;
39 use crate::deployment::contract::{
40 OciLabels, PortContract, SecretEnvContract, SecretGroupContract,
41 };
42 use crate::deployment::keda::KedaContract;
43 use crate::deployment::native_deps::NativeDepsContract;
44
45 fn test_contract() -> DeploymentContract {
46 DeploymentContract {
47 app_name: "dfe-loader".into(),
48 binary_name: "dfe-loader".into(),
49 description: "High-performance Kafka to ClickHouse data loader".into(),
50 metrics_port: 9090,
51 health: super::super::HealthContract::default(),
52 env_prefix: "DFE_LOADER".into(),
53 metric_prefix: "loader".into(),
54 config_mount_path: "/etc/dfe/loader.yaml".into(),
55 image_registry: "ghcr.io/hyperi-io".into(),
56 extra_ports: vec![],
57 entrypoint_args: vec!["--config".into(), "/etc/dfe/loader.yaml".into()],
58 secrets: vec![
59 SecretGroupContract {
60 group_name: "kafka".into(),
61 env_vars: vec![
62 SecretEnvContract {
63 env_var: "DFE_LOADER__KAFKA__USERNAME".into(),
64 key_name: "username".into(),
65 secret_key: "kafka-username".into(),
66 },
67 SecretEnvContract {
68 env_var: "DFE_LOADER__KAFKA__PASSWORD".into(),
69 key_name: "password".into(),
70 secret_key: "kafka-password".into(),
71 },
72 ],
73 },
74 SecretGroupContract {
75 group_name: "clickhouse".into(),
76 env_vars: vec![SecretEnvContract {
77 env_var: "DFE_LOADER__CLICKHOUSE__PASSWORD".into(),
78 key_name: "password".into(),
79 secret_key: "clickhouse-password".into(),
80 }],
81 },
82 ],
83 default_config: None,
84 depends_on: vec!["kafka".into(), "clickhouse".into()],
85 keda: Some(KedaContract::default()),
86 base_image: "ubuntu:24.04".into(),
87 native_deps: NativeDepsContract::default(),
88 image_profile: ImageProfile::default(),
89 schema_version: 2,
90 oci_labels: OciLabels::default(),
91 }
92 }
93
94 #[test]
95 fn test_generate_dockerfile() {
96 let contract = test_contract();
97 let dockerfile = generate_dockerfile(&contract, None);
98
99 assert!(dockerfile.contains("FROM ubuntu:24.04"));
100 assert!(dockerfile.contains("COPY dfe-loader /usr/local/bin/dfe-loader"));
101 assert!(dockerfile.contains("EXPOSE 9090"));
102 assert!(dockerfile.contains("localhost:9090/healthz"));
103 assert!(dockerfile.contains("ENTRYPOINT [\"dfe-loader\"]"));
104 assert!(dockerfile.contains("CMD [\"--config\", \"/etc/dfe/loader.yaml\"]"));
105 }
106
107 #[test]
108 fn test_generate_dockerfile_with_native_deps() {
109 let mut contract = test_contract();
110 contract.native_deps = NativeDepsContract::for_rustlib_features(
111 &["transport-kafka", "spool", "tiered-sink"],
112 "ubuntu:24.04",
113 );
114
115 let dockerfile = generate_dockerfile(&contract, None);
116
117 assert!(dockerfile.contains("packages.confluent.io"));
119 assert!(dockerfile.contains("confluent-clients.gpg"));
120 assert!(dockerfile.contains("librdkafka1"));
122 assert!(dockerfile.contains("libssl3"));
123 assert!(dockerfile.contains("libzstd1"));
124 assert!(dockerfile.contains("gnupg"));
126 }
127
128 #[test]
129 fn test_generate_dockerfile_no_native_deps() {
130 let mut contract = test_contract();
131 contract.native_deps = NativeDepsContract::for_rustlib_features(
132 &["cli", "deployment", "logger"],
133 "ubuntu:24.04",
134 );
135
136 let dockerfile = generate_dockerfile(&contract, None);
137
138 assert!(!dockerfile.contains("confluent"));
140 assert!(!dockerfile.contains("librdkafka1"));
141 assert!(!dockerfile.contains("gnupg"));
142 }
143
144 #[test]
145 fn test_generate_dockerfile_bookworm_codename() {
146 let mut contract = test_contract();
147 contract.base_image = "debian:bookworm-slim".into();
148 contract.native_deps =
149 NativeDepsContract::for_rustlib_features(&["transport-kafka"], "debian:bookworm-slim");
150
151 let dockerfile = generate_dockerfile(&contract, None);
152 assert!(dockerfile.contains("bookworm main"));
153 }
154
155 #[test]
156 fn test_generate_dockerfile_production_profile() {
157 let contract = test_contract();
158 let dockerfile = generate_dockerfile(&contract, None);
159
160 assert!(dockerfile.contains("Purpose: production container image"));
161 assert!(dockerfile.contains("io.hyperi.profile=\"production\""));
162 assert!(!dockerfile.contains("strace"));
163 assert!(!dockerfile.contains("tcpdump"));
164 }
165
166 #[test]
167 fn test_generate_dockerfile_dev_profile() {
168 let contract = test_contract().with_dev_profile();
169 let dockerfile = generate_dockerfile(&contract, None);
170
171 assert!(dockerfile.contains("Purpose: development container image"));
172 assert!(dockerfile.contains("io.hyperi.profile=\"development\""));
173 assert!(dockerfile.contains("strace"));
174 assert!(dockerfile.contains("tcpdump"));
175 assert!(dockerfile.contains("procps"));
176 assert!(dockerfile.contains("bash"));
177 assert!(dockerfile.contains("jq"));
178 }
179
180 #[test]
181 fn test_generate_dockerfile_dev_with_native_deps() {
182 let mut contract = test_contract();
183 contract.native_deps =
184 NativeDepsContract::for_rustlib_features(&["transport-kafka", "spool"], "ubuntu:24.04");
185 let dev = contract.with_dev_profile();
186 let dockerfile = generate_dockerfile(&dev, None);
187
188 assert!(dockerfile.contains("strace"));
190 assert!(dockerfile.contains("librdkafka1"));
191 assert!(dockerfile.contains("libzstd1"));
192 assert!(dockerfile.contains("io.hyperi.profile=\"development\""));
193 }
194
195 #[test]
196 fn test_with_dev_profile_preserves_contract() {
197 let contract = test_contract();
198 let dev = contract.with_dev_profile();
199
200 assert_eq!(dev.app_name, contract.app_name);
201 assert_eq!(dev.metrics_port, contract.metrics_port);
202 assert_eq!(dev.image_profile, ImageProfile::Development);
203 assert_eq!(contract.image_profile, ImageProfile::Production);
204 }
205
206 #[test]
207 fn test_generate_dockerfile_extra_ports() {
208 let mut contract = test_contract();
209 contract.extra_ports = vec![PortContract {
210 name: "http".into(),
211 port: 8080,
212 protocol: "TCP".into(),
213 }];
214
215 let dockerfile = generate_dockerfile(&contract, None);
216 assert!(dockerfile.contains("EXPOSE 9090 8080"));
217 }
218
219 #[test]
220 fn test_generate_compose_fragment() {
221 let contract = test_contract();
222 let compose = generate_compose_fragment(&contract);
223
224 assert!(compose.contains("dfe-loader:"));
225 assert!(compose.contains("ghcr.io/hyperi-io/dfe-loader"));
226 assert!(compose.contains("kafka:"));
227 assert!(compose.contains("clickhouse:"));
228 assert!(compose.contains("condition: service_healthy"));
229 assert!(compose.contains("\"9090:9090\""));
230 assert!(compose.contains("loader.yaml:/etc/dfe/loader.yaml:ro"));
231 }
232
233 #[test]
234 fn test_generate_chart() {
235 let contract = test_contract();
236 let dir = tempfile::tempdir().unwrap();
237
238 generate_chart(&contract, dir.path(), None).unwrap();
239
240 assert!(dir.path().join("Chart.yaml").exists());
242 assert!(dir.path().join("values.yaml").exists());
243 assert!(dir.path().join("templates/_helpers.tpl").exists());
244 assert!(dir.path().join("templates/deployment.yaml").exists());
245 assert!(dir.path().join("templates/service.yaml").exists());
246 assert!(dir.path().join("templates/serviceaccount.yaml").exists());
247 assert!(dir.path().join("templates/configmap.yaml").exists());
248 assert!(dir.path().join("templates/secret.yaml").exists());
249 assert!(dir.path().join("templates/hpa.yaml").exists());
250 assert!(dir.path().join("templates/keda-scaledobject.yaml").exists());
251 assert!(dir.path().join("templates/keda-triggerauth.yaml").exists());
252 assert!(dir.path().join("templates/NOTES.txt").exists());
253 }
254
255 #[test]
256 fn test_chart_yaml_content() {
257 let contract = test_contract();
258 let dir = tempfile::tempdir().unwrap();
259 generate_chart(&contract, dir.path(), None).unwrap();
260
261 let content = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
262 assert!(content.contains("name: dfe-loader"));
263 assert!(content.contains("description: High-performance Kafka to ClickHouse data loader"));
264 }
265
266 #[test]
267 fn test_values_yaml_content() {
268 let contract = test_contract();
269 let dir = tempfile::tempdir().unwrap();
270 generate_chart(&contract, dir.path(), None).unwrap();
271
272 let content = std::fs::read_to_string(dir.path().join("values.yaml")).unwrap();
273 assert!(content.contains("port: 9090"));
274 assert!(content.contains("prometheus.io/port: \"9090\""));
275 assert!(content.contains("prometheus.io/path: \"/metrics\""));
276 assert!(content.contains("lagThreshold: \"1000\""));
277 assert!(content.contains("kafka-username"));
278 assert!(content.contains("kafka-password"));
279 assert!(content.contains("clickhouse-password"));
280 }
281
282 #[test]
283 fn test_values_yaml_has_scaling_pressure_block() {
284 let contract = test_contract();
285 let dir = tempfile::tempdir().unwrap();
286 generate_chart(&contract, dir.path(), None).unwrap();
287
288 let content = std::fs::read_to_string(dir.path().join("values.yaml")).unwrap();
289 assert!(content.contains("scalingPressure:"));
292 assert!(content.contains("query: \"avg(loader_scaling_pressure)\""));
293 assert!(content.contains("threshold: \"70\""));
294 }
295
296 #[test]
297 fn test_keda_scaledobject_has_scaling_pressure_trigger() {
298 let contract = test_contract();
299 let dir = tempfile::tempdir().unwrap();
300 generate_chart(&contract, dir.path(), None).unwrap();
301
302 let keda_yaml =
303 std::fs::read_to_string(dir.path().join("templates/keda-scaledobject.yaml")).unwrap();
304 assert!(
306 keda_yaml.contains("if .Values.keda.scalingPressure.enabled"),
307 "scaledobject missing scalingPressure guard:\n{keda_yaml}"
308 );
309 assert!(keda_yaml.contains("type: prometheus"));
310 assert!(keda_yaml.contains(".Values.keda.scalingPressure.query"));
311 assert!(
312 keda_yaml.contains("metricType: Value"),
313 "capped per-pod composite uses avg()+Value (per KEDA.md):\n{keda_yaml}"
314 );
315 }
316
317 #[test]
318 fn test_helpers_contain_secret_helpers() {
319 let contract = test_contract();
320 let dir = tempfile::tempdir().unwrap();
321 generate_chart(&contract, dir.path(), None).unwrap();
322
323 let content = std::fs::read_to_string(dir.path().join("templates/_helpers.tpl")).unwrap();
324 assert!(content.contains("kafkaSecretName"));
325 assert!(content.contains("clickhouseSecretName"));
326 }
327
328 #[test]
329 fn test_deployment_contains_env_vars() {
330 let contract = test_contract();
331 let dir = tempfile::tempdir().unwrap();
332 generate_chart(&contract, dir.path(), None).unwrap();
333
334 let content =
335 std::fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
336 assert!(content.contains("DFE_LOADER__KAFKA__USERNAME"));
337 assert!(content.contains("DFE_LOADER__KAFKA__PASSWORD"));
338 assert!(content.contains("DFE_LOADER__CLICKHOUSE__PASSWORD"));
339 assert!(content.contains("path: /healthz"));
340 assert!(content.contains("path: /readyz"));
341 assert!(content.contains("/etc/dfe"));
342 }
343
344 #[test]
345 fn test_is_go_identifier() {
346 assert!(is_go_identifier("foo"));
348 assert!(is_go_identifier("FOO"));
349 assert!(is_go_identifier("foo_bar"));
350 assert!(is_go_identifier("_underscore_start"));
351 assert!(is_go_identifier("foo123"));
352 assert!(is_go_identifier("a"));
353
354 assert!(!is_go_identifier("bearer-tokens")); assert!(!is_go_identifier("foo.bar")); assert!(!is_go_identifier("123foo")); assert!(!is_go_identifier("")); assert!(!is_go_identifier("foo bar")); assert!(!is_go_identifier("foo:bar")); }
362
363 #[test]
364 fn test_safe_template_lookup_chooses_form() {
365 assert_eq!(
366 safe_template_lookup(".Values.auth", "username"),
367 ".Values.auth.username"
368 );
369 assert_eq!(
370 safe_template_lookup(".Values.auth", "bearer-tokens"),
371 "(index .Values.auth \"bearer-tokens\")"
372 );
373 assert_eq!(
374 safe_template_lookup(".Values.kafka.secretKeys", "kafka-username"),
375 "(index .Values.kafka.secretKeys \"kafka-username\")"
376 );
377 }
378
379 #[test]
386 fn test_keda_scaledobject_topic_lookup_is_lint_safe() {
387 let contract = test_contract();
388 let dir = tempfile::tempdir().unwrap();
389 generate_chart(&contract, dir.path(), None).unwrap();
390
391 let keda_yaml =
392 std::fs::read_to_string(dir.path().join("templates/keda-scaledobject.yaml")).unwrap();
393
394 assert!(
396 !keda_yaml.contains(
397 ".Values.keda.kafka.topic | default (index .Values.config.kafka.topics 0)"
398 ),
399 "keda-scaledobject.yaml still uses the eagerly-evaluated `default (index ...)` form:\n{keda_yaml}"
400 );
401
402 assert!(
404 keda_yaml.contains("if .Values.keda.kafka.topic"),
405 "keda-scaledobject.yaml missing if/else guard for topic lookup:\n{keda_yaml}"
406 );
407 assert!(
408 keda_yaml.contains("else if .Values.config.kafka.topics"),
409 "keda-scaledobject.yaml missing fallback branch for config.kafka.topics:\n{keda_yaml}"
410 );
411 }
412
413 #[test]
418 fn test_secret_yaml_handles_hyphenated_key_names() {
419 let mut contract = test_contract();
420 contract.secrets.push(SecretGroupContract {
422 group_name: "auth".into(),
423 env_vars: vec![SecretEnvContract {
424 env_var: "DFE_RECEIVER__AUTH__BEARER_TOKENS".into(),
425 key_name: "bearer-tokens".into(),
426 secret_key: "bearer-tokens".into(),
427 }],
428 });
429
430 let dir = tempfile::tempdir().unwrap();
431 generate_chart(&contract, dir.path(), None).unwrap();
432
433 let secret_yaml =
434 std::fs::read_to_string(dir.path().join("templates/secret.yaml")).unwrap();
435 let deployment_yaml =
436 std::fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
437
438 assert!(
440 !secret_yaml.contains(".Values.auth.bearer-tokens"),
441 "secret.yaml still uses broken dot-walked form for hyphenated key:\n{secret_yaml}"
442 );
443 assert!(
444 !secret_yaml.contains(".Values.auth.secretKeys.bearer-tokens"),
445 "secret.yaml still uses broken dot-walked form for hyphenated secretKeys lookup:\n{secret_yaml}"
446 );
447 assert!(
448 !deployment_yaml.contains(".Values.auth.secretKeys.bearer-tokens"),
449 "deployment.yaml still uses broken dot-walked form for hyphenated secretKeys lookup:\n{deployment_yaml}"
450 );
451
452 assert!(
454 secret_yaml.contains("(index .Values.auth.secretKeys \"bearer-tokens\")"),
455 "secret.yaml missing index-form lookup for secretKeys.bearer-tokens:\n{secret_yaml}"
456 );
457 assert!(
458 secret_yaml.contains("(index .Values.auth \"bearer-tokens\")"),
459 "secret.yaml missing index-form lookup for value bearer-tokens:\n{secret_yaml}"
460 );
461 assert!(
462 deployment_yaml.contains("(index .Values.auth.secretKeys \"bearer-tokens\")"),
463 "deployment.yaml missing index-form lookup for secretKeys.bearer-tokens:\n{deployment_yaml}"
464 );
465
466 assert!(
468 secret_yaml.contains(".Values.kafka.secretKeys.username"),
469 "Go-safe key 'username' should still use dot-walked form:\n{secret_yaml}"
470 );
471 }
472
473 #[test]
474 fn test_generate_argocd_application_default() {
475 let contract = test_contract();
476 let argo = ArgocdConfig {
477 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
478 ..Default::default()
479 };
480 let yaml = generate_argocd_application(&contract, &argo, None);
481
482 assert!(yaml.contains("apiVersion: argoproj.io/v1alpha1"));
483 assert!(yaml.contains("kind: Application"));
484 assert!(yaml.contains("name: dfe-loader"));
485 assert!(yaml.contains("namespace: argocd"));
486 assert!(yaml.contains("repoURL: https://github.com/hyperi-io/dfe-loader"));
487 assert!(yaml.contains("targetRevision: main"));
488 assert!(yaml.contains("path: chart"));
489 assert!(yaml.contains("CreateNamespace=true"));
490 assert!(yaml.contains("Schema version: "));
491 }
492
493 #[test]
494 fn test_generate_argocd_custom_namespace_and_path() {
495 let contract = test_contract();
496 let argo = ArgocdConfig {
497 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
498 dest_namespace: "production".into(),
499 chart_path: "deploy/chart".into(),
500 target_revision: "v1.0.0".into(),
501 sync_wave: 5,
502 ..Default::default()
503 };
504 let yaml = generate_argocd_application(&contract, &argo, None);
505 assert!(yaml.contains("namespace: production"));
506 assert!(yaml.contains("path: deploy/chart"));
507 assert!(yaml.contains("targetRevision: v1.0.0"));
508 assert!(yaml.contains("sync-wave: \"5\""));
509 }
510
511 #[test]
512 fn argocd_config_default_uses_wave_apps() {
513 let cfg = ArgocdConfig::default();
514 assert_eq!(cfg.sync_wave, crate::deployment::WAVE_APPS);
515 }
516
517 #[test]
518 fn argocd_config_default_has_no_extra_ignore_differences() {
519 let cfg = ArgocdConfig::default();
520 assert!(cfg.extra_ignore_differences.is_empty());
521 }
522
523 #[test]
524 fn generate_argocd_application_emits_default_ignore_differences() {
525 let contract = test_contract();
526 let argo = ArgocdConfig {
527 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
528 ..Default::default()
529 };
530 let yaml = generate_argocd_application(&contract, &argo, None);
531 assert!(yaml.contains("ignoreDifferences:"));
532 assert!(yaml.contains("/spec/replicas"));
533 assert!(yaml.contains("/spec/clusterIP"));
534 assert!(yaml.contains(".webhooks[].clientConfig.caBundle"));
535 }
536
537 #[test]
538 fn generate_argocd_application_appends_extra_ignore_differences() {
539 let contract = test_contract();
540 let argo = ArgocdConfig {
541 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
542 extra_ignore_differences: vec![
543 "- group: apps\n kind: Deployment\n jsonPointers:\n - /spec/template/spec/containers/0/image".into(),
544 ],
545 ..Default::default()
546 };
547 let yaml = generate_argocd_application(&contract, &argo, None);
548 assert!(yaml.contains("/spec/template/spec/containers/0/image"));
549 }
550
551 #[test]
552 fn generate_argocd_application_sync_wave_annotation_uses_config_value() {
553 let contract = test_contract();
554 let argo = ArgocdConfig {
555 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
556 sync_wave: crate::deployment::WAVE_TOPICS,
557 ..Default::default()
558 };
559 let yaml = generate_argocd_application(&contract, &argo, None);
560 assert!(yaml.contains(r#"argocd.argoproj.io/sync-wave: "-5""#));
561 }
562
563 #[test]
564 fn test_no_keda_files_when_disabled() {
565 let mut contract = test_contract();
566 contract.keda = None;
567
568 let dir = tempfile::tempdir().unwrap();
569 generate_chart(&contract, dir.path(), None).unwrap();
570
571 assert!(!dir.path().join("templates/keda-scaledobject.yaml").exists());
572 assert!(!dir.path().join("templates/keda-triggerauth.yaml").exists());
573 }
574
575 #[test]
576 fn test_to_camel_suffix() {
577 assert_eq!(to_camel_suffix("kafka"), "kafka");
578 assert_eq!(to_camel_suffix("clickhouse"), "clickhouse");
579 assert_eq!(to_camel_suffix("click_house"), "clickHouse");
580 assert_eq!(to_camel_suffix("my-service"), "myService");
581 }
582
583 fn test_identity() -> crate::deployment::ContractIdentity {
591 crate::deployment::ContractIdentity::new(
592 "0123456789abcdef0123456789abcdef01234567",
593 "ghcr.io/hyperi-io/dfe-loader:v2.7.2",
594 )
595 .expect("test fixture must be valid")
596 }
597
598 #[test]
599 fn dockerfile_omits_identity_block_when_none() {
600 let dockerfile = generate_dockerfile(&test_contract(), None);
601 assert!(!dockerfile.contains("io.hyperi.contract"));
602 }
603
604 #[test]
605 fn dockerfile_emits_three_identity_labels_when_some() {
606 let id = test_identity();
607 let dockerfile = generate_dockerfile(&test_contract(), Some(&id));
608 assert!(dockerfile.contains("LABEL io.hyperi.contract.version=\"v1\""));
609 assert!(dockerfile.contains(
610 "LABEL io.hyperi.contract.source-commit=\"0123456789abcdef0123456789abcdef01234567\""
611 ));
612 assert!(dockerfile.contains(
613 "LABEL io.hyperi.contract.image-ref=\"ghcr.io/hyperi-io/dfe-loader:v2.7.2\""
614 ));
615 assert!(dockerfile.contains("LABEL io.hyperi.profile=\"production\""));
617 }
618
619 #[test]
620 fn chart_yaml_omits_identity_block_when_none() {
621 let dir = tempfile::tempdir().unwrap();
622 generate_chart(&test_contract(), dir.path(), None).unwrap();
623 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
624 assert!(!chart.contains("io.hyperi.contract"));
625 }
626
627 #[test]
628 fn chart_yaml_emits_three_identity_annotations_when_some() {
629 let id = test_identity();
630 let dir = tempfile::tempdir().unwrap();
631 generate_chart(&test_contract(), dir.path(), Some(&id)).unwrap();
632 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
633 assert!(chart.contains("\nannotations:\n"));
635 assert!(chart.contains("io.hyperi.contract.version: \"v1\""));
636 assert!(chart.contains(
637 "io.hyperi.contract.source-commit: \"0123456789abcdef0123456789abcdef01234567\""
638 ));
639 assert!(
640 chart.contains("io.hyperi.contract.image-ref: \"ghcr.io/hyperi-io/dfe-loader:v2.7.2\"")
641 );
642 }
643
644 #[test]
645 fn argocd_application_omits_identity_block_when_none() {
646 let argo = ArgocdConfig::default();
647 let yaml = generate_argocd_application(&test_contract(), &argo, None);
648 assert!(!yaml.contains("io.hyperi.contract"));
649 assert!(yaml.contains("argocd.argoproj.io/sync-wave:"));
651 }
652
653 #[test]
654 fn argocd_application_emits_three_identity_annotations_when_some() {
655 let id = test_identity();
656 let argo = ArgocdConfig::default();
657 let yaml = generate_argocd_application(&test_contract(), &argo, Some(&id));
658 assert!(yaml.contains("argocd.argoproj.io/sync-wave:"));
661 assert!(yaml.contains("io.hyperi.contract.version: \"v1\""));
662 assert!(yaml.contains(
663 "io.hyperi.contract.source-commit: \"0123456789abcdef0123456789abcdef01234567\""
664 ));
665 assert!(
666 yaml.contains("io.hyperi.contract.image-ref: \"ghcr.io/hyperi-io/dfe-loader:v2.7.2\"")
667 );
668 }
669
670 #[test]
671 fn all_three_surfaces_share_the_same_key_prefix() {
672 let id = test_identity();
673 let argo = ArgocdConfig::default();
674 let dir = tempfile::tempdir().unwrap();
675
676 let dockerfile = generate_dockerfile(&test_contract(), Some(&id));
677 generate_chart(&test_contract(), dir.path(), Some(&id)).unwrap();
678 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
679 let app = generate_argocd_application(&test_contract(), &argo, Some(&id));
680
681 assert_eq!(dockerfile.matches("io.hyperi.contract").count(), 3);
684 assert_eq!(chart.matches("io.hyperi.contract").count(), 3);
685 assert_eq!(app.matches("io.hyperi.contract").count(), 3);
686 }
687}