use crate::context::GlobalParams;
use crate::error::{Error, ErrorKind, Result};
use crate::modules::{Module, ModuleResult, parse_params};
#[cfg(feature = "docs")]
use rash_derive::DocJsonSchema;
use log::trace;
use std::process::Command;
use minijinja::Value;
#[cfg(feature = "docs")]
use schemars::{JsonSchema, Schema};
use serde::Deserialize;
use serde_json;
use serde_norway::{Value as YamlValue, value};
#[derive(Debug, PartialEq, Deserialize)]
#[cfg_attr(feature = "docs", derive(JsonSchema, DocJsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Params {
#[serde(default = "default_true")]
get_version: bool,
#[serde(default = "default_true")]
get_info: bool,
#[serde(default)]
get_disk_usage: bool,
}
fn default_true() -> bool {
true
}
fn remove_nulls(value: serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::Object(map) => serde_json::Value::Object(
map.into_iter()
.filter_map(|(k, v)| {
let cleaned = remove_nulls(v);
if cleaned.is_null() {
None
} else {
Some((k, cleaned))
}
})
.collect(),
),
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(remove_nulls).collect())
}
other => other,
}
}
#[derive(Debug)]
pub struct DockerInfo;
impl Module for DockerInfo {
fn get_name(&self) -> &str {
"docker_info"
}
fn exec(
&self,
_: &GlobalParams,
optional_params: YamlValue,
_vars: &Value,
_check_mode: bool,
) -> Result<(ModuleResult, Option<Value>)> {
let params = match optional_params {
YamlValue::Null => YamlValue::Mapping(serde_norway::Mapping::new()),
other => other,
};
Ok((docker_info(parse_params(params)?)?, None))
}
#[cfg(feature = "docs")]
fn get_json_schema(&self) -> Option<Schema> {
Some(Params::get_json_schema())
}
}
fn check_docker_available() -> bool {
Command::new("docker")
.args(["info"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn get_docker_version() -> Result<serde_json::Value> {
let output = Command::new("docker")
.args(["version", "--format", "{{json .}}"])
.output()
.map_err(|e| Error::new(ErrorKind::SubprocessFail, e))?;
trace!("docker version output: {:?}", output);
if !output.status.success() {
return Err(Error::new(
ErrorKind::SubprocessFail,
format!(
"Failed to get Docker version: {}",
String::from_utf8_lossy(&output.stderr)
),
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("Invalid JSON: {e}")))
}
fn get_docker_info() -> Result<serde_json::Value> {
let output = Command::new("docker")
.args(["info", "--format", "{{json .}}"])
.output()
.map_err(|e| Error::new(ErrorKind::SubprocessFail, e))?;
trace!("docker info output: {:?}", output);
if !output.status.success() {
return Err(Error::new(
ErrorKind::SubprocessFail,
format!(
"Failed to get Docker info: {}",
String::from_utf8_lossy(&output.stderr)
),
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("Invalid JSON: {e}")))
}
fn get_docker_disk_usage() -> Result<serde_json::Value> {
let output = Command::new("docker")
.args(["system", "df", "--format", "{{json .}}"])
.output()
.map_err(|e| Error::new(ErrorKind::SubprocessFail, e))?;
trace!("docker system df output: {:?}", output);
if !output.status.success() {
return Err(Error::new(
ErrorKind::SubprocessFail,
format!(
"Failed to get Docker disk usage: {}",
String::from_utf8_lossy(&output.stderr)
),
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
serde_json::from_str(&stdout)
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("Invalid JSON: {e}")))
}
fn docker_info(params: Params) -> Result<ModuleResult> {
let available = check_docker_available();
let mut info = serde_json::Map::new();
info.insert("available".to_string(), serde_json::Value::Bool(available));
if !available {
let extra = value::to_value(serde_json::json!({"docker_info": info}))?;
return Ok(ModuleResult {
changed: false,
output: Some("Docker is not available".to_string()),
extra: Some(extra),
});
}
if params.get_version {
match get_docker_version() {
Ok(version) => {
info.insert("version".to_string(), remove_nulls(version));
}
Err(e) => {
trace!("Failed to get Docker version: {}", e);
}
}
}
if params.get_info {
match get_docker_info() {
Ok(docker_info_val) => {
info.insert("info".to_string(), remove_nulls(docker_info_val));
}
Err(e) => {
trace!("Failed to get Docker info: {}", e);
}
}
}
if params.get_disk_usage {
match get_docker_disk_usage() {
Ok(disk_usage) => {
info.insert("disk_usage".to_string(), remove_nulls(disk_usage));
}
Err(e) => {
trace!("Failed to get Docker disk usage: {}", e);
}
}
}
let extra = value::to_value(serde_json::json!({"docker_info": info}))?;
Ok(ModuleResult {
changed: false,
output: Some("Docker information collected".to_string()),
extra: Some(extra),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_params_default() {
let yaml: YamlValue = serde_norway::from_str("{}").unwrap();
let params: Params = parse_params(yaml).unwrap();
assert!(params.get_version);
assert!(params.get_info);
assert!(!params.get_disk_usage);
}
#[test]
fn test_parse_params_all_false() {
let yaml: YamlValue = serde_norway::from_str(
r#"
get_version: false
get_info: false
get_disk_usage: true
"#,
)
.unwrap();
let params: Params = parse_params(yaml).unwrap();
assert!(!params.get_version);
assert!(!params.get_info);
assert!(params.get_disk_usage);
}
#[test]
fn test_parse_params_invalid_field() {
let yaml: YamlValue = serde_norway::from_str(
r#"
invalid_field: value
"#,
)
.unwrap();
let error = parse_params::<Params>(yaml).unwrap_err();
assert_eq!(error.kind(), ErrorKind::InvalidData);
}
#[test]
fn test_module_name() {
let module = DockerInfo;
assert_eq!(module.get_name(), "docker_info");
}
}