use crate::context::Context;
use elasticctl_core::Resolved;
pub(crate) const MUTATING: [&str; 27] = [
"rules enable",
"rules disable",
"rules delete",
"rules import",
"rules prebuilt install",
"exceptions delete",
"exceptions import",
"data-views import",
"data-views delete",
"data-views default set",
"data-views default unset",
"dashboards import",
"dashboards delete",
"dashboards bundle import",
"state push",
"config init",
"alerts ack",
"alerts open",
"alerts close",
"alerts tag",
"alerts assign",
"cases create",
"cases close",
"cases open",
"cases delete",
"cases attach",
"cases comment",
];
pub struct Preview {
pub action: String,
pub details: Vec<String>,
}
pub fn banner(resolved: &Resolved) -> String {
format!("({})", resolved.banner())
}
pub fn preview_text(preview: &Preview, resolved: &Resolved, applying: bool) -> String {
let tag = banner(resolved);
let mut out = if applying {
format!("Applying: {} {tag}\n", preview.action)
} else {
format!("[DRY RUN] {} {tag}\n", preview.action)
};
for d in &preview.details {
out.push_str(" ");
out.push_str(d);
out.push('\n');
}
if !applying {
out.push_str("Pass --yes to apply.\n");
}
out
}
pub fn check(ctx: &Context, path: &'static str, preview: &Preview) -> bool {
assert!(
MUTATING.contains(&path),
"guard::check called from \"{path}\", which is not in MUTATING — \
a mutating command must be declared there or it ships mutates:false \
in the command tree"
);
let applying = ctx.global.yes;
eprint!("{}", preview_text(preview, &ctx.resolved, applying));
applying
}
#[cfg(test)]
mod tests {
use super::*;
use elasticctl_core::{Profile, Resolved, Source};
fn resolved(name: &str, host: &str, space: &str) -> Resolved {
Resolved {
name: name.into(),
source: Source::Profile,
profile: Profile {
kibana_url: format!("https://{host}"),
es_url: None,
api_key: Some("essu_secret".into()),
username: None,
password: None,
space: space.into(),
verify: true,
timeout_secs: 30,
},
}
}
#[test]
fn banner_names_profile_host_and_space() {
let b = banner(&resolved("prod", "kibana.corp.internal", "soc"));
assert!(b.contains("prod"), "{b}");
assert!(b.contains("kibana.corp.internal"), "{b}");
assert!(b.contains("soc"), "{b}");
}
#[test]
fn banner_never_leaks_the_credential() {
let b = banner(&resolved("prod", "kibana.corp.internal", "default"));
assert!(
!b.contains("essu_secret"),
"the banner must not print the key: {b}"
);
}
#[test]
fn preview_text_lists_the_action_and_every_detail() {
let p = Preview {
action: "Disable 2 rules".into(),
details: vec![
"a Alpha enabled -> disabled".into(),
"b Beta enabled -> disabled".into(),
],
};
let text = preview_text(&p, &resolved("uat", "uat.example.com", "default"), false);
assert!(text.starts_with("[DRY RUN]"), "{text}");
assert!(text.contains("Disable 2 rules"));
assert!(text.contains("Alpha") && text.contains("Beta"));
assert!(text.contains("Pass --yes to apply."));
}
#[test]
fn apply_text_drops_the_dry_run_marker_and_the_hint() {
let p = Preview {
action: "Disable 2 rules".into(),
details: vec![],
};
let text = preview_text(&p, &resolved("uat", "uat.example.com", "default"), true);
assert!(text.starts_with("Applying:"), "{text}");
assert!(
!text.contains("--yes"),
"the hint is pointless once applying: {text}"
);
}
#[test]
fn both_modes_carry_the_target_banner() {
let p = Preview {
action: "Delete 1 rule".into(),
details: vec![],
};
let r = resolved("prod", "prod.example.com", "default");
for applying in [false, true] {
let text = preview_text(&p, &r, applying);
assert!(
text.contains("prod"),
"target must be named in both modes: {text}"
);
assert!(text.contains("prod.example.com"), "{text}");
}
}
#[test]
#[should_panic(expected = "rules touch")]
fn check_rejects_an_undeclared_mutating_path() {
let dir = tempfile::tempdir().unwrap();
let config = dir.path().join("config.toml");
std::fs::write(
&config,
"current = \"test\"\n\n\
[profiles.test]\n\
kibana_url = \"https://kb.example.com\"\n\
space = \"default\"\n\
verify = true\n\
timeout_secs = 30\n",
)
.unwrap();
let global = crate::cli::GlobalArgs {
config: Some(config),
..Default::default()
};
let ctx = crate::context::Context::build(&global).unwrap();
let preview = Preview {
action: "Touch 1 rule".into(),
details: vec![],
};
let _ = check(&ctx, "rules touch", &preview);
}
}