Skip to main content

podman_lens/
observation.rs

1//! Typed, provenance-aware observations at the Podman native input boundary.
2//!
3//! This module deliberately models what the selected Podman service reported; it does not infer
4//! desired deployment intent.  In particular, runtime-assigned addresses, local image IDs and
5//! current lifecycle state never share a type with configured facts.  BoxFerry-facing adapters
6//! must make an explicit mapping decision for every [`ObservationOrigin`].
7
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    fmt,
11    net::IpAddr,
12};
13
14use crate::{
15    Diagnostic, DiagnosticCode, InventoryFinding, JsonValueKind, ResourceEvidence, ResourceIdentity, ResourceKind,
16    SensitiveEnvironmentValue,
17};
18
19/// The observation state of one native field.
20///
21/// `Absent` means that the reviewed wire field was absent or `null`; it never means malformed,
22/// unavailable, inapplicable, or omitted from the native model.  Those states are represented
23/// separately so an adapter cannot turn a decoder failure into an intentional empty value.
24#[derive(Clone, Eq, PartialEq)]
25#[non_exhaustive]
26pub enum ObservationField<T> {
27    /// The field was absent from an otherwise decoded response.
28    Absent,
29    /// The field was decoded and carries its source disposition.
30    Observed(ObservedValue<T>),
31    /// The containing resource or section could not be acquired.
32    Unavailable,
33    /// The field was present but could not be decoded according to its reviewed shape.
34    Malformed,
35    /// The reviewed native version does not give this field a usable meaning.
36    VersionInapplicable,
37    /// The field has no meaning for this resource kind.
38    NotApplicable,
39    /// The field was deliberately retained only as bounded unmodelled metadata.
40    Unmodelled(UnmodelledFieldId),
41}
42
43impl<T> fmt::Debug for ObservationField<T> {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        formatter
46            .debug_tuple("ObservationField")
47            .field(&match self {
48                Self::Absent => "absent",
49                Self::Observed(_) => "observed",
50                Self::Unavailable => "unavailable",
51                Self::Malformed => "malformed",
52                Self::VersionInapplicable => "version_inapplicable",
53                Self::NotApplicable => "not_applicable",
54                Self::Unmodelled(id) => id.as_str(),
55            })
56            .finish()
57    }
58}
59
60impl<T> ObservationField<T> {
61    /// Returns the observed value only when the field decoded successfully.
62    #[must_use]
63    pub const fn observed(&self) -> Option<&ObservedValue<T>> {
64        match self {
65            Self::Observed(value) => Some(value),
66            _ => None,
67        }
68    }
69
70    /// Returns whether this field contains a usable observation.
71    #[must_use]
72    pub const fn is_observed(&self) -> bool {
73        matches!(self, Self::Observed(_))
74    }
75
76    /// Returns whether the native field was present but did not match its reviewed shape.
77    #[must_use]
78    pub const fn is_malformed(&self) -> bool {
79        matches!(self, Self::Malformed)
80    }
81}
82
83/// Provenance that prevents observed runtime facts from becoming desired intent accidentally.
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
85#[non_exhaustive]
86pub enum ObservationOrigin {
87    /// An explicit setting declared in the native resource configuration.
88    Configured,
89    /// A native effective value that may incorporate Podman defaults.
90    Effective,
91    /// A value allocated by the runtime, such as an address or a live state.
92    RuntimeAssigned,
93    /// A local resolver result, such as a resolved image ID.
94    LocalResolution,
95}
96
97/// A successfully decoded value and its non-promotable provenance.
98#[derive(Clone, Eq, PartialEq)]
99pub struct ObservedValue<T> {
100    value: T,
101    origin: ObservationOrigin,
102}
103
104impl<T> fmt::Debug for ObservedValue<T> {
105    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
106        formatter
107            .debug_struct("ObservedValue")
108            .field("origin", &self.origin)
109            .finish_non_exhaustive()
110    }
111}
112
113impl<T> ObservedValue<T> {
114    /// Creates an observed value with explicit provenance.
115    #[must_use]
116    pub const fn new(value: T, origin: ObservationOrigin) -> Self {
117        Self { value, origin }
118    }
119
120    /// Returns the value exactly as observed.
121    #[must_use]
122    pub const fn value(&self) -> &T {
123        &self.value
124    }
125
126    /// Returns the source disposition of the value.
127    #[must_use]
128    pub const fn origin(&self) -> ObservationOrigin {
129        self.origin
130    }
131}
132
133/// Stable semantic identifier for bounded metadata not modelled by this release.
134///
135/// The identifier is intentionally independent from a JSON spelling.  The accompanying
136/// [`UnmodelledField`] records its observed JSON path and kind, but never the raw value.
137#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
138#[non_exhaustive]
139pub enum UnmodelledFieldId {
140    /// Container `HostConfig` data outside the bounded typed subset.
141    ContainerHostConfig,
142    /// Container secret-grant metadata outside the bounded typed subset.
143    ContainerSecretGrant,
144    /// Container configuration data outside the bounded typed subset.
145    ContainerConfig,
146    /// Container network-settings data outside the bounded typed subset.
147    ContainerNetworkSettings,
148    /// Container mount data outside the bounded typed subset.
149    ContainerMount,
150    /// Other container inspect data outside the bounded typed subset.
151    ContainerTopLevel,
152    /// Pod membership data outside the bounded typed subset.
153    PodMember,
154    /// Pod infra-configuration data outside the bounded typed subset.
155    PodInfraConfig,
156    /// Other pod inspect data outside the bounded typed subset.
157    PodTopLevel,
158    /// Network subnet data outside the bounded typed subset.
159    NetworkSubnet,
160    /// Network route data outside the bounded typed subset.
161    NetworkRoute,
162    /// Other network inspect data outside the bounded typed subset.
163    NetworkTopLevel,
164    /// Volume inspect data outside the bounded typed subset.
165    VolumeTopLevel,
166    /// Image configuration data outside the bounded typed subset.
167    ImageConfig,
168    /// Other image inspect data outside the bounded typed subset.
169    ImageTopLevel,
170    /// Secret specification metadata outside the bounded typed subset.
171    SecretSpec,
172    /// Other secret inspect data outside the bounded typed subset.
173    SecretTopLevel,
174}
175
176impl UnmodelledFieldId {
177    /// Returns the stable semantic identifier.
178    #[must_use]
179    pub const fn as_str(self) -> &'static str {
180        match self {
181            Self::ContainerHostConfig => "podman.native.container.host-config",
182            Self::ContainerSecretGrant => "podman.native.container.secret-grant",
183            Self::ContainerConfig => "podman.native.container.config",
184            Self::ContainerNetworkSettings => "podman.native.container.network-settings",
185            Self::ContainerMount => "podman.native.container.mount",
186            Self::ContainerTopLevel => "podman.native.container.top-level",
187            Self::PodMember => "podman.native.pod.member",
188            Self::PodInfraConfig => "podman.native.pod.infra-config",
189            Self::PodTopLevel => "podman.native.pod.top-level",
190            Self::NetworkSubnet => "podman.native.network.subnet",
191            Self::NetworkRoute => "podman.native.network.route",
192            Self::NetworkTopLevel => "podman.native.network.top-level",
193            Self::VolumeTopLevel => "podman.native.volume.top-level",
194            Self::ImageConfig => "podman.native.image.config",
195            Self::ImageTopLevel => "podman.native.image.top-level",
196            Self::SecretSpec => "podman.native.secret.spec",
197            Self::SecretTopLevel => "podman.native.secret.top-level",
198        }
199    }
200}
201
202/// Bounded, redacted metadata for one native field that remains unmodelled.
203#[derive(Clone, Debug, Eq, PartialEq)]
204pub struct UnmodelledField {
205    id: UnmodelledFieldId,
206    path: String,
207    json_kind: JsonValueKind,
208    resource: ResourceIdentity,
209    evidence: ResourceEvidence,
210}
211
212impl UnmodelledField {
213    #[allow(clippy::too_many_arguments)] // private typed decoder construction keeps every field explicit.
214    pub(crate) fn new(
215        path: String,
216        json_kind: JsonValueKind,
217        resource: ResourceIdentity,
218        evidence: ResourceEvidence,
219    ) -> Self {
220        Self {
221            id: semantic_unmodelled_id(resource.kind(), &path),
222            path,
223            json_kind,
224            resource,
225            evidence,
226        }
227    }
228
229    /// Returns the stable semantic ID, never a raw native value.
230    #[must_use]
231    pub fn id(&self) -> &UnmodelledFieldId {
232        &self.id
233    }
234
235    /// Returns the observed JSON path.
236    #[must_use]
237    pub fn path(&self) -> &str {
238        &self.path
239    }
240
241    /// Returns the observed JSON value kind.
242    #[must_use]
243    pub const fn json_kind(&self) -> JsonValueKind {
244        self.json_kind
245    }
246
247    /// Returns the carrying resource identity.
248    #[must_use]
249    pub fn resource(&self) -> &ResourceIdentity {
250        &self.resource
251    }
252
253    /// Returns immutable version evidence for this observation.
254    #[must_use]
255    pub fn evidence(&self) -> &ResourceEvidence {
256        &self.evidence
257    }
258}
259
260fn semantic_unmodelled_id(kind: ResourceKind, path: &str) -> UnmodelledFieldId {
261    match (kind, path) {
262        (ResourceKind::Container, value) if value.starts_with("$.HostConfig") => UnmodelledFieldId::ContainerHostConfig,
263        (ResourceKind::Container, value) if value.starts_with("$.Config.Secrets") => {
264            UnmodelledFieldId::ContainerSecretGrant
265        }
266        (ResourceKind::Container, value) if value.starts_with("$.Config") => UnmodelledFieldId::ContainerConfig,
267        (ResourceKind::Container, value) if value.starts_with("$.NetworkSettings") => {
268            UnmodelledFieldId::ContainerNetworkSettings
269        }
270        (ResourceKind::Container, value) if value.starts_with("$.Mounts") => UnmodelledFieldId::ContainerMount,
271        (ResourceKind::Pod, value) if value.starts_with("$.Containers") => UnmodelledFieldId::PodMember,
272        (ResourceKind::Pod, value) if value.starts_with("$.InfraConfig") => UnmodelledFieldId::PodInfraConfig,
273        (ResourceKind::Network, value) if value.starts_with("$.subnets") => UnmodelledFieldId::NetworkSubnet,
274        (ResourceKind::Network, value) if value.starts_with("$.routes") => UnmodelledFieldId::NetworkRoute,
275        (ResourceKind::Image, value) if value.starts_with("$.Config") => UnmodelledFieldId::ImageConfig,
276        (ResourceKind::Secret, value) if value.starts_with("$.Spec") => UnmodelledFieldId::SecretSpec,
277        (ResourceKind::Container, _) => UnmodelledFieldId::ContainerTopLevel,
278        (ResourceKind::Pod, _) => UnmodelledFieldId::PodTopLevel,
279        (ResourceKind::Network, _) => UnmodelledFieldId::NetworkTopLevel,
280        (ResourceKind::Volume, _) => UnmodelledFieldId::VolumeTopLevel,
281        (ResourceKind::Image, _) => UnmodelledFieldId::ImageTopLevel,
282        (ResourceKind::Secret, _) => UnmodelledFieldId::SecretTopLevel,
283    }
284}
285
286/// Completeness state for bounded unmodelled metadata.
287#[derive(Clone, Copy, Debug, Eq, PartialEq)]
288pub enum UnmodelledCompleteness {
289    /// All direct unmodelled fields were retained within configured bounds.
290    Complete,
291    /// The observation is partial or the retention budget overflowed.
292    Incomplete,
293}
294
295/// Acquisition state of one resource observation.
296#[derive(Clone, Copy, Debug, Eq, PartialEq)]
297#[non_exhaustive]
298pub enum ResourceObservationState {
299    /// The inspected response decoded according to the current native contract.
300    Complete,
301    /// The resource could not be acquired during the non-atomic inventory read.
302    Unavailable,
303    /// The resource inspect response was malformed or contradicted the list identity.
304    Malformed,
305}
306
307/// Resource-wide observation information shared by every detail variant.
308#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct ObservationHeader {
310    identity: ResourceIdentity,
311    state: ResourceObservationState,
312    evidence: ResourceEvidence,
313    findings: Vec<InventoryFinding>,
314    unmodelled: Vec<UnmodelledField>,
315    unmodelled_completeness: UnmodelledCompleteness,
316}
317
318impl ObservationHeader {
319    pub(crate) fn complete(
320        identity: ResourceIdentity,
321        evidence: ResourceEvidence,
322        findings: Vec<InventoryFinding>,
323        unmodelled: Vec<UnmodelledField>,
324        unmodelled_completeness: UnmodelledCompleteness,
325    ) -> Self {
326        Self {
327            identity,
328            state: ResourceObservationState::Complete,
329            evidence,
330            findings,
331            unmodelled,
332            unmodelled_completeness,
333        }
334    }
335
336    pub(crate) fn incomplete(
337        identity: ResourceIdentity,
338        evidence: ResourceEvidence,
339        state: ResourceObservationState,
340        findings: Vec<InventoryFinding>,
341    ) -> Self {
342        Self {
343            identity,
344            state,
345            evidence,
346            findings,
347            unmodelled: Vec::new(),
348            unmodelled_completeness: UnmodelledCompleteness::Incomplete,
349        }
350    }
351
352    /// Returns the stable native identity.
353    #[must_use]
354    pub fn identity(&self) -> &ResourceIdentity {
355        &self.identity
356    }
357
358    /// Returns the typed non-atomic acquisition state for this resource.
359    #[must_use]
360    pub const fn state(&self) -> ResourceObservationState {
361        self.state
362    }
363
364    /// Returns immutable source/version evidence.
365    #[must_use]
366    pub fn evidence(&self) -> &ResourceEvidence {
367        &self.evidence
368    }
369
370    /// Returns redacted structured findings for this resource.
371    #[must_use]
372    pub fn findings(&self) -> &[InventoryFinding] {
373        &self.findings
374    }
375
376    pub(crate) fn findings_mut(&mut self) -> &mut Vec<InventoryFinding> {
377        &mut self.findings
378    }
379
380    /// Returns bounded unmodelled metadata without raw values.
381    #[must_use]
382    pub fn unmodelled_fields(&self) -> &[UnmodelledField] {
383        &self.unmodelled
384    }
385
386    /// Returns whether bounded metadata accounts for all unmodelled fields.
387    #[must_use]
388    pub const fn unmodelled_completeness(&self) -> UnmodelledCompleteness {
389        self.unmodelled_completeness
390    }
391}
392
393/// A relationship used internally by canonical discovery derivation.
394#[derive(Clone, Debug, Eq, PartialEq)]
395pub(crate) struct NativeRelationship {
396    pub(crate) kind: ResourceKind,
397    /// One relationship can retain several native references when the wire format carries an
398    /// identifier and a name for the same grant.  Resolution requires every supplied reference
399    /// to select the same target; discovery must never choose one spelling silently.
400    pub(crate) references: Vec<String>,
401    /// Every source location that asserted this one native relationship.
402    pub(crate) field_paths: Vec<String>,
403}
404
405impl NativeRelationship {
406    pub(crate) fn new(kind: ResourceKind, target_id: impl Into<String>, field_path: impl Into<String>) -> Self {
407        Self {
408            kind,
409            references: vec![target_id.into()],
410            field_paths: vec![field_path.into()],
411        }
412    }
413
414    pub(crate) fn coalesced(
415        kind: ResourceKind,
416        references: impl IntoIterator<Item = (String, String)>,
417    ) -> Option<Self> {
418        let mut values = Vec::new();
419        let mut paths = Vec::new();
420        for (value, path) in references {
421            if !values.contains(&value) {
422                values.push(value);
423            }
424            paths.push(path);
425        }
426        (!values.is_empty()).then_some(Self {
427            kind,
428            references: values,
429            field_paths: paths,
430        })
431    }
432}
433
434/// A protected runtime environment observation.
435#[derive(Clone, Debug, Eq, PartialEq)]
436pub struct ProtectedEnvironment {
437    entries: Vec<ProtectedEnvironmentEntry>,
438}
439
440impl ProtectedEnvironment {
441    pub(crate) fn new(entries: Vec<ProtectedEnvironmentEntry>) -> Self {
442        Self { entries }
443    }
444
445    /// Returns variable names and protected value states in source order.
446    #[must_use]
447    pub fn entries(&self) -> &[ProtectedEnvironmentEntry] {
448        &self.entries
449    }
450}
451
452/// One protected runtime environment name/value-state pair.
453#[derive(Clone, Debug, Eq, PartialEq)]
454pub struct ProtectedEnvironmentEntry {
455    name: String,
456    value: ProtectedEnvironmentValue,
457}
458
459impl ProtectedEnvironmentEntry {
460    pub(crate) fn new(name: String, value: ProtectedEnvironmentValue) -> Self {
461        Self { name, value }
462    }
463
464    /// Returns the variable name.
465    #[must_use]
466    pub fn name(&self) -> &str {
467        &self.name
468    }
469
470    /// Returns the protected state, never a public deployment value.
471    #[must_use]
472    pub fn value(&self) -> &ProtectedEnvironmentValue {
473        &self.value
474    }
475}
476
477/// Protected runtime environment value state.
478#[derive(Clone, Debug, Eq, PartialEq)]
479#[non_exhaustive]
480pub enum ProtectedEnvironmentValue {
481    /// The source value is deliberately not retained.
482    Redacted,
483    /// An explicitly authorized opaque value; formatting and snapshots remain redacted.
484    AuthorizedOpaque(SensitiveEnvironmentValue),
485}
486
487/// A bounded configured label collection.
488pub type Labels = BTreeMap<String, String>;
489
490/// A configured container command observed from `Config.Cmd`.
491///
492/// This is native input evidence, not a deployment argument type. Its constructor stays private
493/// so callers cannot accidentally manufacture an observation with invented provenance.
494#[derive(Clone, Eq, PartialEq)]
495pub struct ConfiguredContainerCommand(Vec<String>);
496
497impl ConfiguredContainerCommand {
498    pub(crate) const fn new(arguments: Vec<String>) -> Self {
499        Self(arguments)
500    }
501
502    /// Returns the declared command arguments in their native order.
503    #[must_use]
504    pub fn arguments(&self) -> &[String] {
505        &self.0
506    }
507}
508
509impl fmt::Debug for ConfiguredContainerCommand {
510    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
511        formatter
512            .debug_struct("ConfiguredContainerCommand")
513            .field("argument_count", &self.0.len())
514            .finish()
515    }
516}
517
518/// A configured container entrypoint observed from `Config.Entrypoint`.
519#[derive(Clone, Eq, PartialEq)]
520pub struct ConfiguredContainerEntrypoint(Vec<String>);
521
522impl ConfiguredContainerEntrypoint {
523    pub(crate) const fn new(arguments: Vec<String>) -> Self {
524        Self(arguments)
525    }
526
527    /// Returns the declared entrypoint arguments in their native order.
528    #[must_use]
529    pub fn arguments(&self) -> &[String] {
530        &self.0
531    }
532}
533
534impl fmt::Debug for ConfiguredContainerEntrypoint {
535    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
536        formatter
537            .debug_struct("ConfiguredContainerEntrypoint")
538            .field("argument_count", &self.0.len())
539            .finish()
540    }
541}
542
543macro_rules! configured_container_text {
544    ($type:ident, $doc:literal) => {
545        #[doc = $doc]
546        #[derive(Clone, Eq, PartialEq)]
547        pub struct $type(String);
548
549        impl $type {
550            pub(crate) fn new(value: String) -> Self {
551                Self(value)
552            }
553
554            /// Returns the native configured spelling.
555            #[must_use]
556            pub fn value(&self) -> &str {
557                &self.0
558            }
559        }
560
561        impl fmt::Debug for $type {
562            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
563                formatter.write_str(concat!(stringify!($type), "([redacted])"))
564            }
565        }
566    };
567}
568
569configured_container_text!(
570    ConfiguredContainerUser,
571    "A configured container user from `Config.User`."
572);
573configured_container_text!(
574    ConfiguredContainerWorkdir,
575    "A configured container working directory from `Config.WorkingDir`."
576);
577configured_container_text!(
578    ConfiguredContainerHostname,
579    "A configured container hostname from `Config.Hostname`."
580);
581
582/// One native relationship reference with its exact source location.
583#[derive(Clone, Eq, PartialEq)]
584pub struct NativeResourceReference {
585    reference: String,
586    field_path: String,
587}
588
589impl NativeResourceReference {
590    pub(crate) fn new(reference: String, field_path: String) -> Self {
591        Self { reference, field_path }
592    }
593
594    /// Returns the native identifier or name that requires explicit resolution.
595    #[must_use]
596    pub fn reference(&self) -> &str {
597        &self.reference
598    }
599
600    /// Returns the reviewed native field that supplied this reference.
601    #[must_use]
602    pub fn field_path(&self) -> &str {
603        &self.field_path
604    }
605}
606
607impl fmt::Debug for NativeResourceReference {
608    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
609        formatter
610            .debug_struct("NativeResourceReference")
611            .field("field_path", &self.field_path)
612            .finish_non_exhaustive()
613    }
614}
615
616/// A typed declared container mount kind.
617#[derive(Clone, Copy, Debug, Eq, PartialEq)]
618#[non_exhaustive]
619pub enum ContainerMountKind {
620    /// A named Podman volume.
621    NamedVolume,
622    /// A host bind mount. Its source path stays local-resolution evidence.
623    Bind,
624}
625
626/// The source of a typed native mount.
627#[derive(Clone, Eq, PartialEq)]
628#[non_exhaustive]
629pub enum ContainerMountSource {
630    /// A configured named-volume reference.
631    NamedVolume(String),
632    /// A host-specific path observed from the local Podman service.
633    LocalBindPath(String),
634}
635
636/// An explicitly configured `SELinux` relabel policy for one bind mount.
637///
638/// Podman reports this intent as the case-sensitive `z` or `Z` option. The
639/// decoder retains only that closed semantic choice; it never exposes the
640/// surrounding native bind specification.
641#[derive(Clone, Copy, Debug, Eq, PartialEq)]
642#[non_exhaustive]
643pub enum ContainerMountSelinuxRelabel {
644    /// Relabel content so multiple containers may share it (`z`).
645    Shared,
646    /// Relabel content for private use by one container (`Z`).
647    Private,
648}
649
650/// A privacy-safe consistency result for the image operand in Podman's recorded
651/// creation command.
652///
653/// The native spelling is compared transiently and is never retained. This is
654/// creation evidence only: it neither proves an image was pulled nor records a
655/// build or other image history.
656#[derive(Clone, Copy, Debug, Eq, PartialEq)]
657#[non_exhaustive]
658pub enum AuthoredImageSpellingHint {
659    /// The transient operand matched the configured `ImageName` spelling.
660    MatchesConfiguredImage,
661    /// The transient operand matched the local resolved image identifier.
662    MatchesLocalImageId,
663    /// Both typed image observations were available, and the operand matched
664    /// neither the configured spelling nor the local resolved identifier.
665    Contradictory,
666}
667
668/// A privacy-safe SELinux-relabel result from Podman's recorded creation command.
669///
670/// The index identifies a typed inspect mount. It does not expose a native
671/// command argument, host path, mount source, or mount destination.
672#[derive(Clone, Copy, Debug, Eq, PartialEq)]
673#[non_exhaustive]
674pub enum AuthoredMountRelabelHint {
675    /// A command `z` choice agreed with the indexed typed mount.
676    Shared {
677        /// Index of the correlated typed inspect mount.
678        mount_index: usize,
679    },
680    /// A command `Z` choice agreed with the indexed typed mount.
681    Private {
682        /// Index of the correlated typed inspect mount.
683        mount_index: usize,
684    },
685    /// A command relabel choice could not be reconciled with the typed mount.
686    Contradictory {
687        /// Index of the correlated typed inspect mount.
688        mount_index: usize,
689    },
690}
691
692/// Bounded, redacted evidence derived transiently from `CreateCommand`.
693///
694/// No raw command component is retained. In particular, environment values,
695/// secrets, paths, image spellings, and post-image command payloads cannot be
696/// recovered from this value.
697/// Image-spelling and mount-relabel projections retain independent
698/// [`ObservationField`] states.
699#[derive(Clone, Eq, PartialEq)]
700pub struct ContainerCreationEvidence {
701    image: ObservationField<AuthoredImageSpellingHint>,
702    mount_relabels: ObservationField<Vec<AuthoredMountRelabelHint>>,
703}
704
705impl ContainerCreationEvidence {
706    pub(crate) fn new(
707        image: ObservationField<AuthoredImageSpellingHint>,
708        mount_relabels: ObservationField<Vec<AuthoredMountRelabelHint>>,
709    ) -> Self {
710        Self { image, mount_relabels }
711    }
712
713    /// Returns the closed image-spelling consistency result or its independent
714    /// observation state.
715    #[must_use]
716    pub const fn image(&self) -> &ObservationField<AuthoredImageSpellingHint> {
717        &self.image
718    }
719
720    /// Returns closed relabel consistency results by typed inspect-mount index,
721    /// or their independent observation state.
722    #[must_use]
723    pub const fn mount_relabels(&self) -> &ObservationField<Vec<AuthoredMountRelabelHint>> {
724        &self.mount_relabels
725    }
726}
727
728impl fmt::Debug for ContainerCreationEvidence {
729    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
730        formatter
731            .debug_struct("ContainerCreationEvidence")
732            .field("image", &self.image)
733            .field("mount_relabels", &self.mount_relabels)
734            .finish()
735    }
736}
737
738impl ContainerMountSource {
739    /// Returns the native source spelling. A bind path is local-resolution evidence and must not
740    /// be promoted automatically into portable intent.
741    #[must_use]
742    pub fn value(&self) -> &str {
743        match self {
744            Self::NamedVolume(value) | Self::LocalBindPath(value) => value,
745        }
746    }
747}
748
749impl fmt::Debug for ContainerMountSource {
750    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
751        let kind = match self {
752            Self::NamedVolume(_) => "named_volume",
753            Self::LocalBindPath(_) => "local_bind_path",
754        };
755        formatter.debug_tuple("ContainerMountSource").field(&kind).finish()
756    }
757}
758
759/// One typed native mount. Every nested field keeps its independent native observation state.
760#[derive(Clone, Eq, PartialEq)]
761pub struct ContainerMountObservation {
762    kind: ContainerMountKind,
763    source: ObservationField<ContainerMountSource>,
764    local_backing_path: ObservationField<String>,
765    destination: ObservationField<String>,
766    writable: ObservationField<bool>,
767    options: ObservationField<Vec<String>>,
768    selinux_relabel: ObservationField<ContainerMountSelinuxRelabel>,
769    propagation: ObservationField<String>,
770    subpath: ObservationField<String>,
771}
772
773impl ContainerMountObservation {
774    #[allow(clippy::too_many_arguments)]
775    pub(crate) const fn new(
776        kind: ContainerMountKind,
777        source: ObservationField<ContainerMountSource>,
778        local_backing_path: ObservationField<String>,
779        destination: ObservationField<String>,
780        writable: ObservationField<bool>,
781        options: ObservationField<Vec<String>>,
782        selinux_relabel: ObservationField<ContainerMountSelinuxRelabel>,
783        propagation: ObservationField<String>,
784        subpath: ObservationField<String>,
785    ) -> Self {
786        Self {
787            kind,
788            source,
789            local_backing_path,
790            destination,
791            writable,
792            options,
793            selinux_relabel,
794            propagation,
795            subpath,
796        }
797    }
798
799    /// Returns the accepted native mount kind.
800    #[must_use]
801    pub const fn kind(&self) -> ContainerMountKind {
802        self.kind
803    }
804    /// Returns source evidence; bind paths are always local-resolution evidence.
805    #[must_use]
806    pub fn source(&self) -> &ObservationField<ContainerMountSource> {
807        &self.source
808    }
809    /// Returns the host-specific backing path when Podman supplied one. This is always local
810    /// resolution evidence and cannot be promoted automatically.
811    #[must_use]
812    pub fn local_backing_path(&self) -> &ObservationField<String> {
813        &self.local_backing_path
814    }
815    /// Returns the configured container destination.
816    #[must_use]
817    pub fn destination(&self) -> &ObservationField<String> {
818        &self.destination
819    }
820    /// Returns the observed writable setting.
821    #[must_use]
822    pub fn writable(&self) -> &ObservationField<bool> {
823        &self.writable
824    }
825    /// Returns mount options when the native response supplied them.
826    #[must_use]
827    pub fn options(&self) -> &ObservationField<Vec<String>> {
828        &self.options
829    }
830
831    /// Returns the configured `SELinux` relabel choice recovered from bounded
832    /// native mount evidence.
833    #[must_use]
834    pub fn selinux_relabel(&self) -> &ObservationField<ContainerMountSelinuxRelabel> {
835        &self.selinux_relabel
836    }
837    /// Returns mount propagation when the native response supplied it.
838    #[must_use]
839    pub fn propagation(&self) -> &ObservationField<String> {
840        &self.propagation
841    }
842    /// Returns named-volume subpath evidence when the native response supplied it.
843    #[must_use]
844    pub fn subpath(&self) -> &ObservationField<String> {
845        &self.subpath
846    }
847}
848
849impl fmt::Debug for ContainerMountObservation {
850    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
851        formatter
852            .debug_struct("ContainerMountObservation")
853            .field("kind", &self.kind)
854            .field("source", &self.source)
855            .field(
856                "local_backing_path_state",
857                &observation_field_state(&self.local_backing_path),
858            )
859            .field("destination_state", &observation_field_state(&self.destination))
860            .field("writable", &self.writable)
861            .field(
862                "option_count",
863                &self.options.observed().map_or(0, |options| options.value().len()),
864            )
865            .field("selinux_relabel", &self.selinux_relabel)
866            .field("propagation_state", &observation_field_state(&self.propagation))
867            .field("subpath_state", &observation_field_state(&self.subpath))
868            .finish()
869    }
870}
871
872fn observation_field_state<T>(field: &ObservationField<T>) -> &'static str {
873    match field {
874        ObservationField::Observed(_) => "observed",
875        ObservationField::Absent => "absent",
876        ObservationField::Unavailable => "unavailable",
877        ObservationField::Malformed => "malformed",
878        ObservationField::VersionInapplicable => "version-inapplicable",
879        ObservationField::NotApplicable => "not-applicable",
880        ObservationField::Unmodelled(_) => "unmodelled",
881    }
882}
883
884/// Coalesced secret ID/name evidence. Both spellings must resolve to one secret before traversal.
885#[derive(Clone, Eq, PartialEq)]
886pub struct ContainerSecretReference {
887    id: Option<NativeResourceReference>,
888    name: Option<NativeResourceReference>,
889}
890
891impl ContainerSecretReference {
892    pub(crate) const fn new(id: Option<NativeResourceReference>, name: Option<NativeResourceReference>) -> Self {
893        Self { id, name }
894    }
895    /// Returns the optional native secret-ID source evidence.
896    #[must_use]
897    pub fn id(&self) -> Option<&NativeResourceReference> {
898        self.id.as_ref()
899    }
900    /// Returns the optional native secret-name source evidence.
901    #[must_use]
902    pub fn name(&self) -> Option<&NativeResourceReference> {
903        self.name.as_ref()
904    }
905}
906
907impl fmt::Debug for ContainerSecretReference {
908    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
909        formatter
910            .debug_struct("ContainerSecretReference")
911            .field("has_id", &self.id.is_some())
912            .field("has_name", &self.name.is_some())
913            .finish()
914    }
915}
916
917/// One typed native secret grant. It never carries secret payload bytes.
918#[derive(Clone, Eq, PartialEq)]
919pub struct ContainerSecretGrantObservation {
920    reference: ObservationField<ContainerSecretReference>,
921    uid: ObservationField<u32>,
922    gid: ObservationField<u32>,
923    mode: ObservationField<u32>,
924}
925
926impl ContainerSecretGrantObservation {
927    pub(crate) const fn new(
928        reference: ObservationField<ContainerSecretReference>,
929        uid: ObservationField<u32>,
930        gid: ObservationField<u32>,
931        mode: ObservationField<u32>,
932    ) -> Self {
933        Self {
934            reference,
935            uid,
936            gid,
937            mode,
938        }
939    }
940    /// Returns coalesced ID/name source evidence.
941    #[must_use]
942    pub fn reference(&self) -> &ObservationField<ContainerSecretReference> {
943        &self.reference
944    }
945    /// Returns effective UID metadata. Podman inspect does not preserve whether zero was explicit.
946    #[must_use]
947    pub fn uid(&self) -> &ObservationField<u32> {
948        &self.uid
949    }
950    /// Returns effective GID metadata. Podman inspect does not preserve whether zero was explicit.
951    #[must_use]
952    pub fn gid(&self) -> &ObservationField<u32> {
953        &self.gid
954    }
955    /// Returns effective file-mode metadata. Podman inspect does not preserve whether zero was explicit.
956    #[must_use]
957    pub fn mode(&self) -> &ObservationField<u32> {
958        &self.mode
959    }
960}
961
962impl fmt::Debug for ContainerSecretGrantObservation {
963    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
964        formatter
965            .debug_struct("ContainerSecretGrantObservation")
966            .field("reference", &self.reference)
967            .field("uid", &self.uid)
968            .field("gid", &self.gid)
969            .field("mode", &self.mode)
970            .finish()
971    }
972}
973
974/// A bounded native restart policy name observed from HostConfig.RestartPolicy.
975#[derive(Clone, Copy, Debug, Eq, PartialEq)]
976#[non_exhaustive]
977pub enum NativeRestartPolicyName {
978    /// Never restart automatically.
979    No,
980    /// Always restart automatically.
981    Always,
982    /// Restart after a failure.
983    OnFailure,
984    /// Restart unless explicitly stopped.
985    UnlessStopped,
986}
987
988/// Typed, effective native restart-policy evidence.
989#[derive(Clone, Debug, Eq, PartialEq)]
990pub struct NativeRestartPolicyObservation {
991    name: ObservationField<NativeRestartPolicyName>,
992    maximum_retry_count: ObservationField<u64>,
993}
994
995impl NativeRestartPolicyObservation {
996    pub(crate) const fn new(
997        name: ObservationField<NativeRestartPolicyName>,
998        maximum_retry_count: ObservationField<u64>,
999    ) -> Self {
1000        Self {
1001            name,
1002            maximum_retry_count,
1003        }
1004    }
1005
1006    /// Returns the effective policy name or its field state.
1007    #[must_use]
1008    pub fn name(&self) -> &ObservationField<NativeRestartPolicyName> {
1009        &self.name
1010    }
1011
1012    /// Returns the effective native retry count, including valid zero.
1013    #[must_use]
1014    pub fn maximum_retry_count(&self) -> &ObservationField<u64> {
1015        &self.maximum_retry_count
1016    }
1017}
1018
1019/// A protected health-check command. Its argument values are never formatted or snapshotted.
1020#[derive(Clone, Eq, PartialEq)]
1021pub struct ProtectedHealthCommand {
1022    arguments: Vec<String>,
1023}
1024
1025impl ProtectedHealthCommand {
1026    pub(crate) const fn new(arguments: Vec<String>) -> Self {
1027        Self { arguments }
1028    }
1029
1030    /// Returns the number of protected command arguments without disclosing their values.
1031    #[must_use]
1032    pub fn argument_count(&self) -> usize {
1033        self.arguments.len()
1034    }
1035    /// Lets an explicitly authorized caller use arguments without formatting or serializing them.
1036    pub fn expose<R>(&self, use_arguments: impl FnOnce(&[String]) -> R) -> R {
1037        use_arguments(&self.arguments)
1038    }
1039}
1040
1041impl fmt::Debug for ProtectedHealthCommand {
1042    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1043        formatter
1044            .debug_struct("ProtectedHealthCommand")
1045            .field("argument_count", &self.arguments.len())
1046            .finish()
1047    }
1048}
1049
1050impl fmt::Display for ProtectedHealthCommand {
1051    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1052        formatter.write_str("[redacted]")
1053    }
1054}
1055
1056/// Native health command syntax with values retained as protected evidence.
1057#[derive(Clone, Debug, Eq, PartialEq)]
1058#[non_exhaustive]
1059pub enum NativeHealthCommand {
1060    /// Podman's explicit NONE health-check form.
1061    Disabled,
1062    /// A shell command whose arguments are protected.
1063    Shell(ProtectedHealthCommand),
1064    /// A direct executable command whose arguments are protected.
1065    Exec(ProtectedHealthCommand),
1066}
1067
1068/// Typed normal-health observation from Config.Healthcheck.
1069#[derive(Clone, Debug, Eq, PartialEq)]
1070pub struct NativeHealthCheckObservation {
1071    command: ObservationField<NativeHealthCommand>,
1072    interval: ObservationField<i64>,
1073    timeout: ObservationField<i64>,
1074    retries: ObservationField<u64>,
1075    start_period: ObservationField<i64>,
1076}
1077
1078impl NativeHealthCheckObservation {
1079    pub(crate) const fn new(
1080        command: ObservationField<NativeHealthCommand>,
1081        interval: ObservationField<i64>,
1082        timeout: ObservationField<i64>,
1083        retries: ObservationField<u64>,
1084        start_period: ObservationField<i64>,
1085    ) -> Self {
1086        Self {
1087            command,
1088            interval,
1089            timeout,
1090            retries,
1091            start_period,
1092        }
1093    }
1094
1095    /// Returns protected health-command evidence or its field state.
1096    #[must_use]
1097    pub fn command(&self) -> &ObservationField<NativeHealthCommand> {
1098        &self.command
1099    }
1100    /// Returns the native interval, including effective zero.
1101    #[must_use]
1102    pub fn interval(&self) -> &ObservationField<i64> {
1103        &self.interval
1104    }
1105    /// Returns the native timeout, including effective zero.
1106    #[must_use]
1107    pub fn timeout(&self) -> &ObservationField<i64> {
1108        &self.timeout
1109    }
1110    /// Returns the effective native retry count, including zero.
1111    #[must_use]
1112    pub fn retries(&self) -> &ObservationField<u64> {
1113        &self.retries
1114    }
1115    /// Returns the native start period, including effective zero.
1116    #[must_use]
1117    pub fn start_period(&self) -> &ObservationField<i64> {
1118        &self.start_period
1119    }
1120}
1121
1122/// The bounded normal-health failure action reported by Podman.
1123#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1124#[non_exhaustive]
1125pub enum NativeHealthFailureAction {
1126    /// Retains the unhealthy state without stopping the container.
1127    None,
1128    /// Kills the container.
1129    Kill,
1130    /// Restarts the container.
1131    Restart,
1132    /// Stops the container.
1133    Stop,
1134}
1135
1136/// Typed startup-health observation from Config.StartupHealthCheck.
1137#[derive(Clone, Debug, Eq, PartialEq)]
1138pub struct NativeStartupHealthCheckObservation {
1139    command: ObservationField<NativeHealthCommand>,
1140    interval: ObservationField<i64>,
1141    timeout: ObservationField<i64>,
1142    retries: ObservationField<u64>,
1143    start_period: ObservationField<i64>,
1144    successes: ObservationField<u64>,
1145}
1146
1147impl NativeStartupHealthCheckObservation {
1148    pub(crate) const fn new(
1149        command: ObservationField<NativeHealthCommand>,
1150        interval: ObservationField<i64>,
1151        timeout: ObservationField<i64>,
1152        retries: ObservationField<u64>,
1153        start_period: ObservationField<i64>,
1154        successes: ObservationField<u64>,
1155    ) -> Self {
1156        Self {
1157            command,
1158            interval,
1159            timeout,
1160            retries,
1161            start_period,
1162            successes,
1163        }
1164    }
1165
1166    /// Returns protected startup-command evidence or its field state.
1167    #[must_use]
1168    pub fn command(&self) -> &ObservationField<NativeHealthCommand> {
1169        &self.command
1170    }
1171    /// Returns the native interval, including effective zero.
1172    #[must_use]
1173    pub fn interval(&self) -> &ObservationField<i64> {
1174        &self.interval
1175    }
1176    /// Returns the native timeout, including effective zero.
1177    #[must_use]
1178    pub fn timeout(&self) -> &ObservationField<i64> {
1179        &self.timeout
1180    }
1181    /// Returns the effective native retry count, including zero.
1182    #[must_use]
1183    pub fn retries(&self) -> &ObservationField<u64> {
1184        &self.retries
1185    }
1186    /// Returns the effective native start period, including zero.
1187    #[must_use]
1188    pub fn start_period(&self) -> &ObservationField<i64> {
1189        &self.start_period
1190    }
1191    /// Returns the effective native startup success threshold, including zero.
1192    #[must_use]
1193    pub fn successes(&self) -> &ObservationField<u64> {
1194        &self.successes
1195    }
1196}
1197
1198/// The bounded logging drivers represented by this native observation batch.
1199#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1200#[non_exhaustive]
1201pub enum NativeLogDriver {
1202    /// Podman's journald driver.
1203    Journald,
1204    /// Podman's k8s-file driver.
1205    K8sFile,
1206}
1207
1208/// Native logging observation from HostConfig.LogConfig.
1209#[derive(Clone, Debug, Eq, PartialEq)]
1210pub struct NativeLoggingObservation {
1211    driver: ObservationField<NativeLogDriver>,
1212    size: ObservationField<String>,
1213}
1214
1215impl NativeLoggingObservation {
1216    pub(crate) const fn new(driver: ObservationField<NativeLogDriver>, size: ObservationField<String>) -> Self {
1217        Self { driver, size }
1218    }
1219
1220    /// Returns the effective logging driver or its field state.
1221    #[must_use]
1222    pub fn driver(&self) -> &ObservationField<NativeLogDriver> {
1223        &self.driver
1224    }
1225
1226    /// Returns the effective native log-size spelling or its field state.
1227    #[must_use]
1228    pub fn size(&self) -> &ObservationField<String> {
1229        &self.size
1230    }
1231}
1232
1233/// One reviewed Linux capability spelling from native container inspection.
1234///
1235/// This input-only type is deliberately separate from the deployment capability type.
1236/// Native order and duplicates are evidence and therefore remain intact.
1237#[derive(Clone, Debug, Eq, PartialEq)]
1238pub struct NativeCapability(String);
1239
1240impl NativeCapability {
1241    pub(crate) fn new(value: String) -> Self {
1242        Self(value)
1243    }
1244
1245    /// Returns the exact reviewed native capability spelling.
1246    #[must_use]
1247    pub fn as_str(&self) -> &str {
1248        &self.0
1249    }
1250}
1251
1252/// Opaque native security options. Values are deliberately never retained or exposed.
1253#[derive(Clone, Debug, Eq, PartialEq)]
1254pub struct NativeOpaqueSecurityOptions {
1255    count: usize,
1256}
1257
1258impl NativeOpaqueSecurityOptions {
1259    pub(crate) const fn new(count: usize) -> Self {
1260        Self { count }
1261    }
1262
1263    /// Returns the number of opaque security options.
1264    #[must_use]
1265    pub const fn len(&self) -> usize {
1266        self.count
1267    }
1268
1269    /// Returns whether no opaque security options were observed.
1270    #[must_use]
1271    pub const fn is_empty(&self) -> bool {
1272        self.count == 0
1273    }
1274}
1275
1276/// Effective native security evidence from `HostConfig`.
1277#[derive(Clone, Debug, Eq, PartialEq)]
1278pub struct NativeSecurityObservation {
1279    privileged: ObservationField<bool>,
1280    cap_add: ObservationField<Vec<NativeCapability>>,
1281    cap_drop: ObservationField<Vec<NativeCapability>>,
1282    security_options: ObservationField<NativeOpaqueSecurityOptions>,
1283    read_only_root_filesystem: ObservationField<bool>,
1284}
1285
1286impl NativeSecurityObservation {
1287    pub(crate) const fn new(
1288        privileged: ObservationField<bool>,
1289        cap_add: ObservationField<Vec<NativeCapability>>,
1290        cap_drop: ObservationField<Vec<NativeCapability>>,
1291        security_options: ObservationField<NativeOpaqueSecurityOptions>,
1292        read_only_root_filesystem: ObservationField<bool>,
1293    ) -> Self {
1294        Self {
1295            privileged,
1296            cap_add,
1297            cap_drop,
1298            security_options,
1299            read_only_root_filesystem,
1300        }
1301    }
1302
1303    /// Returns effective privileged state.
1304    #[must_use]
1305    pub fn privileged(&self) -> &ObservationField<bool> {
1306        &self.privileged
1307    }
1308
1309    /// Returns added capabilities in native order, including duplicates.
1310    #[must_use]
1311    pub fn cap_add(&self) -> &ObservationField<Vec<NativeCapability>> {
1312        &self.cap_add
1313    }
1314
1315    /// Returns dropped capabilities in native order, including duplicates.
1316    #[must_use]
1317    pub fn cap_drop(&self) -> &ObservationField<Vec<NativeCapability>> {
1318        &self.cap_drop
1319    }
1320
1321    /// Returns only the count and state of opaque security options.
1322    #[must_use]
1323    pub fn security_options(&self) -> &ObservationField<NativeOpaqueSecurityOptions> {
1324        &self.security_options
1325    }
1326
1327    /// Returns effective read-only-root-filesystem state.
1328    #[must_use]
1329    pub fn read_only_root_filesystem(&self) -> &ObservationField<bool> {
1330        &self.read_only_root_filesystem
1331    }
1332}
1333
1334/// A bounded native private or host namespace mode.
1335#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1336#[non_exhaustive]
1337pub enum NativeNamespaceMode {
1338    /// A private namespace.
1339    Private,
1340    /// The host namespace.
1341    Host,
1342}
1343
1344/// A bounded native IPC namespace mode.
1345#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1346#[non_exhaustive]
1347pub enum NativeIpcNamespaceMode {
1348    /// A private IPC namespace.
1349    Private,
1350    /// The host IPC namespace.
1351    Host,
1352    /// A shareable private IPC namespace.
1353    Shareable,
1354    /// No IPC namespace.
1355    None,
1356}
1357
1358/// Effective native namespace evidence from `HostConfig`.
1359#[derive(Clone, Debug, Eq, PartialEq)]
1360pub struct NativeNamespaceObservation {
1361    pid: ObservationField<NativeNamespaceMode>,
1362    ipc: ObservationField<NativeIpcNamespaceMode>,
1363    uts: ObservationField<NativeNamespaceMode>,
1364    cgroup: ObservationField<NativeNamespaceMode>,
1365}
1366
1367impl NativeNamespaceObservation {
1368    pub(crate) const fn new(
1369        pid: ObservationField<NativeNamespaceMode>,
1370        ipc: ObservationField<NativeIpcNamespaceMode>,
1371        uts: ObservationField<NativeNamespaceMode>,
1372        cgroup: ObservationField<NativeNamespaceMode>,
1373    ) -> Self {
1374        Self { pid, ipc, uts, cgroup }
1375    }
1376
1377    /// Returns effective PID namespace evidence.
1378    #[must_use]
1379    pub fn pid(&self) -> &ObservationField<NativeNamespaceMode> {
1380        &self.pid
1381    }
1382
1383    /// Returns effective IPC namespace evidence.
1384    #[must_use]
1385    pub fn ipc(&self) -> &ObservationField<NativeIpcNamespaceMode> {
1386        &self.ipc
1387    }
1388
1389    /// Returns effective UTS namespace evidence.
1390    #[must_use]
1391    pub fn uts(&self) -> &ObservationField<NativeNamespaceMode> {
1392        &self.uts
1393    }
1394
1395    /// Returns effective cgroup namespace evidence.
1396    #[must_use]
1397    pub fn cgroup(&self) -> &ObservationField<NativeNamespaceMode> {
1398        &self.cgroup
1399    }
1400}
1401
1402/// One native ulimit observation. Values are retained exactly without output-intent validation.
1403#[derive(Clone, Debug, Eq, PartialEq)]
1404pub struct NativeUlimitObservation {
1405    name: ObservationField<String>,
1406    soft: ObservationField<i64>,
1407    hard: ObservationField<i64>,
1408}
1409
1410impl NativeUlimitObservation {
1411    pub(crate) const fn new(
1412        name: ObservationField<String>,
1413        soft: ObservationField<i64>,
1414        hard: ObservationField<i64>,
1415    ) -> Self {
1416        Self { name, soft, hard }
1417    }
1418
1419    /// Returns the exact native limit name.
1420    #[must_use]
1421    pub fn name(&self) -> &ObservationField<String> {
1422        &self.name
1423    }
1424
1425    /// Returns the native soft limit, including zero and -1.
1426    #[must_use]
1427    pub fn soft(&self) -> &ObservationField<i64> {
1428        &self.soft
1429    }
1430
1431    /// Returns the native hard limit, including zero and -1.
1432    #[must_use]
1433    pub fn hard(&self) -> &ObservationField<i64> {
1434        &self.hard
1435    }
1436}
1437
1438/// Effective native CPU, memory, PID, and ulimit evidence from `HostConfig`.
1439#[derive(Clone, Debug, Eq, PartialEq)]
1440pub struct NativeResourceControlObservation {
1441    cpu_shares: ObservationField<u64>,
1442    cpu_period: ObservationField<u64>,
1443    cpu_quota: ObservationField<i64>,
1444    memory: ObservationField<i64>,
1445    pids_limit: ObservationField<i64>,
1446    ulimits: ObservationField<Vec<NativeUlimitObservation>>,
1447}
1448
1449impl NativeResourceControlObservation {
1450    pub(crate) const fn new(
1451        cpu_shares: ObservationField<u64>,
1452        cpu_period: ObservationField<u64>,
1453        cpu_quota: ObservationField<i64>,
1454        memory: ObservationField<i64>,
1455        pids_limit: ObservationField<i64>,
1456        ulimits: ObservationField<Vec<NativeUlimitObservation>>,
1457    ) -> Self {
1458        Self {
1459            cpu_shares,
1460            cpu_period,
1461            cpu_quota,
1462            memory,
1463            pids_limit,
1464            ulimits,
1465        }
1466    }
1467
1468    /// Returns native CPU shares, preserving zero.
1469    #[must_use]
1470    pub fn cpu_shares(&self) -> &ObservationField<u64> {
1471        &self.cpu_shares
1472    }
1473
1474    /// Returns native CPU period, preserving zero.
1475    #[must_use]
1476    pub fn cpu_period(&self) -> &ObservationField<u64> {
1477        &self.cpu_period
1478    }
1479
1480    /// Returns native CPU quota, preserving zero and negative native sentinels.
1481    #[must_use]
1482    pub fn cpu_quota(&self) -> &ObservationField<i64> {
1483        &self.cpu_quota
1484    }
1485
1486    /// Returns native memory bytes, preserving zero and negative native sentinels.
1487    #[must_use]
1488    pub fn memory(&self) -> &ObservationField<i64> {
1489        &self.memory
1490    }
1491
1492    /// Returns native PID limit, preserving zero and -1.
1493    #[must_use]
1494    pub fn pids_limit(&self) -> &ObservationField<i64> {
1495        &self.pids_limit
1496    }
1497
1498    /// Returns native ulimits in source order.
1499    #[must_use]
1500    pub fn ulimits(&self) -> &ObservationField<Vec<NativeUlimitObservation>> {
1501        &self.ulimits
1502    }
1503}
1504
1505/// Container-specific native observations.
1506#[derive(Clone, Eq, PartialEq)]
1507pub struct ContainerObservation {
1508    configured_image: ObservationField<String>,
1509    labels: ObservationField<Labels>,
1510    local_image_id: ObservationField<String>,
1511    relationships: ObservationField<Vec<NativeRelationship>>,
1512    environment: ObservationField<ProtectedEnvironment>,
1513    command: ObservationField<ConfiguredContainerCommand>,
1514    entrypoint: ObservationField<ConfiguredContainerEntrypoint>,
1515    user: ObservationField<ConfiguredContainerUser>,
1516    working_directory: ObservationField<ConfiguredContainerWorkdir>,
1517    hostname: ObservationField<ConfiguredContainerHostname>,
1518    pod_membership: ObservationField<NativeResourceReference>,
1519    native_dependencies: ObservationField<Vec<NativeResourceReference>>,
1520    mounts: ObservationField<Vec<ContainerMountObservation>>,
1521    secret_grants: ObservationField<Vec<ContainerSecretGrantObservation>>,
1522    memory_swappiness: ObservationField<u64>,
1523    infra: ObservationField<bool>,
1524    restart_policy: ObservationField<NativeRestartPolicyObservation>,
1525    health_check: ObservationField<NativeHealthCheckObservation>,
1526    health_failure_action: ObservationField<NativeHealthFailureAction>,
1527    startup_health_check: ObservationField<NativeStartupHealthCheckObservation>,
1528    logging: ObservationField<NativeLoggingObservation>,
1529    security: ObservationField<NativeSecurityObservation>,
1530    namespaces: ObservationField<NativeNamespaceObservation>,
1531    resource_controls: ObservationField<NativeResourceControlObservation>,
1532    networking: ObservationField<NativeNetworkingObservation>,
1533    creation_evidence: ObservationField<ContainerCreationEvidence>,
1534}
1535
1536macro_rules! observation_debug {
1537    ($type:ty, $($field:ident),+ $(,)?) => {
1538        impl fmt::Debug for $type {
1539            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1540                let mut debug = formatter.debug_struct(stringify!($type));
1541                $(debug.field(stringify!($field), &self.$field);)+
1542                debug.finish()
1543            }
1544        }
1545    };
1546}
1547
1548observation_debug!(
1549    ContainerObservation,
1550    labels,
1551    configured_image,
1552    local_image_id,
1553    relationships,
1554    environment,
1555    command,
1556    entrypoint,
1557    user,
1558    working_directory,
1559    hostname,
1560    pod_membership,
1561    native_dependencies,
1562    mounts,
1563    secret_grants,
1564    memory_swappiness,
1565    infra,
1566    networking,
1567    restart_policy,
1568    health_check,
1569    health_failure_action,
1570    startup_health_check,
1571    logging,
1572    security,
1573    namespaces,
1574    resource_controls,
1575);
1576
1577impl ContainerObservation {
1578    #[allow(clippy::too_many_arguments)] // private typed decoder construction keeps every field explicit.
1579    pub(crate) fn new(
1580        labels: ObservationField<Labels>,
1581        configured_image: ObservationField<String>,
1582        local_image_id: ObservationField<String>,
1583        relationships: ObservationField<Vec<NativeRelationship>>,
1584        environment: ObservationField<ProtectedEnvironment>,
1585        command: ObservationField<ConfiguredContainerCommand>,
1586        entrypoint: ObservationField<ConfiguredContainerEntrypoint>,
1587        user: ObservationField<ConfiguredContainerUser>,
1588        working_directory: ObservationField<ConfiguredContainerWorkdir>,
1589        hostname: ObservationField<ConfiguredContainerHostname>,
1590        pod_membership: ObservationField<NativeResourceReference>,
1591        native_dependencies: ObservationField<Vec<NativeResourceReference>>,
1592        mounts: ObservationField<Vec<ContainerMountObservation>>,
1593        secret_grants: ObservationField<Vec<ContainerSecretGrantObservation>>,
1594        memory_swappiness: ObservationField<u64>,
1595        infra: ObservationField<bool>,
1596        restart_policy: ObservationField<NativeRestartPolicyObservation>,
1597        health_check: ObservationField<NativeHealthCheckObservation>,
1598        health_failure_action: ObservationField<NativeHealthFailureAction>,
1599        startup_health_check: ObservationField<NativeStartupHealthCheckObservation>,
1600        logging: ObservationField<NativeLoggingObservation>,
1601        security: ObservationField<NativeSecurityObservation>,
1602        namespaces: ObservationField<NativeNamespaceObservation>,
1603        resource_controls: ObservationField<NativeResourceControlObservation>,
1604        networking: ObservationField<NativeNetworkingObservation>,
1605        creation_evidence: ObservationField<ContainerCreationEvidence>,
1606    ) -> Self {
1607        Self {
1608            configured_image,
1609            labels,
1610            local_image_id,
1611            relationships,
1612            environment,
1613            command,
1614            entrypoint,
1615            user,
1616            working_directory,
1617            hostname,
1618            pod_membership,
1619            native_dependencies,
1620            mounts,
1621            secret_grants,
1622            memory_swappiness,
1623            infra,
1624            restart_policy,
1625            health_check,
1626            health_failure_action,
1627            startup_health_check,
1628            logging,
1629            security,
1630            namespaces,
1631            resource_controls,
1632            networking,
1633            creation_evidence,
1634        }
1635    }
1636
1637    /// Returns the configured container labels or their observation state.
1638    #[must_use]
1639    pub fn labels(&self) -> &ObservationField<Labels> {
1640        &self.labels
1641    }
1642    /// Returns the configured image spelling or its observation state.
1643    ///
1644    /// This is the only container image observation that discovery may use as a dependency edge.
1645    #[must_use]
1646    pub fn configured_image(&self) -> &ObservationField<String> {
1647        &self.configured_image
1648    }
1649    /// Returns the locally resolved image identity or its observation state.
1650    ///
1651    /// A local image ID proves what this Podman service used; it is not deployment intent.
1652    #[must_use]
1653    pub fn local_image_id(&self) -> &ObservationField<String> {
1654        &self.local_image_id
1655    }
1656    /// Returns protected runtime environment observations or their observation state.
1657    #[must_use]
1658    pub fn environment(&self) -> &ObservationField<ProtectedEnvironment> {
1659        &self.environment
1660    }
1661    /// Returns the configured command or its observation state.
1662    #[must_use]
1663    pub fn command(&self) -> &ObservationField<ConfiguredContainerCommand> {
1664        &self.command
1665    }
1666    /// Returns the configured entrypoint or its observation state.
1667    #[must_use]
1668    pub fn entrypoint(&self) -> &ObservationField<ConfiguredContainerEntrypoint> {
1669        &self.entrypoint
1670    }
1671    /// Returns the configured user or its observation state.
1672    #[must_use]
1673    pub fn user(&self) -> &ObservationField<ConfiguredContainerUser> {
1674        &self.user
1675    }
1676    /// Returns the configured working directory or its observation state.
1677    #[must_use]
1678    pub fn working_directory(&self) -> &ObservationField<ConfiguredContainerWorkdir> {
1679        &self.working_directory
1680    }
1681    /// Returns the configured hostname or its observation state.
1682    #[must_use]
1683    pub fn hostname(&self) -> &ObservationField<ConfiguredContainerHostname> {
1684        &self.hostname
1685    }
1686    /// Returns the container's configured pod-membership evidence or its state.
1687    #[must_use]
1688    pub fn pod_membership(&self) -> &ObservationField<NativeResourceReference> {
1689        &self.pod_membership
1690    }
1691    /// Returns declared native container dependencies or their observation state.
1692    #[must_use]
1693    pub fn native_dependencies(&self) -> &ObservationField<Vec<NativeResourceReference>> {
1694        &self.native_dependencies
1695    }
1696    /// Returns accepted named-volume and bind mount observations or their state.
1697    #[must_use]
1698    pub fn mounts(&self) -> &ObservationField<Vec<ContainerMountObservation>> {
1699        &self.mounts
1700    }
1701
1702    /// Returns bounded, redacted creation-command consistency evidence.
1703    ///
1704    /// This is never an accessor for Podman's raw `CreateCommand`, nor evidence
1705    /// of pull, build, runtime, or lifecycle history.
1706    #[must_use]
1707    pub fn creation_evidence(&self) -> &ObservationField<ContainerCreationEvidence> {
1708        &self.creation_evidence
1709    }
1710    /// Returns typed secret grants without secret payload material or their state.
1711    #[must_use]
1712    pub fn secret_grants(&self) -> &ObservationField<Vec<ContainerSecretGrantObservation>> {
1713        &self.secret_grants
1714    }
1715    /// Returns the configured memory-swappiness value or its observation state.
1716    #[must_use]
1717    pub fn memory_swappiness(&self) -> &ObservationField<u64> {
1718        &self.memory_swappiness
1719    }
1720    /// Returns effective restart-policy evidence or its observation state.
1721    #[must_use]
1722    pub fn restart_policy(&self) -> &ObservationField<NativeRestartPolicyObservation> {
1723        &self.restart_policy
1724    }
1725    /// Returns effective normal-health inspect evidence or its observation state.
1726    ///
1727    /// This may include an image default and is not authored deployment intent.
1728    #[must_use]
1729    pub fn health_check(&self) -> &ObservationField<NativeHealthCheckObservation> {
1730        &self.health_check
1731    }
1732    /// Returns effective normal-health failure action or its observation state.
1733    ///
1734    /// This may include an image default and is not authored deployment intent.
1735    #[must_use]
1736    pub fn health_failure_action(&self) -> &ObservationField<NativeHealthFailureAction> {
1737        &self.health_failure_action
1738    }
1739    /// Returns effective startup-health inspect evidence or its observation state.
1740    ///
1741    /// This may include an image default and is not authored deployment intent.
1742    #[must_use]
1743    pub fn startup_health_check(&self) -> &ObservationField<NativeStartupHealthCheckObservation> {
1744        &self.startup_health_check
1745    }
1746    /// Returns effective logging evidence or its observation state.
1747    #[must_use]
1748    pub fn logging(&self) -> &ObservationField<NativeLoggingObservation> {
1749        &self.logging
1750    }
1751    /// Returns effective security evidence or its observation state.
1752    #[must_use]
1753    pub fn security(&self) -> &ObservationField<NativeSecurityObservation> {
1754        &self.security
1755    }
1756    /// Returns effective namespace evidence for any inspected container, including pod members.
1757    #[must_use]
1758    pub fn namespaces(&self) -> &ObservationField<NativeNamespaceObservation> {
1759        &self.namespaces
1760    }
1761    /// Returns effective resource-control evidence or its observation state.
1762    #[must_use]
1763    pub fn resource_controls(&self) -> &ObservationField<NativeResourceControlObservation> {
1764        &self.resource_controls
1765    }
1766    /// Returns the infra-container marker or its observation state.
1767    #[must_use]
1768    pub fn infra(&self) -> &ObservationField<bool> {
1769        &self.infra
1770    }
1771    /// Returns bounded configured networking evidence for an unpodded container.
1772    ///
1773    /// Pod-member networking is topology-owned by its pod and is never promoted here.
1774    #[must_use]
1775    pub fn networking(&self) -> &ObservationField<NativeNetworkingObservation> {
1776        &self.networking
1777    }
1778    pub(crate) fn relationships(&self) -> &ObservationField<Vec<NativeRelationship>> {
1779        &self.relationships
1780    }
1781}
1782
1783/// Pod-specific native observations.
1784#[derive(Clone, Eq, PartialEq)]
1785pub struct PodObservation {
1786    labels: ObservationField<Labels>,
1787    relationships: ObservationField<Vec<NativeRelationship>>,
1788    create_infra: ObservationField<bool>,
1789    networking: ObservationField<NativeNetworkingObservation>,
1790}
1791observation_debug!(PodObservation, labels, relationships, create_infra, networking);
1792
1793impl PodObservation {
1794    pub(crate) fn new(
1795        labels: ObservationField<Labels>,
1796        relationships: ObservationField<Vec<NativeRelationship>>,
1797        create_infra: ObservationField<bool>,
1798        networking: ObservationField<NativeNetworkingObservation>,
1799    ) -> Self {
1800        Self {
1801            labels,
1802            relationships,
1803            create_infra,
1804            networking,
1805        }
1806    }
1807    /// Returns the configured pod labels or their observation state.
1808    #[must_use]
1809    pub fn labels(&self) -> &ObservationField<Labels> {
1810        &self.labels
1811    }
1812    /// Returns whether the pod was created with an infra container.
1813    #[must_use]
1814    pub fn create_infra(&self) -> &ObservationField<bool> {
1815        &self.create_infra
1816    }
1817    /// Returns networking observed only from `Pod.InspectPodData.InfraConfig`.
1818    #[must_use]
1819    pub fn networking(&self) -> &ObservationField<NativeNetworkingObservation> {
1820        &self.networking
1821    }
1822    pub(crate) fn relationships(&self) -> &ObservationField<Vec<NativeRelationship>> {
1823        &self.relationships
1824    }
1825}
1826
1827/// A protocol carried by a native inspected port binding.
1828#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1829#[non_exhaustive]
1830pub enum NativePortProtocol {
1831    /// TCP.
1832    Tcp,
1833    /// UDP.
1834    Udp,
1835    /// SCTP.
1836    Sctp,
1837}
1838
1839/// One bounded native port-binding observation.
1840#[derive(Clone, Debug, Eq, PartialEq)]
1841pub struct NativePortBindingObservation {
1842    container_port: u16,
1843    protocol: NativePortProtocol,
1844    host_ip: ObservationField<IpAddr>,
1845    host_port: ObservationField<u16>,
1846}
1847
1848impl NativePortBindingObservation {
1849    pub(crate) const fn new(
1850        container_port: u16,
1851        protocol: NativePortProtocol,
1852        host_ip: ObservationField<IpAddr>,
1853        host_port: ObservationField<u16>,
1854    ) -> Self {
1855        Self {
1856            container_port,
1857            protocol,
1858            host_ip,
1859            host_port,
1860        }
1861    }
1862    /// Returns the container port from the binding key.
1863    #[must_use]
1864    pub const fn container_port(&self) -> u16 {
1865        self.container_port
1866    }
1867    /// Returns the native transport protocol.
1868    #[must_use]
1869    pub const fn protocol(&self) -> NativePortProtocol {
1870        self.protocol
1871    }
1872    /// Returns the optional host IP or its observation state.
1873    #[must_use]
1874    pub fn host_ip(&self) -> &ObservationField<IpAddr> {
1875        &self.host_ip
1876    }
1877    /// Returns the optional host port or its observation state.
1878    #[must_use]
1879    pub fn host_port(&self) -> &ObservationField<u16> {
1880        &self.host_port
1881    }
1882}
1883
1884/// Bounded, opaque native network options. Option values are deliberately never exposed.
1885#[derive(Clone, Debug, Eq, PartialEq)]
1886pub struct NativeOpaqueNetworkOptions {
1887    count: usize,
1888}
1889
1890impl NativeOpaqueNetworkOptions {
1891    pub(crate) const fn new(count: usize) -> Self {
1892        Self { count }
1893    }
1894    /// Returns the number of opaque options without exposing keys or values.
1895    #[must_use]
1896    pub const fn len(&self) -> usize {
1897        self.count
1898    }
1899    /// Returns whether no opaque options were observed.
1900    #[must_use]
1901    pub const fn is_empty(&self) -> bool {
1902        self.count == 0
1903    }
1904}
1905
1906/// Native networking evidence observed from authoritative pod infra or unpodded host config.
1907///
1908/// This is deliberately separate from declared deployment networking intent. Host entries and
1909/// free-form network options remain non-promotable bounded metadata.
1910#[derive(Clone, Debug, Eq, PartialEq)]
1911pub struct NativeNetworkingObservation {
1912    port_bindings: ObservationField<Vec<NativePortBindingObservation>>,
1913    create_net_ns: ObservationField<bool>,
1914    host_network: ObservationField<bool>,
1915    dns_servers: ObservationField<Vec<IpAddr>>,
1916    dns_search: ObservationField<Vec<String>>,
1917    dns_options: ObservationField<Vec<String>>,
1918    host_entries: ObservationField<NativeOpaqueNetworkOptions>,
1919    networks: ObservationField<Vec<NativeResourceReference>>,
1920    network_options: ObservationField<NativeOpaqueNetworkOptions>,
1921    no_manage_resolv_conf: ObservationField<bool>,
1922    no_manage_hosts: ObservationField<bool>,
1923    static_ip: ObservationField<IpAddr>,
1924    static_mac: ObservationField<String>,
1925}
1926
1927impl NativeNetworkingObservation {
1928    #[allow(clippy::too_many_arguments)] // private decoder construction keeps every source field explicit.
1929    pub(crate) fn new(
1930        port_bindings: ObservationField<Vec<NativePortBindingObservation>>,
1931        create_net_ns: ObservationField<bool>,
1932        host_network: ObservationField<bool>,
1933        dns_servers: ObservationField<Vec<IpAddr>>,
1934        dns_search: ObservationField<Vec<String>>,
1935        dns_options: ObservationField<Vec<String>>,
1936        host_entries: ObservationField<NativeOpaqueNetworkOptions>,
1937        networks: ObservationField<Vec<NativeResourceReference>>,
1938        network_options: ObservationField<NativeOpaqueNetworkOptions>,
1939        no_manage_resolv_conf: ObservationField<bool>,
1940        no_manage_hosts: ObservationField<bool>,
1941        static_ip: ObservationField<IpAddr>,
1942        static_mac: ObservationField<String>,
1943    ) -> Self {
1944        Self {
1945            port_bindings,
1946            create_net_ns,
1947            host_network,
1948            dns_servers,
1949            dns_search,
1950            dns_options,
1951            host_entries,
1952            networks,
1953            network_options,
1954            no_manage_resolv_conf,
1955            no_manage_hosts,
1956            static_ip,
1957            static_mac,
1958        }
1959    }
1960    /// Returns bounded port-binding evidence.
1961    #[must_use]
1962    pub fn port_bindings(&self) -> &ObservationField<Vec<NativePortBindingObservation>> {
1963        &self.port_bindings
1964    }
1965    /// Returns the configured container network-namespace creation gate.
1966    #[must_use]
1967    pub fn create_net_ns(&self) -> &ObservationField<bool> {
1968        &self.create_net_ns
1969    }
1970    /// Returns the effective host-network gate.
1971    #[must_use]
1972    pub fn host_network(&self) -> &ObservationField<bool> {
1973        &self.host_network
1974    }
1975    /// Returns copied/configured DNS server evidence.
1976    #[must_use]
1977    pub fn dns_servers(&self) -> &ObservationField<Vec<IpAddr>> {
1978        &self.dns_servers
1979    }
1980    /// Returns copied/configured DNS search evidence.
1981    #[must_use]
1982    pub fn dns_search(&self) -> &ObservationField<Vec<String>> {
1983        &self.dns_search
1984    }
1985    /// Returns copied/configured DNS option evidence.
1986    #[must_use]
1987    pub fn dns_options(&self) -> &ObservationField<Vec<String>> {
1988        &self.dns_options
1989    }
1990    /// Returns the state of `/etc/hosts` entry data.
1991    ///
1992    /// `PodmanLens` intentionally does not parse this free-form hosts-file syntax as aliases or
1993    /// expose its values. A present entry list is therefore `Unmodelled`.
1994    #[must_use]
1995    pub fn host_entries(&self) -> &ObservationField<NativeOpaqueNetworkOptions> {
1996        &self.host_entries
1997    }
1998    /// Returns effective native network names in native order; that order is not a contract.
1999    #[must_use]
2000    pub fn networks(&self) -> &ObservationField<Vec<NativeResourceReference>> {
2001        &self.networks
2002    }
2003    /// Returns opaque network-option evidence without key/value semantics.
2004    #[must_use]
2005    pub fn network_options(&self) -> &ObservationField<NativeOpaqueNetworkOptions> {
2006        &self.network_options
2007    }
2008    /// Returns the effective resolver-management gate.
2009    #[must_use]
2010    pub fn no_manage_resolv_conf(&self) -> &ObservationField<bool> {
2011        &self.no_manage_resolv_conf
2012    }
2013    /// Returns the effective hosts-file-management gate.
2014    #[must_use]
2015    pub fn no_manage_hosts(&self) -> &ObservationField<bool> {
2016        &self.no_manage_hosts
2017    }
2018    /// Returns static IP evidence only where the inspected field has reviewed meaning.
2019    #[must_use]
2020    pub fn static_ip(&self) -> &ObservationField<IpAddr> {
2021        &self.static_ip
2022    }
2023    /// Returns static MAC evidence only where the inspected field has reviewed meaning.
2024    #[must_use]
2025    pub fn static_mac(&self) -> &ObservationField<String> {
2026        &self.static_mac
2027    }
2028}
2029
2030/// Network-specific native observations.
2031#[derive(Clone, Eq, PartialEq)]
2032pub struct NetworkObservation {
2033    labels: ObservationField<Labels>,
2034    internal: ObservationField<bool>,
2035    options: ObservationField<NetworkOptionKeys>,
2036    subnets: ObservationField<Vec<NativeNetworkSubnetObservation>>,
2037    routes: ObservationField<Vec<NativeNetworkRouteObservation>>,
2038}
2039
2040impl NetworkObservation {
2041    pub(crate) fn new(
2042        labels: ObservationField<Labels>,
2043        internal: ObservationField<bool>,
2044        options: ObservationField<NetworkOptionKeys>,
2045        subnets: ObservationField<Vec<NativeNetworkSubnetObservation>>,
2046        routes: ObservationField<Vec<NativeNetworkRouteObservation>>,
2047    ) -> Self {
2048        Self {
2049            labels,
2050            internal,
2051            options,
2052            subnets,
2053            routes,
2054        }
2055    }
2056    /// Returns the configured network labels or their observation state.
2057    #[must_use]
2058    pub fn labels(&self) -> &ObservationField<Labels> {
2059        &self.labels
2060    }
2061    /// Returns the network-internal flag or its observation state.
2062    #[must_use]
2063    pub fn internal(&self) -> &ObservationField<bool> {
2064        &self.internal
2065    }
2066    /// Returns only network option keys; native option values may contain credentials and are
2067    /// never exposed through the public observation contract.
2068    #[must_use]
2069    pub fn options(&self) -> &ObservationField<NetworkOptionKeys> {
2070        &self.options
2071    }
2072    /// Returns typed effective native IPAM subnet observations or their observation state.
2073    #[must_use]
2074    pub fn subnets(&self) -> &ObservationField<Vec<NativeNetworkSubnetObservation>> {
2075        &self.subnets
2076    }
2077    /// Returns typed effective native static-route observations or their observation state.
2078    #[must_use]
2079    pub fn routes(&self) -> &ObservationField<Vec<NativeNetworkRouteObservation>> {
2080        &self.routes
2081    }
2082}
2083observation_debug!(NetworkObservation, labels, internal, options, subnets, routes);
2084
2085/// A syntax-validated native CIDR wire spelling observed from network inspection.
2086///
2087/// This is defensive raw-wire preservation, not [`crate::NetworkCidr`] deployment intent or a
2088/// claim that every accepted spelling is valid for every native field and Podman version.
2089#[derive(Clone, Debug, Eq, PartialEq)]
2090pub struct NativeNetworkCidr {
2091    spelling: String,
2092    network: IpAddr,
2093    prefix: u8,
2094}
2095
2096impl NativeNetworkCidr {
2097    pub(crate) fn parse(spelling: String) -> Option<Self> {
2098        let (network, prefix) = spelling.split_once('/')?;
2099        let network = network.parse::<IpAddr>().ok()?;
2100        let prefix = prefix.parse::<u8>().ok()?;
2101        (prefix <= if network.is_ipv4() { 32 } else { 128 }).then_some(Self {
2102            spelling,
2103            network,
2104            prefix,
2105        })
2106    }
2107
2108    /// Returns the exact syntax-validated native CIDR wire spelling.
2109    #[must_use]
2110    pub fn as_str(&self) -> &str {
2111        &self.spelling
2112    }
2113
2114    /// Returns whether an address of the same family lies within this CIDR.
2115    #[must_use]
2116    pub(crate) fn contains(&self, address: IpAddr) -> bool {
2117        self.network.is_ipv4() == address.is_ipv4()
2118            && native_masked_address(self.network, self.prefix) == native_masked_address(address, self.prefix)
2119    }
2120
2121    /// Returns whether an address has the same family as this CIDR.
2122    #[must_use]
2123    pub(crate) const fn has_address_family(&self, address: IpAddr) -> bool {
2124        self.network.is_ipv4() == address.is_ipv4()
2125    }
2126}
2127
2128/// An effective native network lease range with independently optional endpoint evidence.
2129#[derive(Clone, Debug, Eq, PartialEq)]
2130pub struct NativeNetworkLeaseRange {
2131    start_ip: ObservationField<IpAddr>,
2132    end_ip: ObservationField<IpAddr>,
2133}
2134
2135impl NativeNetworkLeaseRange {
2136    pub(crate) const fn new(start_ip: ObservationField<IpAddr>, end_ip: ObservationField<IpAddr>) -> Self {
2137        Self { start_ip, end_ip }
2138    }
2139    /// Returns the optional inclusive lease-range start address or its observation state.
2140    #[must_use]
2141    pub const fn start_ip(&self) -> &ObservationField<IpAddr> {
2142        &self.start_ip
2143    }
2144    /// Returns the optional inclusive lease-range end address or its observation state.
2145    #[must_use]
2146    pub const fn end_ip(&self) -> &ObservationField<IpAddr> {
2147        &self.end_ip
2148    }
2149}
2150
2151/// One typed native IPAM subnet observation. Every nested member keeps its own observation state.
2152#[derive(Clone, Debug, Eq, PartialEq)]
2153pub struct NativeNetworkSubnetObservation {
2154    cidr: ObservationField<NativeNetworkCidr>,
2155    gateway: ObservationField<IpAddr>,
2156    lease_range: ObservationField<NativeNetworkLeaseRange>,
2157}
2158
2159impl NativeNetworkSubnetObservation {
2160    pub(crate) const fn new(
2161        cidr: ObservationField<NativeNetworkCidr>,
2162        gateway: ObservationField<IpAddr>,
2163        lease_range: ObservationField<NativeNetworkLeaseRange>,
2164    ) -> Self {
2165        Self {
2166            cidr,
2167            gateway,
2168            lease_range,
2169        }
2170    }
2171    /// Returns the native subnet CIDR evidence.
2172    #[must_use]
2173    pub fn cidr(&self) -> &ObservationField<NativeNetworkCidr> {
2174        &self.cidr
2175    }
2176    /// Returns the optional effective native gateway.
2177    #[must_use]
2178    pub fn gateway(&self) -> &ObservationField<IpAddr> {
2179        &self.gateway
2180    }
2181    /// Returns the optional effective native lease range.
2182    #[must_use]
2183    pub fn lease_range(&self) -> &ObservationField<NativeNetworkLeaseRange> {
2184        &self.lease_range
2185    }
2186}
2187
2188/// A route kind observed from the native network inspect response.
2189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2190#[non_exhaustive]
2191pub enum NativeNetworkRouteType {
2192    /// A forwarding route that requires a gateway.
2193    Unicast,
2194    /// A route that drops matching traffic.
2195    Blackhole,
2196    /// A route that reports the destination as unreachable.
2197    Unreachable,
2198    /// A route that reports the destination as administratively prohibited.
2199    Prohibit,
2200}
2201
2202/// One typed native static-route observation. Every nested member keeps its own observation state.
2203#[derive(Clone, Debug, Eq, PartialEq)]
2204pub struct NativeNetworkRouteObservation {
2205    destination: ObservationField<NativeNetworkCidr>,
2206    gateway: ObservationField<IpAddr>,
2207    metric: ObservationField<u32>,
2208    route_type: ObservationField<NativeNetworkRouteType>,
2209}
2210
2211impl NativeNetworkRouteObservation {
2212    pub(crate) const fn new(
2213        destination: ObservationField<NativeNetworkCidr>,
2214        gateway: ObservationField<IpAddr>,
2215        metric: ObservationField<u32>,
2216        route_type: ObservationField<NativeNetworkRouteType>,
2217    ) -> Self {
2218        Self {
2219            destination,
2220            gateway,
2221            metric,
2222            route_type,
2223        }
2224    }
2225    /// Returns the native destination CIDR evidence.
2226    #[must_use]
2227    pub fn destination(&self) -> &ObservationField<NativeNetworkCidr> {
2228        &self.destination
2229    }
2230    /// Returns the optional effective native route gateway.
2231    #[must_use]
2232    pub fn gateway(&self) -> &ObservationField<IpAddr> {
2233        &self.gateway
2234    }
2235    /// Returns the optional effective native route metric, preserving an explicit zero.
2236    #[must_use]
2237    pub fn metric(&self) -> &ObservationField<u32> {
2238        &self.metric
2239    }
2240    /// Returns the native route type. This is version-inapplicable before Podman 6.0.
2241    #[must_use]
2242    pub fn route_type(&self) -> &ObservationField<NativeNetworkRouteType> {
2243        &self.route_type
2244    }
2245}
2246
2247fn native_masked_address(address: IpAddr, prefix: u8) -> IpAddr {
2248    match address {
2249        IpAddr::V4(address) => {
2250            let mask = if prefix == 0 { 0 } else { u32::MAX << (32 - prefix) };
2251            IpAddr::V4(std::net::Ipv4Addr::from(u32::from(address) & mask))
2252        }
2253        IpAddr::V6(address) => {
2254            let mask = if prefix == 0 { 0 } else { u128::MAX << (128 - prefix) };
2255            IpAddr::V6(std::net::Ipv6Addr::from(u128::from(address) & mask))
2256        }
2257    }
2258}
2259
2260/// Public, value-free network option observation.
2261#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
2262pub struct NetworkOptionKeys(BTreeSet<String>);
2263
2264impl NetworkOptionKeys {
2265    pub(crate) fn new(keys: impl IntoIterator<Item = String>) -> Self {
2266        Self(keys.into_iter().collect())
2267    }
2268
2269    /// Returns observed option keys in deterministic order, without their values.
2270    pub fn keys(&self) -> impl Iterator<Item = &str> {
2271        self.0.iter().map(String::as_str)
2272    }
2273
2274    /// Returns the number of observed option keys.
2275    #[must_use]
2276    pub fn len(&self) -> usize {
2277        self.0.len()
2278    }
2279
2280    /// Returns whether no option keys were observed.
2281    #[must_use]
2282    pub fn is_empty(&self) -> bool {
2283        self.0.is_empty()
2284    }
2285}
2286
2287impl fmt::Debug for NetworkOptionKeys {
2288    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2289        formatter
2290            .debug_struct("NetworkOptionKeys")
2291            .field("count", &self.len())
2292            .finish()
2293    }
2294}
2295
2296/// The native wire representation of a volume owner ID.
2297#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2298pub enum VolumeOwnerIdWireValue {
2299    /// Podman's reviewed `omitempty` wire shape omitted the property, which canonically may mean
2300    /// the Podman default of zero.
2301    WireAbsentMayMeanZero,
2302    /// A concrete numeric value was present, including literal zero.
2303    Explicit(UnixId),
2304}
2305
2306/// Bounded Unix user or group identifier from a native volume response.
2307#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2308pub struct UnixId(u32);
2309
2310impl UnixId {
2311    pub(crate) const fn new(value: u32) -> Self {
2312        Self(value)
2313    }
2314    /// Returns the literal value reported by Podman.
2315    #[must_use]
2316    pub const fn get(self) -> u32 {
2317        self.0
2318    }
2319}
2320
2321/// An exact, validated RFC 3339 timestamp from a native Podman response.
2322#[derive(Clone, Debug, Eq, PartialEq)]
2323pub struct NativeTimestamp(String);
2324
2325impl NativeTimestamp {
2326    pub(crate) fn new(value: String) -> Self {
2327        Self(value)
2328    }
2329
2330    /// Returns the exact timestamp spelling reported by Podman.
2331    #[must_use]
2332    pub fn as_str(&self) -> &str {
2333        &self.0
2334    }
2335}
2336
2337/// Count-only evidence for secret-driver options. Option names and values are never retained.
2338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2339pub struct NativeSecretDriverOptions {
2340    count: usize,
2341}
2342
2343impl NativeSecretDriverOptions {
2344    pub(crate) const fn new(count: usize) -> Self {
2345        Self { count }
2346    }
2347
2348    /// Returns the number of opaque driver options.
2349    #[must_use]
2350    pub const fn len(&self) -> usize {
2351        self.count
2352    }
2353
2354    /// Returns whether no driver options were observed.
2355    #[must_use]
2356    pub const fn is_empty(&self) -> bool {
2357        self.count == 0
2358    }
2359}
2360
2361/// Native secret-driver metadata without option names or values.
2362#[derive(Clone, Debug, Eq, PartialEq)]
2363pub struct NativeSecretDriverObservation {
2364    name: ObservationField<String>,
2365    options: ObservationField<NativeSecretDriverOptions>,
2366}
2367
2368impl NativeSecretDriverObservation {
2369    pub(crate) const fn new(
2370        name: ObservationField<String>,
2371        options: ObservationField<NativeSecretDriverOptions>,
2372    ) -> Self {
2373        Self { name, options }
2374    }
2375
2376    /// Returns the effective driver name.
2377    #[must_use]
2378    pub fn name(&self) -> &ObservationField<String> {
2379        &self.name
2380    }
2381
2382    /// Returns only the state and count of opaque driver options.
2383    #[must_use]
2384    pub fn options(&self) -> &ObservationField<NativeSecretDriverOptions> {
2385        &self.options
2386    }
2387}
2388
2389/// Volume-specific native observations.
2390#[derive(Clone, Eq, PartialEq)]
2391pub struct VolumeObservation {
2392    labels: ObservationField<Labels>,
2393    uid: ObservationField<VolumeOwnerIdWireValue>,
2394    gid: ObservationField<VolumeOwnerIdWireValue>,
2395    driver: ObservationField<String>,
2396    created_at: ObservationField<NativeTimestamp>,
2397    anonymous: ObservationField<bool>,
2398}
2399observation_debug!(VolumeObservation, labels, uid, gid, driver, created_at, anonymous);
2400
2401impl VolumeObservation {
2402    pub(crate) fn new(
2403        labels: ObservationField<Labels>,
2404        uid: ObservationField<VolumeOwnerIdWireValue>,
2405        gid: ObservationField<VolumeOwnerIdWireValue>,
2406        driver: ObservationField<String>,
2407        created_at: ObservationField<NativeTimestamp>,
2408        anonymous: ObservationField<bool>,
2409    ) -> Self {
2410        Self {
2411            labels,
2412            uid,
2413            gid,
2414            driver,
2415            created_at,
2416            anonymous,
2417        }
2418    }
2419    /// Returns the configured volume labels or their observation state.
2420    #[must_use]
2421    pub fn labels(&self) -> &ObservationField<Labels> {
2422        &self.labels
2423    }
2424    /// Returns the wire-level volume UID observation or its observation state.
2425    #[must_use]
2426    pub fn uid(&self) -> &ObservationField<VolumeOwnerIdWireValue> {
2427        &self.uid
2428    }
2429    /// Returns the wire-level volume GID observation or its observation state.
2430    #[must_use]
2431    pub fn gid(&self) -> &ObservationField<VolumeOwnerIdWireValue> {
2432        &self.gid
2433    }
2434    /// Returns the effective volume driver name.
2435    #[must_use]
2436    pub fn driver(&self) -> &ObservationField<String> {
2437        &self.driver
2438    }
2439    /// Returns the effective creation timestamp.
2440    #[must_use]
2441    pub fn created_at(&self) -> &ObservationField<NativeTimestamp> {
2442        &self.created_at
2443    }
2444    /// Returns whether Podman reported this as an anonymous volume.
2445    #[must_use]
2446    pub fn anonymous(&self) -> &ObservationField<bool> {
2447        &self.anonymous
2448    }
2449}
2450
2451/// Image-specific native observations.
2452#[derive(Clone, Eq, PartialEq)]
2453pub struct ImageObservation {
2454    labels: ObservationField<Labels>,
2455    repo_tags: ObservationField<Vec<String>>,
2456    repo_digests: ObservationField<Vec<String>>,
2457    environment: ObservationField<ProtectedEnvironment>,
2458    digest: ObservationField<String>,
2459    created: ObservationField<NativeTimestamp>,
2460    author: ObservationField<String>,
2461    architecture: ObservationField<String>,
2462    operating_system: ObservationField<String>,
2463    manifest_type: ObservationField<String>,
2464}
2465
2466pub(crate) struct ImageObservationFields {
2467    pub(crate) labels: ObservationField<Labels>,
2468    pub(crate) repo_tags: ObservationField<Vec<String>>,
2469    pub(crate) repo_digests: ObservationField<Vec<String>>,
2470    pub(crate) environment: ObservationField<ProtectedEnvironment>,
2471    pub(crate) digest: ObservationField<String>,
2472    pub(crate) created: ObservationField<NativeTimestamp>,
2473    pub(crate) author: ObservationField<String>,
2474    pub(crate) architecture: ObservationField<String>,
2475    pub(crate) operating_system: ObservationField<String>,
2476    pub(crate) manifest_type: ObservationField<String>,
2477}
2478observation_debug!(
2479    ImageObservation,
2480    labels,
2481    repo_tags,
2482    repo_digests,
2483    environment,
2484    digest,
2485    created,
2486    author,
2487    architecture,
2488    operating_system,
2489    manifest_type
2490);
2491
2492impl ImageObservation {
2493    pub(crate) fn new(fields: ImageObservationFields) -> Self {
2494        let ImageObservationFields {
2495            labels,
2496            repo_tags,
2497            repo_digests,
2498            environment,
2499            digest,
2500            created,
2501            author,
2502            architecture,
2503            operating_system,
2504            manifest_type,
2505        } = fields;
2506        Self {
2507            labels,
2508            repo_tags,
2509            repo_digests,
2510            environment,
2511            digest,
2512            created,
2513            author,
2514            architecture,
2515            operating_system,
2516            manifest_type,
2517        }
2518    }
2519    /// Returns the configured image labels or their observation state.
2520    #[must_use]
2521    pub fn labels(&self) -> &ObservationField<Labels> {
2522        &self.labels
2523    }
2524    /// Returns locally resolved repository tags or their observation state.
2525    #[must_use]
2526    pub fn repo_tags(&self) -> &ObservationField<Vec<String>> {
2527        &self.repo_tags
2528    }
2529    /// Returns locally resolved repository digests or their observation state.
2530    #[must_use]
2531    pub fn repo_digests(&self) -> &ObservationField<Vec<String>> {
2532        &self.repo_digests
2533    }
2534    /// Returns protected image-environment observations or their observation state.
2535    #[must_use]
2536    pub fn environment(&self) -> &ObservationField<ProtectedEnvironment> {
2537        &self.environment
2538    }
2539    /// Returns the effective image digest.
2540    #[must_use]
2541    pub fn digest(&self) -> &ObservationField<String> {
2542        &self.digest
2543    }
2544    /// Returns the effective image creation timestamp.
2545    #[must_use]
2546    pub fn created(&self) -> &ObservationField<NativeTimestamp> {
2547        &self.created
2548    }
2549    /// Returns the configured image author metadata.
2550    #[must_use]
2551    pub fn author(&self) -> &ObservationField<String> {
2552        &self.author
2553    }
2554    /// Returns the effective image architecture.
2555    #[must_use]
2556    pub fn architecture(&self) -> &ObservationField<String> {
2557        &self.architecture
2558    }
2559    /// Returns the effective image operating-system name.
2560    #[must_use]
2561    pub fn operating_system(&self) -> &ObservationField<String> {
2562        &self.operating_system
2563    }
2564    /// Returns the effective image manifest media type.
2565    #[must_use]
2566    pub fn manifest_type(&self) -> &ObservationField<String> {
2567        &self.manifest_type
2568    }
2569}
2570
2571/// Secret metadata observations.  Secret payload bytes are never represented.
2572#[derive(Clone, Eq, PartialEq)]
2573pub struct SecretObservation {
2574    labels: ObservationField<Labels>,
2575    driver: ObservationField<NativeSecretDriverObservation>,
2576    created_at: ObservationField<NativeTimestamp>,
2577    updated_at: ObservationField<NativeTimestamp>,
2578}
2579observation_debug!(SecretObservation, labels, driver, created_at, updated_at);
2580
2581impl SecretObservation {
2582    pub(crate) fn new(
2583        labels: ObservationField<Labels>,
2584        driver: ObservationField<NativeSecretDriverObservation>,
2585        created_at: ObservationField<NativeTimestamp>,
2586        updated_at: ObservationField<NativeTimestamp>,
2587    ) -> Self {
2588        Self {
2589            labels,
2590            driver,
2591            created_at,
2592            updated_at,
2593        }
2594    }
2595    /// Returns the configured secret labels or their observation state.
2596    #[must_use]
2597    pub fn labels(&self) -> &ObservationField<Labels> {
2598        &self.labels
2599    }
2600    /// Returns the secret-driver metadata or its observation state.
2601    #[must_use]
2602    pub fn driver(&self) -> &ObservationField<NativeSecretDriverObservation> {
2603        &self.driver
2604    }
2605    /// Returns the effective creation timestamp.
2606    #[must_use]
2607    pub fn created_at(&self) -> &ObservationField<NativeTimestamp> {
2608        &self.created_at
2609    }
2610    /// Returns the effective last-update timestamp.
2611    #[must_use]
2612    pub fn updated_at(&self) -> &ObservationField<NativeTimestamp> {
2613        &self.updated_at
2614    }
2615}
2616
2617/// Resource-kind-specific observation payload.
2618#[derive(Clone, Eq, PartialEq)]
2619#[non_exhaustive]
2620#[allow(clippy::large_enum_variant)] // kind-safe public enum avoids heap allocation at every observation access.
2621pub enum ResourceDetails {
2622    /// Container-only fields.
2623    Container(ContainerObservation),
2624    /// Pod-only fields.
2625    Pod(PodObservation),
2626    /// Network-only fields.
2627    Network(NetworkObservation),
2628    /// Volume-only fields.
2629    Volume(VolumeObservation),
2630    /// Image-only fields.
2631    Image(ImageObservation),
2632    /// Secret metadata-only fields.
2633    Secret(SecretObservation),
2634}
2635
2636impl fmt::Debug for ResourceDetails {
2637    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2638        match self {
2639            Self::Container(value) => formatter
2640                .debug_tuple("ResourceDetails::Container")
2641                .field(value)
2642                .finish(),
2643            Self::Pod(value) => formatter.debug_tuple("ResourceDetails::Pod").field(value).finish(),
2644            Self::Network(value) => formatter.debug_tuple("ResourceDetails::Network").field(value).finish(),
2645            Self::Volume(value) => formatter.debug_tuple("ResourceDetails::Volume").field(value).finish(),
2646            Self::Image(value) => formatter.debug_tuple("ResourceDetails::Image").field(value).finish(),
2647            Self::Secret(value) => formatter.debug_tuple("ResourceDetails::Secret").field(value).finish(),
2648        }
2649    }
2650}
2651
2652impl ResourceDetails {
2653    /// Returns the exact resource kind carried by this variant.
2654    #[must_use]
2655    pub const fn kind(&self) -> ResourceKind {
2656        match self {
2657            Self::Container(_) => ResourceKind::Container,
2658            Self::Pod(_) => ResourceKind::Pod,
2659            Self::Network(_) => ResourceKind::Network,
2660            Self::Volume(_) => ResourceKind::Volume,
2661            Self::Image(_) => ResourceKind::Image,
2662            Self::Secret(_) => ResourceKind::Secret,
2663        }
2664    }
2665}
2666
2667/// One complete or partial typed native resource observation.
2668#[derive(Clone, Eq, PartialEq)]
2669pub struct ResourceObservation {
2670    header: ObservationHeader,
2671    details: ResourceDetails,
2672}
2673
2674impl ResourceObservation {
2675    pub(crate) fn try_new(header: ObservationHeader, details: ResourceDetails) -> Result<Self, Diagnostic> {
2676        if header.identity().kind() != details.kind() {
2677            return Err(Diagnostic::new(DiagnosticCode::ResourceMalformed));
2678        }
2679        Ok(Self { header, details })
2680    }
2681
2682    pub(crate) fn incomplete(header: ObservationHeader) -> Self {
2683        let details = incomplete_details(header.identity().kind(), header.state());
2684        Self { header, details }
2685    }
2686
2687    /// Returns resource-wide identity, evidence, findings, and completeness information.
2688    #[must_use]
2689    pub fn header(&self) -> &ObservationHeader {
2690        &self.header
2691    }
2692    /// Returns a kind-safe resource-specific payload.
2693    #[must_use]
2694    pub fn details(&self) -> &ResourceDetails {
2695        &self.details
2696    }
2697
2698    pub(crate) fn header_mut(&mut self) -> &mut ObservationHeader {
2699        &mut self.header
2700    }
2701
2702    pub(crate) fn relationships(&self) -> Option<&ObservationField<Vec<NativeRelationship>>> {
2703        match &self.details {
2704            ResourceDetails::Container(value) => Some(value.relationships()),
2705            ResourceDetails::Pod(value) => Some(value.relationships()),
2706            _ => None,
2707        }
2708    }
2709
2710    pub(crate) fn labels(&self) -> &ObservationField<Labels> {
2711        match &self.details {
2712            ResourceDetails::Container(value) => value.labels(),
2713            ResourceDetails::Pod(value) => value.labels(),
2714            ResourceDetails::Network(value) => value.labels(),
2715            ResourceDetails::Volume(value) => value.labels(),
2716            ResourceDetails::Image(value) => value.labels(),
2717            ResourceDetails::Secret(value) => value.labels(),
2718        }
2719    }
2720
2721    pub(crate) fn image_repo_tags(&self) -> Option<&ObservationField<Vec<String>>> {
2722        match &self.details {
2723            ResourceDetails::Image(value) => Some(value.repo_tags()),
2724            _ => None,
2725        }
2726    }
2727
2728    pub(crate) fn image_repo_digests(&self) -> Option<&ObservationField<Vec<String>>> {
2729        match &self.details {
2730            ResourceDetails::Image(value) => Some(value.repo_digests()),
2731            _ => None,
2732        }
2733    }
2734}
2735
2736fn incomplete_field<T>(state: ResourceObservationState) -> ObservationField<T> {
2737    if state == ResourceObservationState::Malformed {
2738        ObservationField::Malformed
2739    } else {
2740        ObservationField::Unavailable
2741    }
2742}
2743
2744fn incomplete_details(kind: ResourceKind, state: ResourceObservationState) -> ResourceDetails {
2745    match kind {
2746        ResourceKind::Container => ResourceDetails::Container(ContainerObservation::new(
2747            incomplete_field(state),
2748            incomplete_field(state),
2749            incomplete_field(state),
2750            incomplete_field(state),
2751            incomplete_field(state),
2752            incomplete_field(state),
2753            incomplete_field(state),
2754            incomplete_field(state),
2755            incomplete_field(state),
2756            incomplete_field(state),
2757            incomplete_field(state),
2758            incomplete_field(state),
2759            incomplete_field(state),
2760            incomplete_field(state),
2761            incomplete_field(state),
2762            incomplete_field(state),
2763            incomplete_field(state),
2764            incomplete_field(state),
2765            incomplete_field(state),
2766            incomplete_field(state),
2767            incomplete_field(state),
2768            incomplete_field(state),
2769            incomplete_field(state),
2770            incomplete_field(state),
2771            incomplete_field(state),
2772            incomplete_field(state),
2773        )),
2774        ResourceKind::Pod => ResourceDetails::Pod(PodObservation::new(
2775            incomplete_field(state),
2776            incomplete_field(state),
2777            incomplete_field(state),
2778            incomplete_field(state),
2779        )),
2780        ResourceKind::Network => ResourceDetails::Network(NetworkObservation::new(
2781            incomplete_field(state),
2782            incomplete_field(state),
2783            incomplete_field(state),
2784            incomplete_field(state),
2785            incomplete_field(state),
2786        )),
2787        ResourceKind::Volume => ResourceDetails::Volume(VolumeObservation::new(
2788            incomplete_field(state),
2789            incomplete_field(state),
2790            incomplete_field(state),
2791            incomplete_field(state),
2792            incomplete_field(state),
2793            incomplete_field(state),
2794        )),
2795        ResourceKind::Image => ResourceDetails::Image(ImageObservation::new(ImageObservationFields {
2796            labels: incomplete_field(state),
2797            repo_tags: incomplete_field(state),
2798            repo_digests: incomplete_field(state),
2799            environment: incomplete_field(state),
2800            digest: incomplete_field(state),
2801            created: incomplete_field(state),
2802            author: incomplete_field(state),
2803            architecture: incomplete_field(state),
2804            operating_system: incomplete_field(state),
2805            manifest_type: incomplete_field(state),
2806        })),
2807        ResourceKind::Secret => ResourceDetails::Secret(SecretObservation::new(
2808            incomplete_field(state),
2809            incomplete_field(state),
2810            incomplete_field(state),
2811            incomplete_field(state),
2812        )),
2813    }
2814}
2815
2816impl fmt::Debug for ResourceObservation {
2817    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
2818        formatter
2819            .debug_struct("ResourceObservation")
2820            .field("identity", self.header.identity())
2821            .field("state", &self.header.state())
2822            .field("finding_count", &self.header.findings().len())
2823            .field("unmodelled_field_count", &self.header.unmodelled_fields().len())
2824            .field("detail_kind", &self.details.kind())
2825            .finish()
2826    }
2827}