use std::{collections::HashMap, fs};
use anyhow::{Context, Result};
use aws_sdk_eks::types::Cluster;
use handlebars::Handlebars;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use crate::{Playbook, analysis, eks, finding::Findings, version};
#[derive(RustEmbed)]
#[folder = "templates/"]
struct Templates;
#[derive(Debug, Serialize, Deserialize)]
struct Release {
release_url: String,
deprecation_url: Option<String>,
}
type Version = String;
#[derive(Debug, Serialize, Deserialize)]
pub struct TemplateData {
region: String,
cluster_name: String,
current_version: String,
target_version: String,
k8s_release_url: String,
k8s_deprecation_url: String,
control_plane_ips: String,
pod_ips: String,
cluster_health: String,
addon_health: String,
addon_version_compatibility: String,
node_ips: String,
data_plane_findings: eks::DataPlaneFindings,
version_skew: String,
eks_managed_nodegroup_template: String,
self_managed_nodegroup_template: String,
fargate_profile_template: String,
min_replicas: String,
min_ready_seconds: String,
pod_topology_distribution: String,
readiness_probe: String,
termination_grace_period: String,
docker_socket: String,
kube_proxy_version_skew: String,
kube_proxy_ipvs_mode: String,
ingress_nginx_retirement: String,
pod_disruption_budgets: String,
ec2_limits: String,
ebs_gp2_limits: String,
ebs_gp3_limits: String,
upgrade_readiness_insights: String,
misconfiguration_insights: String,
}
fn get_release_data() -> Result<HashMap<Version, Release>> {
let data_file = Templates::get("data.yaml").context("Embedded data.yaml template not found")?;
let contents = std::str::from_utf8(data_file.data.as_ref())?;
let data: HashMap<Version, Release> = serde_yaml::from_str(contents)?;
Ok(data)
}
#[derive(Debug, Serialize, Deserialize)]
struct EksManagedNodeGroupTemplateData {
region: String,
cluster_name: String,
target_version: String,
eks_managed_nodegroup_health: String,
eks_managed_nodegroup_update: String,
al2_ami_deprecation: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct SelfManagedNodeGroupTemplateData {
region: String,
cluster_name: String,
target_version: String,
self_managed_nodegroup_update: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct FargateProfileTemplateData {
region: String,
cluster_name: String,
target_version: String,
}
pub fn render(
region: &str,
cluster: &Cluster,
analysis: analysis::Results,
target_minor: i32,
show_suppressed: bool,
) -> Result<String> {
let mut handlebars = Handlebars::new();
handlebars.register_escape_fn(handlebars::no_escape);
handlebars.register_embed_templates::<Templates>()?;
let cluster_name = cluster.name().context("Cluster name missing")?;
let cluster_version = cluster.version().context("Cluster version missing")?;
let target_version = version::format_version(target_minor);
let release_data = get_release_data()?;
let release = release_data
.get(&target_version)
.context(format!("No release data found for version {target_version}"))?;
let cluster_findings = analysis.cluster;
let data_plane_findings = analysis.data_plane;
let subnet_findings = analysis.subnets;
let addon_findings = analysis.addons;
let kubernetes_findings = analysis.kubernetes;
let kubernetes_suppressed = analysis.kubernetes_suppressed;
let eks_mng_tmpl_data = EksManagedNodeGroupTemplateData {
region: region.to_owned(),
cluster_name: cluster_name.to_owned(),
target_version: target_version.to_owned(),
eks_managed_nodegroup_health: data_plane_findings
.eks_managed_nodegroup_health
.to_markdown_table("\t")?,
eks_managed_nodegroup_update: data_plane_findings
.eks_managed_nodegroup_update
.to_markdown_table("\t")?,
al2_ami_deprecation: data_plane_findings.al2_ami_deprecation.to_markdown_table("\t")?,
};
let eks_managed_nodegroup_template = handlebars.render("eks-managed-nodegroup.md", &eks_mng_tmpl_data)?;
let self_mng_tmpl_data = SelfManagedNodeGroupTemplateData {
region: region.to_owned(),
cluster_name: cluster_name.to_owned(),
target_version: target_version.to_owned(),
self_managed_nodegroup_update: data_plane_findings
.self_managed_nodegroup_update
.to_markdown_table("\t")?,
};
let self_managed_nodegroup_template = handlebars.render("self-managed-nodegroup.md", &self_mng_tmpl_data)?;
let fargate_tmpl_data = FargateProfileTemplateData {
region: region.to_owned(),
cluster_name: cluster_name.to_owned(),
target_version: target_version.to_owned(),
};
let fargate_profile_template = handlebars.render("fargate-node.md", &fargate_tmpl_data)?;
let node_ips = data_plane_findings.node_ips.to_markdown_table("\t")?;
let tmpl_data = TemplateData {
region: region.to_owned(),
cluster_name: cluster_name.to_owned(),
current_version: cluster_version.to_owned(),
target_version,
k8s_release_url: release.release_url.to_string(),
k8s_deprecation_url: match &release.deprecation_url {
Some(url) => url.to_string(),
None => "".to_string(),
},
control_plane_ips: subnet_findings.control_plane_ips.to_markdown_table("\t")?,
pod_ips: subnet_findings.pod_ips.to_markdown_table("\t")?,
cluster_health: cluster_findings.cluster_health.to_markdown_table("\t")?,
addon_health: addon_findings.health.to_markdown_table("\t")?,
addon_version_compatibility: addon_findings.version_compatibility.to_markdown_table("\t")?,
node_ips,
data_plane_findings,
eks_managed_nodegroup_template,
self_managed_nodegroup_template,
fargate_profile_template,
version_skew: kubernetes_findings.version_skew.to_markdown_table("\t")?,
min_replicas: kubernetes_findings.min_replicas.to_markdown_table("\t")?,
min_ready_seconds: kubernetes_findings.min_ready_seconds.to_markdown_table("\t")?,
pod_topology_distribution: kubernetes_findings.pod_topology_distribution.to_markdown_table("\t")?,
readiness_probe: kubernetes_findings.readiness_probe.to_markdown_table("\t")?,
termination_grace_period: kubernetes_findings.termination_grace_period.to_markdown_table("\t")?,
docker_socket: kubernetes_findings.docker_socket.to_markdown_table("\t")?,
kube_proxy_version_skew: kubernetes_findings.kube_proxy_version_skew.to_markdown_table("\t")?,
kube_proxy_ipvs_mode: kubernetes_findings.kube_proxy_ipvs_mode.to_markdown_table("\t")?,
ingress_nginx_retirement: kubernetes_findings.ingress_nginx_retirement.to_markdown_table("\t")?,
pod_disruption_budgets: kubernetes_findings.pod_disruption_budgets.to_markdown_table("\t")?,
ec2_limits: analysis.service_limits.ec2_limits.to_markdown_table("\t")?,
ebs_gp2_limits: analysis.service_limits.ebs_gp2_limits.to_markdown_table("\t")?,
ebs_gp3_limits: analysis.service_limits.ebs_gp3_limits.to_markdown_table("\t")?,
upgrade_readiness_insights: analysis.insights.upgrade_readiness.to_markdown_table("\t")?,
misconfiguration_insights: analysis.insights.misconfiguration.to_markdown_table("\t")?,
};
let mut rendered = handlebars.render("playbook.md", &tmpl_data)?;
let suppressed_total = kubernetes_suppressed.total();
if suppressed_total > 0 {
if show_suppressed {
rendered.push_str(&format!(
"\n## Suppressed by configuration ({suppressed_total})\n\nThe following findings were suppressed by `.eksup.yaml` ignore rules.\n\n"
));
if !kubernetes_suppressed.min_replicas.is_empty() {
rendered.push_str("#### Check [[K8S002]](https://clowdhaus.github.io/eksup/info/checks/#k8s002)\n");
rendered.push_str(&kubernetes_suppressed.min_replicas.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.min_ready_seconds.is_empty() {
rendered.push_str("#### Check [[K8S003]](https://clowdhaus.github.io/eksup/info/checks/#k8s003)\n");
rendered.push_str(&kubernetes_suppressed.min_ready_seconds.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.pod_disruption_budgets.is_empty() {
rendered.push_str("#### Check [[K8S004]](https://clowdhaus.github.io/eksup/info/checks/#k8s004)\n");
rendered.push_str(&kubernetes_suppressed.pod_disruption_budgets.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.pod_topology_distribution.is_empty() {
rendered.push_str("#### Check [[K8S005]](https://clowdhaus.github.io/eksup/info/checks/#k8s005)\n");
rendered.push_str(&kubernetes_suppressed.pod_topology_distribution.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.readiness_probe.is_empty() {
rendered.push_str("#### Check [[K8S006]](https://clowdhaus.github.io/eksup/info/checks/#k8s006)\n");
rendered.push_str(&kubernetes_suppressed.readiness_probe.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.termination_grace_period.is_empty() {
rendered.push_str("#### Check [[K8S007]](https://clowdhaus.github.io/eksup/info/checks/#k8s007)\n");
rendered.push_str(&kubernetes_suppressed.termination_grace_period.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.docker_socket.is_empty() {
rendered.push_str("#### Check [[K8S008]](https://clowdhaus.github.io/eksup/info/checks/#k8s008)\n");
rendered.push_str(&kubernetes_suppressed.docker_socket.to_markdown_table("")?);
rendered.push('\n');
}
if !kubernetes_suppressed.ingress_nginx_retirement.is_empty() {
rendered.push_str("#### Check [[K8S013]](https://clowdhaus.github.io/eksup/info/checks/#k8s013)\n");
rendered.push_str(&kubernetes_suppressed.ingress_nginx_retirement.to_markdown_table("")?);
rendered.push('\n');
}
} else {
rendered.push_str(&format!(
"\n_{suppressed_total} additional finding(s) suppressed by `.eksup.yaml`. Run with `--show-suppressed` to view._\n"
));
}
}
Ok(rendered)
}
pub(crate) fn create(
args: Playbook,
region: String,
cluster: &Cluster,
analysis: analysis::Results,
target_minor: i32,
) -> Result<()> {
let cluster_name = cluster.name().context("Cluster name missing")?;
let target_version = version::format_version(target_minor);
let default_playbook_name = format!("{cluster_name}_v{target_version}_upgrade.md");
let rendered = render(®ion, cluster, analysis, target_minor, args.show_suppressed)?;
let filename = args.filename.as_deref().unwrap_or(&default_playbook_name);
fs::write(filename, rendered)?;
Ok(())
}