Skip to main content

conmonrs/streaming_server/remotecommand/
status.rs

1//! The final status of a session, as the client gets it.
2
3use super::{SUCCESS_STATUS, Version};
4use anyhow::{Context, Result};
5use serde::Serialize;
6use std::fmt;
7
8/// What a session runs, which the status of its failure names.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub(crate) enum Operation {
11    Exec,
12    Attach,
13}
14
15impl Operation {
16    /// What k8s.io/cri-streaming puts before the error of a session.
17    fn context(self) -> &'static str {
18        match self {
19            Self::Exec => "error executing command in container",
20            Self::Attach => "error attaching to container",
21        }
22    }
23}
24
25impl fmt::Display for Operation {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(match self {
28            Self::Exec => "exec",
29            Self::Attach => "attach",
30        })
31    }
32}
33
34/// The final status of a session.
35#[derive(Debug, PartialEq)]
36pub(crate) enum Status {
37    Success,
38    ExitCode(i32),
39
40    /// The session itself failed, for example a command which could not be
41    /// started. The message is the one of the internal error, see
42    /// [`Status::failed`].
43    Error(String),
44}
45
46impl Status {
47    /// The status of a command which exited with `code`. A negative one
48    /// tells that it could not be waited for, clients only take exit codes
49    /// from 1 to 255.
50    pub(crate) fn exited(code: i32) -> Self {
51        match code {
52            0 => Self::Success,
53            1.. => Self::ExitCode(code),
54            _ => Self::failed(
55                Operation::Exec,
56                format_args!("unable to get the exit code of the command ({code})"),
57            ),
58        }
59    }
60
61    /// The failure of a session, worded like k8s.io/cri-streaming does.
62    pub(crate) fn failed(operation: Operation, error: impl fmt::Display) -> Self {
63        Self::Error(format!(
64            "Internal error occurred: {}: {error}",
65            operation.context()
66        ))
67    }
68
69    /// The status on the error stream, none for a success before v4.
70    pub(super) fn encode(&self, version: Version) -> Result<Option<Vec<u8>>> {
71        if version.json_status() {
72            return match self {
73                Self::Success => Ok(Some(SUCCESS_STATUS.to_vec())),
74                Self::ExitCode(code) => Self::json(&ErrorMessage::new(code)).map(Some),
75                Self::Error(message) => {
76                    Self::json(&ErrorMessage::internal(message.clone())).map(Some)
77                }
78            };
79        }
80        Ok(match self {
81            Self::Success => None,
82            Self::ExitCode(code) => Some(
83                format!("command terminated with non-zero exit code: exit status {code}")
84                    .into_bytes(),
85            ),
86            Self::Error(message) => Some(message.clone().into_bytes()),
87        })
88    }
89
90    fn json(status: &impl Serialize) -> Result<Vec<u8>> {
91        serde_json::to_vec(status).context("serialize status message")
92    }
93}
94
95/// The status of a failed session in JSON, the fields in the order of
96/// k8s.io/cri-streaming.
97#[derive(Debug, Serialize)]
98pub(super) struct ErrorMessage {
99    status: &'static str,
100    message: String,
101    reason: &'static str,
102    #[serde(skip_serializing_if = "Option::is_none")]
103    details: Option<ErrorDetails>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    code: Option<u16>,
106}
107
108impl ErrorMessage {
109    /// The status of a command which exited with the code, its message ends
110    /// with the exit error like the text status before v4.
111    pub(super) fn new<T>(exit_code: T) -> Self
112    where
113        T: fmt::Display,
114    {
115        Self {
116            status: "Failure",
117            message: format!("command terminated with non-zero exit code: exit status {exit_code}"),
118            reason: "NonZeroExitCode",
119            details: Some(ErrorDetails {
120                causes: vec![ErrorCause {
121                    reason: "ExitCode",
122                    message: exit_code.to_string(),
123                }],
124            }),
125            code: None,
126        }
127    }
128
129    /// An error of the session itself, for example a command which could not
130    /// be started, see [`Status::failed`].
131    fn internal(message: String) -> Self {
132        Self {
133            status: "Failure",
134            message,
135            reason: "InternalError",
136            details: None,
137            code: Some(500),
138        }
139    }
140}
141
142/// Error details for the ErrorMessage.
143#[derive(Debug, Serialize)]
144struct ErrorDetails {
145    causes: Vec<ErrorCause>,
146}
147
148/// Error cause for the ErrorDetails.
149#[derive(Debug, Serialize)]
150struct ErrorCause {
151    reason: &'static str,
152    message: String,
153}