什么是webhook

Kubernetes 通过rbac进行权限控制,实现了哪些account对哪些资源具有哪些权限的控制,但它并不是万能的, 因为rbac控制的操作权限类型是有限的,需要再进行一些细化的权限管控就无从下手了,比如需要限制一些controller只能从制定的harbor进行image的下载,比如需要限制一些controller只能使用指定范围的端口号等,所幸Kubernetes在各个方面都可以进行一些自定义的开发,而webhook就是用来实现类似需求的。

webhook官网介绍

先看下官网的说明:
Kubernetes开发(4)-webhook 实现拦截请求_Go

官网写的很清楚,webhook本质上就是一个拦截器+回调器,它在拦截了用户的请求之后对通过以下2类webhook对请求做处理,然后再回调api-server。

  • MutatingWebhookConfiguration: 修改用户请求的配置
  • ValidatingWebhookConfiguration: 验证 用户请求的配置是否合法

这2类webhook的调用顺序可以参考下图:
Kubernetes开发(4)-webhook 实现拦截请求_k8s_02

前置检查:

  • 先要看下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资源配置的处理过程了。

Kubernetes开发(4)-webhook 实现拦截请求_k8s_03