pub mod analysis;
pub mod clients;
pub mod config;
pub mod eks;
pub mod finding;
pub mod k8s;
pub mod output;
pub mod playbook;
pub mod version;
use std::{env, str};
use anyhow::{Context, Result};
use aws_config::default_provider::{credentials::DefaultCredentialsChain, region::DefaultRegionChain};
use aws_sdk_eks::config::Region;
use clap::{
Args, Parser, Subcommand,
builder::styling::{AnsiColor, Color, Style, Styles},
};
use clap_verbosity_flag::Verbosity;
use clients::AwsClients;
use indicatif::{ProgressBar, ProgressFinish, ProgressStyle};
use serde::{Deserialize, Serialize};
fn get_styles() -> Styles {
Styles::styled()
.header(
Style::new()
.bold()
.underline()
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
)
.literal(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::BrightCyan))))
.usage(Style::new().bold().fg_color(Some(Color::Ansi(AnsiColor::Green))))
.placeholder(
Style::new()
.bold()
.underline()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
)
}
#[derive(Parser, Debug)]
#[command(author, about, version)]
#[command(propagate_version = true)]
#[command(styles=get_styles())]
pub struct Cli {
#[command(subcommand)]
pub commands: Commands,
#[clap(flatten)]
pub verbose: Verbosity,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
#[command(arg_required_else_help = true)]
Analyze(Analysis),
#[command(arg_required_else_help = true)]
Create(Create),
#[command(arg_required_else_help = true)]
Completion {
#[arg(value_enum)]
shell: clap_complete::Shell,
},
Man,
}
#[derive(Args, Debug, Serialize, Deserialize)]
pub struct Analysis {
#[arg(short, long, alias = "cluster-name", value_enum)]
pub cluster: String,
#[arg(short, long)]
pub region: Option<String>,
#[arg(short, long)]
pub profile: Option<String>,
#[arg(short, long, value_enum, default_value_t)]
pub format: output::Format,
#[arg(short, long)]
pub output: Option<String>,
#[arg(short = 't', long, alias = "target")]
pub target_version: Option<String>,
#[arg(long)]
pub ignore_recommended: bool,
#[arg(long)]
pub config: Option<String>,
#[arg(long)]
pub show_suppressed: bool,
}
#[derive(Args, Debug, Serialize, Deserialize)]
pub struct Create {
#[command(subcommand)]
pub command: CreateCommands,
}
#[derive(Debug, Subcommand, Serialize, Deserialize)]
pub enum CreateCommands {
#[command(arg_required_else_help = true)]
Playbook(Playbook),
}
#[derive(Args, Debug, Serialize, Deserialize)]
pub struct Playbook {
#[arg(short, long, alias = "cluster-name", value_enum)]
pub cluster: String,
#[arg(short, long)]
pub region: Option<String>,
#[arg(short, long)]
pub profile: Option<String>,
#[arg(short, long)]
pub filename: Option<String>,
#[arg(short = 't', long, alias = "target")]
pub target_version: Option<String>,
#[arg(long)]
pub ignore_recommended: bool,
#[arg(long)]
pub config: Option<String>,
#[arg(long)]
pub show_suppressed: bool,
}
fn new_spinner() -> ProgressBar {
let spinner = ProgressBar::new_spinner()
.with_style(ProgressStyle::with_template("{spinner:.cyan} {msg}").unwrap())
.with_finish(ProgressFinish::AndClear);
spinner.enable_steady_tick(std::time::Duration::from_millis(80));
spinner
}
pub async fn analyze(args: Analysis) -> Result<()> {
let spinner = new_spinner();
let config = config::load(args.config.as_deref())?;
spinner.set_message("Loading AWS configuration...");
let aws_config = get_config(&args.region, &args.profile).await?;
let aws = clients::RealAwsClients::new(&aws_config);
spinner.set_message("Fetching cluster details...");
let cluster = aws.get_cluster(&args.cluster).await?;
let cluster_version = cluster.version().context("Cluster version not found")?;
let target_minor = match &args.target_version {
Some(tv) => version::validate_target_version(tv, cluster_version)?,
None => match version::check_version_supported(cluster_version)? {
Some(target) => target,
None => {
spinner.finish_and_clear();
println!("Cluster is already at the latest supported version: {cluster_version}");
println!("Nothing to upgrade at this time");
return Ok(());
}
},
};
spinner.set_message("Connecting to cluster...");
let k8s = clients::RealK8sClients::new(&args.cluster).await?;
spinner.set_message("Analyzing cluster...");
let mut results = analysis::analyze(&aws, &k8s, &cluster, target_minor, &config).await?;
if args.ignore_recommended {
results.filter_recommended();
}
spinner.finish_and_clear();
output::output(&results, &args.format, &args.output, args.show_suppressed)?;
Ok(())
}
async fn get_config(region: &Option<String>, profile: &Option<String>) -> Result<aws_config::SdkConfig> {
let region = match region {
Some(region) => Some(Region::new(region.to_owned())),
None => match profile {
Some(profile) => {
DefaultRegionChain::builder()
.profile_name(profile)
.build()
.region()
.await
}
None => match env::var("AWS_REGION") {
Ok(region) => Some(Region::new(region)),
Err(_) => env::var("AWS_DEFAULT_REGION").ok().map(Region::new),
},
},
};
let mut creds = DefaultCredentialsChain::builder().region(region.clone());
if let Some(profile) = profile {
creds = creds.profile_name(profile);
};
let config = aws_config::from_env()
.credentials_provider(creds.build().await)
.region(region)
.load()
.await;
Ok(config)
}
pub async fn create(args: Create) -> Result<()> {
match args.command {
CreateCommands::Playbook(playbook) => {
let spinner = new_spinner();
let config = config::load(playbook.config.as_deref())?;
spinner.set_message("Loading AWS configuration...");
let aws_config = get_config(&playbook.region, &playbook.profile).await?;
let region = aws_config.region().context("AWS region not configured")?.to_string();
let aws = clients::RealAwsClients::new(&aws_config);
spinner.set_message("Fetching cluster details...");
let cluster = aws.get_cluster(&playbook.cluster).await?;
let cluster_version = cluster.version().context("Cluster version not found")?;
let target_minor = match &playbook.target_version {
Some(tv) => version::validate_target_version(tv, cluster_version)?,
None => match version::check_version_supported(cluster_version)? {
Some(target) => target,
None => {
spinner.finish_and_clear();
println!("Cluster is already at the latest supported version: {cluster_version}");
println!("Nothing to upgrade at this time");
return Ok(());
}
},
};
spinner.set_message("Connecting to cluster...");
let k8s = clients::RealK8sClients::new(&playbook.cluster).await?;
spinner.set_message("Analyzing cluster...");
let mut results = analysis::analyze(&aws, &k8s, &cluster, target_minor, &config).await?;
if playbook.ignore_recommended {
results.filter_recommended();
}
spinner.set_message("Creating playbook...");
playbook::create(playbook, region, &cluster, results, target_minor)?;
spinner.finish_and_clear();
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use clap_complete::{Shell, generate};
use super::*;
#[test]
fn completion_generates_for_all_shells() {
let mut cmd = Cli::command();
for shell in [Shell::Bash, Shell::Elvish, Shell::Fish, Shell::PowerShell, Shell::Zsh] {
let mut buf = Vec::new();
generate(shell, &mut cmd, "eksup", &mut buf);
assert!(!buf.is_empty(), "{shell:?} produced empty completion output");
}
}
#[test]
fn man_renders() {
let mut buf = Vec::new();
clap_mangen::Man::new(Cli::command()).render(&mut buf).unwrap();
assert!(!buf.is_empty(), "man page rendered empty");
}
}