1use std::{error::Error, fmt};
4
5use boxferry_model::{
6 HealthcheckCommand, HealthcheckDuration, HealthcheckRetries, Identifier, ImageReference, Mount, NetworkAttachment,
7 Port, ProtectedString, RestartPolicy, SourceId,
8};
9
10#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum RuntimeImplementation {
14 Docker,
16 Podman,
18 Other(Identifier),
20}
21
22impl RuntimeImplementation {
23 #[must_use]
25 pub fn as_str(&self) -> &str {
26 match self {
27 Self::Docker => "docker",
28 Self::Podman => "podman",
29 Self::Other(name) => name.as_str(),
30 }
31 }
32}
33
34#[derive(Clone, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub enum EffectiveCommand {
41 Exec(Vec<ProtectedString>),
43 Empty,
45}
46
47impl EffectiveCommand {
48 #[must_use]
50 pub fn exec<I, S>(arguments: I) -> Self
51 where
52 I: IntoIterator<Item = S>,
53 S: Into<String>,
54 {
55 Self::Exec(
56 arguments
57 .into_iter()
58 .map(|argument| ProtectedString::sensitive(argument.into()))
59 .collect(),
60 )
61 }
62
63 #[must_use]
65 pub fn arguments(&self) -> Option<&[ProtectedString]> {
66 match self {
67 Self::Exec(arguments) => Some(arguments),
68 Self::Empty => None,
69 }
70 }
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct RuntimeEnvironmentVariable {
80 name: Identifier,
81 value: ProtectedString,
82}
83
84#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct RuntimeMetadataLabel {
91 name: Identifier,
92 value: ProtectedString,
93}
94
95impl RuntimeMetadataLabel {
96 #[must_use]
98 pub fn new(name: Identifier, value: impl Into<String>) -> Self {
99 Self {
100 name,
101 value: ProtectedString::sensitive(value),
102 }
103 }
104
105 #[must_use]
107 pub const fn name(&self) -> &Identifier {
108 &self.name
109 }
110
111 #[must_use]
113 pub const fn value(&self) -> &ProtectedString {
114 &self.value
115 }
116}
117
118impl RuntimeEnvironmentVariable {
119 #[must_use]
121 pub fn new(name: Identifier, value: impl Into<String>) -> Self {
122 Self {
123 name,
124 value: ProtectedString::sensitive(value),
125 }
126 }
127
128 #[must_use]
130 pub const fn name(&self) -> &Identifier {
131 &self.name
132 }
133
134 #[must_use]
136 pub const fn value(&self) -> &ProtectedString {
137 &self.value
138 }
139}
140
141#[derive(Clone, Debug, Default, Eq, PartialEq)]
147pub struct RuntimeHealthcheck {
148 command: Option<HealthcheckCommand>,
149 disabled: Option<bool>,
150 interval: Option<HealthcheckDuration>,
151 timeout: Option<HealthcheckDuration>,
152 retries: Option<HealthcheckRetries>,
153 start_period: Option<HealthcheckDuration>,
154 start_interval: Option<HealthcheckDuration>,
155}
156
157impl RuntimeHealthcheck {
158 #[must_use]
160 pub const fn new() -> Self {
161 Self {
162 command: None,
163 disabled: None,
164 interval: None,
165 timeout: None,
166 retries: None,
167 start_period: None,
168 start_interval: None,
169 }
170 }
171
172 #[must_use]
174 pub const fn is_empty(&self) -> bool {
175 self.command.is_none()
176 && self.disabled.is_none()
177 && self.interval.is_none()
178 && self.timeout.is_none()
179 && self.retries.is_none()
180 && self.start_period.is_none()
181 && self.start_interval.is_none()
182 }
183
184 pub fn set_command(&mut self, command: HealthcheckCommand) {
186 self.command = Some(command);
187 }
188
189 #[must_use]
191 pub const fn command(&self) -> Option<&HealthcheckCommand> {
192 self.command.as_ref()
193 }
194
195 pub fn set_disabled(&mut self, disabled: bool) {
197 self.disabled = Some(disabled);
198 }
199
200 #[must_use]
202 pub const fn disabled(&self) -> Option<bool> {
203 self.disabled
204 }
205
206 pub fn set_interval(&mut self, interval: HealthcheckDuration) {
208 self.interval = Some(interval);
209 }
210
211 #[must_use]
213 pub const fn interval(&self) -> Option<&HealthcheckDuration> {
214 self.interval.as_ref()
215 }
216
217 pub fn set_timeout(&mut self, timeout: HealthcheckDuration) {
219 self.timeout = Some(timeout);
220 }
221
222 #[must_use]
224 pub const fn timeout(&self) -> Option<&HealthcheckDuration> {
225 self.timeout.as_ref()
226 }
227
228 pub fn set_retries(&mut self, retries: HealthcheckRetries) {
230 self.retries = Some(retries);
231 }
232
233 #[must_use]
235 pub const fn retries(&self) -> Option<&HealthcheckRetries> {
236 self.retries.as_ref()
237 }
238
239 pub fn set_start_period(&mut self, start_period: HealthcheckDuration) {
241 self.start_period = Some(start_period);
242 }
243
244 #[must_use]
246 pub const fn start_period(&self) -> Option<&HealthcheckDuration> {
247 self.start_period.as_ref()
248 }
249
250 pub fn set_start_interval(&mut self, start_interval: HealthcheckDuration) {
252 self.start_interval = Some(start_interval);
253 }
254
255 #[must_use]
257 pub const fn start_interval(&self) -> Option<&HealthcheckDuration> {
258 self.start_interval.as_ref()
259 }
260}
261
262#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct CreationEvidence {
268 source_id: SourceId,
269 arguments: Vec<ProtectedString>,
270}
271
272impl CreationEvidence {
273 #[must_use]
275 pub fn new<I, S>(source_id: SourceId, arguments: I) -> Self
276 where
277 I: IntoIterator<Item = S>,
278 S: Into<String>,
279 {
280 Self {
281 source_id,
282 arguments: arguments
283 .into_iter()
284 .map(|argument| ProtectedString::sensitive(argument.into()))
285 .collect(),
286 }
287 }
288
289 #[must_use]
291 pub const fn source_id(&self) -> &SourceId {
292 &self.source_id
293 }
294
295 #[must_use]
297 pub fn arguments(&self) -> &[ProtectedString] {
298 &self.arguments
299 }
300}
301
302#[derive(Clone, Debug, Eq, PartialEq)]
304pub struct ImageObservation {
305 source_id: SourceId,
306 command: Option<EffectiveCommand>,
307 environment: Option<Vec<RuntimeEnvironmentVariable>>,
308 labels: Option<Vec<RuntimeMetadataLabel>>,
309 user: Option<ProtectedString>,
310 working_directory: Option<ProtectedString>,
311 healthcheck: Option<RuntimeHealthcheck>,
312}
313
314impl ImageObservation {
315 #[must_use]
317 pub const fn new(source_id: SourceId) -> Self {
318 Self {
319 source_id,
320 command: None,
321 environment: None,
322 labels: None,
323 user: None,
324 working_directory: None,
325 healthcheck: None,
326 }
327 }
328
329 #[must_use]
331 pub const fn source_id(&self) -> &SourceId {
332 &self.source_id
333 }
334
335 pub fn set_command(&mut self, command: EffectiveCommand) {
337 self.command = Some(command);
338 }
339
340 #[must_use]
342 pub const fn command(&self) -> Option<&EffectiveCommand> {
343 self.command.as_ref()
344 }
345
346 pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
348 self.environment = Some(environment);
349 }
350
351 #[must_use]
353 pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
354 self.environment.as_deref()
355 }
356
357 pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
359 self.labels = Some(labels);
360 }
361
362 #[must_use]
364 pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
365 self.labels.as_deref()
366 }
367
368 pub fn set_user(&mut self, user: impl Into<String>) {
370 self.user = Some(ProtectedString::sensitive(user));
371 }
372
373 #[must_use]
375 pub const fn user(&self) -> Option<&ProtectedString> {
376 self.user.as_ref()
377 }
378
379 pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
381 self.working_directory = Some(ProtectedString::sensitive(working_directory));
382 }
383
384 #[must_use]
386 pub const fn working_directory(&self) -> Option<&ProtectedString> {
387 self.working_directory.as_ref()
388 }
389
390 pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
392 self.healthcheck = Some(healthcheck);
393 }
394
395 #[must_use]
397 pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
398 self.healthcheck.as_ref()
399 }
400}
401
402#[derive(Clone, Debug, Eq, PartialEq)]
404pub struct ContainerObservation {
405 source_id: SourceId,
406 name: Identifier,
407 image: Option<ImageReference>,
408 image_source_id: Option<SourceId>,
409 command: Option<EffectiveCommand>,
410 restart_policy: Option<RestartPolicy>,
411 environment: Option<Vec<RuntimeEnvironmentVariable>>,
412 labels: Option<Vec<RuntimeMetadataLabel>>,
413 user: Option<ProtectedString>,
414 working_directory: Option<ProtectedString>,
415 healthcheck: Option<RuntimeHealthcheck>,
416 read_only_root_filesystem: Option<bool>,
417 ports: Vec<Port>,
418 mounts: Vec<Mount>,
419 networks: Vec<NetworkAttachment>,
420 pod_source_id: Option<SourceId>,
421 creation_evidence: Option<CreationEvidence>,
422}
423
424impl ContainerObservation {
425 #[must_use]
427 pub const fn new(source_id: SourceId, name: Identifier) -> Self {
428 Self {
429 source_id,
430 name,
431 image: None,
432 image_source_id: None,
433 command: None,
434 restart_policy: None,
435 environment: None,
436 labels: None,
437 user: None,
438 working_directory: None,
439 healthcheck: None,
440 read_only_root_filesystem: None,
441 ports: Vec::new(),
442 mounts: Vec::new(),
443 networks: Vec::new(),
444 pod_source_id: None,
445 creation_evidence: None,
446 }
447 }
448
449 #[must_use]
451 pub const fn source_id(&self) -> &SourceId {
452 &self.source_id
453 }
454
455 #[must_use]
457 pub const fn name(&self) -> &Identifier {
458 &self.name
459 }
460
461 pub fn set_image(&mut self, image: ImageReference, image_source_id: Option<SourceId>) {
463 self.image = Some(image);
464 self.image_source_id = image_source_id;
465 }
466
467 #[must_use]
469 pub const fn image(&self) -> Option<&ImageReference> {
470 self.image.as_ref()
471 }
472
473 #[must_use]
475 pub const fn image_source_id(&self) -> Option<&SourceId> {
476 self.image_source_id.as_ref()
477 }
478
479 pub fn set_command(&mut self, command: EffectiveCommand) {
481 self.command = Some(command);
482 }
483
484 #[must_use]
486 pub const fn command(&self) -> Option<&EffectiveCommand> {
487 self.command.as_ref()
488 }
489
490 pub fn set_restart_policy(&mut self, restart_policy: RestartPolicy) {
492 self.restart_policy = Some(restart_policy);
493 }
494
495 #[must_use]
497 pub const fn restart_policy(&self) -> Option<RestartPolicy> {
498 self.restart_policy
499 }
500
501 pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
503 self.environment = Some(environment);
504 }
505
506 #[must_use]
508 pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
509 self.environment.as_deref()
510 }
511
512 pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
514 self.labels = Some(labels);
515 }
516
517 #[must_use]
519 pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
520 self.labels.as_deref()
521 }
522
523 pub fn set_user(&mut self, user: impl Into<String>) {
525 self.user = Some(ProtectedString::sensitive(user));
526 }
527
528 #[must_use]
530 pub const fn user(&self) -> Option<&ProtectedString> {
531 self.user.as_ref()
532 }
533
534 pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
536 self.working_directory = Some(ProtectedString::sensitive(working_directory));
537 }
538
539 #[must_use]
541 pub const fn working_directory(&self) -> Option<&ProtectedString> {
542 self.working_directory.as_ref()
543 }
544
545 pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
547 self.healthcheck = Some(healthcheck);
548 }
549
550 #[must_use]
552 pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
553 self.healthcheck.as_ref()
554 }
555
556 pub fn set_read_only_root_filesystem(&mut self, read_only: bool) {
558 self.read_only_root_filesystem = Some(read_only);
559 }
560
561 #[must_use]
563 pub const fn read_only_root_filesystem(&self) -> Option<bool> {
564 self.read_only_root_filesystem
565 }
566
567 pub fn add_port(&mut self, port: Port) {
569 self.ports.push(port);
570 }
571
572 #[must_use]
574 pub fn ports(&self) -> &[Port] {
575 &self.ports
576 }
577
578 pub fn add_mount(&mut self, mount: Mount) {
580 self.mounts.push(mount);
581 }
582
583 #[must_use]
585 pub fn mounts(&self) -> &[Mount] {
586 &self.mounts
587 }
588
589 pub fn add_network(&mut self, network: NetworkAttachment) {
591 self.networks.push(network);
592 }
593
594 #[must_use]
596 pub fn networks(&self) -> &[NetworkAttachment] {
597 &self.networks
598 }
599
600 pub fn set_pod_source_id(&mut self, source_id: SourceId) {
602 self.pod_source_id = Some(source_id);
603 }
604
605 #[must_use]
607 pub const fn pod_source_id(&self) -> Option<&SourceId> {
608 self.pod_source_id.as_ref()
609 }
610
611 pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
613 self.creation_evidence = Some(evidence);
614 }
615
616 #[must_use]
618 pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
619 self.creation_evidence.as_ref()
620 }
621}
622
623#[derive(Clone, Debug, Eq, PartialEq)]
625pub struct NetworkObservation {
626 source_id: SourceId,
627 name: Identifier,
628}
629
630impl NetworkObservation {
631 #[must_use]
633 pub const fn new(source_id: SourceId, name: Identifier) -> Self {
634 Self { source_id, name }
635 }
636
637 #[must_use]
639 pub const fn source_id(&self) -> &SourceId {
640 &self.source_id
641 }
642
643 #[must_use]
645 pub const fn name(&self) -> &Identifier {
646 &self.name
647 }
648}
649
650#[derive(Clone, Debug, Eq, PartialEq)]
652pub struct VolumeObservation {
653 source_id: SourceId,
654 name: Identifier,
655}
656
657impl VolumeObservation {
658 #[must_use]
660 pub const fn new(source_id: SourceId, name: Identifier) -> Self {
661 Self { source_id, name }
662 }
663
664 #[must_use]
666 pub const fn source_id(&self) -> &SourceId {
667 &self.source_id
668 }
669
670 #[must_use]
672 pub const fn name(&self) -> &Identifier {
673 &self.name
674 }
675}
676
677#[derive(Clone, Debug, Eq, PartialEq)]
679pub struct PodObservation {
680 source_id: SourceId,
681 name: Identifier,
682 members: Vec<SourceId>,
683 creation_evidence: Option<CreationEvidence>,
684}
685
686impl PodObservation {
687 #[must_use]
689 pub const fn new(source_id: SourceId, name: Identifier) -> Self {
690 Self {
691 source_id,
692 name,
693 members: Vec::new(),
694 creation_evidence: None,
695 }
696 }
697
698 #[must_use]
700 pub const fn source_id(&self) -> &SourceId {
701 &self.source_id
702 }
703
704 #[must_use]
706 pub const fn name(&self) -> &Identifier {
707 &self.name
708 }
709
710 pub fn add_member(&mut self, source_id: SourceId) {
712 self.members.push(source_id);
713 }
714
715 #[must_use]
717 pub fn members(&self) -> &[SourceId] {
718 &self.members
719 }
720
721 pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
723 self.creation_evidence = Some(evidence);
724 }
725
726 #[must_use]
728 pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
729 self.creation_evidence.as_ref()
730 }
731}
732
733#[derive(Clone, Debug, Eq, PartialEq)]
735pub struct RuntimeSnapshot {
736 application_name: Identifier,
737 implementation: RuntimeImplementation,
738 containers: Vec<ContainerObservation>,
739 images: Vec<ImageObservation>,
740 networks: Vec<NetworkObservation>,
741 volumes: Vec<VolumeObservation>,
742 pods: Vec<PodObservation>,
743}
744
745impl RuntimeSnapshot {
746 #[must_use]
748 pub const fn new(application_name: Identifier, implementation: RuntimeImplementation) -> Self {
749 Self {
750 application_name,
751 implementation,
752 containers: Vec::new(),
753 images: Vec::new(),
754 networks: Vec::new(),
755 volumes: Vec::new(),
756 pods: Vec::new(),
757 }
758 }
759
760 #[must_use]
762 pub const fn application_name(&self) -> &Identifier {
763 &self.application_name
764 }
765
766 #[must_use]
768 pub const fn implementation(&self) -> &RuntimeImplementation {
769 &self.implementation
770 }
771
772 pub fn add_container(&mut self, container: ContainerObservation) -> Result<(), RuntimeSnapshotError> {
778 self.ensure_source_unique(container.source_id())?;
779 Self::ensure_name_unique(
780 "container",
781 container.name(),
782 self.containers.iter().map(ContainerObservation::name),
783 )?;
784 self.containers.push(container);
785 Ok(())
786 }
787
788 #[must_use]
790 pub fn containers(&self) -> &[ContainerObservation] {
791 &self.containers
792 }
793
794 pub fn add_image(&mut self, image: ImageObservation) -> Result<(), RuntimeSnapshotError> {
800 self.ensure_source_unique(image.source_id())?;
801 self.images.push(image);
802 Ok(())
803 }
804
805 #[must_use]
807 pub fn images(&self) -> &[ImageObservation] {
808 &self.images
809 }
810
811 pub fn add_network(&mut self, network: NetworkObservation) -> Result<(), RuntimeSnapshotError> {
817 self.ensure_source_unique(network.source_id())?;
818 Self::ensure_name_unique(
819 "network",
820 network.name(),
821 self.networks.iter().map(NetworkObservation::name),
822 )?;
823 self.networks.push(network);
824 Ok(())
825 }
826
827 #[must_use]
829 pub fn networks(&self) -> &[NetworkObservation] {
830 &self.networks
831 }
832
833 pub fn add_volume(&mut self, volume: VolumeObservation) -> Result<(), RuntimeSnapshotError> {
839 self.ensure_source_unique(volume.source_id())?;
840 Self::ensure_name_unique(
841 "volume",
842 volume.name(),
843 self.volumes.iter().map(VolumeObservation::name),
844 )?;
845 self.volumes.push(volume);
846 Ok(())
847 }
848
849 #[must_use]
851 pub fn volumes(&self) -> &[VolumeObservation] {
852 &self.volumes
853 }
854
855 pub fn add_pod(&mut self, pod: PodObservation) -> Result<(), RuntimeSnapshotError> {
861 self.ensure_source_unique(pod.source_id())?;
862 Self::ensure_name_unique("pod", pod.name(), self.pods.iter().map(PodObservation::name))?;
863 self.pods.push(pod);
864 Ok(())
865 }
866
867 #[must_use]
869 pub fn pods(&self) -> &[PodObservation] {
870 &self.pods
871 }
872
873 fn ensure_source_unique(&self, source_id: &SourceId) -> Result<(), RuntimeSnapshotError> {
874 if self.source_ids().any(|candidate| candidate == source_id) {
875 return Err(RuntimeSnapshotError::DuplicateSourceIdentity {
876 source_id: source_id.clone(),
877 });
878 }
879 Ok(())
880 }
881
882 fn ensure_name_unique<'a>(
883 kind: &'static str,
884 name: &Identifier,
885 existing: impl Iterator<Item = &'a Identifier>,
886 ) -> Result<(), RuntimeSnapshotError> {
887 if existing.into_iter().any(|candidate| candidate == name) {
888 return Err(RuntimeSnapshotError::DuplicateResource {
889 kind,
890 name: name.as_str().to_owned(),
891 });
892 }
893 Ok(())
894 }
895
896 fn source_ids(&self) -> impl Iterator<Item = &SourceId> {
897 self.containers
898 .iter()
899 .map(ContainerObservation::source_id)
900 .chain(self.images.iter().map(ImageObservation::source_id))
901 .chain(self.networks.iter().map(NetworkObservation::source_id))
902 .chain(self.volumes.iter().map(VolumeObservation::source_id))
903 .chain(self.pods.iter().map(PodObservation::source_id))
904 }
905}
906
907#[derive(Clone, Debug, Eq, PartialEq)]
909#[non_exhaustive]
910pub enum RuntimeSnapshotError {
911 DuplicateSourceIdentity {
913 source_id: SourceId,
915 },
916 DuplicateResource {
918 kind: &'static str,
920 name: String,
922 },
923}
924
925impl fmt::Display for RuntimeSnapshotError {
926 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
927 match self {
928 Self::DuplicateSourceIdentity { source_id } => {
929 write!(formatter, "duplicate runtime source identity `{}`", source_id.as_str())
930 }
931 Self::DuplicateResource { kind, name } => write!(formatter, "duplicate runtime {kind} `{name}`"),
932 }
933 }
934}
935
936impl Error for RuntimeSnapshotError {}
937
938#[cfg(test)]
939mod tests {
940 use boxferry_model::{Identifier, SourceId};
941
942 use super::{
943 ContainerObservation, CreationEvidence, EffectiveCommand, RuntimeEnvironmentVariable, RuntimeImplementation,
944 RuntimeMetadataLabel, RuntimeSnapshot,
945 };
946
947 #[test]
948 fn inspected_values_are_redacted_by_default() -> Result<(), String> {
949 let command = EffectiveCommand::exec(["server", "--password=never-print-this"]);
950 let environment = RuntimeEnvironmentVariable::new(id("PASSWORD")?, "never-print-this");
951 let label = RuntimeMetadataLabel::new(id("com.example.token")?, "never-print-this");
952 let mut container = ContainerObservation::new(source("runtime:podman:container:web")?, id("web")?);
953 container.set_user("never-print-this");
954 container.set_working_directory("/never-print-this");
955 let evidence = CreationEvidence::new(
956 source("runtime:podman:create:web")?,
957 ["--env", "PASSWORD=never-print-this"],
958 );
959
960 for debug in [
961 format!("{command:?}"),
962 format!("{environment:?}"),
963 format!("{label:?}"),
964 format!("{container:?}"),
965 format!("{evidence:?}"),
966 ] {
967 assert!(!debug.contains("never-print-this"));
968 assert!(debug.contains("[REDACTED]"));
969 }
970 Ok(())
971 }
972
973 #[test]
974 fn snapshot_rejects_ambiguous_source_identities_and_names() -> Result<(), String> {
975 let mut snapshot = RuntimeSnapshot::new(id("example")?, RuntimeImplementation::Podman);
976 snapshot
977 .add_container(ContainerObservation::new(source("runtime:container:web")?, id("web")?))
978 .map_err(|error| error.to_string())?;
979
980 let duplicate_source = snapshot
981 .add_container(ContainerObservation::new(
982 source("runtime:container:web")?,
983 id("worker")?,
984 ))
985 .err()
986 .ok_or("duplicate source must fail")?;
987 assert!(duplicate_source.to_string().contains("source identity"));
988
989 let duplicate_name = snapshot
990 .add_container(ContainerObservation::new(
991 source("runtime:container:web-2")?,
992 id("web")?,
993 ))
994 .err()
995 .ok_or("duplicate name must fail")?;
996 assert!(duplicate_name.to_string().contains("container `web`"));
997 Ok(())
998 }
999
1000 fn id(value: &str) -> Result<Identifier, String> {
1001 Identifier::new(value).map_err(|error| error.to_string())
1002 }
1003
1004 fn source(value: &str) -> Result<SourceId, String> {
1005 SourceId::new(value).map_err(|error| error.to_string())
1006 }
1007}