什么是webhook
Kubernetes 通过rbac进行权限控制,实现了哪些account对哪些资源具有哪些权限的控制,但它并不是万能的, 因为rbac控制的操作权限类型是有限的,需要再进行一些细化的权限管控就无从下手了,比如需要限制一些controller只能从制定的harbor进行image的下载,比如需要限制一些controller只能使用指定范围的端口号等,所幸Kubernetes在各个方面都可以进行一些自定义的开发,而webhook就是用来实现类似需求的。
webhook官网介绍
先看下官网的说明:
官网写的很清楚,webhook本质上就是一个拦截器+回调器,它在拦截了用户的请求之后对通过以下2类webhook对请求做处理,然后再回调api-server。
- MutatingWebhookConfiguration: 修改用户请求的配置
- ValidatingWebhookConfiguration: 验证 用户请求的配置是否合法
这2类webhook的调用顺序可以参考下图:
前置检查:
- 先要看下api-server的启动参数里有没有开启准入配置
–enable-admission-plugins=NodeRestriction,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,由于开发环境是mac 上,用docker-desktop部署的,所以可以通过pod配置进行查看。
kubectl get pods kube-apiserver-docker-desktop -n kube-system -o yaml | grep enable-admiss
AdmissionReview
webhook本质上是一个http 服务,由api-server通过一个叫做AdmissionReview的对象来发送&接受请求的。
一个官网的AdmissionReview示例(admissionregistration.k8s.io/v1):
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "request": { # Random uid uniquely identifying this admission call "uid": "705ab4f5-6393-11e8-b7cc-42010a800002", # Fully-qualified group/version/kind of the incoming object "kind": {"group":"autoscaling","version":"v1","kind":"Scale"}, # Fully-qualified group/version/kind of the resource being modified "resource": {"group":"apps","version":"v1","resource":"deployments"}, # subresource, if the request is to a subresource "subResource": "scale", # Fully-qualified group/version/kind of the incoming object in the original request to the API server. # This only differs from `kind` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestKind": {"group":"autoscaling","version":"v1","kind":"Scale"}, # Fully-qualified group/version/kind of the resource being modified in the original request to the API server. # This only differs from `resource` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestResource": {"group":"apps","version":"v1","resource":"deployments"}, # subresource, if the request is to a subresource # This only differs from `subResource` if the webhook specified `matchPolicy: Equivalent` and the # original request to the API server was converted to a version the webhook registered for. "requestSubResource": "scale", # Name of the resource being modified "name": "my-deployment", # Namespace of the resource being modified, if the resource is namespaced (or is a Namespace object) "namespace": "my-namespace", # operation can be CREATE, UPDATE, DELETE, or CONNECT "operation": "UPDATE", "userInfo": { # Username of the authenticated user making the request to the API server "username": "admin", # UID of the authenticated user making the request to the API server "uid": "014fbff9a07c", # Group memberships of the authenticated user making the request to the API server "groups": ["system:authenticated","my-admin-group"], # Arbitrary extra info associated with the user making the request to the API server. # This is populated by the API server authentication layer and should be included # if any SubjectAccessReview checks are performed by the webhook. "extra": { "some-key":["some-value1", "some-value2"] } }, # object is the new object being admitted. # It is null for DELETE operations. "object": {"apiVersion":"autoscaling/v1","kind":"Scale",...}, # oldObject is the existing object. # It is null for CREATE and CONNECT operations. "oldObject": {"apiVersion":"autoscaling/v1","kind":"Scale",...}, # options contains the options for the operation being admitted, like meta.k8s.io/v1 CreateOptions, UpdateOptions, or DeleteOptions. # It is null for CONNECT operations. "options": {"apiVersion":"meta.k8s.io/v1","kind":"UpdateOptions",...}, # dryRun indicates the API request is running in dry run mode and will not be persisted. # Webhooks with side effects should avoid actuating those side effects when dryRun is true. # See http://k8s.io/docs/reference/using-api/api-concepts/#make-a-dry-run-request for more details. "dryRun": false } }
可以看到在结构体内的request里包含了一个完整的资源配置,而webhook 对这些资源配置进行各种的资源修改/准入的操作后,再回调api-server,同时在AdmissionReview结构体内加上一个response的结构体,类似如下:
{ "apiVersion": "admission.k8s.io/v1", "kind": "AdmissionReview", "response": { "uid": "<value from request.uid>", "allowed": false } }
其中response里的allowed 里的布尔值,就是webhook判断该资源是否准入的结果。
所以一个webhook的实现流程可以初步认为是api-server --(admissionreview) – webhook --(admissionreview) – api-server。
admissionreview结构体完整代码
webhook的代码逻辑
前面有提到webhook本质上是一个web server,详细点说的话就是一个带tls的web server,官网有对应的例子: webhook server,根据官网的例子可以分析一下:
func main(cmd *cobra.Command, args []string) { config := Config{ CertFile: certFile, KeyFile: keyFile, } http.HandleFunc("/always-allow-delay-5s", serveAlwaysAllowDelayFiveSeconds) http.HandleFunc("/always-deny", serveAlwaysDeny) http.HandleFunc("/add-label", serveAddLabel) http.HandleFunc("/pods", servePods) http.HandleFunc("/pods/attach", serveAttachingPods) http.HandleFunc("/mutating-pods", serveMutatePods) http.HandleFunc("/mutating-pods-sidecar", serveMutatePodsSidecar) http.HandleFunc("/configmaps", serveConfigmaps) http.HandleFunc("/mutating-configmaps", serveMutateConfigmaps) http.HandleFunc("/custom-resource", serveCustomResource) http.HandleFunc("/mutating-custom-resource", serveMutateCustomResource) http.HandleFunc("/crd", serveCRD) http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) }) server := &http.Server{ Addr: fmt.Sprintf(":%d", port), TLSConfig: configTLS(config), } err := server.ListenAndServeTLS("", "") if err != nil { panic(err) } }
上面主函数可以看出就是个简单的 http server , 其中有很多api的 url,拿**http.HandleFunc("/mutating-pods", serveMutatePods)**举例好了
func serveMutatePods(w http.ResponseWriter, r *http.Request) { serve(w, r, newDelegateToV1AdmitHandler(mutatePods)) } func serve(w http.ResponseWriter, r *http.Request, admit admitHandler) { var body []byte if r.Body != nil { if data, err := ioutil.ReadAll(r.Body); err == nil { body = data } } // verify the content type is accurate contentType := r.Header.Get("Content-Type") if contentType != "application/json" { klog.Errorf("contentType=%s, expect application/json", contentType) return } klog.V(2).Info(fmt.Sprintf("handling request: %s", body)) deserializer := codecs.UniversalDeserializer() obj, gvk, err := deserializer.Decode(body, nil, nil) if err != nil { msg := fmt.Sprintf("Request could not be decoded: %v", err) klog.Error(msg) http.Error(w, msg, http.StatusBadRequest) return } var responseObj runtime.Object switch *gvk { case v1beta1.SchemeGroupVersion.WithKind("AdmissionReview"): requestedAdmissionReview, ok := obj.(*v1beta1.AdmissionReview) if !ok { klog.Errorf("Expected v1beta1.AdmissionReview but got: %T", obj) return } responseAdmissionReview := &v1beta1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(*gvk) responseAdmissionReview.Response = admit.v1beta1(*requestedAdmissionReview) responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID responseObj = responseAdmissionReview case v1.SchemeGroupVersion.WithKind("AdmissionReview"): requestedAdmissionReview, ok := obj.(*v1.AdmissionReview) if !ok { klog.Errorf("Expected v1.AdmissionReview but got: %T", obj) return } responseAdmissionReview := &v1.AdmissionReview{} responseAdmissionReview.SetGroupVersionKind(*gvk) responseAdmissionReview.Response = admit.v1(*requestedAdmissionReview) responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID responseObj = responseAdmissionReview default: msg := fmt.Sprintf("Unsupported group version kind: %v", gvk) klog.Error(msg) http.Error(w, msg, http.StatusBadRequest) return } klog.V(2).Info(fmt.Sprintf("sending response: %v", responseObj)) respBytes, err := json.Marshal(responseObj) if err != nil { klog.Error(err) http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(respBytes); err != nil { klog.Error(err) } }
这里的代码主要只是举例,所以serve方法写的比较简单,基本没做啥判断,也没有对资源配置做修改,但从基本的框架上看到其实一个简单的webhook是不难的,大致的代码逻辑就是写一个tls 的web server,然后渲染路由,路由的作用是后面注册到ValidatingWebhookConfiguration用的,不同路由有不同的准入规则,然后路由对应的处理函数就是实际对admissionreview资源配置的处理过程了。