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_helpers_contain_secret_helpers() {
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("templates/_helpers.tpl")).unwrap();
289 assert!(content.contains("kafkaSecretName"));
290 assert!(content.contains("clickhouseSecretName"));
291 }
292
293 #[test]
294 fn test_deployment_contains_env_vars() {
295 let contract = test_contract();
296 let dir = tempfile::tempdir().unwrap();
297 generate_chart(&contract, dir.path(), None).unwrap();
298
299 let content =
300 std::fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
301 assert!(content.contains("DFE_LOADER__KAFKA__USERNAME"));
302 assert!(content.contains("DFE_LOADER__KAFKA__PASSWORD"));
303 assert!(content.contains("DFE_LOADER__CLICKHOUSE__PASSWORD"));
304 assert!(content.contains("path: /healthz"));
305 assert!(content.contains("path: /readyz"));
306 assert!(content.contains("/etc/dfe"));
307 }
308
309 #[test]
310 fn test_is_go_identifier() {
311 assert!(is_go_identifier("foo"));
313 assert!(is_go_identifier("FOO"));
314 assert!(is_go_identifier("foo_bar"));
315 assert!(is_go_identifier("_underscore_start"));
316 assert!(is_go_identifier("foo123"));
317 assert!(is_go_identifier("a"));
318
319 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")); }
327
328 #[test]
329 fn test_safe_template_lookup_chooses_form() {
330 assert_eq!(
331 safe_template_lookup(".Values.auth", "username"),
332 ".Values.auth.username"
333 );
334 assert_eq!(
335 safe_template_lookup(".Values.auth", "bearer-tokens"),
336 "(index .Values.auth \"bearer-tokens\")"
337 );
338 assert_eq!(
339 safe_template_lookup(".Values.kafka.secretKeys", "kafka-username"),
340 "(index .Values.kafka.secretKeys \"kafka-username\")"
341 );
342 }
343
344 #[test]
351 fn test_keda_scaledobject_topic_lookup_is_lint_safe() {
352 let contract = test_contract();
353 let dir = tempfile::tempdir().unwrap();
354 generate_chart(&contract, dir.path(), None).unwrap();
355
356 let keda_yaml =
357 std::fs::read_to_string(dir.path().join("templates/keda-scaledobject.yaml")).unwrap();
358
359 assert!(
361 !keda_yaml.contains(
362 ".Values.keda.kafka.topic | default (index .Values.config.kafka.topics 0)"
363 ),
364 "keda-scaledobject.yaml still uses the eagerly-evaluated `default (index ...)` form:\n{keda_yaml}"
365 );
366
367 assert!(
369 keda_yaml.contains("if .Values.keda.kafka.topic"),
370 "keda-scaledobject.yaml missing if/else guard for topic lookup:\n{keda_yaml}"
371 );
372 assert!(
373 keda_yaml.contains("else if .Values.config.kafka.topics"),
374 "keda-scaledobject.yaml missing fallback branch for config.kafka.topics:\n{keda_yaml}"
375 );
376 }
377
378 #[test]
383 fn test_secret_yaml_handles_hyphenated_key_names() {
384 let mut contract = test_contract();
385 contract.secrets.push(SecretGroupContract {
387 group_name: "auth".into(),
388 env_vars: vec![SecretEnvContract {
389 env_var: "DFE_RECEIVER__AUTH__BEARER_TOKENS".into(),
390 key_name: "bearer-tokens".into(),
391 secret_key: "bearer-tokens".into(),
392 }],
393 });
394
395 let dir = tempfile::tempdir().unwrap();
396 generate_chart(&contract, dir.path(), None).unwrap();
397
398 let secret_yaml =
399 std::fs::read_to_string(dir.path().join("templates/secret.yaml")).unwrap();
400 let deployment_yaml =
401 std::fs::read_to_string(dir.path().join("templates/deployment.yaml")).unwrap();
402
403 assert!(
405 !secret_yaml.contains(".Values.auth.bearer-tokens"),
406 "secret.yaml still uses broken dot-walked form for hyphenated key:\n{secret_yaml}"
407 );
408 assert!(
409 !secret_yaml.contains(".Values.auth.secretKeys.bearer-tokens"),
410 "secret.yaml still uses broken dot-walked form for hyphenated secretKeys lookup:\n{secret_yaml}"
411 );
412 assert!(
413 !deployment_yaml.contains(".Values.auth.secretKeys.bearer-tokens"),
414 "deployment.yaml still uses broken dot-walked form for hyphenated secretKeys lookup:\n{deployment_yaml}"
415 );
416
417 assert!(
419 secret_yaml.contains("(index .Values.auth.secretKeys \"bearer-tokens\")"),
420 "secret.yaml missing index-form lookup for secretKeys.bearer-tokens:\n{secret_yaml}"
421 );
422 assert!(
423 secret_yaml.contains("(index .Values.auth \"bearer-tokens\")"),
424 "secret.yaml missing index-form lookup for value bearer-tokens:\n{secret_yaml}"
425 );
426 assert!(
427 deployment_yaml.contains("(index .Values.auth.secretKeys \"bearer-tokens\")"),
428 "deployment.yaml missing index-form lookup for secretKeys.bearer-tokens:\n{deployment_yaml}"
429 );
430
431 assert!(
433 secret_yaml.contains(".Values.kafka.secretKeys.username"),
434 "Go-safe key 'username' should still use dot-walked form:\n{secret_yaml}"
435 );
436 }
437
438 #[test]
439 fn test_generate_argocd_application_default() {
440 let contract = test_contract();
441 let argo = ArgocdConfig {
442 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
443 ..Default::default()
444 };
445 let yaml = generate_argocd_application(&contract, &argo, None);
446
447 assert!(yaml.contains("apiVersion: argoproj.io/v1alpha1"));
448 assert!(yaml.contains("kind: Application"));
449 assert!(yaml.contains("name: dfe-loader"));
450 assert!(yaml.contains("namespace: argocd"));
451 assert!(yaml.contains("repoURL: https://github.com/hyperi-io/dfe-loader"));
452 assert!(yaml.contains("targetRevision: main"));
453 assert!(yaml.contains("path: chart"));
454 assert!(yaml.contains("CreateNamespace=true"));
455 assert!(yaml.contains("Schema version: "));
456 }
457
458 #[test]
459 fn test_generate_argocd_custom_namespace_and_path() {
460 let contract = test_contract();
461 let argo = ArgocdConfig {
462 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
463 dest_namespace: "production".into(),
464 chart_path: "deploy/chart".into(),
465 target_revision: "v1.0.0".into(),
466 sync_wave: 5,
467 ..Default::default()
468 };
469 let yaml = generate_argocd_application(&contract, &argo, None);
470 assert!(yaml.contains("namespace: production"));
471 assert!(yaml.contains("path: deploy/chart"));
472 assert!(yaml.contains("targetRevision: v1.0.0"));
473 assert!(yaml.contains("sync-wave: \"5\""));
474 }
475
476 #[test]
477 fn argocd_config_default_uses_wave_apps() {
478 let cfg = ArgocdConfig::default();
479 assert_eq!(cfg.sync_wave, crate::deployment::WAVE_APPS);
480 }
481
482 #[test]
483 fn argocd_config_default_has_no_extra_ignore_differences() {
484 let cfg = ArgocdConfig::default();
485 assert!(cfg.extra_ignore_differences.is_empty());
486 }
487
488 #[test]
489 fn generate_argocd_application_emits_default_ignore_differences() {
490 let contract = test_contract();
491 let argo = ArgocdConfig {
492 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
493 ..Default::default()
494 };
495 let yaml = generate_argocd_application(&contract, &argo, None);
496 assert!(yaml.contains("ignoreDifferences:"));
497 assert!(yaml.contains("/spec/replicas"));
498 assert!(yaml.contains("/spec/clusterIP"));
499 assert!(yaml.contains(".webhooks[].clientConfig.caBundle"));
500 }
501
502 #[test]
503 fn generate_argocd_application_appends_extra_ignore_differences() {
504 let contract = test_contract();
505 let argo = ArgocdConfig {
506 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
507 extra_ignore_differences: vec![
508 "- group: apps\n kind: Deployment\n jsonPointers:\n - /spec/template/spec/containers/0/image".into(),
509 ],
510 ..Default::default()
511 };
512 let yaml = generate_argocd_application(&contract, &argo, None);
513 assert!(yaml.contains("/spec/template/spec/containers/0/image"));
514 }
515
516 #[test]
517 fn generate_argocd_application_sync_wave_annotation_uses_config_value() {
518 let contract = test_contract();
519 let argo = ArgocdConfig {
520 repo_url: "https://github.com/hyperi-io/dfe-loader".into(),
521 sync_wave: crate::deployment::WAVE_TOPICS,
522 ..Default::default()
523 };
524 let yaml = generate_argocd_application(&contract, &argo, None);
525 assert!(yaml.contains(r#"argocd.argoproj.io/sync-wave: "-5""#));
526 }
527
528 #[test]
529 fn test_no_keda_files_when_disabled() {
530 let mut contract = test_contract();
531 contract.keda = None;
532
533 let dir = tempfile::tempdir().unwrap();
534 generate_chart(&contract, dir.path(), None).unwrap();
535
536 assert!(!dir.path().join("templates/keda-scaledobject.yaml").exists());
537 assert!(!dir.path().join("templates/keda-triggerauth.yaml").exists());
538 }
539
540 #[test]
541 fn test_to_camel_suffix() {
542 assert_eq!(to_camel_suffix("kafka"), "kafka");
543 assert_eq!(to_camel_suffix("clickhouse"), "clickhouse");
544 assert_eq!(to_camel_suffix("click_house"), "clickHouse");
545 assert_eq!(to_camel_suffix("my-service"), "myService");
546 }
547
548 fn test_identity() -> crate::deployment::ContractIdentity {
556 crate::deployment::ContractIdentity::new(
557 "0123456789abcdef0123456789abcdef01234567",
558 "ghcr.io/hyperi-io/dfe-loader:v2.7.2",
559 )
560 .expect("test fixture must be valid")
561 }
562
563 #[test]
564 fn dockerfile_omits_identity_block_when_none() {
565 let dockerfile = generate_dockerfile(&test_contract(), None);
566 assert!(!dockerfile.contains("io.hyperi.contract"));
567 }
568
569 #[test]
570 fn dockerfile_emits_three_identity_labels_when_some() {
571 let id = test_identity();
572 let dockerfile = generate_dockerfile(&test_contract(), Some(&id));
573 assert!(dockerfile.contains("LABEL io.hyperi.contract.version=\"v1\""));
574 assert!(dockerfile.contains(
575 "LABEL io.hyperi.contract.source-commit=\"0123456789abcdef0123456789abcdef01234567\""
576 ));
577 assert!(dockerfile.contains(
578 "LABEL io.hyperi.contract.image-ref=\"ghcr.io/hyperi-io/dfe-loader:v2.7.2\""
579 ));
580 assert!(dockerfile.contains("LABEL io.hyperi.profile=\"production\""));
582 }
583
584 #[test]
585 fn chart_yaml_omits_identity_block_when_none() {
586 let dir = tempfile::tempdir().unwrap();
587 generate_chart(&test_contract(), dir.path(), None).unwrap();
588 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
589 assert!(!chart.contains("io.hyperi.contract"));
590 }
591
592 #[test]
593 fn chart_yaml_emits_three_identity_annotations_when_some() {
594 let id = test_identity();
595 let dir = tempfile::tempdir().unwrap();
596 generate_chart(&test_contract(), dir.path(), Some(&id)).unwrap();
597 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
598 assert!(chart.contains("\nannotations:\n"));
600 assert!(chart.contains("io.hyperi.contract.version: \"v1\""));
601 assert!(chart.contains(
602 "io.hyperi.contract.source-commit: \"0123456789abcdef0123456789abcdef01234567\""
603 ));
604 assert!(
605 chart.contains("io.hyperi.contract.image-ref: \"ghcr.io/hyperi-io/dfe-loader:v2.7.2\"")
606 );
607 }
608
609 #[test]
610 fn argocd_application_omits_identity_block_when_none() {
611 let argo = ArgocdConfig::default();
612 let yaml = generate_argocd_application(&test_contract(), &argo, None);
613 assert!(!yaml.contains("io.hyperi.contract"));
614 assert!(yaml.contains("argocd.argoproj.io/sync-wave:"));
616 }
617
618 #[test]
619 fn argocd_application_emits_three_identity_annotations_when_some() {
620 let id = test_identity();
621 let argo = ArgocdConfig::default();
622 let yaml = generate_argocd_application(&test_contract(), &argo, Some(&id));
623 assert!(yaml.contains("argocd.argoproj.io/sync-wave:"));
626 assert!(yaml.contains("io.hyperi.contract.version: \"v1\""));
627 assert!(yaml.contains(
628 "io.hyperi.contract.source-commit: \"0123456789abcdef0123456789abcdef01234567\""
629 ));
630 assert!(
631 yaml.contains("io.hyperi.contract.image-ref: \"ghcr.io/hyperi-io/dfe-loader:v2.7.2\"")
632 );
633 }
634
635 #[test]
636 fn all_three_surfaces_share_the_same_key_prefix() {
637 let id = test_identity();
638 let argo = ArgocdConfig::default();
639 let dir = tempfile::tempdir().unwrap();
640
641 let dockerfile = generate_dockerfile(&test_contract(), Some(&id));
642 generate_chart(&test_contract(), dir.path(), Some(&id)).unwrap();
643 let chart = std::fs::read_to_string(dir.path().join("Chart.yaml")).unwrap();
644 let app = generate_argocd_application(&test_contract(), &argo, Some(&id));
645
646 assert_eq!(dockerfile.matches("io.hyperi.contract").count(), 3);
649 assert_eq!(chart.matches("io.hyperi.contract").count(), 3);
650 assert_eq!(app.matches("io.hyperi.contract").count(), 3);
651 }
652}