在本地调试基于 controller-runtime 的 webhook
概述
在使用 Kubebuilder 开发 Kubernetes Operator 的时候,通过在本地直接运行 Operator 进行功能调试往往更为方便、效率。
一般情况下开发者仅需要执行 make run
即可在本地运行 Operator。但是当 Operator 启用了 webhook 服务时,就需要对配置内容进行一些调整,才能使 Operator 在本地正常运行。
本文记录了如何在本地调试基于 controller-runtime 实现的启用 webhook 的 Operator。
案例
hub 为 v3
spoke 为 v4
group 为 core.demo.io
crd 为 foo
Kubebuilder 版本为 v3.1.0
Controller-runtime 版本为 v0.9.7
目录结构如下:
1 | . |
webhook 的入口
v3(hub) 不需要编写 v3/foo_conversion.go 的转换逻辑,只需要标注自己为 Hub;v4(spoke) 需要在 v4/foo_conversion.go 中编写 ConvertTo()
和 ConvertFrom()
方法。
webhook 入口可以选择任意版本,如选择 v3(hub),则 v3/foo_webhook.go 内容如下:
1 | package v3 |
webhook 注册的原理
调用
ctrl.NewWebhookManagedBy(mgr).For(r).Complete()
注册 webhookcontroller-runtime/pkg/builder/webhook.go
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// Complete builds the webhook.
func (blder *WebhookBuilder) Complete() error {
// Set the Config
blder.loadRestConfig()
// Set the Webhook if needed
return blder.registerWebhooks()
}
func (blder *WebhookBuilder) registerWebhooks() error {
// Create webhook(s) for each type
var err error
blder.gvk, err = apiutil.GVKForObject(blder.apiType, blder.mgr.GetScheme())
if err != nil {
return err
}
blder.registerDefaultingWebhook() // 注册 mutate webhook
blder.registerValidatingWebhook() // 注册 validate webhook
err = blder.registerConversionWebhook() // 注册 conversion webhook
if err != nil {
return err
}
return nil
}注册 webhook 的服务路径
在 mutate 和 validate webhook 的注册中,controller-runtime 通过以下规则生成 path
1
2
3
4
5
6
7
8
9func generateMutatePath(gvk schema.GroupVersionKind) string {
return "/mutate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
gvk.Version + "-" + strings.ToLower(gvk.Kind)
}
func generateValidatePath(gvk schema.GroupVersionKind) string {
return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
gvk.Version + "-" + strings.ToLower(gvk.Kind)
}在 conversion webhook 的注册中,controller-runtime 通过以下规则生成 path
即 conversion webhook 必须使用
/convert
作为服务路径;同时如果有多个资源需要 conversion,仅需要注册一次即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func (blder *WebhookBuilder) registerConversionWebhook() error {
ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
if err != nil {
log.Error(err, "conversion check failed", "GVK", blder.gvk)
return err
}
if ok {
if !blder.isAlreadyHandled("/convert") {
blder.mgr.GetWebhookServer().Register("/convert", &conversion.Webhook{})
}
log.Info("Conversion webhook enabled", "GVK", blder.gvk)
}
return nil
}注册的具体逻辑
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// Register marks the given webhook as being served at the given path.
// It panics if two hooks are registered on the same path.
func (s *Server) Register(path string, hook http.Handler) {
s.mu.Lock()
defer s.mu.Unlock()
s.defaultingOnce.Do(s.setDefaults)
if _, found := s.webhooks[path]; found {
panic(fmt.Errorf("can't register duplicate path: %v", path))
}
// TODO(directxman12): call setfields if we've already started the server
s.webhooks[path] = hook
s.WebhookMux.Handle(path, metrics.InstrumentedHook(path, hook))
regLog := log.WithValues("path", path)
regLog.Info("Registering webhook")
// we've already been "started", inject dependencies here.
// Otherwise, InjectFunc will do this for us later.
if s.setFields != nil {
if err := s.setFields(hook); err != nil {
// TODO(directxman12): swallowing this error isn't great, but we'd have to
// change the signature to fix that
regLog.Error(err, "unable to inject fields into webhook during registration")
}
baseHookLog := log.WithName("webhooks")
// NB(directxman12): we don't propagate this further by wrapping setFields because it's
// unclear if this is how we want to deal with log propagation. In this specific instance,
// we want to be able to pass a logger to webhooks because they don't know their own path.
if _, err := inject.LoggerInto(baseHookLog.WithValues("webhook", path), hook); err != nil {
regLog.Error(err, "unable to logger into webhook during registration")
}
}
}
启用 webhook
然后在 main.go 中需要匹配正确的 webhook 入口:
1 | if os.Getenv("ENABLE_WEBHOOKS") != "false" { |
使用 url 而非 service 触发 webhook
本地调试时,需要将 Dynamic Admission Control 的地址调整为本地地址(代替原本的 svc),即如:https://<node-ip>:9443
(webhook 默认端口为 9443)
本例中仅使用了 MutatingWebhookConfiguration,需要修改以下部分:
config/webhook/manifests.yaml
在配置了 cert-manager 的情况下,caBundle 由 cert-manager 自动生成
1 |
|
在 Foo 资源 CRD 的 patch 中也需要调整 webhook 的地址:
config/crd/patches/webhook_in_a.yaml
1 | # The following patch enables a conversion webhook for the CRD |
同时需要在 config/webhook/kustomizeconfig.yaml 中注释掉 svc 的配置:
1 | # the following config is for teaching kustomize where to look at when substituting vars. |
配置证书
根据 kubebuilder 推荐的方案,使用 cert-manager 作为 webhook 的证书生成、管理工具,需要为 Certificate 资源增加 ipAddressses 配置:
config/certmanager/certificate.yaml
我们需要将 <node-ip> 加入证书的 SAN 范围
1 | # The following manifests contain a self-signed issuer CR and a certificate CR. |
之后执行 make manifests 生成相关的配置文件,然后执行 make install 完成安装。
此时,需要在本地准备 tls.key 和 tls.crt 文件,用于 apiserver 与 webhook 之间的通信:
可以直接从 secret 资源中找到;生成的 tls.* 文件需要存放在
/tmp/k8s-webhook-server/serving-certs
路径下
1 | kubectl get secret -n <namespace> webhook-server-cert -o=jsonpath='{.data.tls\.crt}' |base64 -d > tls.crt |
启动服务
最后使用 make run 启动服务。
现在就可以在本地调试 Operator 的 webhook 功能了。