forked from kube-rs/kube
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathadmission_controller.rs
91 lines (83 loc) · 3.47 KB
/
admission_controller.rs
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
use kube::core::{
admission::{AdmissionRequest, AdmissionResponse, AdmissionReview},
DynamicObject, Resource, ResourceExt,
};
use std::{convert::Infallible, error::Error};
use tracing::*;
use warp::{reply, Filter, Reply};
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let routes = warp::path("mutate")
.and(warp::body::json())
.and_then(mutate_handler)
.with(warp::trace::request());
// 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());
warp::serve(warp::post().and(routes))
.tls()
.cert_path("admission-controller-tls.crt")
.key_path("admission-controller-tls.key")
//.run(([0, 0, 0, 0], 8443)) // in-cluster
.run(addr.parse::<std::net::SocketAddr>().unwrap()) // local-dev
.await;
}
// A general /mutate handler, handling errors from the underlying business logic
async fn mutate_handler(body: AdmissionReview<DynamicObject>) -> Result<impl Reply, Infallible> {
// Parse incoming webhook AdmissionRequest first
let req: AdmissionRequest<_> = match body.try_into() {
Ok(req) => req,
Err(err) => {
error!("invalid request: {}", err.to_string());
return Ok(reply::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
res = match mutate(res.clone(), &obj) {
Ok(res) => {
info!("accepted: {:?} on Foo {}", req.operation, name);
res
}
Err(err) => {
warn!("denied: {:?} on {} ({})", req.operation, name, err);
res.deny(err.to_string())
}
};
};
// Wrap the AdmissionResponse wrapped in an AdmissionReview
Ok(reply::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: "/metadata/labels".into(),
value: serde_json::json!({}),
}));
}
// Add our label
patches.push(json_patch::PatchOperation::Add(json_patch::AddOperation {
path: "/metadata/labels/admission".into(),
value: serde_json::Value::String("modified-by-admission-controller".into()),
}));
Ok(res.with_patch(json_patch::Patch(patches))?)
} else {
Ok(res)
}
}