Skip to main content

boxferry_runtime/
observation.rs

1//! Runtime-independent effective-state observations.
2
3use std::{error::Error, fmt};
4
5use boxferry_model::{
6    HealthcheckCommand, HealthcheckDuration, HealthcheckRetries, Identifier, ImageReference, Mount, NetworkAttachment,
7    Port, ProtectedString, RestartPolicy, SourceId,
8};
9
10/// Container implementation from which a snapshot was obtained.
11#[derive(Clone, Debug, Eq, PartialEq)]
12#[non_exhaustive]
13pub enum RuntimeImplementation {
14    /// Docker Engine or a compatible Docker API implementation.
15    Docker,
16    /// Podman or the Podman service API.
17    Podman,
18    /// Another explicitly named runtime implementation.
19    Other(Identifier),
20}
21
22impl RuntimeImplementation {
23    /// Returns the stable implementation name used in diagnostics.
24    #[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/// Effective command arguments observed from a container or image.
35///
36/// Runtime inspection cannot recover whether the original definition used shell or exec syntax.
37/// Arguments are therefore retained as an effective exec vector and are sensitive by default.
38#[derive(Clone, Debug, Eq, PartialEq)]
39#[non_exhaustive]
40pub enum EffectiveCommand {
41    /// Effective argument vector, redacted from debug output.
42    Exec(Vec<ProtectedString>),
43    /// The runtime reported an explicitly empty command.
44    Empty,
45}
46
47impl EffectiveCommand {
48    /// Creates an effective argument vector whose values are sensitive by default.
49    #[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    /// Returns the effective argument vector, or `None` for an explicit empty command.
64    #[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/// One effective runtime environment value.
74///
75/// Values from inspection are sensitive by default because environment data frequently contains
76/// credentials. Callers must explicitly expose the value when mapping or rendering authorized
77/// output.
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct RuntimeEnvironmentVariable {
80    name: Identifier,
81    value: ProtectedString,
82}
83
84/// One effective runtime metadata label.
85///
86/// Inspection exposes only the merged effective value; it does not prove whether the value came
87/// from image metadata, a container-create request, or runtime-generated orchestration metadata.
88/// Values are therefore sensitive by default until an output caller explicitly exposes them.
89#[derive(Clone, Debug, Eq, PartialEq)]
90pub struct RuntimeMetadataLabel {
91    name: Identifier,
92    value: ProtectedString,
93}
94
95impl RuntimeMetadataLabel {
96    /// Creates a sensitive effective metadata label.
97    #[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    /// Returns the opaque metadata-label name.
106    #[must_use]
107    pub const fn name(&self) -> &Identifier {
108        &self.name
109    }
110
111    /// Returns the protected effective metadata-label value.
112    #[must_use]
113    pub const fn value(&self) -> &ProtectedString {
114        &self.value
115    }
116}
117
118impl RuntimeEnvironmentVariable {
119    /// Creates a sensitive effective environment value.
120    #[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    /// Returns the environment-variable name.
129    #[must_use]
130    pub const fn name(&self) -> &Identifier {
131        &self.name
132    }
133
134    /// Returns the protected effective value.
135    #[must_use]
136    pub const fn value(&self) -> &ProtectedString {
137        &self.value
138    }
139}
140
141/// Effective regular health-check configuration observed from a container or image.
142///
143/// An empty value records that the native adapter inspected the field and found no regular health
144/// check. Commands and their arguments remain sensitive by default through [`ProtectedString`].
145/// Podman's separate startup-healthcheck family is intentionally not represented here.
146#[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    /// Creates an observed absence of regular health-check configuration.
159    #[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    /// Returns whether inspection proved that no regular health-check field is configured.
173    #[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    /// Records the effective regular health-check command.
185    pub fn set_command(&mut self, command: HealthcheckCommand) {
186        self.command = Some(command);
187    }
188
189    /// Returns the effective regular health-check command.
190    #[must_use]
191    pub const fn command(&self) -> Option<&HealthcheckCommand> {
192        self.command.as_ref()
193    }
194
195    /// Records an explicit disabled state.
196    pub fn set_disabled(&mut self, disabled: bool) {
197        self.disabled = Some(disabled);
198    }
199
200    /// Returns the explicit disabled state.
201    #[must_use]
202    pub const fn disabled(&self) -> Option<bool> {
203        self.disabled
204    }
205
206    /// Records the effective interval between regular checks.
207    pub fn set_interval(&mut self, interval: HealthcheckDuration) {
208        self.interval = Some(interval);
209    }
210
211    /// Returns the effective interval between regular checks.
212    #[must_use]
213    pub const fn interval(&self) -> Option<&HealthcheckDuration> {
214        self.interval.as_ref()
215    }
216
217    /// Records the effective timeout of one regular check.
218    pub fn set_timeout(&mut self, timeout: HealthcheckDuration) {
219        self.timeout = Some(timeout);
220    }
221
222    /// Returns the effective timeout of one regular check.
223    #[must_use]
224    pub const fn timeout(&self) -> Option<&HealthcheckDuration> {
225        self.timeout.as_ref()
226    }
227
228    /// Records the effective regular-check retry count.
229    pub fn set_retries(&mut self, retries: HealthcheckRetries) {
230        self.retries = Some(retries);
231    }
232
233    /// Returns the effective regular-check retry count.
234    #[must_use]
235    pub const fn retries(&self) -> Option<&HealthcheckRetries> {
236        self.retries.as_ref()
237    }
238
239    /// Records the effective regular health-check start period.
240    pub fn set_start_period(&mut self, start_period: HealthcheckDuration) {
241        self.start_period = Some(start_period);
242    }
243
244    /// Returns the effective regular health-check start period.
245    #[must_use]
246    pub const fn start_period(&self) -> Option<&HealthcheckDuration> {
247        self.start_period.as_ref()
248    }
249
250    /// Records the effective start interval where the native runtime exposes equivalent semantics.
251    pub fn set_start_interval(&mut self, start_interval: HealthcheckDuration) {
252        self.start_interval = Some(start_interval);
253    }
254
255    /// Returns the effective start interval.
256    #[must_use]
257    pub const fn start_interval(&self) -> Option<&HealthcheckDuration> {
258        self.start_interval.as_ref()
259    }
260}
261
262/// Optional runtime creation-command evidence.
263///
264/// Arguments are sensitive by default and never override contradictory effective inspection
265/// fields. The evidence can contribute provenance without being present at all.
266#[derive(Clone, Debug, Eq, PartialEq)]
267pub struct CreationEvidence {
268    source_id: SourceId,
269    arguments: Vec<ProtectedString>,
270}
271
272impl CreationEvidence {
273    /// Creates optional evidence with sensitive command arguments.
274    #[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    /// Returns the stable, caller-redacted evidence identity.
290    #[must_use]
291    pub const fn source_id(&self) -> &SourceId {
292        &self.source_id
293    }
294
295    /// Returns the protected creation arguments.
296    #[must_use]
297    pub fn arguments(&self) -> &[ProtectedString] {
298        &self.arguments
299    }
300}
301
302/// Effective image configuration used to classify container overrides.
303#[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    /// Creates an image observation with no assumed fields.
316    #[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    /// Returns the stable, caller-redacted image identity.
330    #[must_use]
331    pub const fn source_id(&self) -> &SourceId {
332        &self.source_id
333    }
334
335    /// Records the image's effective command default.
336    pub fn set_command(&mut self, command: EffectiveCommand) {
337        self.command = Some(command);
338    }
339
340    /// Returns the observed command default, if the adapter supplied it.
341    #[must_use]
342    pub const fn command(&self) -> Option<&EffectiveCommand> {
343        self.command.as_ref()
344    }
345
346    /// Records the complete ordered image environment defaults, including an empty collection.
347    pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
348        self.environment = Some(environment);
349    }
350
351    /// Returns the observed environment defaults, if the adapter supplied them.
352    #[must_use]
353    pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
354        self.environment.as_deref()
355    }
356
357    /// Records the complete deterministic image metadata-label map, including an empty map.
358    pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
359        self.labels = Some(labels);
360    }
361
362    /// Returns the observed image metadata labels, if the adapter supplied them.
363    #[must_use]
364    pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
365        self.labels.as_deref()
366    }
367
368    /// Records a non-empty image user default as sensitive runtime data.
369    pub fn set_user(&mut self, user: impl Into<String>) {
370        self.user = Some(ProtectedString::sensitive(user));
371    }
372
373    /// Returns the observed image user default.
374    #[must_use]
375    pub const fn user(&self) -> Option<&ProtectedString> {
376        self.user.as_ref()
377    }
378
379    /// Records a non-empty image working-directory default as sensitive runtime data.
380    pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
381        self.working_directory = Some(ProtectedString::sensitive(working_directory));
382    }
383
384    /// Returns the observed image working-directory default.
385    #[must_use]
386    pub const fn working_directory(&self) -> Option<&ProtectedString> {
387        self.working_directory.as_ref()
388    }
389
390    /// Records the complete effective regular health-check configuration, including its absence.
391    pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
392        self.healthcheck = Some(healthcheck);
393    }
394
395    /// Returns the observed regular health-check configuration, if the adapter supplied it.
396    #[must_use]
397    pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
398        self.healthcheck.as_ref()
399    }
400}
401
402/// Effective state and relationships of one runtime container.
403#[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    /// Creates an empty container observation for an explicitly named resource.
426    #[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    /// Returns the stable, caller-redacted container identity.
450    #[must_use]
451    pub const fn source_id(&self) -> &SourceId {
452        &self.source_id
453    }
454
455    /// Returns the neutral service name selected by the runtime adapter.
456    #[must_use]
457    pub const fn name(&self) -> &Identifier {
458        &self.name
459    }
460
461    /// Records the effective image reference and optional linked image observation.
462    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    /// Returns the effective image reference.
468    #[must_use]
469    pub const fn image(&self) -> Option<&ImageReference> {
470        self.image.as_ref()
471    }
472
473    /// Returns the linked image-observation identity used for override reconstruction.
474    #[must_use]
475    pub const fn image_source_id(&self) -> Option<&SourceId> {
476        self.image_source_id.as_ref()
477    }
478
479    /// Records the effective container command.
480    pub fn set_command(&mut self, command: EffectiveCommand) {
481        self.command = Some(command);
482    }
483
484    /// Returns the effective command, if the adapter supplied it.
485    #[must_use]
486    pub const fn command(&self) -> Option<&EffectiveCommand> {
487        self.command.as_ref()
488    }
489
490    /// Records the effective container-level automatic restart policy.
491    pub fn set_restart_policy(&mut self, restart_policy: RestartPolicy) {
492        self.restart_policy = Some(restart_policy);
493    }
494
495    /// Returns the effective container-level automatic restart policy, if supplied.
496    #[must_use]
497    pub const fn restart_policy(&self) -> Option<RestartPolicy> {
498        self.restart_policy
499    }
500
501    /// Records the complete ordered effective environment, including an empty collection.
502    pub fn set_environment(&mut self, environment: Vec<RuntimeEnvironmentVariable>) {
503        self.environment = Some(environment);
504    }
505
506    /// Returns the effective environment, if the adapter supplied it.
507    #[must_use]
508    pub fn environment(&self) -> Option<&[RuntimeEnvironmentVariable]> {
509        self.environment.as_deref()
510    }
511
512    /// Records the complete deterministic effective metadata-label map, including an empty map.
513    pub fn set_labels(&mut self, labels: Vec<RuntimeMetadataLabel>) {
514        self.labels = Some(labels);
515    }
516
517    /// Returns the effective metadata labels, if the adapter supplied them.
518    #[must_use]
519    pub fn labels(&self) -> Option<&[RuntimeMetadataLabel]> {
520        self.labels.as_deref()
521    }
522
523    /// Records a non-empty effective container user as sensitive runtime data.
524    pub fn set_user(&mut self, user: impl Into<String>) {
525        self.user = Some(ProtectedString::sensitive(user));
526    }
527
528    /// Returns the effective container user, if the adapter supplied one.
529    #[must_use]
530    pub const fn user(&self) -> Option<&ProtectedString> {
531        self.user.as_ref()
532    }
533
534    /// Records a non-empty effective container working directory as sensitive runtime data.
535    pub fn set_working_directory(&mut self, working_directory: impl Into<String>) {
536        self.working_directory = Some(ProtectedString::sensitive(working_directory));
537    }
538
539    /// Returns the effective container working directory, if supplied.
540    #[must_use]
541    pub const fn working_directory(&self) -> Option<&ProtectedString> {
542        self.working_directory.as_ref()
543    }
544
545    /// Records the complete effective regular health-check configuration, including its absence.
546    pub fn set_healthcheck(&mut self, healthcheck: RuntimeHealthcheck) {
547        self.healthcheck = Some(healthcheck);
548    }
549
550    /// Returns the observed regular health-check configuration, if the adapter supplied it.
551    #[must_use]
552    pub const fn healthcheck(&self) -> Option<&RuntimeHealthcheck> {
553        self.healthcheck.as_ref()
554    }
555
556    /// Records the effective read-only-root-filesystem choice.
557    pub fn set_read_only_root_filesystem(&mut self, read_only: bool) {
558        self.read_only_root_filesystem = Some(read_only);
559    }
560
561    /// Returns the effective read-only-root-filesystem choice, if supplied.
562    #[must_use]
563    pub const fn read_only_root_filesystem(&self) -> Option<bool> {
564        self.read_only_root_filesystem
565    }
566
567    /// Appends one observed published port.
568    pub fn add_port(&mut self, port: Port) {
569        self.ports.push(port);
570    }
571
572    /// Returns observed ports in runtime response order.
573    #[must_use]
574    pub fn ports(&self) -> &[Port] {
575        &self.ports
576    }
577
578    /// Appends one observed storage relationship.
579    pub fn add_mount(&mut self, mount: Mount) {
580        self.mounts.push(mount);
581    }
582
583    /// Returns observed mounts in runtime response order.
584    #[must_use]
585    pub fn mounts(&self) -> &[Mount] {
586        &self.mounts
587    }
588
589    /// Appends one observed network relationship with all aliases in runtime response order.
590    pub fn add_network(&mut self, network: NetworkAttachment) {
591        self.networks.push(network);
592    }
593
594    /// Returns observed network relationships in runtime response order.
595    #[must_use]
596    pub fn networks(&self) -> &[NetworkAttachment] {
597        &self.networks
598    }
599
600    /// Records the containing Podman pod observation, if any.
601    pub fn set_pod_source_id(&mut self, source_id: SourceId) {
602        self.pod_source_id = Some(source_id);
603    }
604
605    /// Returns the containing pod identity.
606    #[must_use]
607    pub const fn pod_source_id(&self) -> Option<&SourceId> {
608        self.pod_source_id.as_ref()
609    }
610
611    /// Attaches optional creation-command evidence without changing effective values.
612    pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
613        self.creation_evidence = Some(evidence);
614    }
615
616    /// Returns optional creation-command evidence.
617    #[must_use]
618    pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
619        self.creation_evidence.as_ref()
620    }
621}
622
623/// One inspected runtime network.
624#[derive(Clone, Debug, Eq, PartialEq)]
625pub struct NetworkObservation {
626    source_id: SourceId,
627    name: Identifier,
628}
629
630impl NetworkObservation {
631    /// Creates a runtime network observation.
632    #[must_use]
633    pub const fn new(source_id: SourceId, name: Identifier) -> Self {
634        Self { source_id, name }
635    }
636
637    /// Returns the stable, caller-redacted network identity.
638    #[must_use]
639    pub const fn source_id(&self) -> &SourceId {
640        &self.source_id
641    }
642
643    /// Returns the neutral resource name selected by the runtime adapter.
644    #[must_use]
645    pub const fn name(&self) -> &Identifier {
646        &self.name
647    }
648}
649
650/// One inspected runtime volume.
651#[derive(Clone, Debug, Eq, PartialEq)]
652pub struct VolumeObservation {
653    source_id: SourceId,
654    name: Identifier,
655}
656
657impl VolumeObservation {
658    /// Creates a runtime volume observation.
659    #[must_use]
660    pub const fn new(source_id: SourceId, name: Identifier) -> Self {
661        Self { source_id, name }
662    }
663
664    /// Returns the stable, caller-redacted volume identity.
665    #[must_use]
666    pub const fn source_id(&self) -> &SourceId {
667        &self.source_id
668    }
669
670    /// Returns the neutral resource name selected by the runtime adapter.
671    #[must_use]
672    pub const fn name(&self) -> &Identifier {
673        &self.name
674    }
675}
676
677/// One inspected Podman pod and its ordered container relationships.
678#[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    /// Creates an empty pod observation.
688    #[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    /// Returns the stable, caller-redacted pod identity.
699    #[must_use]
700    pub const fn source_id(&self) -> &SourceId {
701        &self.source_id
702    }
703
704    /// Returns the pod name selected by the runtime adapter.
705    #[must_use]
706    pub const fn name(&self) -> &Identifier {
707        &self.name
708    }
709
710    /// Appends one member container identity in runtime response order.
711    pub fn add_member(&mut self, source_id: SourceId) {
712        self.members.push(source_id);
713    }
714
715    /// Returns member container identities in runtime response order.
716    #[must_use]
717    pub fn members(&self) -> &[SourceId] {
718        &self.members
719    }
720
721    /// Attaches optional creation-command evidence without changing effective values.
722    pub fn set_creation_evidence(&mut self, evidence: CreationEvidence) {
723        self.creation_evidence = Some(evidence);
724    }
725
726    /// Returns optional pod creation-command evidence.
727    #[must_use]
728    pub const fn creation_evidence(&self) -> Option<&CreationEvidence> {
729        self.creation_evidence.as_ref()
730    }
731}
732
733/// A complete caller-selected set of related runtime observations.
734#[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    /// Creates an empty snapshot without reading a runtime or ambient state.
747    #[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    /// Returns the neutral application name selected by the caller.
761    #[must_use]
762    pub const fn application_name(&self) -> &Identifier {
763        &self.application_name
764    }
765
766    /// Returns the runtime implementation that produced this snapshot.
767    #[must_use]
768    pub const fn implementation(&self) -> &RuntimeImplementation {
769        &self.implementation
770    }
771
772    /// Adds a uniquely identified container observation.
773    ///
774    /// # Errors
775    ///
776    /// Returns [`RuntimeSnapshotError`] for duplicate source identities or container names.
777    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    /// Returns containers in caller-selected discovery order.
789    #[must_use]
790    pub fn containers(&self) -> &[ContainerObservation] {
791        &self.containers
792    }
793
794    /// Adds a uniquely identified image observation.
795    ///
796    /// # Errors
797    ///
798    /// Returns [`RuntimeSnapshotError`] for a duplicate source identity.
799    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    /// Returns images in caller-selected discovery order.
806    #[must_use]
807    pub fn images(&self) -> &[ImageObservation] {
808        &self.images
809    }
810
811    /// Adds a uniquely identified and named network observation.
812    ///
813    /// # Errors
814    ///
815    /// Returns [`RuntimeSnapshotError`] for duplicate source identities or network names.
816    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    /// Returns networks in caller-selected discovery order.
828    #[must_use]
829    pub fn networks(&self) -> &[NetworkObservation] {
830        &self.networks
831    }
832
833    /// Adds a uniquely identified and named volume observation.
834    ///
835    /// # Errors
836    ///
837    /// Returns [`RuntimeSnapshotError`] for duplicate source identities or volume names.
838    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    /// Returns volumes in caller-selected discovery order.
850    #[must_use]
851    pub fn volumes(&self) -> &[VolumeObservation] {
852        &self.volumes
853    }
854
855    /// Adds a uniquely identified and named pod observation.
856    ///
857    /// # Errors
858    ///
859    /// Returns [`RuntimeSnapshotError`] for duplicate source identities or pod names.
860    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    /// Returns pods in caller-selected discovery order.
868    #[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/// Invalid runtime snapshot structure.
908#[derive(Clone, Debug, Eq, PartialEq)]
909#[non_exhaustive]
910pub enum RuntimeSnapshotError {
911    /// Two top-level observations used the same caller-selected source identity.
912    DuplicateSourceIdentity {
913        /// Duplicate stable, caller-redacted identity.
914        source_id: SourceId,
915    },
916    /// Two resources of one kind used the same neutral name.
917    DuplicateResource {
918        /// Runtime resource kind.
919        kind: &'static str,
920        /// Duplicate neutral name.
921        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}