use std::path::PathBuf;
#[macro_use] extern crate log;
use anyhow::{anyhow, Context, Result};
use clap::{CommandFactory, Parser, Subcommand};
use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::{
CustomResourceDefinition, CustomResourceDefinitionVersion, CustomResourceSubresources,
};
use kopium::{analyze, Config, Container};
use kube::{api, core::Version, Api, Client, ResourceExt};
use quote::format_ident;
#[derive(Parser)]
#[command(
version = clap::crate_version!(),
author = "clux <sszynrae@gmail.com>",
about = "Kubernetes OPenapI UnMangler",
)]
struct Kopium {
#[arg(conflicts_with("file"))]
crd: Option<String>,
#[arg(long = "filename", short, conflicts_with("crd"))]
file: Option<PathBuf>,
#[arg(long)]
api_version: Option<String>,
#[arg(long)]
hide_prelude: bool,
#[arg(long)]
hide_kube: bool,
#[arg(long, short)]
docs: bool,
#[arg(long, short)]
builders: bool,
#[arg(
long,
default_value = "disabled",
value_parser = ["disabled", "manual", "derived"],
)]
schema: String,
#[arg(long,
short = 'D',
value_parser = ["Copy", "Default", "PartialEq", "Eq", "PartialOrd", "Ord", "Hash", "JsonSchema"],
)]
derive: Vec<String>,
#[command(subcommand)]
command: Option<Command>,
#[arg(long, short = 'A')]
auto: bool,
#[arg(long, short = 'e')]
elide: Vec<String>,
#[arg(long)]
no_condition: bool,
}
#[derive(Clone, Copy, Debug, Subcommand)]
#[command(args_conflicts_with_subcommands = true)]
enum Command {
#[command(about = "List available CRDs", hide = true)]
ListCrds,
#[command(about = "Generate completions", hide = true)]
Completions {
#[arg(help = "The shell to generate completions for")]
shell: clap_complete::Shell,
},
}
#[tokio::main]
async fn main() -> Result<()> {
env_logger::init();
#[cfg(unix)]
unsafe {
libc::signal(libc::SIGPIPE, libc::SIG_DFL);
}
let mut args = Kopium::parse();
if args.auto {
args.docs = true;
args.schema = "derived".into();
}
if args.schema == "derived" && !args.derive.contains(&"JsonSchema".to_string()) {
args.derive.push("JsonSchema".to_string());
}
args.dispatch().await
}
fn get_stdin_data() -> Result<String> {
use std::io::{stdin, Read};
let mut buf = Vec::new();
stdin().read_to_end(&mut buf)?;
let input = String::from_utf8(buf)?;
Ok(input)
}
impl Kopium {
async fn dispatch(&self) -> Result<()> {
if let Some(name) = self.crd.as_deref() {
let api = Client::try_default()
.await
.map(Api::<CustomResourceDefinition>::all)?;
let crd = api.get(name).await?;
self.generate(crd).await
} else if let Some(f) = self.file.as_deref() {
let data = if f.to_string_lossy() == "-" {
get_stdin_data().with_context(|| "Failed to read from stdin".to_string())?
} else {
std::fs::read_to_string(f).with_context(|| format!("Failed to read {}", f.display()))?
};
let crd: CustomResourceDefinition = serde_yaml::from_str(&data)?;
self.generate(crd).await
} else if let Some(command) = self.command {
match command {
Command::ListCrds => {
let api = Client::try_default()
.await
.map(Api::<CustomResourceDefinition>::all)?;
self.list_crds(api).await
}
Command::Completions { shell } => self.completions(shell),
}
} else {
self.help()
}
}
async fn generate(&self, crd: CustomResourceDefinition) -> Result<()> {
let version = self.api_version.as_deref();
let version = find_crd_version(&crd, version)?;
let data = version
.schema
.as_ref()
.and_then(|schema| schema.open_api_v3_schema.clone());
let version_name = version.name.clone();
let kind = &crd.spec.names.kind;
let plural = &crd.spec.names.plural;
let group = &crd.spec.group;
let scope = &crd.spec.scope;
self.print_generation_warning();
if let Some(schema) = data {
log::debug!("schema: {}", serde_json::to_string_pretty(&schema)?);
let cfg = Config {
no_condition: self.no_condition,
};
let structs = analyze(schema, kind, cfg)?
.rename()
.builder_fields(self.builders)
.0;
if !self.hide_prelude {
self.print_prelude(&structs);
}
for s in &structs {
if s.level == 0 {
continue; } else {
if self.elide.contains(&s.name) {
debug!("eliding {} from the output", s.name);
continue;
}
self.print_docstr(&s.docs, "");
if s.is_main_container() {
self.print_derives(s);
if !self.hide_kube {
println!(
r#"#[kube(group = "{}", version = "{}", kind = "{}", plural = "{}")]"#,
group, version_name, kind, plural
);
if scope == "Namespaced" {
println!(r#"#[kube(namespaced)]"#);
}
if let Some(CustomResourceSubresources { status: Some(_), .. }) =
version.subresources
{
println!(r#"#[kube(status = "{}Status")]"#, kind);
}
if self.schema != "derived" {
println!(r#"#[kube(schema = "{}")]"#, self.schema);
}
}
if s.is_enum {
println!("pub enum {} {{", s.name);
} else {
println!("pub struct {} {{", s.name);
}
} else {
self.print_derives(s);
let spec_trimmed_name = s.name.as_str().replace(&format!("{}Spec", kind), kind);
if s.is_enum {
println!("pub enum {} {{", spec_trimmed_name);
} else {
println!("pub struct {} {{", spec_trimmed_name);
}
}
for m in &s.members {
self.print_docstr(&m.docs, " ");
if !m.serde_annot.is_empty() {
println!(" #[serde({})]", m.serde_annot.join(", "));
}
let name = format_ident!("{}", m.name);
for annot in &m.extra_annot {
println!(" {}", annot);
}
let spec_trimmed_type = m.type_.as_str().replace(&format!("{}Spec", kind), kind);
if s.is_enum {
println!(" {},", name);
} else {
println!(" pub {}: {},", name, spec_trimmed_type);
}
}
println!("}}");
println!();
}
}
} else {
log::error!("no schema found for crd");
}
Ok(())
}
async fn list_crds(&self, api: Api<CustomResourceDefinition>) -> Result<()> {
let lp = api::ListParams::default();
api.list(&lp).await?.items.iter().for_each(|crd| {
println!("{}", crd.name_any());
});
Ok(())
}
fn completions(&self, shell: clap_complete::Shell) -> Result<()> {
let mut command = Self::command();
clap_complete::generate(shell, &mut command, "kopium", &mut std::io::stdout());
Ok(())
}
fn help(&self) -> Result<()> {
Self::command().print_help()?;
Ok(())
}
fn print_docstr(&self, doc: &Option<String>, indent: &str) {
if self.docs {
if let Some(d) = doc {
println!("{}/// {}", indent, d.replace('\n', &format!("\n{}/// ", indent)));
}
}
}
fn print_derives(&self, s: &Container) {
let mut derives: Vec<String> = vec!["Serialize", "Deserialize", "Clone", "Debug"]
.into_iter()
.map(String::from)
.collect();
if s.is_main_container() && !self.hide_kube {
derives.insert(0, "CustomResource".to_string());
}
if self.builders {
derives.push("TypedBuilder".to_string());
}
for d in &self.derive {
if s.is_enum && d == "Default" {
continue;
}
derives.push(d.clone());
}
println!("#[derive({})]", derives.join(", "));
}
fn print_prelude(&self, results: &[Container]) {
if !self.hide_kube {
println!("use kube::CustomResource;");
}
if self.builders {
println!("use typed_builder::TypedBuilder;");
}
if self.derive.contains(&"JsonSchema".to_string()) {
println!("use schemars::JsonSchema;");
}
println!("use serde::{{Serialize, Deserialize}};");
if results.iter().any(|o| o.uses_btreemaps()) {
println!("use std::collections::BTreeMap;");
}
if results.iter().any(|o| o.uses_hashmaps()) {
println!("use std::collections::HashMap;");
}
if results.iter().any(|o| o.uses_datetime()) {
println!("use chrono::{{DateTime, Utc}};");
}
if results.iter().any(|o| o.uses_date()) {
println!("use chrono::naive::NaiveDate;");
}
if results.iter().any(|o| o.uses_int_or_string()) {
println!("use k8s_openapi::apimachinery::pkg::util::intstr::IntOrString;");
}
if results.iter().any(|o| o.contains_conditions()) && !self.no_condition {
println!("use k8s_openapi::apimachinery::pkg::apis::meta::v1::Condition;");
}
println!();
}
fn print_generation_warning(&self) {
println!("// WARNING: generated by kopium - manual changes will be overwritten");
let args = std::env::args().skip(1).collect::<Vec<_>>().join(" ");
println!("// kopium command: kopium {}", args);
println!("// kopium version: {}", clap::crate_version!());
println!();
}
}
fn find_crd_version<'a>(
crd: &'a CustomResourceDefinition,
version: Option<&str>,
) -> Result<&'a CustomResourceDefinitionVersion> {
if let Some(version) = version {
crd.spec
.versions
.iter()
.find(|v| v.name == version)
.ok_or_else(|| {
anyhow!(
"Version '{}' not found in CRD '{}'\navailable versions are '{}'",
version,
crd.name_any(),
all_versions(crd)
)
})
} else {
crd.spec
.versions
.iter()
.max_by_key(|v| Version::parse(&v.name).priority())
.ok_or_else(|| anyhow!("CRD '{}' has no versions", crd.name_any()))
}
}
fn all_versions(crd: &CustomResourceDefinition) -> String {
let mut vers = crd
.spec
.versions
.iter()
.map(|v| v.name.as_str())
.collect::<Vec<_>>();
vers.sort_by_cached_key(|v| std::cmp::Reverse(Version::parse(v).priority()));
vers.join(", ")
}