firkin-core 0.0.1

Container orchestration surface for the firkin Rust containerization library
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
//! io — auto-split from the parent module by `split-by-grouping`.
#![allow(missing_docs)]
#[allow(unused_imports)]
use crate::builder::ContainerStdio;
#[allow(unused_imports)]
use crate::error::{Error, Result};
#[allow(unused_imports)]
use crate::runtime::VminitdClient;
#[allow(unused_imports)]
use crate::runtime_rpc_error;
#[allow(unused_imports)]
use crate::sealed;
#[allow(unused_imports)]
pub use firkin_oci::{
    LinuxSeccompAction as SeccompAction, LinuxSeccompArch as SeccompArch,
    LinuxSeccompArg as SeccompArgRule, LinuxSeccompFlag as SeccompFlag,
    LinuxSeccompOperator as SeccompOp, LinuxSeccompProfile as Seccomp,
    LinuxSyscall as SeccompSyscallRule, Mount,
};
#[allow(unused_imports)]
use firkin_types::VirtiofsTag;
#[allow(unused_imports)]
use firkin_types::VsockPort;
#[allow(unused_imports)]
use firkin_types::{ContainerId, ProcessId};
#[allow(unused_imports)]
use firkin_vminitd_client::ProcessCreate;
use sha2::Digest as _;
#[allow(unused_imports)]
use sha2::Sha256;
#[allow(unused_imports)]
use std::io;
#[allow(unused_imports)]
use std::net::IpAddr;
#[allow(unused_imports)]
use std::path::Path;
#[allow(unused_imports)]
use std::path::PathBuf;
#[allow(unused_imports)]
use std::pin::Pin;
#[allow(unused_imports)]
use std::task::{Context, Poll};
#[allow(unused_imports)]
use std::time::Duration;
#[allow(unused_imports)]
use tokio::io::AsyncReadExt;
#[allow(unused_imports)]
use tokio::io::AsyncWriteExt;
#[allow(unused_imports)]
use tokio::io::{AsyncRead, AsyncWrite};
pub(crate) const STDIN_PORT: VsockPort = VsockPort::new(0x1000_0000);
pub(crate) const STDOUT_PORT: VsockPort = VsockPort::new(0x1000_0001);
pub(crate) const STDERR_PORT: VsockPort = VsockPort::new(0x1000_0002);
pub(crate) const EXEC_STDIO_PORT_START: u32 = 0x1000_0100;
pub(crate) const STDIO_CAPTURE_TIMEOUT: Duration = Duration::from_secs(30);
/// Marker for stream-backed stdio.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Streams;
impl sealed::Sealed for Streams {}
impl ContainerStdio for Streams {
    const TERMINAL: bool = false;
}
/// Terminal size for a pseudo-terminal.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PtyConfig {
    /// Terminal columns.
    pub cols: u16,
    /// Terminal rows.
    pub rows: u16,
}
impl PtyConfig {
    /// Construct a terminal size.
    #[must_use]
    pub const fn new(cols: u16, rows: u16) -> Self {
        Self { cols, rows }
    }
}
impl Default for PtyConfig {
    fn default() -> Self {
        Self { cols: 80, rows: 24 }
    }
}
impl From<(u16, u16)> for PtyConfig {
    fn from((cols, rows): (u16, u16)) -> Self {
        Self { cols, rows }
    }
}
/// DNS resolver configuration for a container rootfs.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DnsConfig {
    /// Nameserver IP addresses.
    pub nameservers: Vec<IpAddr>,
    /// Optional DNS domain.
    pub domain: Option<String>,
    /// DNS search domains.
    pub search: Vec<String>,
    /// Resolver options, such as `ndots:2`.
    pub options: Vec<String>,
}
/// Static `/etc/hosts` configuration for a container rootfs.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HostsConfig {
    /// Hosts file entries.
    pub entries: Vec<HostsEntry>,
    /// Optional file-level comment.
    pub comment: Option<String>,
}
/// One `/etc/hosts` entry.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HostsEntry {
    /// IPv4 or IPv6 address.
    pub ip: IpAddr,
    /// Hostnames attached to this address.
    pub hostnames: Vec<String>,
    /// Optional entry comment.
    pub comment: Option<String>,
}
/// Unix domain socket relay configuration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnixSocketConfig {
    /// Unique relay identifier inside the container.
    pub id: String,
    /// Source socket path.
    pub source: PathBuf,
    /// Destination socket path.
    pub destination: PathBuf,
    /// Relay direction.
    pub direction: SocketDirection,
    /// Mode bits for the socket created by the relay, when supported.
    pub permissions: Option<u32>,
}
impl UnixSocketConfig {
    /// Construct a socket relay configuration.
    #[must_use]
    pub fn new(
        id: impl Into<String>,
        source: impl Into<PathBuf>,
        destination: impl Into<PathBuf>,
        direction: SocketDirection,
    ) -> Self {
        Self {
            id: id.into(),
            source: source.into(),
            destination: destination.into(),
            direction,
            permissions: None,
        }
    }
    /// Construct a relay from a host Unix socket into the container.
    #[must_use]
    pub fn into_guest(
        id: impl Into<String>,
        source: impl Into<PathBuf>,
        destination: impl Into<PathBuf>,
    ) -> Self {
        Self::new(id, source, destination, SocketDirection::Into)
    }
    /// Construct a relay from a container Unix socket out to the host.
    #[must_use]
    pub fn out_of_guest(
        id: impl Into<String>,
        source: impl Into<PathBuf>,
        destination: impl Into<PathBuf>,
    ) -> Self {
        Self::new(id, source, destination, SocketDirection::OutOf)
    }
    /// Set mode bits for the socket created by the relay.
    #[must_use]
    pub const fn permissions(mut self, permissions: u32) -> Self {
        self.permissions = Some(permissions);
        self
    }
}
/// Direction for a Unix domain socket relay.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SocketDirection {
    /// Host socket to guest socket.
    Into,
    /// Guest socket to host socket.
    OutOf,
}
/// Single-file mount declaration.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileMount {
    /// Host file source.
    pub source: PathBuf,
    /// Container file destination.
    pub destination: PathBuf,
    /// Whether the mounted file should be read-only in the container.
    pub read_only: bool,
}
impl FileMount {
    /// Construct a read-only file mount.
    #[must_use]
    pub fn read_only(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
        Self {
            source: source.into(),
            destination: destination.into(),
            read_only: true,
        }
    }
    /// Construct a writable file mount.
    #[must_use]
    pub fn read_write(source: impl Into<PathBuf>, destination: impl Into<PathBuf>) -> Self {
        Self {
            source: source.into(),
            destination: destination.into(),
            read_only: false,
        }
    }
}
/// Duplex pseudo-terminal stream for a process.
pub struct Pty {
    input: firkin_vsock::VsockStream,
    output: firkin_vsock::VsockStream,
    size: PtyConfig,
    pub(crate) client: VminitdClient,
    pub(crate) container_id: ContainerId,
    pub(crate) process_id: ProcessId,
}
impl std::fmt::Debug for Pty {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Pty")
            .field("size", &self.size)
            .field("container_id", &self.container_id)
            .field("process_id", &self.process_id)
            .finish_non_exhaustive()
    }
}
impl Pty {
    pub(crate) fn new(
        input: firkin_vsock::VsockStream,
        output: firkin_vsock::VsockStream,
        size: PtyConfig,
        client: VminitdClient,
        container_id: ContainerId,
        process_id: ProcessId,
    ) -> Self {
        Self {
            input,
            output,
            size,
            client,
            container_id,
            process_id,
        }
    }
    /// Resize the guest pseudo-terminal.
    ///
    /// # Errors
    ///
    /// Returns a runtime error if vminitd rejects the resize request.
    pub async fn resize(&mut self, size: impl Into<PtyConfig>) -> Result<()> {
        let size = size.into();
        self.client
            .resize_process(tonic::Request::new(ProcessCreate::resize_request(
                &self.process_id,
                Some(&self.container_id),
                u32::from(size.rows),
                u32::from(size.cols),
            )))
            .await
            .map_err(runtime_rpc_error("resize pty"))?;
        self.size = size;
        Ok(())
    }
    /// Return the last terminal size requested by the host.
    #[must_use]
    pub const fn size(&self) -> PtyConfig {
        self.size
    }
    /// Split this pseudo-terminal into independent input, output, and control handles.
    #[must_use]
    pub fn split(self) -> (PtyInput, PtyOutput, PtyControl) {
        (
            PtyInput { inner: self.input },
            PtyOutput { inner: self.output },
            PtyControl {
                size: self.size,
                client: self.client,
                container_id: self.container_id,
                process_id: self.process_id,
            },
        )
    }
}
impl AsyncRead for Pty {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Pin::new(&mut self.output).poll_read(cx, buf)
    }
}
impl AsyncWrite for Pty {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Pin::new(&mut self.input).poll_write(cx, buf)
    }
    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.input).poll_flush(cx)
    }
    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.input).poll_shutdown(cx)
    }
}
impl sealed::Sealed for Pty {}
impl ContainerStdio for Pty {
    const TERMINAL: bool = true;
}
/// Writable input handle for a pseudo-terminal process.
#[derive(Debug)]
pub struct PtyInput {
    inner: firkin_vsock::VsockStream,
}
impl AsyncWrite for PtyInput {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Pin::new(&mut self.inner).poll_write(cx, buf)
    }
    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_flush(cx)
    }
    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_shutdown(cx)
    }
}
/// Readable output handle for a pseudo-terminal process.
#[derive(Debug)]
pub struct PtyOutput {
    inner: firkin_vsock::VsockStream,
}
impl AsyncRead for PtyOutput {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_read(cx, buf)
    }
}
/// Resize/control handle for a pseudo-terminal process.
#[derive(Debug)]
pub struct PtyControl {
    size: PtyConfig,
    pub(crate) client: VminitdClient,
    pub(crate) container_id: ContainerId,
    pub(crate) process_id: ProcessId,
}
impl PtyControl {
    /// Resize the guest pseudo-terminal.
    ///
    /// # Errors
    ///
    /// Returns a runtime error if vminitd rejects the resize request.
    pub async fn resize(&mut self, size: impl Into<PtyConfig>) -> Result<()> {
        let size = size.into();
        self.client
            .resize_process(tonic::Request::new(ProcessCreate::resize_request(
                &self.process_id,
                Some(&self.container_id),
                u32::from(size.rows),
                u32::from(size.cols),
            )))
            .await
            .map_err(runtime_rpc_error("resize pty"))?;
        self.size = size;
        Ok(())
    }
    /// Return the last terminal size requested by the host.
    #[must_use]
    pub const fn size(&self) -> PtyConfig {
        self.size
    }
}
/// Standard stream configuration for process stdio.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Stdio {
    /// Do not allocate a host-side stream.
    #[default]
    Null,
    /// Allocate a host-side vsock stream and return it through `take_*`.
    Piped,
    /// Relay the guest-side stream to or from this process's stdio.
    Inherit,
}
impl Stdio {
    /// Construct a null stdio configuration.
    #[must_use]
    pub const fn null() -> Self {
        Self::Null
    }
    /// Construct a piped stdio configuration.
    #[must_use]
    pub const fn piped() -> Self {
        Self::Piped
    }
    /// Construct an inherited stdio configuration.
    #[must_use]
    pub const fn inherit() -> Self {
        Self::Inherit
    }
}
/// Writable stdin handle for an exec'd process.
#[derive(Debug)]
pub struct ChildStdin {
    inner: firkin_vsock::VsockStream,
}
impl ChildStdin {
    pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
        Self { inner }
    }
    /// Return the underlying vsock stream.
    #[must_use]
    pub fn into_inner(self) -> firkin_vsock::VsockStream {
        self.inner
    }
}
impl AsyncWrite for ChildStdin {
    fn poll_write(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &[u8],
    ) -> Poll<io::Result<usize>> {
        Pin::new(&mut self.inner).poll_write(cx, buf)
    }
    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_flush(cx)
    }
    fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_shutdown(cx)
    }
}
/// Readable stdout handle for an exec'd process.
#[derive(Debug)]
pub struct ChildStdout {
    inner: firkin_vsock::VsockStream,
}
impl ChildStdout {
    pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
        Self { inner }
    }
    /// Return the underlying vsock stream.
    #[must_use]
    pub fn into_inner(self) -> firkin_vsock::VsockStream {
        self.inner
    }
}
impl AsyncRead for ChildStdout {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_read(cx, buf)
    }
}
/// Readable stderr handle for an exec'd process.
#[derive(Debug)]
pub struct ChildStderr {
    inner: firkin_vsock::VsockStream,
}
impl ChildStderr {
    pub(crate) fn new(inner: firkin_vsock::VsockStream) -> Self {
        Self { inner }
    }
    /// Return the underlying vsock stream.
    #[must_use]
    pub fn into_inner(self) -> firkin_vsock::VsockStream {
        self.inner
    }
}
impl AsyncRead for ChildStderr {
    fn poll_read(
        mut self: Pin<&mut Self>,
        cx: &mut Context<'_>,
        buf: &mut tokio::io::ReadBuf<'_>,
    ) -> Poll<io::Result<()>> {
        Pin::new(&mut self.inner).poll_read(cx, buf)
    }
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PreparedFileMount {
    pub(crate) tag: VirtiofsTag,
    pub(crate) parent: PathBuf,
    filename: String,
    pub(crate) guest_holding_path: String,
    container_destination: PathBuf,
    pub(crate) read_only: bool,
}
impl PreparedFileMount {
    fn from_mount(mount: &FileMount) -> Result<Self> {
        let source =
            std::fs::canonicalize(&mount.source).map_err(|error| Error::RuntimeOperation {
                operation: "prepare file mount",
                reason: format!("{}: {error}", mount.source.display()),
            })?;
        let metadata = std::fs::metadata(&source).map_err(|error| Error::RuntimeOperation {
            operation: "prepare file mount",
            reason: format!("{}: {error}", source.display()),
        })?;
        if !metadata.is_file() {
            return Err(Error::RuntimeOperation {
                operation: "prepare file mount",
                reason: format!("{} is not a regular file", source.display()),
            });
        }
        let filename = source
            .file_name()
            .and_then(|filename| filename.to_str())
            .ok_or_else(|| Error::RuntimeOperation {
                operation: "prepare file mount",
                reason: format!("{} has no UTF-8 file name", source.display()),
            })?
            .to_owned();
        let parent = source
            .parent()
            .ok_or_else(|| Error::RuntimeOperation {
                operation: "prepare file mount",
                reason: format!("{} has no parent directory", source.display()),
            })?
            .to_path_buf();
        let tag = file_mount_tag(&parent)?;
        let guest_holding_path = format!("/run/file-mounts/{}", tag.as_str());
        Ok(Self {
            tag,
            parent,
            filename,
            guest_holding_path,
            container_destination: mount.destination.clone(),
            read_only: mount.read_only,
        })
    }
    pub(crate) fn oci_bind_mount(&self) -> Result<Mount> {
        let destination =
            self.container_destination
                .to_str()
                .ok_or_else(|| Error::RuntimeOperation {
                    operation: "build file bind mount",
                    reason: format!(
                        "file mount destination {} is not valid UTF-8",
                        self.container_destination.display()
                    ),
                })?;
        let mut mount = Mount::custom(
            "none",
            format!("{}/{}", self.guest_holding_path, self.filename),
            destination,
        )
        .extra_option("bind");
        mount = if self.read_only {
            mount.read_only()
        } else {
            mount.extra_option("rw")
        };
        Ok(mount)
    }
}
pub(crate) fn prepare_file_mounts(file_mounts: &[FileMount]) -> Result<Vec<PreparedFileMount>> {
    file_mounts
        .iter()
        .map(PreparedFileMount::from_mount)
        .collect()
}
fn file_mount_tag(parent: &Path) -> Result<VirtiofsTag> {
    let parent = parent.to_str().ok_or_else(|| Error::RuntimeOperation {
        operation: "prepare file mount",
        reason: format!("file mount parent {} is not valid UTF-8", parent.display()),
    })?;
    let digest = Sha256::digest(parent.as_bytes());
    let hex = format!("{digest:x}");
    VirtiofsTag::new(hex[..36].to_owned()).map_err(|error| Error::RuntimeOperation {
        operation: "prepare file mount",
        reason: error.to_string(),
    })
}