use std::collections::HashMap;
use anyhow::Result;
use clap::Parser;
use serde_json::json;
use tracing::{error, warn};
use crate::{
common::{boxed_err_to_anyhow, find_host_id},
config::WashConnectionOptions,
};
use super::{CliConnectionOpts, CommandOutput};
#[derive(Debug, Clone, Parser)]
pub struct LabelHostCommand {
#[clap(flatten)]
pub opts: CliConnectionOpts,
#[clap(name = "host-id")]
pub host_id: String,
#[clap(long = "delete", default_value = "false")]
pub delete: bool,
#[clap(name = "label", alias = "label", value_delimiter = ',')]
pub labels: Vec<String>,
}
pub async fn handle_label_host(cmd: LabelHostCommand) -> Result<CommandOutput> {
let wco: WashConnectionOptions = cmd.opts.try_into()?;
let client = wco.into_ctl_client(None).await?;
let (host_id, friendly_name) = find_host_id(&cmd.host_id, &client).await?;
let friendly_name = if friendly_name.is_empty() {
host_id.to_string()
} else {
friendly_name
};
let labels = cmd
.labels
.iter()
.map(|orig| match orig.split_once('=') {
Some((k, v)) => (k, v),
None => (orig.as_str(), ""),
})
.collect::<Vec<(&str, &str)>>();
let mut succeeded = true;
let mut processed: Vec<(&str, &str)> = Vec::new();
for (key, value) in &labels {
let op = if cmd.delete {
client
.delete_label(&host_id, key)
.await
.map_err(boxed_err_to_anyhow)
} else {
client
.put_label(&host_id, key, value)
.await
.map_err(boxed_err_to_anyhow)
};
match op {
Ok(ack) => {
if !ack.success {
warn!(message = ack.message, "operation failed");
succeeded = false;
break;
}
processed.push((key, value));
}
Err(error) => {
error!(?error, "failed to set/delete label");
succeeded = false;
break;
}
}
}
let output = format!(
"Host `{friendly_name}` {} with `{}`",
if cmd.delete { "unlabeled" } else { "labeled" },
labels
.iter()
.map(|(key, value)| format!("{key}={value}"))
.collect::<Vec<String>>()
.join(",")
);
Ok(CommandOutput::new(
output,
HashMap::from([
("success".into(), json!(succeeded)),
("deleted".into(), json!(cmd.delete)),
("processed".into(), json!(processed)),
]),
))
}
#[cfg(test)]
mod tests {
use clap::Parser;
use super::LabelHostCommand;
const HOST_ID: &str = "host-id";
#[test]
fn test_label_multiple_joined() {
#[derive(Parser, Debug)]
struct Cmd {
#[clap(flatten)]
command: LabelHostCommand,
}
let expected_labels = vec!["key1=value1", "key2=value2"];
let cmd: Cmd =
Parser::try_parse_from(["label", HOST_ID, &expected_labels.join(",")]).unwrap();
let LabelHostCommand {
host_id,
delete,
labels,
..
} = cmd.command;
assert_eq!(host_id, HOST_ID);
assert!(!delete);
assert_eq!(labels, expected_labels);
}
#[test]
fn test_label_single() {
#[derive(Parser, Debug)]
struct Cmd {
#[clap(flatten)]
command: LabelHostCommand,
}
let cmd: Cmd = Parser::try_parse_from(["label", HOST_ID, "key1=value1"]).unwrap();
let LabelHostCommand {
host_id,
delete,
labels,
..
} = cmd.command;
assert_eq!(host_id, HOST_ID);
assert!(!delete);
assert_eq!(labels, vec!["key1=value1"]);
}
}