diff --git a/src/main.rs b/src/main.rs index 44683e7..0280fde 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,199 +21,9 @@ use tracing::*; mod packets; mod types; -#[derive(clap::Parser)] -struct App { - #[arg(long, short, default_value_t = OutputMode::Pretty)] - output: OutputMode, - #[arg(long, short)] - file: Option, - #[arg(long, short = 'l')] - selector: Option, - #[arg(long, short)] - namespace: Option, - #[arg(long, short = 'A')] - all: bool, - verb: Verb, - resource: Option, - name: Option, -} - -#[derive(Clone, PartialEq, Eq, clap::ValueEnum)] -enum OutputMode { - Pretty, - Yaml, -} - -impl OutputMode { - fn as_str(&self) -> &'static str { - match self { - Self::Pretty => "pretty", - Self::Yaml => "yaml", - } - } -} - -impl std::fmt::Display for OutputMode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.pad(self.as_str()) - } -} - -#[derive(Clone, PartialEq, Eq, Debug, clap::ValueEnum)] -enum Verb { - Get, - Delete, - Edit, - Watch, - Apply, -} - -fn resolve_api_resource( - discovery: &Discovery, - name: &str, -) -> Option<(ApiResource, ApiCapabilities)> { - // iterate through groups to find matching kind/plural names at recommended versions - // and then take the minimal match by group.name (equivalent to sorting groups by group.name). - // this is equivalent to kubectl's api group preference - discovery - .groups() - .flat_map(|group| { - group - .resources_by_stability() - .into_iter() - .map(move |res| (group, res)) - }) - .filter(|(_, (res, _))| { - // match on both resource name and kind name - // ideally we should allow shortname matches as well - name.eq_ignore_ascii_case(&res.kind) || name.eq_ignore_ascii_case(&res.plural) - }) - .min_by_key(|(group, _res)| group.name()) - .map(|(_, res)| res) -} - -impl App { - async fn get(&self, api: Api, lp: ListParams) -> Result<()> { - let mut result: Vec<_> = if let Some(n) = &self.name { - vec![api.get(n).await?] - } else { - api.list(&lp).await?.items - }; - result - .iter_mut() - .for_each(|x| x.managed_fields_mut().clear()); // hide managed fields - - match self.output { - OutputMode::Yaml => println!("{}", serde_yaml::to_string(&result)?), - OutputMode::Pretty => { - // Display style; size columns according to longest name - let max_name = result - .iter() - .map(|x| x.name_any().len() + 2) - .max() - .unwrap_or(63); - println!("{0:, lp: ListParams) -> Result<()> { - if let Some(n) = &self.name { - if let either::Either::Left(pdel) = api.delete(n, &Default::default()).await? { - // await delete before returning - await_condition(api, n, is_deleted(&pdel.uid().unwrap())).await?; - } - } else { - api.delete_collection(&Default::default(), &lp).await?; - } - Ok(()) - } - - async fn watch(&self, api: Api, mut wc: watcher::Config) -> Result<()> { - if let Some(n) = &self.name { - wc = wc.fields(&format!("metadata.name={n}")); - } - // present a dumb table for it for now. kubectl does not do this anymore. - let mut stream = watcher(api, wc).applied_objects().boxed(); - println!("{0:) -> Result<()> { - if let Some(n) = &self.name { - let mut orig = api.get(n).await?; - orig.managed_fields_mut().clear(); // hide managed fields - let input = serde_yaml::to_string(&orig)?; - debug!("opening {} in {:?}", orig.name_any(), edit::get_editor()); - let edited = edit::edit(&input)?; - if edited != input { - info!("updating changed object {}", orig.name_any()); - let data: DynamicObject = serde_yaml::from_str(&edited)?; - // NB: simplified kubectl constructs a merge-patch of differences - api.replace(n, &Default::default(), &data).await?; - } - } else { - warn!("need a name to edit"); - } - Ok(()) - } - - async fn apply(&self, client: Client, discovery: &Discovery) -> Result<()> { - let ssapply = PatchParams::apply("kubectl-light").force(); - let pth = self.file.clone().expect("apply needs a -f file supplied"); - let yaml = std::fs::read_to_string(&pth) - .with_context(|| format!("Failed to read {}", pth.display()))?; - for doc in multidoc_deserialize(&yaml)? { - let obj: DynamicObject = serde_yaml::from_value(doc)?; - let namespace = obj - .metadata - .namespace - .as_deref() - .or(self.namespace.as_deref()); - let gvk = if let Some(tm) = &obj.types { - GroupVersionKind::try_from(tm)? - } else { - bail!("cannot apply object without valid TypeMeta {:?}", obj); - }; - let name = obj.name_any(); - if let Some((ar, caps)) = discovery.resolve_gvk(&gvk) { - let api = dynamic_api(ar, caps, client.clone(), namespace, false); - trace!("Applying {}: \n{}", gvk.kind, serde_yaml::to_string(&obj)?); - let data: serde_json::Value = serde_json::to_value(&obj)?; - let _r = api.patch(&name, &ssapply, &Patch::Apply(data)).await?; - info!("applied {} {}", gvk.kind, name); - } else { - warn!("Cannot apply document for unknown {:?}", gvk); - } - } - Ok(()) - } -} - #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt::init(); - // let app: App = clap::Parser::parse(); let kubeconfig = kube::config::Kubeconfig::read()?; let client = Client::try_from(kubeconfig)?; @@ -223,70 +33,5 @@ async fn main() -> Result<()> { let lp: ListParams = ListParams::default(); println!("{:?}", deployments.list(&lp).await?); - // // discovery (to be able to infer apis from kind/plural only) - // let discovery = Discovery::new(client.clone()).run().await?; - - // // Defer to methods for verbs - // if let Some(resource) = &app.resource { - // // Common discovery, parameters, and api configuration for a single resource - // let (ar, caps) = resolve_api_resource(&discovery, resource) - // .with_context(|| format!("resource {resource:?} not found in cluster"))?; - // let mut lp = ListParams::default(); - // if let Some(label) = &app.selector { - // lp = lp.labels(label); - // } - - // let mut wc = watcher::Config::default(); - // if let Some(label) = &app.selector { - // wc = wc.labels(label); - // } - - // let api = dynamic_api(ar, caps, client, app.namespace.as_deref(), app.all); - - // tracing::info!(?app.verb, ?resource, name = ?app.name.clone().unwrap_or_default(), "requested objects"); - // match app.verb { - // Verb::Edit => app.edit(api).await?, - // Verb::Get => app.get(api, lp).await?, - // Verb::Delete => app.delete(api, lp).await?, - // Verb::Watch => app.watch(api, wc).await?, - // Verb::Apply => bail!("verb {:?} cannot act on an explicit resource", app.verb), - // } - // } else if app.verb == Verb::Apply { - // app.apply(client, &discovery).await? // multi-resource special behaviour - // } Ok(()) } - -fn dynamic_api( - ar: ApiResource, - caps: ApiCapabilities, - client: Client, - ns: Option<&str>, - all: bool, -) -> Api { - if caps.scope == Scope::Cluster || all { - Api::all_with(client, &ar) - } else if let Some(namespace) = ns { - Api::namespaced_with(client, namespace, &ar) - } else { - Api::default_namespaced_with(client, &ar) - } -} - -fn format_creation(time: Time) -> String { - let dur = Utc::now().signed_duration_since(time.0); - match (dur.num_days(), dur.num_hours(), dur.num_minutes()) { - (days, _, _) if days > 0 => format!("{days}d"), - (_, hours, _) if hours > 0 => format!("{hours}h"), - (_, _, mins) => format!("{mins}m"), - } -} - -pub fn multidoc_deserialize(data: &str) -> Result> { - use serde::Deserialize; - let mut docs = vec![]; - for de in serde_yaml::Deserializer::from_str(data) { - docs.push(serde_yaml::Value::deserialize(de)?); - } - Ok(docs) -}