1use std::sync::Arc;
36
37use kube::core::admission::{AdmissionRequest, AdmissionResponse, AdmissionReview, Operation};
38use kube::core::conversion::{ConversionRequest, ConversionResponse, ConversionReview};
39use kube::core::Status;
40use kube::Resource;
41use serde::de::DeserializeOwned;
42use serde::Serialize;
43
44use rws_server::application::Application;
45use rws_server::request::Request;
46use rws_server::response::{Response, STATUS_CODE_REASON_PHRASE};
47use rws_server::server::ConnectionInfo;
48
49pub use kube::core::admission::Operation as AdmissionOperation;
50
51type Handler = Arc<dyn Fn(&[u8]) -> Result<Vec<u8>, String> + Send + Sync>;
52
53#[derive(Clone, Default)]
55pub struct Webhooks {
56 routes: Vec<(String, Handler)>,
57 fallback: Option<Arc<dyn Application + Send + Sync>>,
58}
59
60fn json_response(body: Vec<u8>) -> Response {
61 Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, body)
62}
63
64pub fn admission<K, F>(body: &[u8], decide: F) -> Result<Vec<u8>, String>
68where
69 K: Resource + DeserializeOwned + Serialize + Clone,
70 F: Fn(&AdmissionRequest<K>) -> Result<Option<json_patch::Patch>, String>,
71{
72 let review: AdmissionReview<K> = serde_json::from_slice(body).map_err(|e| format!("not an AdmissionReview: {e}"))?;
73 let request: AdmissionRequest<K> = review.try_into().map_err(|_| "the AdmissionReview has no request".to_string())?;
74 let response = match decide(&request) {
75 Ok(None) => AdmissionResponse::from(&request),
76 Ok(Some(patch)) => AdmissionResponse::from(&request).with_patch(patch).map_err(|e| e.to_string())?,
77 Err(message) => AdmissionResponse::from(&request).deny(message),
78 };
79 let mut review = serde_json::to_value(response.into_review()).map_err(|e| e.to_string())?;
82 if let Some(bytes) = review["response"]["patch"].as_array().map(|a| a.iter().filter_map(|b| b.as_u64().map(|b| b as u8)).collect::<Vec<u8>>()) {
83 review["response"]["patch"] = serde_json::Value::String(super::manifests::base64(&bytes));
84 }
85 serde_json::to_vec(&review).map_err(|e| e.to_string())
86}
87
88pub fn conversion<F>(body: &[u8], convert: F) -> Result<Vec<u8>, String>
91where
92 F: Fn(serde_json::Value, &str) -> Result<serde_json::Value, String>,
93{
94 let review: ConversionReview = serde_json::from_slice(body).map_err(|e| format!("not a ConversionReview: {e}"))?;
95 let request = ConversionRequest::from_review(review).map_err(|_| "the ConversionReview has no request".to_string())?;
96 let desired = request.desired_api_version.clone();
97 let objects = request.objects.clone();
98 let response = ConversionResponse::for_request(request);
99 let mut converted = Vec::with_capacity(objects.len());
100 let mut failure = None;
101 for object in objects {
102 match convert(object, &desired) {
103 Ok(mut o) => {
104 o["apiVersion"] = serde_json::Value::String(desired.clone());
105 converted.push(o);
106 }
107 Err(e) => {
108 failure = Some(e);
109 break;
110 }
111 }
112 }
113 let response = match failure {
114 None => response.success(converted),
115 Some(e) => response.failure(Status::failure(&e, "ConversionFailed")),
116 };
117 serde_json::to_vec(&response.into_review()).map_err(|e| e.to_string())
118}
119
120impl Webhooks {
121 pub fn new() -> Self {
122 Webhooks::default()
123 }
124
125 pub fn validate<K, F>(mut self, path: &str, check: F) -> Self
129 where
130 K: Resource + DeserializeOwned + Serialize + Clone + 'static,
131 F: Fn(&AdmissionRequest<K>) -> Result<(), String> + Send + Sync + 'static,
132 {
133 self.routes.push((path.to_string(), Arc::new(move |body| admission::<K, _>(body, |r| check(r).map(|_| None)))));
134 self
135 }
136
137 pub fn mutate<K, F>(mut self, path: &str, change: F) -> Self
141 where
142 K: Resource + DeserializeOwned + Serialize + Clone + 'static,
143 F: Fn(K) -> Result<K, String> + Send + Sync + 'static,
144 {
145 self.routes.push((
146 path.to_string(),
147 Arc::new(move |body| {
148 admission::<K, _>(body, |request| {
149 let Some(object) = request.object.clone() else { return Ok(None) };
150 if !matches!(request.operation, Operation::Create | Operation::Update) {
151 return Ok(None);
152 }
153 let before = serde_json::to_value(&object).map_err(|e| e.to_string())?;
154 let after = serde_json::to_value(change(object)?).map_err(|e| e.to_string())?;
155 let patch = json_patch::diff(&before, &after);
156 Ok((!patch.0.is_empty()).then_some(patch))
157 })
158 }),
159 ));
160 self
161 }
162
163 pub fn convert<F>(mut self, path: &str, convert: F) -> Self
168 where
169 F: Fn(serde_json::Value, &str) -> Result<serde_json::Value, String> + Send + Sync + 'static,
170 {
171 self.routes.push((path.to_string(), Arc::new(move |body| conversion(body, &convert))));
172 self
173 }
174
175 pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> Self {
177 self.fallback = Some(Arc::new(app));
178 self
179 }
180}
181
182impl Application for Webhooks {
183 fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
184 let path = request.request_uri.split('?').next().unwrap_or("");
185 if let Some((_, handler)) = self.routes.iter().find(|(p, _)| p == path) {
186 if request.method != "POST" {
187 return Ok(Response::text(STATUS_CODE_REASON_PHRASE.n405_method_not_allowed, "POST an AdmissionReview or a ConversionReview"));
188 }
189 return Ok(match handler(&request.body) {
190 Ok(body) => json_response(body),
191 Err(message) => Response::text(STATUS_CODE_REASON_PHRASE.n400_bad_request, &message),
192 });
193 }
194 match &self.fallback {
195 Some(app) => app.execute(request, connection),
196 None => {
197 use rws_server::core::New;
198 rws_server::app::App::new().execute(request, connection)
199 }
200 }
201 }
202}
203
204#[derive(Clone)]
208pub struct WebhookCertificate {
209 pub ca_pem: String,
210 pub cert_pem: String,
211 pub key_pem: String,
212}
213
214impl WebhookCertificate {
215 pub fn generate(service: &str, namespace: &str) -> Result<Self, String> {
218 let names = vec![
219 service.to_string(),
220 format!("{service}.{namespace}"),
221 format!("{service}.{namespace}.svc"),
222 format!("{service}.{namespace}.svc.cluster.local"),
223 ];
224 Self::generate_for(&format!("{service}.{namespace}.svc"), names)
225 }
226
227 pub fn generate_for(common_name: &str, names: Vec<String>) -> Result<Self, String> {
230 use rcgen::{BasicConstraints, CertificateParams, DnType, IsCa, KeyPair, KeyUsagePurpose};
231 let ca_key = KeyPair::generate().map_err(|e| e.to_string())?;
232 let mut ca_params = CertificateParams::new(Vec::<String>::new()).map_err(|e| e.to_string())?;
233 ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
234 ca_params.distinguished_name.push(DnType::CommonName, format!("{common_name} webhook CA"));
235 ca_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::CrlSign];
236 ten_years(&mut ca_params);
237 let ca = ca_params.self_signed(&ca_key).map_err(|e| e.to_string())?;
238 let issuer = rcgen::Issuer::new(ca_params, ca_key);
239 let key = KeyPair::generate().map_err(|e| e.to_string())?;
240 let mut params = CertificateParams::new(names).map_err(|e| e.to_string())?;
241 params.distinguished_name.push(DnType::CommonName, common_name.to_string());
242 ten_years(&mut params);
243 let cert = params.signed_by(&key, &issuer).map_err(|e| e.to_string())?;
244 Ok(WebhookCertificate { ca_pem: ca.pem(), cert_pem: cert.pem(), key_pem: key.serialize_pem() })
245 }
246
247 pub async fn shared(client: &kube::Client, namespace: &str, secret: &str, service: &str) -> super::Result<Self> {
254 use k8s_openapi::api::core::v1::Secret;
255 use kube::api::{Api, PostParams};
256 let api: Api<Secret> = Api::namespaced(client.clone(), namespace);
257 if let Some(existing) = api.get_opt(secret).await? {
258 return Self::from_secret(&existing).ok_or_else(|| super::Error::Other(format!("secret {namespace}/{secret} has no tls.crt, tls.key and ca.crt")));
259 }
260 let cert = Self::generate(service, namespace)?;
261 match api.create(&PostParams::default(), &cert.to_secret(secret, namespace)).await {
262 Ok(_) => Ok(cert),
263 Err(kube::Error::Api(status)) if status.is_already_exists() => {
264 let existing = api.get(secret).await?;
265 Self::from_secret(&existing).ok_or_else(|| super::Error::Other(format!("secret {namespace}/{secret} has no tls.crt, tls.key and ca.crt")))
266 }
267 Err(e) => Err(e.into()),
268 }
269 }
270
271 pub fn to_secret(&self, name: &str, namespace: &str) -> k8s_openapi::api::core::v1::Secret {
273 use k8s_openapi::ByteString;
274 k8s_openapi::api::core::v1::Secret {
275 metadata: k8s_openapi::apimachinery::pkg::apis::meta::v1::ObjectMeta { name: Some(name.to_string()), namespace: Some(namespace.to_string()), ..Default::default() },
276 type_: Some("kubernetes.io/tls".to_string()),
277 data: Some(std::collections::BTreeMap::from([
278 ("tls.crt".to_string(), ByteString(self.cert_pem.clone().into_bytes())),
279 ("tls.key".to_string(), ByteString(self.key_pem.clone().into_bytes())),
280 ("ca.crt".to_string(), ByteString(self.ca_pem.clone().into_bytes())),
281 ])),
282 ..Default::default()
283 }
284 }
285
286 pub fn from_secret(secret: &k8s_openapi::api::core::v1::Secret) -> Option<Self> {
289 let data = secret.data.as_ref()?;
290 let text = |key: &str| data.get(key).and_then(|v| String::from_utf8(v.0.clone()).ok()).filter(|t| !t.is_empty());
291 Some(WebhookCertificate { cert_pem: text("tls.crt")?, key_pem: text("tls.key")?, ca_pem: text("ca.crt")? })
292 }
293
294 pub fn install(&self, dir: &str) -> Result<(), String> {
298 std::fs::create_dir_all(dir).map_err(|e| format!("{dir}: {e}"))?;
299 let path = |f: &str| format!("{}/{f}", dir.trim_end_matches('/'));
300 std::fs::write(path("tls.crt"), &self.cert_pem).map_err(|e| e.to_string())?;
301 std::fs::write(path("tls.key"), &self.key_pem).map_err(|e| e.to_string())?;
302 std::fs::write(path("ca.crt"), &self.ca_pem).map_err(|e| e.to_string())?;
303 unsafe {
305 std::env::set_var("RWS_CONFIG_TLS_CERT_FILE", path("tls.crt"));
306 std::env::set_var("RWS_CONFIG_TLS_KEY_FILE", path("tls.key"));
307 }
308 rws_server::server_config::invalidate();
309 Ok(())
310 }
311
312 pub async fn patch_ca_bundle(&self, client: &kube::Client, validating: Option<&str>, mutating: Option<&str>, crd: Option<&str>) -> super::Result<()> {
317 use k8s_openapi::api::admissionregistration::v1::{MutatingWebhookConfiguration, ValidatingWebhookConfiguration};
318 use k8s_openapi::apiextensions_apiserver::pkg::apis::apiextensions::v1::CustomResourceDefinition;
319 use kube::api::{Api, Patch, PatchParams};
320 let bundle = super::manifests::base64(self.ca_pem.as_bytes());
321 let params = PatchParams::default();
322 if let Some(name) = validating {
323 let api: Api<ValidatingWebhookConfiguration> = Api::all(client.clone());
324 let current = api.get(name).await?;
325 let hooks: Vec<serde_json::Value> = current.webhooks.unwrap_or_default().iter().map(|w| serde_json::json!({ "name": w.name, "clientConfig": { "caBundle": bundle } })).collect();
326 api.patch(name, ¶ms, &Patch::Strategic(serde_json::json!({ "webhooks": hooks }))).await?;
327 }
328 if let Some(name) = mutating {
329 let api: Api<MutatingWebhookConfiguration> = Api::all(client.clone());
330 let current = api.get(name).await?;
331 let hooks: Vec<serde_json::Value> = current.webhooks.unwrap_or_default().iter().map(|w| serde_json::json!({ "name": w.name, "clientConfig": { "caBundle": bundle } })).collect();
332 api.patch(name, ¶ms, &Patch::Strategic(serde_json::json!({ "webhooks": hooks }))).await?;
333 }
334 if let Some(name) = crd {
335 let api: Api<CustomResourceDefinition> = Api::all(client.clone());
336 let patch = serde_json::json!({ "spec": { "conversion": { "webhook": { "clientConfig": { "caBundle": bundle } } } } });
337 api.patch(name, ¶ms, &Patch::Merge(&patch)).await?;
338 }
339 Ok(())
340 }
341}
342
343fn ten_years(params: &mut rcgen::CertificateParams) {
344 let now = std::time::SystemTime::now();
345 params.not_before = now.into();
346 params.not_after = (now + std::time::Duration::from_secs(10 * 365 * 86_400)).into();
347}
348
349#[cfg(test)]
350mod tests;