northstar_runtime/api/
model.rs

1use serde::{Deserialize, Serialize, Serializer};
2use std::collections::{HashMap, HashSet};
3
4/// Container name
5pub type Name = crate::common::name::Name;
6/// Container identification
7pub type Container = crate::common::container::Container;
8/// Container exit code
9pub type ExitCode = i32;
10/// Manifest
11pub type Manifest = crate::npk::manifest::Manifest;
12/// String that never contains a null byte
13pub type NonNulString = crate::common::non_nul_string::NonNulString;
14/// Process id
15pub type Pid = u32;
16/// Repository id
17pub type RepositoryId = String;
18/// Unix signal
19pub type Signal = u32;
20/// Version
21pub type Version = crate::common::version::Version;
22/// Container statistics
23pub type ContainerStats = HashMap<String, serde_json::Value>;
24
25/// Message
26#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
27#[allow(missing_docs)]
28#[serde(untagged)]
29pub enum Message {
30    Connect { connect: Connect },
31    ConnectAck { connect_ack: ConnectAck },
32    ConnectNack { connect_nack: ConnectNack },
33    Request { request: Request },
34    Response { response: Response },
35    Notification { notification: Notification },
36}
37
38/// Notification / Event
39#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41#[allow(missing_docs)]
42pub enum Notification {
43    CGroup(Container, CgroupNotification),
44    Exit(Container, ExitStatus),
45    Install(Container),
46    Shutdown,
47    Started(Container),
48    Uninstall(Container),
49}
50
51/// Cgroup event
52#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
53#[serde(rename_all = "snake_case")]
54#[allow(missing_docs)]
55pub enum CgroupNotification {
56    Memory(MemoryNotification),
57}
58
59/// CGroup memory event data
60#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62#[allow(missing_docs)]
63pub struct MemoryNotification {
64    pub low: Option<u64>,
65    pub high: Option<u64>,
66    pub max: Option<u64>,
67    pub oom: Option<u64>,
68    pub oom_kill: Option<u64>,
69}
70
71/// Connect
72#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
73#[serde(rename_all = "snake_case")]
74#[allow(missing_docs)]
75pub struct Connect {
76    /// API version
77    pub version: Version,
78    /// Subscribe this connection to notifications
79    pub subscribe_notifications: bool,
80}
81
82/// Connection ack
83#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case")]
85#[allow(missing_docs)]
86pub struct ConnectAck;
87
88/// Connection nack
89#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91#[allow(missing_docs)]
92pub enum ConnectNack {
93    InvalidProtocolVersion { version: Version },
94    PermissionDenied,
95}
96
97/// Request
98#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
99#[serde(rename_all = "snake_case")]
100#[allow(missing_docs)]
101pub enum Request {
102    Inspect {
103        container: Container,
104    },
105    Ident,
106    Install {
107        repository: RepositoryId,
108        size: u64,
109    },
110    Kill {
111        container: Container,
112        signal: i32,
113    },
114    List,
115    Mount {
116        containers: Vec<Container>,
117    },
118    Repositories,
119    Shutdown,
120    Start {
121        container: Container,
122        init: Option<NonNulString>,
123        arguments: Vec<NonNulString>,
124        environment: HashMap<NonNulString, NonNulString>,
125    },
126    TokenCreate {
127        target: Name,
128        #[serde(with = "base64")]
129        shared: Vec<u8>,
130    },
131    TokenVerify {
132        token: Token,
133        user: Name,
134        #[serde(with = "base64")]
135        shared: Vec<u8>,
136    },
137    Umount {
138        containers: Vec<Container>,
139    },
140    Uninstall {
141        container: Container,
142        wipe: bool,
143    },
144}
145
146/// Token
147#[derive(Clone, Eq, PartialEq, Debug)]
148pub struct Token(Vec<u8>);
149
150impl AsRef<[u8]> for Token {
151    fn as_ref(&self) -> &[u8] {
152        &self.0
153    }
154}
155
156impl From<Vec<u8>> for Token {
157    fn from(value: Vec<u8>) -> Self {
158        Token(value)
159    }
160}
161
162/// Token verification result
163#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
164pub enum VerificationResult {
165    /// Verification succeeded
166    Ok,
167    /// Verification failed
168    Invalid,
169    /// Token is expired
170    Expired,
171    /// Token time is in the future
172    Future,
173}
174
175/// Container information
176#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub struct ContainerData {
179    /// Container manifest
180    pub manifest: Manifest,
181    /// Repository in which the container is installed
182    pub repository: RepositoryId,
183    /// Mount state
184    pub mounted: bool,
185    /// Process if the container is started
186    pub process: Option<Process>,
187}
188
189/// Process information
190#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub struct Process {
193    /// Process id
194    pub pid: Pid,
195    /// Process uptime in nanoseconds
196    pub uptime: u64,
197    /// Container statistics
198    pub statistics: ContainerStats,
199}
200
201/// Mount result
202#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
203#[serde(rename_all = "snake_case")]
204#[allow(missing_docs)]
205pub enum MountResult {
206    Ok { container: Container },
207    Error { container: Container, error: Error },
208}
209
210/// Unmount result
211#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
212#[serde(rename_all = "snake_case")]
213#[allow(missing_docs)]
214pub enum UmountResult {
215    Ok { container: Container },
216    Error { container: Container, error: Error },
217}
218
219/// Start result
220#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
221#[serde(rename_all = "snake_case")]
222#[allow(missing_docs)]
223pub enum StartResult {
224    Ok { container: Container },
225    Error { container: Container, error: Error },
226}
227
228/// Kill result
229#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
230#[serde(rename_all = "snake_case")]
231#[allow(missing_docs)]
232pub enum KillResult {
233    Ok { container: Container },
234    Error { container: Container, error: Error },
235}
236
237/// Installation result
238#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
239#[serde(rename_all = "snake_case")]
240#[allow(missing_docs)]
241pub enum InstallResult {
242    Ok { container: Container },
243    Error { error: Error },
244}
245
246/// Uninstallation result
247#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
248#[serde(rename_all = "snake_case")]
249#[allow(missing_docs)]
250pub enum UninstallResult {
251    Ok { container: Container },
252    Error { container: Container, error: Error },
253}
254
255/// Inspect result
256#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
257#[serde(rename_all = "snake_case")]
258#[allow(missing_docs)]
259pub enum InspectResult {
260    Ok {
261        container: Container,
262        data: Box<ContainerData>,
263    },
264    Error {
265        container: Container,
266        error: Error,
267    },
268}
269
270/// Response
271#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273#[allow(missing_docs)]
274pub enum Response {
275    Ident(Container),
276    Inspect(InspectResult),
277    Install(InstallResult),
278    Kill(KillResult),
279    List(Vec<Container>),
280    Mount(Vec<MountResult>),
281    PermissionDenied(Request),
282    Repositories(HashSet<RepositoryId>),
283    Shutdown,
284    Start(StartResult),
285    Token(Token),
286    TokenVerification(VerificationResult),
287    Umount(Vec<UmountResult>),
288    Uninstall(UninstallResult),
289}
290
291/// Container exit status
292#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)]
293#[serde(rename_all = "snake_case")]
294pub enum ExitStatus {
295    /// Process exited with exit code
296    Exit {
297        /// Exit code
298        code: ExitCode,
299    },
300    /// Process was terminated by a signal
301    Signalled {
302        /// Signal
303        signal: Signal,
304    },
305}
306
307impl ExitStatus {
308    /// Exit success
309    pub const SUCCESS: ExitCode = 0;
310
311    /// Was termination successful? Signal termination is not considered a success,
312    /// and success is defined as a zero exit status.
313    pub fn success(&self) -> bool {
314        matches!(self, ExitStatus::Exit { code } if *code == Self::SUCCESS)
315    }
316
317    /// Returns the exit code of the process, if any.
318    pub fn code(&self) -> Option<ExitCode> {
319        match self {
320            ExitStatus::Exit { code } => Some(*code),
321            ExitStatus::Signalled { .. } => None,
322        }
323    }
324}
325
326impl std::fmt::Display for ExitStatus {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        match self {
329            ExitStatus::Exit { code } => write!(f, "Exit({code})"),
330            ExitStatus::Signalled { signal } => write!(f, "Signalled({signal})"),
331        }
332    }
333}
334
335/// API error
336#[derive(Clone, Eq, thiserror::Error, PartialEq, Debug, Serialize, Deserialize)]
337#[serde(rename_all = "snake_case")]
338#[allow(missing_docs)]
339pub enum Error {
340    Configuration {
341        context: String,
342    },
343    DuplicateContainer {
344        container: Container,
345    },
346    InvalidContainer {
347        container: Container,
348    },
349    InvalidArguments {
350        cause: String,
351    },
352    MountBusy {
353        container: Container,
354    },
355    UmountBusy {
356        container: Container,
357    },
358    StartContainerStarted {
359        container: Container,
360    },
361    StartContainerResource {
362        container: Container,
363    },
364    StartContainerMissingResource {
365        container: Container,
366        resource: Name,
367        version: String,
368    },
369    StartContainerFailed {
370        container: Container,
371        error: String,
372    },
373    StopContainerNotStarted {
374        container: Container,
375    },
376    InvalidRepository {
377        repository: RepositoryId,
378    },
379    InstallDuplicate {
380        container: Container,
381    },
382    CriticalContainer {
383        container: Container,
384        status: ExitStatus,
385    },
386    Unexpected {
387        error: String,
388    },
389}
390
391impl std::fmt::Display for Error {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        write!(f, "{:?}", self)
394    }
395}
396
397impl Serialize for Token {
398    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
399        if self.0.len() == 40 {
400            base64::serialize(&self.0, serializer)
401        } else {
402            Err(serde::ser::Error::custom("invalid length"))
403        }
404    }
405}
406
407impl<'de> Deserialize<'de> for Token {
408    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
409        let token = base64::deserialize(deserializer)?;
410        if token.len() == 40 {
411            Ok(Token(token))
412        } else {
413            Err(serde::de::Error::custom("invalid length"))
414        }
415    }
416}
417
418mod base64 {
419    use base64::{engine::general_purpose::STANDARD as Base64, Engine as _};
420    use serde::{Deserialize, Deserializer, Serialize, Serializer};
421
422    pub fn serialize<S: Serializer>(v: &Vec<u8>, s: S) -> Result<S::Ok, S::Error> {
423        let base64 = Base64.encode(v);
424        String::serialize(&base64, s)
425    }
426
427    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<u8>, D::Error> {
428        let base64 = String::deserialize(d)?;
429        Base64
430            .decode(base64.as_bytes())
431            .map_err(serde::de::Error::custom)
432    }
433}