forked from kube-rs/kube
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathadmission_controller.rs
More file actions
98 lines (90 loc) · 3.82 KB
/
Copy pathadmission_controller.rs
File metadata and controls
98 lines (90 loc) · 3.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
use axum::{Json, Router, response::IntoResponse, routing::post};
use axum_server::tls_rustls::RustlsConfig;
use json_patch::jsonptr::PointerBuf;
use kube::core::{
DynamicObject, Resource, ResourceExt,
admission::{AdmissionRequest, AdmissionResponse, AdmissionReview},
};
use std::{error::Error, net::SocketAddr};
use tower_http::trace::TraceLayer;
use tracing::*;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let app = Router::new().route("/mutate", post(mutate_handler)).layer(
TraceLayer::new_for_http()
.make_span_with(tower_http::trace::DefaultMakeSpan::new().level(Level::INFO)),
);
// You must generate a certificate for the service / url,
// encode the CA in the MutatingWebhookConfiguration, and terminate TLS here.
// See admission_setup.sh + admission_controller.yaml.tpl for how to do this.
let addr = format!("{}:8443", std::env::var("ADMISSION_PRIVATE_IP").unwrap());
axum_server::bind_rustls(
// SocketAddr::from(([0, 0, 0, 0], 8443)), // in-cluster
addr.parse::<SocketAddr>().unwrap(), // local-dev
RustlsConfig::from_pem_file("admission-controller-tls.crt", "admission-controller-tls.key")
.await
.unwrap(),
)
.serve(app.into_make_service())
.await
.unwrap();
}
// A general /mutate handler, handling errors from the underlying business logic
async fn mutate_handler(
Json(body): Json<AdmissionReview<DynamicObject>>,
) -> Json<AdmissionReview<DynamicObject>> {
// Parse incoming webhook AdmissionRequest first
let req: AdmissionRequest<_> = match body.try_into() {
Ok(req) => req,
Err(err) => {
error!("invalid request: {}", err.to_string());
return Json(AdmissionResponse::invalid(err.to_string()).into_review());
}
};
// Then construct a AdmissionResponse
let mut res = AdmissionResponse::from(&req);
// req.Object always exists for us, but could be None if extending to DELETE events
if let Some(obj) = req.object {
let name = obj.name_any(); // apiserver may not have generated a name yet
let kind = obj.types.clone().unwrap_or_default().kind;
res = match mutate(res.clone(), &obj) {
Ok(res) => {
info!("accepted: {:?} on {kind}/{name}", req.operation);
res
}
Err(err) => {
warn!("denied: {:?} on {kind}/{name} ({})", req.operation, err);
res.deny(err.to_string())
}
};
};
// Wrap the AdmissionResponse wrapped in an AdmissionReview
Json(res.into_review())
}
// The main handler and core business logic, failures here implies rejected applies
fn mutate(res: AdmissionResponse, obj: &DynamicObject) -> Result<AdmissionResponse, Box<dyn Error>> {
// If the resource contains an "illegal" label, we reject it
if obj.labels().contains_key("illegal") {
return Err("Resource contained 'illegal' label".into());
}
// If the resource doesn't contain "admission", we add it to the resource.
if !obj.labels().contains_key("admission") {
let mut patches = Vec::new();
// Ensure labels exist before adding a key to it
if obj.meta().labels.is_none() {
patches.push(json_patch::PatchOperation::Add(json_patch::AddOperation {
path: PointerBuf::from_tokens(["metadata", "labels"]),
value: serde_json::json!({}),
}));
}
// Add our label
patches.push(json_patch::PatchOperation::Add(json_patch::AddOperation {
path: PointerBuf::from_tokens(["metadata", "labels", "admission"]),
value: serde_json::Value::String("modified-by-admission-controller".into()),
}));
Ok(res.with_patch(json_patch::Patch(patches))?)
} else {
Ok(res)
}
}