use anyhow::{Context, Result, bail};
use clap::{Args, Subcommand};
use serde_json::Value;
use std::path::PathBuf;
use crate::{NamespacePlugin, PluginView};
use crate::cmd::Ctx;
use crate::output::{self, Table};
#[derive(Subcommand, Debug)]
pub enum PluginsCmd {
#[command(alias = "ls")]
List {
#[arg(long, short = 'n', value_name = "NAMESPACE")]
namespace: Option<String>,
},
Describe { id: String },
Enable { id: String },
Disable { id: String },
Set(SetArgs),
Install(InstallArgs),
Uninstall {
id: String,
#[arg(long, short = 'n', value_name = "NAMESPACE")]
namespace: Option<String>,
},
Installs { id: String },
}
#[derive(Args, Debug)]
pub struct InstallArgs {
pub id: String,
#[arg(long, short = 'n', value_name = "NAMESPACE")]
pub namespace: Option<String>,
#[arg(long, short = 'f', value_name = "FILE")]
pub file: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct SetArgs {
pub id: String,
#[arg(long, short = 'f', value_name = "FILE")]
pub file: PathBuf,
#[arg(long)]
pub enable: bool,
}
pub fn run(ctx: &Ctx, cmd: &PluginsCmd) -> Result<()> {
match cmd {
PluginsCmd::List { namespace: None } => list(ctx),
PluginsCmd::List { namespace: Some(ns) } => list_namespace(ctx, ns),
PluginsCmd::Describe { id } => describe(ctx, id),
PluginsCmd::Enable { id } => report(ctx.client.set_plugin(id, true, None)?),
PluginsCmd::Disable { id } => report(ctx.client.set_plugin(id, false, None)?),
PluginsCmd::Set(args) => set(ctx, args),
PluginsCmd::Install(args) => install(ctx, args),
PluginsCmd::Uninstall { id, namespace } => {
let ns = ctx.namespace(namespace.as_deref())?;
let p = ctx.client.uninstall_plugin(&ns, id)?;
eprintln!("{} uninstalled from namespace {ns:?}.", p.id);
Ok(())
}
PluginsCmd::Installs { id } => installs(ctx, id),
}
}
fn read_json(file: &std::path::Path) -> Result<Value> {
let text = if file.as_os_str() == "-" {
std::io::read_to_string(std::io::stdin()).context("reading configuration from stdin")?
} else {
std::fs::read_to_string(file).with_context(|| format!("reading {}", file.display()))?
};
serde_json::from_str(&text).context("the configuration is not valid JSON")
}
fn namespace_state(p: &NamespacePlugin) -> &'static str {
match (p.installed, p.enabled) {
(true, true) => "installed",
(true, false) => "installed (disabled for the fleet)",
(false, true) => "available",
(false, false) => "disabled for the fleet",
}
}
fn list_namespace(ctx: &Ctx, ns: &str) -> Result<()> {
if ctx.out.is_machine() {
let raw = ctx.client.raw().namespace_plugins(ns)?;
let names: Vec<String> = raw
.as_array()
.map(|a| {
a.iter()
.filter_map(|p| p.get("id").and_then(Value::as_str))
.map(|id| format!("plugin/{id}"))
.collect()
})
.unwrap_or_default();
return output::emit(&raw, ctx.out, &names);
}
let plugins = ctx.client.namespace_plugins(ns)?;
if plugins.is_empty() {
println!("This app-lb has no plugins that install per namespace.");
return Ok(());
}
let mut table = Table::new(["ID", "NAME", "STATE", "INSTALLED", "DASHBOARD", "DESCRIPTION"]);
for p in &plugins {
let dashboard = match (&p.dashboard, p.enabled && p.installed) {
(Some(_), true) => format!("/namespaces/{ns}/plugin-console/{}", p.id),
_ => "-".into(),
};
table.row([
p.id.clone(),
p.name.clone(),
namespace_state(p).to_string(),
p.installed_at.map(output::timestamp).unwrap_or_else(|| "-".into()),
dashboard,
p.description.clone(),
]);
}
table.print();
Ok(())
}
fn install(ctx: &Ctx, args: &InstallArgs) -> Result<()> {
let ns = ctx.namespace(args.namespace.as_deref())?;
let config = args.file.as_deref().map(read_json).transpose()?;
let p = ctx.client.install_plugin(&ns, &args.id, config.as_ref())?;
eprintln!("{} installed in namespace {ns:?}.", p.id);
if p.id == crate::OBS_PLUGIN {
eprintln!(
"Telemetry for every deployment in {ns:?} is collected from now on. \
Read it with `heyctl top -n {ns}` and `heyctl logs <deployment> -n {ns}`."
);
}
if p.dashboard.is_some() && p.enabled {
eprintln!("Its dashboard is at /namespaces/{ns}/plugin-console/{} on app-lb.", p.id);
}
Ok(())
}
fn installs(ctx: &Ctx, id: &str) -> Result<()> {
if ctx.out.is_machine() {
let raw = ctx.client.raw().plugin_installs(id)?;
let names: Vec<String> = raw
.get("namespaces")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(Value::as_str)
.map(|n| format!("namespace/{n}"))
.collect()
})
.unwrap_or_default();
return output::emit(&raw, ctx.out, &names);
}
let i = ctx.client.plugin_installs(id)?;
if !i.enabled {
eprintln!("note: {id} is disabled for the fleet, so no namespace is served.");
}
if i.installs.is_empty() {
println!("{id} is not installed in any namespace.");
return Ok(());
}
let mut table = Table::new(["NAMESPACE", "INSTALLED", "BY"]);
for (ns, install) in &i.installs {
table.row([
ns.clone(),
output::timestamp(install.installed_at),
install.installed_by.clone().unwrap_or_else(|| "operator".into()),
]);
}
table.print();
Ok(())
}
fn state_cell(p: &PluginView) -> &'static str {
match (p.enabled, p.last_error.is_some()) {
(true, true) => "error",
(true, false) => "enabled",
(false, _) => "disabled",
}
}
fn list(ctx: &Ctx) -> Result<()> {
if ctx.out.is_machine() {
let raw = ctx.client.raw().plugins()?;
let names: Vec<String> = raw
.as_array()
.map(|a| {
a.iter()
.filter_map(|p| p.get("id").and_then(Value::as_str))
.map(|id| format!("plugin/{id}"))
.collect()
})
.unwrap_or_default();
return output::emit(&raw, ctx.out, &names);
}
let plugins = ctx.client.plugins()?;
if plugins.is_empty() {
println!("This app-lb has no plugins.");
return Ok(());
}
let mut table = Table::new(["ID", "NAME", "STATE", "DESCRIPTION"]);
for p in &plugins {
table.row([
p.id.clone(),
p.name.clone(),
state_cell(p).to_string(),
p.description.clone(),
]);
}
table.print();
Ok(())
}
fn describe(ctx: &Ctx, id: &str) -> Result<()> {
if ctx.out.is_machine() {
let raw = ctx.client.raw().plugins()?;
let one = raw
.as_array()
.and_then(|a| {
a.iter()
.find(|p| p.get("id").and_then(Value::as_str) == Some(id))
})
.cloned();
let Some(one) = one else {
bail!("no plugin named {id:?}")
};
return output::emit(&one, ctx.out, &[format!("plugin/{id}")]);
}
print_plugin(&ctx.client.plugin(id)?);
Ok(())
}
fn set(ctx: &Ctx, args: &SetArgs) -> Result<()> {
let config = read_json(&args.file)?;
let enabled = args.enable || ctx.client.plugin(&args.id)?.enabled;
report(ctx.client.set_plugin(&args.id, enabled, Some(&config))?)
}
fn report(p: PluginView) -> Result<()> {
match &p.last_error {
Some(e) if p.enabled => {
eprintln!("{} is enabled, but it failed to start: {e}", p.id);
}
_ => eprintln!(
"{} {}.",
p.id,
if p.enabled { "enabled" } else { "disabled" }
),
}
Ok(())
}
fn print_plugin(p: &PluginView) {
output::section("Plugin");
output::field("ID", &p.id);
output::field("Name", &p.name);
output::field("State", state_cell(p));
output::field("Description", &p.description);
if let Some(e) = &p.last_error {
output::field("Last error", e);
}
output::section("Configuration");
println!(
"{}",
serde_json::to_string_pretty(&p.config).unwrap_or_default()
);
output::section("Status");
println!(
"{}",
serde_json::to_string_pretty(&p.status).unwrap_or_default()
);
}