Skip to main content

conmonrs/
version.rs

1//! Version information of conmonrs, for `--version` and the `version` RPC.
2
3use anyhow::{Context, Result};
4use serde::Serialize;
5use std::io::{self, Write};
6
7/// The dependency tree of the build, written by the build script.
8const CARGO_TREE: &str = include_str!(concat!(env!("OUT_DIR"), "/cargo-tree.txt"));
9
10/// The version structure.
11#[derive(Debug, Default, Eq, PartialEq, Serialize)]
12pub(crate) struct Version {
13    /// Specifies if the output should contain verbose debug information.
14    #[serde(skip)]
15    verbose: bool,
16
17    /// The current crate version.
18    version: &'static str,
19
20    /// The tag of the build, empty if not available.
21    tag: &'static str,
22
23    /// The git commit SHA of the build.
24    commit: &'static str,
25
26    /// The build date string.
27    build_date: &'static str,
28
29    /// The target triple string.
30    target: &'static str,
31
32    /// The used Rust version.
33    rust_version: &'static str,
34
35    /// The used Cargo version.
36    cargo_version: &'static str,
37
38    /// The cargo dependency tree, only available in verbose output.
39    cargo_tree: &'static str,
40
41    /// The optional features this build supports, which the version alone
42    /// does not tell.
43    features: &'static [&'static str],
44}
45
46impl Version {
47    /// Create a new Version instance.
48    pub(crate) fn new(verbose: bool) -> Self {
49        Self {
50            verbose,
51            version: env!("CARGO_PKG_VERSION"),
52            tag: env!("BUILD_TAG"),
53            commit: env!("BUILD_COMMIT"),
54            build_date: env!("BUILD_TIME"),
55            target: env!("BUILD_TARGET"),
56            rust_version: env!("BUILD_RUST_VERSION"),
57            cargo_version: env!("BUILD_CARGO_VERSION"),
58            cargo_tree: if verbose { CARGO_TREE } else { "" },
59            features: Self::FEATURES,
60        }
61    }
62
63    /// Forwarding ports by `servePortForwardContainer`.
64    const FEATURE_PORT_FORWARD: &'static str = "portforward";
65
66    /// The `idleTimeoutSec` of the exec and attach serve requests, which
67    /// older servers ignore. Port forwarding always applies its own.
68    const FEATURE_IDLE_TIMEOUT: &'static str = "idletimeout";
69
70    /// Exec and attach sessions over SPDY and the WebSocket protocols before
71    /// `v5.channel.k8s.io`.
72    const FEATURE_STREAMING_PROTOCOLS: &'static str = "streamingprotocols";
73
74    /// The `restorePath` of `createContainer`, which restores the container
75    /// from a checkpoint with the `restore` command of the runtime. Older
76    /// servers ignore the field and create a new container instead.
77    const FEATURE_RESTORE: &'static str = "restore";
78
79    /// The features of this build. Earlier servers also reported `sdnotify`
80    /// and `journaldfields`, whose fields got retired, see the schema.
81    const FEATURES: &'static [&'static str] = &[
82        Self::FEATURE_PORT_FORWARD,
83        Self::FEATURE_IDLE_TIMEOUT,
84        Self::FEATURE_STREAMING_PROTOCOLS,
85        Self::FEATURE_RESTORE,
86    ];
87
88    /// The optional features this build supports.
89    pub(crate) fn features(&self) -> &'static [&'static str] {
90        self.features
91    }
92
93    /// The current crate version.
94    pub(crate) fn version(&self) -> &'static str {
95        self.version
96    }
97
98    /// The tag of the build.
99    pub(crate) fn tag(&self) -> &'static str {
100        self.tag
101    }
102
103    /// The git commit SHA.
104    pub(crate) fn commit(&self) -> &'static str {
105        self.commit
106    }
107
108    /// The build date string.
109    pub(crate) fn build_date(&self) -> &'static str {
110        self.build_date
111    }
112
113    /// The target triple.
114    pub(crate) fn target(&self) -> &'static str {
115        self.target
116    }
117
118    /// The Rust version used.
119    pub(crate) fn rust_version(&self) -> &'static str {
120        self.rust_version
121    }
122
123    /// The Cargo version used.
124    pub(crate) fn cargo_version(&self) -> &'static str {
125        self.cargo_version
126    }
127
128    /// The cargo dependency tree.
129    pub(crate) fn cargo_tree(&self) -> &'static str {
130        self.cargo_tree
131    }
132
133    /// The version information as text, one field per line. The format is an
134    /// interface: CRI-O takes a binary whose `--version` output starts with
135    /// `version:` for conmonrs, to reject it as the monitor of a runtime
136    /// handler of the `oci` type.
137    fn text(&self) -> String {
138        let tag = if self.tag().is_empty() {
139            "none"
140        } else {
141            self.tag()
142        };
143        let mut text = format!(
144            "version: {}\ntag: {tag}\ncommit: {}\nbuild: {}\ntarget: {}\n{}\n{}\nfeatures: {}\n",
145            self.version(),
146            self.commit(),
147            self.build_date(),
148            self.target(),
149            self.rust_version(),
150            self.cargo_version(),
151            self.features().join(", "),
152        );
153        if self.verbose {
154            text.push_str(&format!("\ncargo tree: {}\n", self.cargo_tree()));
155        }
156        text
157    }
158
159    /// Print the version information to stdout.
160    pub(crate) fn print(&self) -> Result<()> {
161        write_output(io::stdout().lock(), &self.text())
162    }
163
164    /// Print the version information as JSON to stdout.
165    pub(crate) fn print_json(&self) -> Result<()> {
166        let mut json = serde_json::to_string(&self).context("serialize result")?;
167        json.push('\n');
168        write_output(io::stdout().lock(), &json)
169    }
170}
171
172/// Write the output of a command. A reader which is gone, like `head`, ends
173/// the output without an error, `println!` would panic and abort.
174fn write_output(mut writer: impl Write, text: &str) -> Result<()> {
175    match writer
176        .write_all(text.as_bytes())
177        .and_then(|()| writer.flush())
178    {
179        Err(e) if e.kind() != io::ErrorKind::BrokenPipe => Err(e).context("write output"),
180        _ => Ok(()),
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn version_fields_and_verbosity() {
190        let v = Version::new(false);
191        assert_eq!(v.version(), env!("CARGO_PKG_VERSION"));
192        assert!(!v.target().is_empty());
193        assert!(v.rust_version().starts_with("rustc "));
194        assert!(v.cargo_version().starts_with("cargo "));
195        // The tree is only part of the verbose output.
196        assert!(v.cargo_tree().is_empty());
197        assert_eq!(Version::new(true).cargo_tree(), CARGO_TREE);
198
199        let text = v.text();
200        assert!(text.starts_with(&format!("version: {}\n", v.version())));
201        assert!(text.ends_with(&format!("features: {}\n", v.features().join(", "))));
202        assert!(Version::new(true).text().contains("\ncargo tree: "));
203    }
204
205    /// A writer which fails every write with `kind`.
206    struct Failing(io::ErrorKind);
207
208    impl Write for Failing {
209        fn write(&mut self, _: &[u8]) -> io::Result<usize> {
210            Err(self.0.into())
211        }
212
213        fn flush(&mut self) -> io::Result<()> {
214            Ok(())
215        }
216    }
217
218    #[test]
219    fn output_to_a_closed_pipe_is_no_error() {
220        assert!(write_output(Failing(io::ErrorKind::BrokenPipe), "version").is_ok());
221        assert!(write_output(Failing(io::ErrorKind::PermissionDenied), "version").is_err());
222    }
223
224    /// The JSON keys are what the Go client reads, see pkg/client/version.go.
225    #[test]
226    fn version_json() -> Result<()> {
227        let v = Version::new(false);
228        let json = serde_json::to_value(&v)?;
229        let object = json.as_object().context("not an object")?;
230        for (key, value) in [
231            ("build_date", v.build_date()),
232            ("cargo_tree", v.cargo_tree()),
233            ("cargo_version", v.cargo_version()),
234            ("commit", v.commit()),
235            ("rust_version", v.rust_version()),
236            ("tag", v.tag()),
237            ("target", v.target()),
238            ("version", v.version()),
239        ] {
240            assert_eq!(
241                object.get(key).and_then(|v| v.as_str()),
242                Some(value),
243                "key {key}"
244            );
245        }
246        // The verbosity is a presentation flag, not version information.
247        assert!(!object.contains_key("verbose"));
248        Ok(())
249    }
250
251    /// The Go client matches the names, see the `Feature` constants in
252    /// pkg/client/version.go.
253    #[test]
254    fn version_features() -> Result<()> {
255        let v = Version::new(false);
256        assert_eq!(
257            v.features(),
258            [
259                "portforward",
260                "idletimeout",
261                "streamingprotocols",
262                "restore"
263            ]
264        );
265        let json = serde_json::to_value(&v)?;
266        assert_eq!(json.get("features"), Some(&serde_json::json!(v.features())));
267        Ok(())
268    }
269}