1use std::path::Path;
6use std::sync::Arc;
7use std::time::Duration;
8
9use engenho_apiserver::{ApiServer, ResourceHandler, StoreBackedHandler};
10use engenho_kube_client::{Connection, Kubeconfig, ReqwestKubeClient};
11use engenho_store::{InProcessRouter, StoreMesh, default_config};
12use engenho_types::auth::KubeAuth;
13use serde_json::Value;
14
15use crate::observe::{DiffError, HttpMethod, Observation};
16use crate::verdict::{Side, Verdict};
17
18#[async_trait::async_trait]
32pub trait DiffTarget: Send + Sync {
33 fn side(&self) -> Side;
35
36 async fn raw(
45 &self,
46 method: HttpMethod,
47 path: &str,
48 body: Option<&[u8]>,
49 content_type: Option<&str>,
50 ) -> Result<Observation, DiffError>;
51}
52
53async fn http_raw(
54 conn: &Connection,
55 side: Side,
56 method: HttpMethod,
57 path: &str,
58 body: Option<&[u8]>,
59 content_type: Option<&str>,
60) -> Result<Observation, DiffError> {
61 let mut url = String::from(conn.server());
62 url.push_str(path);
63 let mut rb = conn.http().request(method.as_reqwest(), &url);
64 if let Some(ct) = content_type {
65 rb = rb.header("Content-Type", ct);
66 }
67 if let Some(b) = body {
68 rb = rb.body(b.to_vec());
69 }
70 let rb = conn
71 .auth_header(rb)
72 .map_err(|e| DiffError::Kube(e.to_string()))?;
73 let resp = rb.send().await.map_err(|source| DiffError::Transport {
74 side: side.label(),
75 source,
76 })?;
77 let status = resp.status().as_u16();
78 let content_type = resp
79 .headers()
80 .get(reqwest::header::CONTENT_TYPE)
81 .and_then(|v| v.to_str().ok())
82 .map(str::to_string);
83 let text = resp.text().await.map_err(|source| DiffError::Transport {
84 side: side.label(),
85 source,
86 })?;
87 let json = if text.trim().is_empty() {
88 Value::Null
89 } else {
90 serde_json::from_str(&text).unwrap_or(Value::String(text))
91 };
92 Ok(Observation {
93 status,
94 content_type,
95 json,
96 })
97}
98
99pub struct EngenhoTarget {
103 _store: Arc<StoreMesh>,
104 server: ApiServer,
105 conn: Connection,
106 client: ReqwestKubeClient,
107}
108
109impl EngenhoTarget {
110 pub async fn boot_in_process(kinds: &[&str]) -> Result<Self, DiffError> {
118 let router = InProcessRouter::new();
119 let cfg = default_config("engenho-diff").map_err(|e| DiffError::Boot(format!("{e:?}")))?;
120 let store = Arc::new(
121 StoreMesh::start(1, "in-process://1".into(), router, cfg)
122 .await
123 .map_err(|e| DiffError::Boot(format!("{e:?}")))?,
124 );
125 store
126 .initialize_singleton()
127 .await
128 .map_err(|e| DiffError::Boot(format!("{e:?}")))?;
129 if !store.wait_for_leadership(Duration::from_secs(5)).await {
130 return Err(DiffError::Boot("store did not acquire leadership".into()));
131 }
132 let mut handlers: Vec<Arc<dyn ResourceHandler>> = Vec::with_capacity(kinds.len());
133 for k in kinds {
134 let h = StoreBackedHandler::for_kind(store.clone(), k)
135 .ok_or_else(|| DiffError::Boot(format!("for_kind {k}: not a cataloged kind")))?;
136 handlers.push(Arc::new(h));
137 }
138 let addr = "127.0.0.1:0"
139 .parse()
140 .map_err(|e| DiffError::Boot(format!("addr parse: {e}")))?;
141 let server = ApiServer::start(addr, handlers, None)
142 .await
143 .map_err(|e| DiffError::Boot(format!("{e:?}")))?;
144 let mut base = String::from("http://");
145 base.push_str(&server.local_addr().to_string());
146 let conn = Connection::new(&base, KubeAuth::Anonymous, None)
147 .map_err(|e| DiffError::Boot(format!("{e:?}")))?;
148 let client = ReqwestKubeClient::new(conn.clone());
149 Ok(Self {
150 _store: store,
151 server,
152 conn,
153 client,
154 })
155 }
156
157 #[must_use]
160 pub fn kube_client(&self) -> &ReqwestKubeClient {
161 &self.client
162 }
163
164 pub async fn shutdown(self) -> Result<(), DiffError> {
170 self.server
171 .shutdown()
172 .await
173 .map_err(|e| DiffError::Boot(e.to_string()))
174 }
175}
176
177#[async_trait::async_trait]
178impl DiffTarget for EngenhoTarget {
179 fn side(&self) -> Side {
180 Side::Engenho
181 }
182
183 async fn raw(
184 &self,
185 method: HttpMethod,
186 path: &str,
187 body: Option<&[u8]>,
188 content_type: Option<&str>,
189 ) -> Result<Observation, DiffError> {
190 http_raw(&self.conn, Side::Engenho, method, path, body, content_type).await
191 }
192}
193
194pub struct K3sTarget {
196 conn: Connection,
197 client: ReqwestKubeClient,
198}
199
200impl K3sTarget {
201 pub fn from_kubeconfig(path: &Path) -> Result<Self, DiffError> {
211 let kc = Kubeconfig::load(path).map_err(|e| DiffError::Kube(e.to_string()))?;
212 let conn = kc
213 .resolve_connection()
214 .map_err(|e| DiffError::Kube(e.to_string()))?;
215 let client = ReqwestKubeClient::new(conn.clone());
216 Ok(Self { conn, client })
217 }
218
219 #[must_use]
221 pub fn kube_client(&self) -> &ReqwestKubeClient {
222 &self.client
223 }
224
225 pub async fn preflight(&self) -> Option<Verdict> {
229 match self.raw(HttpMethod::Get, "/api/v1", None, None).await {
230 Ok(obs) if obs.is_success() => None,
231 _ => Some(Verdict::ReferenceUnreachable),
232 }
233 }
234}
235
236#[async_trait::async_trait]
237impl DiffTarget for K3sTarget {
238 fn side(&self) -> Side {
239 Side::K3s
240 }
241
242 async fn raw(
243 &self,
244 method: HttpMethod,
245 path: &str,
246 body: Option<&[u8]>,
247 content_type: Option<&str>,
248 ) -> Result<Observation, DiffError> {
249 http_raw(&self.conn, Side::K3s, method, path, body, content_type).await
250 }
251}