Skip to main content

gatehouse_runtime_protocol/lifecycle_v2/
readonly.rs

1//! Read-only info/inspect/info composition over an injected GET exchange.
2//! The exchange must supply bounded, deadline-controlled HTTP transport. This
3//! module does not prove a connection's identity, authentication or lifetime.
4use super::{
5    native_image::{decode_image_for, image_route_for, ImageFacts},
6    native_info::decode_info_read_only,
7    native_status::{decode_inspect, inspect_route, InspectOutcome, State},
8    observation::{info_route, read_only_api, API},
9    ownership::ExpectedContainer,
10    protected_context::ProtectedContext,
11};
12
13/// Evidence of the protected observation sequence, not transport authentication,
14/// freshness, restart continuity, installation verification or dispatch authority.
15/// There is deliberately no public constructor or deserializer. The injected
16/// exchange remains a trust boundary and can supply fabricated HTTP responses.
17pub struct CapturedImage {
18    facts: ImageFacts,
19    image_bytes: Vec<u8>,
20    config_bytes: Vec<u8>,
21    epoch: String,
22}
23impl std::fmt::Debug for CapturedImage {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("CapturedImage").finish_non_exhaustive()
26    }
27}
28impl CapturedImage {
29    pub fn facts(&self) -> &ImageFacts {
30        &self.facts
31    }
32    pub fn image_bytes(&self) -> &[u8] {
33        &self.image_bytes
34    }
35    pub fn config_bytes(&self) -> &[u8] {
36        &self.config_bytes
37    }
38    pub fn operator_epoch(&self) -> &str {
39        &self.epoch
40    }
41    /// Same exact context only; socket recreation/restart is not adopted.
42    pub fn match_context(&self, context: &ProtectedContext) -> Result<(), &'static str> {
43        let (expected, bytes) = context.snapshot()?;
44        if bytes != self.config_bytes || expected.operator_epoch != self.epoch {
45            return Err("captured image context mismatch");
46        }
47        Ok(())
48    }
49}
50
51impl ProtectedContext {
52    /// Capture exact bounded evidence only after protected info/image/info.
53    /// An eventual record constructor must accept this type, never ImageFacts.
54    pub fn capture_image_for_plan<F>(&self, id: &str, get: F) -> Result<CapturedImage, &'static str>
55    where
56        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
57    {
58        let (expected, bytes) = self.snapshot()?;
59        let config_bytes = bytes.to_vec();
60        let epoch = expected.operator_epoch.clone();
61        let api = self.selected_read_only_api()?;
62        let route = image_route_for(&api, id)?;
63        let (facts, image_bytes) =
64            self.inspect_checked_with_api(&api, &route, get, |route, status, bytes| {
65                Ok((
66                    decode_image_for(&api, id, "GET", route, status, bytes)?,
67                    bytes.to_vec(),
68                ))
69            })?;
70        let captured = CapturedImage {
71            facts,
72            image_bytes,
73            config_bytes,
74            epoch,
75        };
76        captured.match_context(self)?;
77        Ok(captured)
78    }
79
80    /// No retry, mutation or lease authority. Even an exact Missing result is
81    /// withheld until the final info observation and filesystem check succeed.
82    pub fn inspect_with<F>(&self, id: &str, get: F) -> Result<InspectOutcome, &'static str>
83    where
84        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
85    {
86        let api = self.selected_read_only_api()?;
87        if api != API {
88            return Err("5.7 container inspect is not supported");
89        }
90        self.inspect_checked_with_api(&api, &inspect_route(id)?, get, |route, status, bytes| {
91            decode_inspect(id, "GET", route, status, bytes)
92        })
93    }
94    /// Selected ownership facts under the same protected observation sequence.
95    /// The expected plan still requires durable installation/slot binding.
96    pub fn inspect_owned_with<F>(
97        &self,
98        owner: &ExpectedContainer,
99        get: F,
100    ) -> Result<State, &'static str>
101    where
102        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
103    {
104        owner.validate()?;
105        let api = self.selected_read_only_api()?;
106        if api != API {
107            return Err("5.7 ownership inspect is not supported");
108        }
109        self.inspect_checked_with_api(
110            &api,
111            &inspect_route(&owner.id)?,
112            get,
113            |route, status, bytes| owner.match_inspect("GET", route, status, bytes),
114        )
115    }
116    /// Image facts are returned only after matching both protected observations.
117    pub fn inspect_image_with<F>(&self, id: &str, get: F) -> Result<ImageFacts, &'static str>
118    where
119        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
120    {
121        let api = self.selected_read_only_api()?;
122        let route = image_route_for(&api, id)?;
123        self.inspect_checked_with_api(&api, &route, get, |route, status, bytes| {
124            decode_image_for(&api, id, "GET", route, status, bytes)
125        })
126    }
127    fn selected_read_only_api(&self) -> Result<String, &'static str> {
128        let (expected, _) = self.snapshot()?;
129        let api = expected.observation.selected_api.clone();
130        if !read_only_api(&api) || expected.observation.server_version != api {
131            return Err("unsupported read-only Podman API");
132        }
133        Ok(api)
134    }
135
136    fn inspect_checked_with_api<F, T, D>(
137        &self,
138        api: &str,
139        route: &str,
140        mut get: F,
141        decode: D,
142    ) -> Result<T, &'static str>
143    where
144        F: FnMut(&str) -> Result<(u16, Vec<u8>), &'static str>,
145        D: FnOnce(&str, u16, &[u8]) -> Result<T, &'static str>,
146    {
147        let (expected, _) = self.snapshot()?;
148        if expected.observation.selected_api != api {
149            return Err("native route API differs from protected context");
150        }
151        expected.validate_local_socket_policy_read_only()?;
152        let info = info_route(api)?;
153        let exchange = |get: &mut F, path: &str| {
154            self.revalidate()?;
155            let response = get(path)?;
156            self.revalidate()?;
157            Ok::<_, &'static str>(response)
158        };
159        let (status, bytes) = exchange(&mut get, &info)?;
160        if status != 200 {
161            return Err("initial native info unavailable");
162        }
163        let before = decode_info_read_only(&bytes, expected.observation.socket.clone(), api)?;
164        expected.match_current_operation_read_only(expected, &before, &before)?;
165        let (status, bytes) = exchange(&mut get, route)?;
166        let result = decode(route, status, &bytes)?;
167        let (status, bytes) = exchange(&mut get, &info)?;
168        if status != 200 {
169            return Err("final native info unavailable");
170        }
171        let after = decode_info_read_only(&bytes, expected.observation.socket.clone(), api)?;
172        expected.match_current_operation_read_only(expected, &before, &after)?;
173        self.revalidate()?;
174        Ok(result)
175    }
176}