From 68ac3789439007b85010ed2c891fa463a70577a9 Mon Sep 17 00:00:00 2001 From: Ho Kim Date: Mon, 22 Jul 2024 08:04:30 +0000 Subject: [PATCH] feat(kube): begin implementation of api discovery --- .../src/api.rs | 67 ++++++++++++++++++- .../src/hooks.rs | 21 +++++- .../src/lib.rs | 57 ---------------- .../src/lib.rs | 27 ++++---- 4 files changed, 99 insertions(+), 73 deletions(-) diff --git a/crates/cassette-plugin-kubernetes-core/src/api.rs b/crates/cassette-plugin-kubernetes-core/src/api.rs index ef5262d..ebe56f5 100644 --- a/crates/cassette-plugin-kubernetes-core/src/api.rs +++ b/crates/cassette-plugin-kubernetes-core/src/api.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, rc::Rc}; use anyhow::Result; use kube_core::{params::ListParams, ObjectList, Request}; @@ -16,7 +16,68 @@ pub struct Api { } impl Api { - pub(crate) async fn list(self, lp: ListParams) -> Result> + pub(crate) async fn find(api_version: String, kind: String) -> Result { + let (api_group, version) = match api_version.split_once('/') { + Some(("", "")) => (None, None), + Some((api_group, "")) => (Some(api_group), None), + Some(("", version)) => (None, Some(version)), + Some((api_group, version)) => (Some(api_group), Some(version)), + None => { + if api_version.is_empty() { + (None, None) + } else { + (None, Some(api_version.as_str())) + } + } + }; + + // TODO: to be implemented (server-side) + let _ = kind; + + Ok(Self { + api_group: api_group.map(|s| s.into()), + namespace: Some("default".into()), + plural: "deployments".into(), + version: version.unwrap_or("v1").into(), + _type: PhantomData, + }) + + // let url_path = match api_group { + // Some(api_group) => format!("/apis/"), + // }; + + // // Discover most stable version variant of document + // let apigroup = discovery::group(&kube, api_group).await?; + // let (ar, caps) = match match version { + // Some(version) => apigroup.versioned_resources(version), + // None => apigroup.recommended_resources(), + // } + // .into_iter() + // .find(|(ar, _)| ar.kind == kind) + // { + // Some((ar, caps)) => (ar, caps), + // None => bail!( + // "Cannot find resource: {kind}.{api_group}/{version}", + // api_group = if api_group.is_empty() { + // "core" + // } else { + // api_group + // }, + // version = version.unwrap_or("auto"), + // ), + // }; + + // // Use the discovered kind in an Api, and Controller with the ApiResource as its DynamicType + // Ok(match caps.scope { + // Scope::Cluster => Api::all_with(kube, &ar), + // Scope::Namespaced => match namespace { + // Some(namespace) => Api::namespaced_with(kube, namespace, &ar), + // None => Api::default_namespaced_with(kube, &ar), + // }, + // }); + } + + pub(crate) async fn list(self: Rc, lp: ListParams) -> Result> where K: Clone + DeserializeOwned, { @@ -26,7 +87,7 @@ impl Api { plural, version, _type: PhantomData, - } = self; + } = &*self; let url_path = match (api_group, namespace) { (None, None) => format!("/api/{version}/{plural}"), diff --git a/crates/cassette-plugin-kubernetes-core/src/hooks.rs b/crates/cassette-plugin-kubernetes-core/src/hooks.rs index 69d4574..9ba8443 100644 --- a/crates/cassette-plugin-kubernetes-core/src/hooks.rs +++ b/crates/cassette-plugin-kubernetes-core/src/hooks.rs @@ -11,9 +11,28 @@ use yew::platform::spawn_local; use crate::api::Api; +pub fn use_kubernetes_api( + ctx: &mut CassetteContext, + api_version: String, + kind: String, +) -> CassetteTaskHandle>> +where + K: 'static + Clone + DeserializeOwned, +{ + let handler_name = "kubernetes api"; + let force_init = false; + let state = ctx.use_state(handler_name, force_init, || FetchState::Pending); + { + let state = state.clone(); + let f = move || Api::find(api_version, kind); + try_fetch(state, f); + } + state +} + pub fn use_kubernetes_list( ctx: &mut CassetteContext, - api: Api, + api: Rc>, lp: ListParams, ) -> CassetteTaskHandle>> where diff --git a/crates/cassette-plugin-kubernetes-core/src/lib.rs b/crates/cassette-plugin-kubernetes-core/src/lib.rs index ba9916d..9823f18 100644 --- a/crates/cassette-plugin-kubernetes-core/src/lib.rs +++ b/crates/cassette-plugin-kubernetes-core/src/lib.rs @@ -1,60 +1,3 @@ pub mod api; pub mod client; pub mod hooks; - -// use anyhow::{anyhow, bail, Result}; -// use inflector::Inflector; -// use kube::{ -// api::DynamicObject, -// discovery::{self, ApiGroup, Scope}, -// Api, Client, -// }; -// use tracing::{instrument, Level}; - -// pub async fn init_client() -> Result { -// Client::try_default() -// .await -// .map_err(|error| anyhow!("failed to init kubernetes client: {error}")) -// } - -// #[instrument(level = Level::INFO, skip(kube))] -// pub async fn get_api( -// kube: Client, -// namespace: Option<&str>, -// api_group: Option<&str>, -// version: Option<&str>, -// kind: &str, -// ) -> Result> { -// let api_group = api_group.unwrap_or(ApiGroup::CORE_GROUP); -// let kind_pascal = kind.to_pascal_case(); - -// // Discover most stable version variant of document -// let apigroup = discovery::group(&kube, api_group).await?; -// let (ar, caps) = match match version { -// Some(version) => apigroup.versioned_resources(version), -// None => apigroup.recommended_resources(), -// } -// .into_iter() -// .find(|(ar, _)| ar.kind == kind_pascal) -// { -// Some((ar, caps)) => (ar, caps), -// None => bail!( -// "Cannot find resource: {kind}.{api_group}/{version}", -// api_group = if api_group.is_empty() { -// "core" -// } else { -// api_group -// }, -// version = version.unwrap_or("auto"), -// ), -// }; - -// // Use the discovered kind in an Api, and Controller with the ApiResource as its DynamicType -// Ok(match caps.scope { -// Scope::Cluster => Api::all_with(kube, &ar), -// Scope::Namespaced => match namespace { -// Some(namespace) => Api::namespaced_with(kube, namespace, &ar), -// None => Api::default_namespaced_with(kube, &ar), -// }, -// }) -// } diff --git a/crates/cassette-plugin-kubernetes-list/src/lib.rs b/crates/cassette-plugin-kubernetes-list/src/lib.rs index a3dc3ce..d4dde9a 100644 --- a/crates/cassette-plugin-kubernetes-list/src/lib.rs +++ b/crates/cassette-plugin-kubernetes-list/src/lib.rs @@ -1,5 +1,3 @@ -use std::marker::PhantomData; - use cassette_core::{ cassette::{CassetteContext, GenericCassetteTaskHandle}, components::ComponentRenderer, @@ -7,7 +5,7 @@ use cassette_core::{ prelude::*, task::{TaskResult, TaskState}, }; -use cassette_plugin_kubernetes_core::{api::Api, hooks::use_kubernetes_list}; +use cassette_plugin_kubernetes_core::hooks::{use_kubernetes_api, use_kubernetes_list}; use kube_core::{params::ListParams, DynamicObject}; use serde::{Deserialize, Serialize}; use yew::prelude::*; @@ -42,15 +40,20 @@ impl ComponentRenderer for State { fn render(self, ctx: &mut CassetteContext, spec: Spec) -> TaskResult> { let Spec { api_version, kind } = spec; - // TODO: to be implemented - let _ = (api_version, kind); - - let api: Api = Api { - api_group: Some("apps".into()), - namespace: Some("default".into()), - plural: "deployments".into(), - version: "v1".into(), - _type: PhantomData, + let api = match use_kubernetes_api(ctx, api_version, kind).get() { + FetchState::Pending | FetchState::Fetching => { + return Ok(TaskState::Break { + body: html! { }, + state: None, + }) + } + FetchState::Collecting(api) | FetchState::Completed(api) => api.clone(), + FetchState::Error(msg) => { + return Ok(TaskState::Break { + body: html! { }, + state: None, + }) + } }; let lp = ListParams::default();