Skip to content

Commit dbba557

Browse files
committed
Add validating webhook as well
1 parent d79e432 commit dbba557

File tree

3 files changed

+184
-1
lines changed

3 files changed

+184
-1
lines changed

crates/stackable-webhook/src/webhooks/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ pub use conversion_webhook::{ConversionReview, ConversionWebhook, ConversionWebh
44
use k8s_openapi::ByteString;
55
pub use mutating_webhook::{MutatingWebhook, MutatingWebhookError};
66
use snafu::Snafu;
7+
pub use validating_webhook::{ValidatingWebhook, ValidatingWebhookError};
78
use x509_cert::Certificate;
89

910
use crate::WebhookServerOptions;
1011

1112
mod conversion_webhook;
1213
mod mutating_webhook;
14+
mod validating_webhook;
1315

1416
#[derive(Snafu, Debug)]
1517
pub enum WebhookError {
@@ -22,6 +24,11 @@ pub enum WebhookError {
2224
MutatingWebhookError {
2325
source: mutating_webhook::MutatingWebhookError,
2426
},
27+
28+
#[snafu(display("validating webhook error"), context(false))]
29+
ValidatingWebhookError {
30+
source: validating_webhook::ValidatingWebhookError,
31+
},
2532
}
2633

2734
/// A webhook (such as a conversion or mutating webhook) needs to implement this trait.

crates/stackable-webhook/src/webhooks/mutating_webhook.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ pub struct MutatingWebhook<H, S, R> {
108108
disable_mutating_webhook_configuration_maintenance: bool,
109109
client: Client,
110110

111-
/// The field manager used when maintaining the CRDs.
111+
/// The field manager used when maintaining the MutatingWebhookConfigurations.
112112
field_manager: String,
113113
}
114114

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
use std::{fmt::Debug, marker::PhantomData, sync::Arc};
2+
3+
use async_trait::async_trait;
4+
use axum::{Json, Router, routing::post};
5+
use k8s_openapi::{
6+
ByteString,
7+
api::admissionregistration::v1::{
8+
ServiceReference, ValidatingWebhookConfiguration, WebhookClientConfig,
9+
},
10+
};
11+
use kube::{
12+
Api, Client, Resource, ResourceExt,
13+
api::{Patch, PatchParams},
14+
core::admission::{AdmissionRequest, AdmissionResponse, AdmissionReview},
15+
};
16+
use serde::{Serialize, de::DeserializeOwned};
17+
use snafu::{ResultExt, Snafu};
18+
use tracing::instrument;
19+
use x509_cert::Certificate;
20+
21+
use super::{Webhook, WebhookError};
22+
use crate::WebhookServerOptions;
23+
24+
#[derive(Debug, Snafu)]
25+
pub enum ValidatingWebhookError {
26+
#[snafu(display("failed to patch ValidatingWebhookConfiguration {vwc_name:?}"))]
27+
PatchValidatingWebhookConfiguration {
28+
source: kube::Error,
29+
vwc_name: String,
30+
},
31+
}
32+
33+
/// Validating webhook, which let's you intercept object creations/modification and allow or deny
34+
/// the object.
35+
///
36+
/// As the webhook is typed with the Resource type `R`, it can only handle a single resource
37+
/// validation. Use multiple [`ValidatingWebhook`] if you need to validate multiple resource kinds.
38+
///
39+
/// ### Example usage
40+
///
41+
/// TODO
42+
pub struct ValidatingWebhook<H, S, R> {
43+
validating_webhook_configuration: ValidatingWebhookConfiguration,
44+
handler: H,
45+
handler_state: Arc<S>,
46+
_resource: PhantomData<R>,
47+
48+
disable_validating_webhook_configuration_maintenance: bool,
49+
client: Client,
50+
51+
/// The field manager used when maintaining the ValidatingWebhookConfigurations.
52+
field_manager: String,
53+
}
54+
55+
impl<H, S, R> ValidatingWebhook<H, S, R> {
56+
/// All webhooks need to set the admissionReviewVersions to `["v1"]`, as this validating webhook
57+
/// only supports that version! A failure to do so will result in a panic.
58+
///
59+
/// Your [`ValidatingWebhookConfiguration`] can contain 0..n webhooks, but it is recommended to
60+
/// only have a single entry in there, as the clientConfig of all entries will be set to the
61+
/// same service, port and HTTP path.
62+
pub fn new(
63+
validating_webhook_configuration: ValidatingWebhookConfiguration,
64+
handler: H,
65+
handler_state: Arc<S>,
66+
disable_validating_webhook_configuration_maintenance: bool,
67+
client: Client,
68+
field_manager: String,
69+
) -> Self {
70+
for webhook in validating_webhook_configuration.webhooks.iter().flatten() {
71+
assert_eq!(
72+
webhook.admission_review_versions,
73+
vec!["v1"],
74+
"We decide how we de-serialize the JSON and with that what AdmissionReview version we support (currently only v1)"
75+
);
76+
}
77+
78+
Self {
79+
validating_webhook_configuration,
80+
handler,
81+
handler_state,
82+
_resource: PhantomData,
83+
disable_validating_webhook_configuration_maintenance,
84+
client,
85+
field_manager,
86+
}
87+
}
88+
89+
fn http_path(&self) -> String {
90+
let validating_webhook_configuration_name =
91+
self.validating_webhook_configuration.name_any();
92+
format!("/validate/{validating_webhook_configuration_name}")
93+
}
94+
}
95+
96+
#[async_trait]
97+
impl<H, S, R, Fut> Webhook for ValidatingWebhook<H, S, R>
98+
where
99+
H: Fn(Arc<S>, AdmissionRequest<R>) -> Fut + Clone + Send + Sync + 'static,
100+
Fut: Future<Output = AdmissionResponse> + Send + 'static,
101+
R: Resource + Send + Sync + DeserializeOwned + Serialize + 'static,
102+
S: Send + Sync + 'static,
103+
{
104+
fn register_routes(&self, router: Router) -> Router {
105+
let handler_state = self.handler_state.clone();
106+
let handler = self.handler.clone();
107+
let handler_fn = |Json(review): Json<AdmissionReview<R>>| async move {
108+
let request: AdmissionRequest<R> = match review.try_into() {
109+
Ok(request) => request,
110+
Err(err) => {
111+
return Json(
112+
AdmissionResponse::invalid(format!("failed to convert to request: {err}"))
113+
.into_review(),
114+
);
115+
}
116+
};
117+
118+
let response = handler(handler_state, request).await;
119+
let review = response.into_review();
120+
Json(review)
121+
};
122+
123+
let route = self.http_path();
124+
router.route(&route, post(handler_fn))
125+
}
126+
127+
#[instrument(skip(self))]
128+
async fn handle_certificate_rotation(
129+
&mut self,
130+
_new_certificate: &Certificate,
131+
new_ca_bundle: &ByteString,
132+
options: &WebhookServerOptions,
133+
) -> Result<(), WebhookError> {
134+
if self.disable_validating_webhook_configuration_maintenance {
135+
return Ok(());
136+
}
137+
138+
let mut validating_webhook_configuration = self.validating_webhook_configuration.clone();
139+
let vwc_name = validating_webhook_configuration.name_any();
140+
tracing::info!(
141+
k8s.validatingwebhookconfiguration.name = vwc_name,
142+
"reconciling validating webhook configurations"
143+
);
144+
145+
for webhook in validating_webhook_configuration
146+
.webhooks
147+
.iter_mut()
148+
.flatten()
149+
{
150+
// We know how we can be called (and with what certificate), so we can always set that
151+
webhook.client_config = WebhookClientConfig {
152+
service: Some(ServiceReference {
153+
name: options.webhook_service_name.to_owned(),
154+
namespace: options.webhook_namespace.to_owned(),
155+
path: Some(self.http_path()),
156+
port: Some(options.socket_addr.port().into()),
157+
}),
158+
// Here, ByteString takes care of encoding the provided content as base64.
159+
ca_bundle: Some(new_ca_bundle.to_owned()),
160+
url: None,
161+
};
162+
}
163+
164+
let vwc_api: Api<ValidatingWebhookConfiguration> = Api::all(self.client.clone());
165+
// Other than with the CRDs we don't need to force-apply the ValidatingWebhookConfiguration
166+
let patch = Patch::Apply(&validating_webhook_configuration);
167+
let patch_params = PatchParams::apply(&self.field_manager);
168+
169+
vwc_api
170+
.patch(&vwc_name, &patch_params, &patch)
171+
.await
172+
.with_context(|_| PatchValidatingWebhookConfigurationSnafu { vwc_name })?;
173+
174+
Ok(())
175+
}
176+
}

0 commit comments

Comments
 (0)