一篇带你kubebuilder 进阶: webhook

开发 项目管理
准入控制存在两种 WebHook,变更准入控制 MutatingAdmissionWebhook,和验证准入控制 ValidatingAdmissionWebhook,执行的顺序是先执行 MutatingAdmissionWebhook 再执行 ValidatingAdmissionWebhook。

[[399996]]

在前面的文章当中我们已经完成了 NodePool Operator 的基本功能开发与测试,但是有时候我们会有这种需求,例如创建或者删除资源的时候需要对资源进行一些检查的操作,如果校验不成功就不通过。或者是需要在完成实际的创建之前做一些其他操作,例如我创建一个 pod 之前对 pod 的资源做一些调整等。这些都可以通过准入控制的WebHook来实现。

准入控制存在两种 WebHook,变更准入控制 MutatingAdmissionWebhook,和验证准入控制 ValidatingAdmissionWebhook,执行的顺序是先执行 MutatingAdmissionWebhook 再执行 ValidatingAdmissionWebhook。

创建 webhook

我们通过命令创建相关的脚手架代码和 api

  1. kubebuilder create webhook --group nodes --version v1 --kind NodePool --defaulting --programmatic-validation 

执行之后可以看到多了一些 webhook 相关的文件和配置

  1.   ├── api 
  2.   │   └── v1 
  3.   │       ├── groupversion_info.go 
  4.   │       ├── nodepool_types.go 
  5. + │       ├── nodepool_webhook.go # 在这里实现 webhook 的相关接口 
  6. + │       ├── webhook_suite_test.go # webhook 测试 
  7.   │       └── zz_generated.deepcopy.go 
  8.   ├── bin 
  9.   ├── config 
  10. + │   ├── certmanager # 用于部署 
  11.   │   ├── crd 
  12.   │   │   ├── bases 
  13.   │   │   │   └── nodes.lailin.xyz_nodepools.yaml 
  14.   │   │   ├── kustomization.yaml 
  15.   │   │   ├── kustomizeconfig.yaml 
  16.   │   │   └── patches 
  17.   │   │       ├── cainjection_in_nodepools.yaml 
  18. + │   │       └── webhook_in_nodepools.yaml 
  19.   │   ├── default 
  20.   │   │   ├── kustomization.yaml 
  21.   │   │   ├── manager_auth_proxy_patch.yaml 
  22.   │   │   ├── manager_config_patch.yaml 
  23. + │   │   ├── manager_webhook_patch.yaml 
  24. + │   │   └── webhookcainjection_patch.yaml 
  25.   │   ├── manager 
  26.   │   ├── prometheus 
  27.   │   ├── rbac 
  28.   │   ├── samples 
  29.   │   │   └── nodes_v1_nodepool.yaml 
  30. + │   └── webhook # webhook 部署配置 
  31.   ├── controllers 
  32.   ├── main.go 

实现逻辑

实现 MutatingAdmissionWebhook 接口

这个只需要实现 Default 方法就行

  1. // Default implements webhook.Defaulter so a webhook will be registered for the type 
  2. func (r *NodePool) Default() { 
  3.  nodepoollog.Info("default""name", r.Name
  4.  
  5.  // 如果 labels 为空,我们就给 labels 加一个默认值 
  6.  if len(r.Labels) == 0 { 
  7.   r.Labels["node-pool.lailin.xyz"] = r.Name 
  8.  } 

实现 ValidatingAdmissionWebhook 接口

实现 ValidatingAdmissionWebhook也是一样只需要实现对应的方法就行了,默认是注册了 Create 和 Update 事件的校验,我们这里主要是限制 Labels 和 Taints 的 key 只能是满足正则 ^node-pool.lailin.xyz/*[a-zA-z0-9]*$ 的固定格式

  1. // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 
  2. //+kubebuilder:webhook:path=/validate-nodes-lailin-xyz-v1-nodepool,mutating=false,failurePolicy=fail,sideEffects=None,groups=nodes.lailin.xyz,resources=nodepools,verbs=create;update,versions=v1,name=vnodepool.kb.io,admissionReviewVersions={v1,v1beta1} 
  3.  
  4. var _ webhook.Validator = &NodePool{} 
  5.  
  6. // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 
  7. func (r *NodePool) ValidateCreate() error { 
  8.  nodePoolLog.Info("validate create""name", r.Name
  9.  
  10.  return r.validate() 
  11.  
  12. // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 
  13. func (r *NodePool) ValidateUpdate(old runtime.Object) error { 
  14.  nodePoolLog.Info("validate update""name", r.Name
  15.  
  16.  return r.validate() 
  17.  
  18. // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 
  19. func (r *NodePool) ValidateDelete() error { 
  20.  nodePoolLog.Info("validate delete""name", r.Name
  21.  
  22.  // TODO(user): fill in your validation logic upon object deletion. 
  23.  return nil 
  24.  
  25. // validate 验证 
  26. func (r *NodePool) validate() error { 
  27.  err := errors.Errorf("taint or label key must validatedy by %s", keyReg.String()) 
  28.  
  29.  for k := range r.Spec.Labels { 
  30.   if !keyReg.MatchString(k) { 
  31.    return errors.WithMessagef(err, "label key: %s", k) 
  32.   } 
  33.  } 
  34.  
  35.  for _, taint := range r.Spec.Taints { 
  36.   if !keyReg.MatchString(taint.Key) { 
  37.    return errors.WithMessagef(err, "taint key: %s", taint.Key
  38.   } 
  39.  } 
  40.  
  41.  return nil 

部署

实现了之后直接在 make run 是跑不起来的,因为 webhook 注册的地址不对,我们这里先看一下如何进行部署运行,然后再来看如何对 WebHook 进行本地调试。

WebHook 的运行需要校验证书,kubebuilder 官方建议我们使用 cert-manager 简化对证书的管理,所以我们先部署一下 cert-manager 的服务

  1. kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.3.1/cert-manager.yaml 

然后我们 build 镜像并且将镜像 load 到集群中

  1. make docker-build 
  2.  
  3. kind load docker-image --name kind --nodes kind-worker controller:latest 

然后查看一下 config/default/kustomization.yaml文件,确认 webhook 相关的配置没有被注释掉

  1. # Adds namespace to all resources. 
  2. namespace: node-pool-operator-system 
  3.  
  4. # Value of this field is prepended to the 
  5. # names of all resources, e.g. a deployment named 
  6. "wordpress" becomes "alices-wordpress"
  7. # Note that it should also match with the prefix (text before '-'of the namespace 
  8. # field above. 
  9. namePrefix: node-pool-operator- 
  10.  
  11. # Labels to add to all resources and selectors. 
  12. #commonLabels: 
  13. #  someName: someValue 
  14.  
  15. bases: 
  16. - ../crd 
  17. - ../rbac 
  18. - ../manager 
  19. # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 
  20. # crd/kustomization.yaml 
  21. - ../webhook 
  22. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER''WEBHOOK' components are required. 
  23. - ../certmanager 
  24. # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'
  25. #- ../prometheus 
  26.  
  27. patchesStrategicMerge: 
  28. # Protect the /metrics endpoint by putting it behind auth. 
  29. # If you want your controller-manager to expose the /metrics 
  30. # endpoint w/o any authn/z, please comment the following line. 
  31. - manager_auth_proxy_patch.yaml 
  32.  
  33. # Mount the controller config file for loading manager configurations 
  34. # through a ComponentConfig type 
  35. #- manager_config_patch.yaml 
  36.  
  37. # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 
  38. # crd/kustomization.yaml 
  39. - manager_webhook_patch.yaml 
  40.  
  41. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'
  42. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 
  43. 'CERTMANAGER' needs to be enabled to use ca injection 
  44. - webhookcainjection_patch.yaml 
  45.  
  46. # the following config is for teaching kustomize how to do var substitution 
  47. vars: 
  48. # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 
  49. name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 
  50.   objref: 
  51.     kind: Certificate 
  52.     group: cert-manager.io 
  53.     version: v1 
  54.     name: serving-cert # this name should match the one in certificate.yaml 
  55.   fieldref: 
  56.     fieldpath: metadata.namespace 
  57. name: CERTIFICATE_NAME 
  58.   objref: 
  59.     kind: Certificate 
  60.     group: cert-manager.io 
  61.     version: v1 
  62.     name: serving-cert # this name should match the one in certificate.yaml 
  63. name: SERVICE_NAMESPACE # namespace of the service 
  64.   objref: 
  65.     kind: Service 
  66.     version: v1 
  67.     name: webhook-service 
  68.   fieldref: 
  69.     fieldpath: metadata.namespace 
  70. name: SERVICE_NAME 
  71.   objref: 
  72.     kind: Service 
  73.     version: v1 
  74.     name: webhook-service 

检查一下 manager/manager.yaml 是否存在 imagePullPolicy: IfNotPresent不存在要加上

然后执行部署命令即可

  1. make deploy 
  2.  
  3. # 检查 pod 是否正常启动 
  4. ▶ kubectl -n node-pool-operator-system get pods 
  5. NAME                                                     READY   STATUS              RESTARTS   AGE 
  6. node-pool-operator-controller-manager-66bd747899-lf7xb   0/2     ContainerCreating   0          7s 

使用 yaml 文件测试一下

  1. apiVersion: nodes.lailin.xyz/v1 
  2. kind: NodePool 
  3. metadata: 
  4.   name: worker 
  5. spec: 
  6.   labels: 
  7.     "xxx""10" 
  8.   handler: runc 

提交之后可以发现报错,因为 label key 不满足我们的要求

  1. ▶ kubectl apply -f config/samples/                                           
  2. Error from server (label key: xxx: taint or label key must validatedy by ^node-pool.lailin.xyz/*[a-zA-z0-9]*$): error when applying patch: 
  3. {"metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"nodes.lailin.xyz/v1\",\"kind\":\"NodePool\",\"metadata\":{\"annotations\":{},\"name\":\"worker\"},\"spec\":{\"handler\":\"runc\",\"labels\":{\"xxx\":\"10\"}}}\n"}},"spec":{"labels":{"node-pool.lailin.xyz/worker":null,"xxx":"10"},"taints":null}} 
  4. to
  5. Resource: "nodes.lailin.xyz/v1, Resource=nodepools", GroupVersionKind: "nodes.lailin.xyz/v1, Kind=NodePool" 
  6. Name"worker", Namespace: "" 
  7. for"config/samples/nodes_v1_nodepool.yaml": admission webhook "vnodepool.kb.io" denied the request: label key: xxx: taint or label key must validatedy by ^node-pool.lailin.xyz/*[a-zA-z0-9]*$ 

再用一个正常的 yaml 测试

  1. apiVersion: nodes.lailin.xyz/v1 
  2. kind: NodePool 
  3. metadata: 
  4.   name: worker 
  5. spec: 
  6.   labels: 
  7.     "node-pool.lailin.xyz/xxx""10" 
  8.   handler: runc 

可以正常提交

  1. ▶ kubectl apply -f config/samples/                      
  2. nodepool.nodes.lailin.xyz/worker configured 

本地调试

虽然 kubebuilder 已经为我们做了很多事情将服务部署运行基本傻瓜化了,但是每次做一点点修改就需要重新编译部署还是非常的麻烦,所以我们来看看如何在本地进行联调。

  • PS: 这里会用到之前 4. kustomize 简明教程 讲到的 kustomize 的特性构建开发环境,如果忘记了可以先看看之前的文章哦

我们先看看 config/webhook/manifests.yaml这里面包含了两个准入控制的信息,不过他们的配置类似,我们看一个就行了,这里以 MutatingWebhookConfiguration 为例

  1. apiVersion: admissionregistration.k8s.io/v1 
  2. kind: MutatingWebhookConfiguration 
  3. metadata: 
  4.   creationTimestamp: null 
  5.   name: mutating-webhook-configuration 
  6. webhooks: 
  7. - admissionReviewVersions: 
  8.   - v1 
  9.   - v1beta1 
  10.   clientConfig: 
  11.     service: 
  12.       name: webhook-service 
  13.       namespace: system 
  14.       path: /mutate-nodes-lailin-xyz-v1-nodepool 
  15.   failurePolicy: Fail 
  16.   name: mnodepool.kb.io 
  17.   rules: 
  18.   - apiGroups: 
  19.     - nodes.lailin.xyz 
  20.     apiVersions: 
  21.     - v1 
  22.     operations: 
  23.     - CREATE 
  24.     - UPDATE 
  25.     resources: 
  26.     - nodepools 
  27.   sideEffects: None 

主要是 clientConfig 的配置,如果想要本地联调,我们需要将 clientConfig.service 删掉,替换成

  1. clientConfig: 
  2.   url: https://host.docker.internal:9443/mutate-nodes-lailin-xyz-v1-nodepool 

注意: host.docker.internal是 docker desktop 的默认域名,通过这个可以调用到宿主机上的服务,url path mutate-nodes-lailin-xyz-v1-nodepool需要和 service 中的 path 保持一致

然后再加上 caBundle

  1. clientConfig: 
  2.   caBundle: CA证书 base64 后的字符串 

证书

想要本地联调需要先生成证书,我们使用 openssl 来生成,先创建一个 config/cert 文件夹,我们把证书都放到这里

首先创建一个 csr.conf文件

  1. [ req ] 
  2. default_bits = 2048 
  3. prompt = no 
  4. default_md = sha256 
  5. req_extensions = req_ext 
  6. distinguished_name = dn 
  7.  
  8. [ dn ] 
  9. C = CN 
  10. ST = Guangzhou 
  11. L = Shenzhen 
  12. CN = host.docker.internal 
  13.  
  14. [ req_ext ] 
  15. subjectAltName = @alt_names 
  16.  
  17. [ alt_names ] 
  18. DNS.1 = host.docker.internal # 这里由于我们直接访问的是域名所以用 DNS 
  19.  
  20. [ v3_ext ] 
  21. authorityKeyIdentifier=keyid,issuer:always 
  22. basicConstraints=CA:FALSE 
  23. keyUsage=keyEncipherment,dataEncipherment 
  24. extendedKeyUsage=serverAuth,clientAuth 
  25. subjectAltName=@alt_names 

然后生成 CA 证书并且签发本地证书

  1. # 生成 CA 证书 
  2. openssl genrsa -out ca.key 2048 
  3. openssl req -x509 -new -nodes -key ca.key -subj "/CN=host.docker.internal" -days 10000 -out ca.crt 
  4.  
  5. # 签发本地证书 
  6. openssl genrsa -out tls.key 2048 
  7. openssl req -new -SHA256 -newkey rsa:2048 -nodes -keyout tls.key -out tls.csr -subj "/C=CN/ST=Shanghai/L=Shanghai/O=/OU=/CN=host.docker.internal" 
  8. openssl req -new -key tls.key -out tls.csr -config csr.conf 
  9. openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out tls.crt -days 10000 -extensions v3_ext -extfile csr.conf 

配置变更

我们为了和原本的开发体验保持一致,所以利用 kustomize 的特性新建一个 config/dev 文件夹,包含两个文件修改我们想要的配置

  1. ▶ tree config/dev 
  2. config/dev 
  3. ├── kustomization.yaml 
  4. └── webhook_patch.yaml 

先看一下 kustomization.yaml,从 default 文件夹中继承配置,然后使用 patches 修改一些配置,主要是分别给两种准入控制 WebHook 添加 url 字段,然后使用 webhook_patch.yaml对两个文件做些统一的配置

  1. resources: 
  2. - ../default 
  3.  
  4. patches: 
  5. - patch: | 
  6.     - op: "add" 
  7.       path: "/webhooks/0/clientConfig/url" 
  8.       value: "https://host.docker.internal:9443/mutate-nodes-lailin-xyz-v1-nodepool" 
  9.   target: 
  10.     kind: MutatingWebhookConfiguration 
  11. - patch: | 
  12.     - op: "add" 
  13.       path: "/webhooks/0/clientConfig/url" 
  14.       value: "https://host.docker.internal:9443/validate-nodes-lailin-xyz-v1-nodepool" 
  15.   target: 
  16.     kind: ValidatingWebhookConfiguration 
  17. - path: webhook_patch.yaml 
  18.   target: 
  19.     group: admissionregistration.k8s.io 

webhook_patch.yaml 这个主要是移除 cert-manager.io 的 annotation,本地调试不需要使用它进行证书注入,然后移除掉 service 并且添加 CA 证书

  1. - op: "remove" 
  2.   path: "/metadata/annotations/cert-manager.io~1inject-ca-from" 
  3. - op: "remove" 
  4.   path: "/webhooks/0/clientConfig/service" 
  5. - op: "add" 
  6.   path: "/webhooks/0/clientConfig/caBundle" 
  7.   value: CA 证书 base64 后的值 

CA 证书的值可以通过以下命令获取

  1. cat config/cert/ca.crt | base64 | tr -d '\n' 

然后修改一下 main.go将证书文件夹指定到我们刚刚生成好的文件目录

  1. mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 
  2.   Scheme:                 scheme, 
  3.   MetricsBindAddress:     metricsAddr, 
  4.   Port:                   9443, 
  5.   HealthProbeBindAddress: probeAddr, 
  6.   LeaderElection:         enableLeaderElection, 
  7.   LeaderElectionID:       "97acaccf.lailin.xyz"
  8. +  CertDir:                "config/cert/", // 手动指定证书位置用于测试 
  9. }) 

为了方便调试,在 makefile 中添加

  1. dev: manifests kustomize 
  2.  cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} 
  3.  $(KUSTOMIZE) build config/dev | kubectl apply -f - 

最后执行一下 make dev 然后再执行 make run 就行了

总结

今天完成了准入控制 WebHook 的实现,虽然这个例子可能不太好,如果只需要校验正则,直接配置一下//+kubebuilder:validation:Pattern=string就行了,但是学习了这个之后其实可以做很多事情,例如给 pod 增加 sidecar 根据应用类型的不同注入不同的一些 agent 等等.

本文转载自微信公众号「mohuishou」,可以通过以下二维码关注。转载本文请联系mohuishou公众号。

 

责任编辑:姜华 来源: mohuishou
相关推荐

2021-05-17 05:51:31

KubeBuilderOperator测试

2021-05-12 06:18:19

KubeBuilderOperatork8s

2021-05-16 10:52:58

kubebuilderstatus event

2021-05-08 09:02:48

KubeBuilderOperatork8s

2021-01-01 09:20:20

操作DjangoORM

2021-05-20 06:57:16

RabbitMQ开源消息

2023-04-20 08:00:00

ES搜索引擎MySQL

2021-06-16 08:28:25

unary 方法函数技术

2022-03-10 08:31:51

REST接口规范设计Restful架构

2022-02-24 07:56:42

开发Viteesbuild

2021-10-27 09:59:35

存储

2022-02-21 09:44:45

Git开源分布式

2023-05-12 08:19:12

Netty程序框架

2021-07-28 10:02:54

建造者模式代码

2022-04-08 08:32:40

mobx状态管理库redux

2021-07-14 08:24:23

TCPIP 通信协议

2021-06-30 00:20:12

Hangfire.NET平台

2021-08-11 07:02:21

npm包管理器工具

2021-05-17 09:50:06

Kubebuilde源码CURD

2021-11-08 08:42:44

CentOS Supervisor运维
点赞
收藏

51CTO技术栈公众号