Skip to main content

microvms_core/
prelude.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The production wiring, under the method names it had in 0.10.
3//!
4//! Two kinds of method live here as extension traits. The domain can't read the clock or the OS
5//! random pool (ARCH-6), so the methods its types had that did moved here. And the use cases
6//! take their I/O through ports, so the constructors that built the production transport, HTTP
7//! backend, clock or entropy for their caller moved here too: `ControlPlane::new`,
8//! `Sandbox::new`, `Session::connect` and the rest. Each wires the production adapters into a
9//! port-taking constructor the type keeps, such as [`ControlPlane::from_ports`] or
10//! [`SessionBuilder::try_build`].
11//!
12//! With `use microvms_core::prelude::*;` in scope, a call written against 0.10, such as
13//! `CalendarDate::today_utc()` or `ControlPlane::new(region)`, compiles unchanged: a path to a
14//! type's associated function also finds the methods of traits in scope.
15//!
16//! Every async method returns a `Send` future, as the inherent `async fn` it replaces did, so
17//! the bindings can still hand one to a multi-threaded runtime.
18
19use std::future::Future;
20use std::path::Path;
21use std::sync::Arc;
22use std::time::Duration;
23
24use microvms_edges::identity::HandshakeState;
25
26use crate::adapters::SystemAdapters;
27use crate::agents::{AgentSpec, AgentVm, require_specs};
28use crate::clock::{Clock, SystemClock};
29use crate::control::transport::{SignedTransport, Transport};
30use crate::control::{BuildContext, ControlPlane};
31use crate::cost::CalendarDate;
32use crate::entropy::OsEntropy;
33use crate::error::Error;
34use crate::identity::{LaunchIdentity, TunnelIdentity};
35use crate::names::{NameRecord, NameStore};
36use crate::region::Region;
37use crate::sandbox::{ControlPlaneMinter, Sandbox};
38use crate::session::{ProxyAuth, Session, SessionBuilder, TokenMinter};
39
40/// [`CalendarDate`]'s clock read.
41pub trait CalendarDateExt {
42    /// Today, UTC, from the system clock.
43    ///
44    /// Falls back to the epoch if the clock is set before 1970 or after 9999, which is a
45    /// machine whose age arithmetic is already meaningless. That's what 0.10 did, and the
46    /// signature can't grow an error without breaking the callers the prelude exists for.
47    fn today_utc() -> CalendarDate;
48}
49
50impl CalendarDateExt for CalendarDate {
51    fn today_utc() -> CalendarDate {
52        SystemClock::new()
53            .today_utc()
54            .unwrap_or(CalendarDate::from_ymd(1970, 1, 1))
55    }
56}
57
58/// [`NameRecord`]'s timestamp.
59pub trait NameRecordExt: Sized {
60    /// A record for a VM this process can already address, stamped with the current time.
61    ///
62    /// Refuses an illegal name or an empty id, endpoint, or token: a record missing any of
63    /// them resolves to a VM nobody can adopt.
64    fn new(
65        name: impl Into<String>,
66        microvm_id: impl Into<String>,
67        endpoint: impl Into<String>,
68        agent_token: impl Into<String>,
69        region: impl Into<String>,
70    ) -> Result<Self, Error>;
71}
72
73impl NameRecordExt for NameRecord {
74    fn new(
75        name: impl Into<String>,
76        microvm_id: impl Into<String>,
77        endpoint: impl Into<String>,
78        agent_token: impl Into<String>,
79        region: impl Into<String>,
80    ) -> Result<Self, Error> {
81        let at = SystemClock::new().unix_now().as_secs();
82        NameRecord::new_at(name, microvm_id, endpoint, agent_token, region, at)
83    }
84}
85
86/// [`LaunchIdentity`]'s seeds, from the OS random pool.
87pub trait LaunchIdentityExt: Sized {
88    /// Fresh material from the OS random pool.
89    ///
90    /// An all-zero draw isn't reachable from a working pool. It's still refused, because
91    /// the protocol crate refuses a zero seed (a shared identity proves nothing), and this
92    /// constructor must never hand out material the daemon would reject at bootstrap.
93    fn generate() -> Result<Self, Error>;
94}
95
96impl LaunchIdentityExt for LaunchIdentity {
97    fn generate() -> Result<Self, Error> {
98        crate::entropy::launch_identity(&OsEntropy)
99    }
100}
101
102/// [`TunnelIdentity`]'s Noise initiator, which draws its ephemeral key from the OS pool.
103pub trait TunnelIdentityExt {
104    /// A Noise initiator bound to this identity: proves the host, verifies the pin.
105    ///
106    /// Built per connection. A `HandshakeState` carries nonces and must never be reused.
107    fn initiator(&self) -> Result<HandshakeState, Error>;
108}
109
110impl TunnelIdentityExt for TunnelIdentity {
111    fn initiator(&self) -> Result<HandshakeState, Error> {
112        microvms_edges::identity::initiator(self)
113    }
114}
115
116/// [`ControlPlane`]'s production constructors.
117pub trait ControlPlaneExt: Sized {
118    /// Resolves credentials for `region` and returns a usable client.
119    ///
120    /// The region is a [`Region`], so an unsupported one is either a compile error or a
121    /// visible [`Region::unlisted`] at the call site. TRAP-6 is closed by the type before
122    /// this function is reached, which is why there is no region check here.
123    fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
124
125    /// A client over a caller-supplied transport and clock, with the OS random pool and the
126    /// production adapters.
127    ///
128    /// A caller who passes a real transport here gets exactly what `new` builds. The entropy
129    /// is the OS pool's even over a scripted transport, and that's deliberate: such a plane
130    /// still mints client tokens, and a deterministic default would mint repeating ones
131    /// (TRAP-1). [`ControlPlane::from_ports`] is how a test pins it.
132    fn with_transport(transport: Arc<dyn Transport>, region: Region, clock: Arc<dyn Clock>)
133    -> Self;
134}
135
136impl ControlPlaneExt for ControlPlane {
137    async fn new(region: Region) -> Result<Self, Error> {
138        let transport = SignedTransport::new(region.clone()).await?;
139        Ok(Self::with_transport(
140            Arc::new(transport),
141            region,
142            Arc::new(SystemClock::new()),
143        ))
144    }
145
146    fn with_transport(
147        transport: Arc<dyn Transport>,
148        region: Region,
149        clock: Arc<dyn Clock>,
150    ) -> Self {
151        ControlPlane::from_ports(
152            transport,
153            region,
154            clock,
155            Arc::new(OsEntropy),
156            Arc::new(SystemAdapters),
157        )
158    }
159}
160
161/// A production plane for `region`, on `port` when one is given.
162async fn plane_for(region: Region, port: Option<u16>) -> Result<ControlPlane, Error> {
163    let control = ControlPlane::new(region).await?;
164    match port {
165        Some(port) => control.with_port(port),
166        None => Ok(control),
167    }
168}
169
170/// [`Sandbox`]'s constructors that resolve credentials for a region.
171pub trait SandboxExt: Sized {
172    /// Resolves credentials for `region` and returns a sandbox with nothing launched.
173    ///
174    /// TRAP-6 is closed by [`Region`] before this is reached, which is why there is no
175    /// region check here.
176    fn new(region: Region) -> impl Future<Output = Result<Self, Error>> + Send;
177
178    /// [`Sandbox::adopt`] over a plane resolved for `region`, on `port` when the image's
179    /// daemon listens somewhere other than the default. The bindings' entry point.
180    fn adopt_in(
181        region: Region,
182        microvm_id: impl Into<String>,
183        endpoint: impl Into<String>,
184        agent_token: impl Into<String>,
185        port: Option<u16>,
186    ) -> impl Future<Output = Result<Self, Error>> + Send;
187
188    /// Adopts the VM registered as `name` in `store`: [`Sandbox::adopt`] from a record.
189    ///
190    /// `region`, when given, must match the record's; see [`crate::names::resolve`].
191    fn from_name(
192        store: &dyn NameStore,
193        name: &str,
194        region: Option<Region>,
195        port: Option<u16>,
196    ) -> impl Future<Output = Result<Self, Error>> + Send;
197}
198
199impl SandboxExt for Sandbox {
200    async fn new(region: Region) -> Result<Self, Error> {
201        Ok(Self::with_control_plane(ControlPlane::new(region).await?))
202    }
203
204    fn adopt_in(
205        region: Region,
206        microvm_id: impl Into<String>,
207        endpoint: impl Into<String>,
208        agent_token: impl Into<String>,
209        port: Option<u16>,
210    ) -> impl Future<Output = Result<Self, Error>> + Send {
211        // Converted before the future is built, so it holds strings rather than the caller's
212        // argument types and stays `Send` whatever they were.
213        let (microvm_id, endpoint, agent_token) =
214            (microvm_id.into(), endpoint.into(), agent_token.into());
215        async move {
216            Self::adopt(
217                plane_for(region, port).await?,
218                microvm_id,
219                endpoint,
220                agent_token,
221            )
222            .await
223        }
224    }
225
226    async fn from_name(
227        store: &dyn NameStore,
228        name: &str,
229        region: Option<Region>,
230        port: Option<u16>,
231    ) -> Result<Self, Error> {
232        let record = crate::names::resolve(store, name, region.as_ref())?;
233        let control = plane_for(record.region(), port).await?;
234        Self::adopt_record(control, record).await
235    }
236}
237
238/// [`AgentVm`]'s constructors that resolve credentials for a region.
239pub trait AgentVmExt: Sized {
240    /// Adopts the VM registered as `name` in `store`; see `Sandbox::from_name`
241    /// ([`SandboxExt::from_name`]).
242    fn from_name(
243        store: &dyn NameStore,
244        specs: Vec<AgentSpec>,
245        name: &str,
246        region: Option<Region>,
247        port: Option<u16>,
248    ) -> impl Future<Output = Result<Self, Error>> + Send;
249
250    /// [`AgentVm::adopt`] over a plane resolved for `region`; the bindings' entry point.
251    fn adopt_in(
252        region: Region,
253        specs: Vec<AgentSpec>,
254        microvm_id: impl Into<String>,
255        endpoint: impl Into<String>,
256        agent_token: impl Into<String>,
257        port: Option<u16>,
258    ) -> impl Future<Output = Result<Self, Error>> + Send;
259}
260
261impl AgentVmExt for AgentVm {
262    async fn from_name(
263        store: &dyn NameStore,
264        specs: Vec<AgentSpec>,
265        name: &str,
266        region: Option<Region>,
267        port: Option<u16>,
268    ) -> Result<Self, Error> {
269        // The specs first, as the adopt path checks them: a set this client refuses costs no
270        // credential resolution and no call.
271        require_specs(&specs)?;
272        let sandbox = Sandbox::from_name(store, name, region, port).await?;
273        AgentVm::new(sandbox, specs)
274    }
275
276    fn adopt_in(
277        region: Region,
278        specs: Vec<AgentSpec>,
279        microvm_id: impl Into<String>,
280        endpoint: impl Into<String>,
281        agent_token: impl Into<String>,
282        port: Option<u16>,
283    ) -> impl Future<Output = Result<Self, Error>> + Send {
284        let (microvm_id, endpoint, agent_token) =
285            (microvm_id.into(), endpoint.into(), agent_token.into());
286        async move {
287            require_specs(&specs)?;
288            let sandbox =
289                Sandbox::adopt_in(region, microvm_id, endpoint, agent_token, port).await?;
290            AgentVm::new(sandbox, specs)
291        }
292    }
293}
294
295/// [`Session`]'s constructors over the production HTTP backend.
296pub trait SessionExt: Sized {
297    /// A session against `endpoint`, minting proxy tokens through `minter`.
298    ///
299    /// Does not talk to the VM. Constructing a session is free and re-doable, and a
300    /// constructor that probed would make "do I have a session" mean "is the VM up",
301    /// which are different questions with different answers during a launch.
302    fn connect(
303        endpoint: impl Into<String>,
304        agent_token: impl Into<String>,
305        minter: Arc<dyn TokenMinter>,
306    ) -> impl Future<Output = Result<Self, Error>> + Send;
307
308    /// Reattach to an existing AWS VM using a privately retained agent token.
309    ///
310    /// Resolves AWS credentials but does not probe the guest or bootstrap it again.
311    /// The returned session is independent of any sandbox lock: use a separate attach
312    /// with a short request timeout for supervisor keepalives during long transfers.
313    /// Keep `agent_token` in private encrypted storage, never in public job status.
314    fn attach(
315        region: Region,
316        microvm_id: impl Into<String>,
317        endpoint: impl Into<String>,
318        agent_token: impl Into<String>,
319        port: Option<u16>,
320        request_timeout: Option<Duration>,
321    ) -> impl Future<Output = Result<Self, Error>> + Send;
322
323    /// A session with no proxy auth, for a daemon reached directly.
324    ///
325    /// The conformance path and every local-binary test go through here. See
326    /// [`crate::session::Transport::proxy`] on why this is a supported shape rather than a
327    /// test-only escape hatch.
328    fn direct(endpoint: impl Into<String>, agent_token: impl Into<String>) -> Result<Self, Error>;
329}
330
331impl SessionExt for Session {
332    fn connect(
333        endpoint: impl Into<String>,
334        agent_token: impl Into<String>,
335        minter: Arc<dyn TokenMinter>,
336    ) -> impl Future<Output = Result<Self, Error>> + Send {
337        // Built now rather than inside the future, which is then `Send` whatever the caller's
338        // argument types were. Nothing here awaits.
339        let session = Session::builder(endpoint, agent_token)
340            .with_minter(minter)
341            .build();
342        std::future::ready(session)
343    }
344
345    fn attach(
346        region: Region,
347        microvm_id: impl Into<String>,
348        endpoint: impl Into<String>,
349        agent_token: impl Into<String>,
350        port: Option<u16>,
351        request_timeout: Option<Duration>,
352    ) -> impl Future<Output = Result<Self, Error>> + Send {
353        let (microvm_id, endpoint, agent_token) =
354            (microvm_id.into(), endpoint.into(), agent_token.into());
355        async move {
356            if request_timeout.is_some_and(|timeout| timeout.is_zero()) {
357                return Err(Error::invalid_arg("request timeout must be positive"));
358            }
359            let control = plane_for(region, port).await?;
360            let port = control.port();
361            let minter = Arc::new(ControlPlaneMinter::new(Arc::new(control), microvm_id));
362            let mut builder = Session::builder(endpoint, agent_token)
363                .with_minter(minter)
364                .with_port(port);
365            if let Some(timeout) = request_timeout {
366                builder = builder.with_timeout(timeout);
367            }
368            builder.build()
369        }
370    }
371
372    fn direct(endpoint: impl Into<String>, agent_token: impl Into<String>) -> Result<Self, Error> {
373        Session::builder(endpoint, agent_token).build()
374    }
375}
376
377/// [`SessionBuilder`]'s build over the production adapters.
378pub trait SessionBuilderExt {
379    /// Builds the session, with reqwest for an unset backend and tokio's clock for an unset
380    /// clock. [`SessionBuilder::build_with`] over [`SystemAdapters`].
381    fn build(self) -> Result<Session, Error>;
382}
383
384impl SessionBuilderExt for SessionBuilder {
385    fn build(self) -> Result<Session, Error> {
386        self.build_with(&SystemAdapters)
387    }
388}
389
390/// [`ProxyAuth`]'s constructor on tokio's clock.
391pub trait ProxyAuthExt {
392    /// A [`ProxyAuth`] with the validated defaults: [`ProxyAuth::with_clock`] on tokio's clock.
393    fn new(minter: Arc<dyn TokenMinter>, port: u16) -> Self;
394}
395
396impl ProxyAuthExt for ProxyAuth {
397    fn new(minter: Arc<dyn TokenMinter>, port: u16) -> Self {
398        ProxyAuth::with_clock(minter, port, Arc::new(SystemClock::new()))
399    }
400}
401
402/// [`BuildContext`]'s directory read.
403pub trait BuildContextExt: Sized {
404    /// Reads `dir` as a build context; see [`crate::control::context::from_dir`].
405    fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error>;
406}
407
408impl BuildContextExt for BuildContext {
409    fn from_dir(dir: impl AsRef<Path>) -> Result<Self, Error> {
410        crate::control::context::from_dir(dir)
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use crate::error::ErrorKind;
418
419    /// A plane `with_transport` builds draws from the OS pool, never a deterministic source.
420    /// A scripted transport still gets real client tokens, which is what TRAP-1 needs.
421    ///
422    /// **Falsification**: wire `SequenceEntropy` into `with_transport` and two planes draw
423    /// the same bytes.
424    #[test]
425    fn with_transport_draws_from_the_os_pool() {
426        let plane = || {
427            ControlPlane::with_transport(
428                Arc::new(microvms_app::testing::FakeControlPlane::new()),
429                Region::UsEast1,
430                Arc::new(microvms_app::testing::TestClock::new()),
431            )
432        };
433        let draw = |plane: &ControlPlane| {
434            let mut bytes = [0_u8; 16];
435            plane
436                .entropy()
437                .fill(&mut bytes)
438                .expect("the pool is available");
439            bytes
440        };
441        assert_ne!(draw(&plane()), draw(&plane()));
442    }
443
444    /// A name registered in another region, or not registered at all, is refused locally by
445    /// `Sandbox::from_name` and `AgentVm::from_name`, and the refusal never carries the token.
446    ///
447    /// **Falsification**: resolve the record without the caller's region and the foreign
448    /// record goes on to adopt, which fails some other way (run the break with no
449    /// credentials in the environment, so nothing reaches AWS).
450    #[tokio::test]
451    async fn from_name_refuses_a_foreign_or_missing_name_before_any_plane() {
452        struct One(NameRecord);
453        impl NameStore for One {
454            fn get(&self, name: &str) -> Result<Option<NameRecord>, Error> {
455                Ok((name == self.0.name).then(|| self.0.clone()))
456            }
457            fn put(&self, _: &NameRecord) -> Result<(), Error> {
458                unreachable!()
459            }
460            fn delete(&self, _: &str) -> Result<bool, Error> {
461                unreachable!()
462            }
463            fn list(&self) -> Result<Vec<NameRecord>, Error> {
464                Ok(vec![self.0.clone()])
465            }
466        }
467        const TOKEN: &str = "from-name-canary-token";
468        let record = NameRecord::new(
469            "ci",
470            "mvm-abc123",
471            "https://mvm-abc123.microvm.us-west-2.amazonaws.com",
472            TOKEN,
473            "us-west-2",
474        )
475        .expect("record");
476        let store = One(record);
477        let foreign = Sandbox::from_name(&store, "ci", Some(Region::UsEast1), None)
478            .await
479            .expect_err("registered in another region");
480        assert_eq!(foreign.kind(), ErrorKind::InvalidArg);
481        let missing = Sandbox::from_name(&store, "nope", None, None)
482            .await
483            .expect_err("no such name");
484        assert_eq!(missing.kind(), ErrorKind::Precondition);
485        let specs = vec![AgentSpec::new(crate::agents::Agent::Codex)];
486        let agent = AgentVm::from_name(&store, specs, "ci", Some(Region::UsEast1), None)
487            .await
488            .expect_err("registered in another region");
489        assert_eq!(agent.kind(), ErrorKind::InvalidArg);
490        assert!(!format!("{foreign} {missing} {agent}").contains(TOKEN));
491    }
492
493    /// The production session builds without a backend or a clock set: the prelude fills
494    /// both, as 0.10's `build` did.
495    ///
496    /// **Falsification**: make `SessionBuilderExt::build` call `try_build` and the direct
497    /// session is refused for having neither.
498    #[test]
499    fn the_production_build_fills_the_backend_and_the_clock() {
500        let session = Session::direct("http://127.0.0.1:9", "token").expect("a direct session");
501        assert!(session.proxy_auth().is_none());
502    }
503
504    /// `ProxyAuth::new` keeps 0.10's defaults: the default refresh interval, and nothing
505    /// minted yet.
506    ///
507    /// **Falsification**: build it with any other interval and its `Debug` names that one.
508    #[test]
509    fn proxy_auth_new_keeps_the_defaults() {
510        let auth = ProxyAuth::new(
511            Arc::new(microvms_app::testing::CountingMinter::default()),
512            crate::session::DEFAULT_AGENT_PORT,
513        );
514        assert_eq!(auth.port(), crate::session::DEFAULT_AGENT_PORT);
515        assert_eq!(auth.mint_count(), 0);
516        assert!(!auth.is_cached());
517        let described = format!("{auth:?}");
518        assert!(
519            described.contains(&format!(
520                "refresh_after: {:?}",
521                crate::session::DEFAULT_REFRESH_AFTER
522            )),
523            "{described}"
524        );
525    }
526}