1
16
17 package builder
18
19 import (
20 "errors"
21 "net/http"
22 "net/url"
23 "strings"
24
25 "github.com/go-logr/logr"
26 "k8s.io/apimachinery/pkg/runtime"
27 "k8s.io/apimachinery/pkg/runtime/schema"
28 "k8s.io/client-go/rest"
29 "k8s.io/klog/v2"
30
31 "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
32 "sigs.k8s.io/controller-runtime/pkg/manager"
33 "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
34 "sigs.k8s.io/controller-runtime/pkg/webhook/conversion"
35 )
36
37
38 type WebhookBuilder struct {
39 apiType runtime.Object
40 customDefaulter admission.CustomDefaulter
41 customValidator admission.CustomValidator
42 gvk schema.GroupVersionKind
43 mgr manager.Manager
44 config *rest.Config
45 recoverPanic bool
46 logConstructor func(base logr.Logger, req *admission.Request) logr.Logger
47 err error
48 }
49
50
51 func WebhookManagedBy(m manager.Manager) *WebhookBuilder {
52 return &WebhookBuilder{mgr: m}
53 }
54
55
56
57
58
59
60 func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
61 if blder.apiType != nil {
62 blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration")
63 }
64 blder.apiType = apiType
65 return blder
66 }
67
68
69 func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter) *WebhookBuilder {
70 blder.customDefaulter = defaulter
71 return blder
72 }
73
74
75 func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder {
76 blder.customValidator = validator
77 return blder
78 }
79
80
81 func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder {
82 blder.logConstructor = logConstructor
83 return blder
84 }
85
86
87 func (blder *WebhookBuilder) RecoverPanic() *WebhookBuilder {
88 blder.recoverPanic = true
89 return blder
90 }
91
92
93 func (blder *WebhookBuilder) Complete() error {
94
95 blder.loadRestConfig()
96
97
98 blder.setLogConstructor()
99
100
101 return blder.registerWebhooks()
102 }
103
104 func (blder *WebhookBuilder) loadRestConfig() {
105 if blder.config == nil {
106 blder.config = blder.mgr.GetConfig()
107 }
108 }
109
110 func (blder *WebhookBuilder) setLogConstructor() {
111 if blder.logConstructor == nil {
112 blder.logConstructor = func(base logr.Logger, req *admission.Request) logr.Logger {
113 log := base.WithValues(
114 "webhookGroup", blder.gvk.Group,
115 "webhookKind", blder.gvk.Kind,
116 )
117 if req != nil {
118 return log.WithValues(
119 blder.gvk.Kind, klog.KRef(req.Namespace, req.Name),
120 "namespace", req.Namespace, "name", req.Name,
121 "resource", req.Resource, "user", req.UserInfo.Username,
122 "requestID", req.UID,
123 )
124 }
125 return log
126 }
127 }
128 }
129
130 func (blder *WebhookBuilder) registerWebhooks() error {
131 typ, err := blder.getType()
132 if err != nil {
133 return err
134 }
135
136 blder.gvk, err = apiutil.GVKForObject(typ, blder.mgr.GetScheme())
137 if err != nil {
138 return err
139 }
140
141
142 blder.registerDefaultingWebhook()
143 blder.registerValidatingWebhook()
144
145 err = blder.registerConversionWebhook()
146 if err != nil {
147 return err
148 }
149 return blder.err
150 }
151
152
153 func (blder *WebhookBuilder) registerDefaultingWebhook() {
154 mwh := blder.getDefaultingWebhook()
155 if mwh != nil {
156 mwh.LogConstructor = blder.logConstructor
157 path := generateMutatePath(blder.gvk)
158
159
160
161 if !blder.isAlreadyHandled(path) {
162 log.Info("Registering a mutating webhook",
163 "GVK", blder.gvk,
164 "path", path)
165 blder.mgr.GetWebhookServer().Register(path, mwh)
166 }
167 }
168 }
169
170 func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
171 if defaulter := blder.customDefaulter; defaulter != nil {
172 return admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter).WithRecoverPanic(blder.recoverPanic)
173 }
174 if defaulter, ok := blder.apiType.(admission.Defaulter); ok {
175 return admission.DefaultingWebhookFor(blder.mgr.GetScheme(), defaulter).WithRecoverPanic(blder.recoverPanic)
176 }
177 log.Info(
178 "skip registering a mutating webhook, object does not implement admission.Defaulter or WithDefaulter wasn't called",
179 "GVK", blder.gvk)
180 return nil
181 }
182
183
184 func (blder *WebhookBuilder) registerValidatingWebhook() {
185 vwh := blder.getValidatingWebhook()
186 if vwh != nil {
187 vwh.LogConstructor = blder.logConstructor
188 path := generateValidatePath(blder.gvk)
189
190
191
192 if !blder.isAlreadyHandled(path) {
193 log.Info("Registering a validating webhook",
194 "GVK", blder.gvk,
195 "path", path)
196 blder.mgr.GetWebhookServer().Register(path, vwh)
197 }
198 }
199 }
200
201 func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
202 if validator := blder.customValidator; validator != nil {
203 return admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator).WithRecoverPanic(blder.recoverPanic)
204 }
205 if validator, ok := blder.apiType.(admission.Validator); ok {
206 return admission.ValidatingWebhookFor(blder.mgr.GetScheme(), validator).WithRecoverPanic(blder.recoverPanic)
207 }
208 log.Info(
209 "skip registering a validating webhook, object does not implement admission.Validator or WithValidator wasn't called",
210 "GVK", blder.gvk)
211 return nil
212 }
213
214 func (blder *WebhookBuilder) registerConversionWebhook() error {
215 ok, err := conversion.IsConvertible(blder.mgr.GetScheme(), blder.apiType)
216 if err != nil {
217 log.Error(err, "conversion check failed", "GVK", blder.gvk)
218 return err
219 }
220 if ok {
221 if !blder.isAlreadyHandled("/convert") {
222 blder.mgr.GetWebhookServer().Register("/convert", conversion.NewWebhookHandler(blder.mgr.GetScheme()))
223 }
224 log.Info("Conversion webhook enabled", "GVK", blder.gvk)
225 }
226
227 return nil
228 }
229
230 func (blder *WebhookBuilder) getType() (runtime.Object, error) {
231 if blder.apiType != nil {
232 return blder.apiType, nil
233 }
234 return nil, errors.New("For() must be called with a valid object")
235 }
236
237 func (blder *WebhookBuilder) isAlreadyHandled(path string) bool {
238 if blder.mgr.GetWebhookServer().WebhookMux() == nil {
239 return false
240 }
241 h, p := blder.mgr.GetWebhookServer().WebhookMux().Handler(&http.Request{URL: &url.URL{Path: path}})
242 if p == path && h != nil {
243 return true
244 }
245 return false
246 }
247
248 func generateMutatePath(gvk schema.GroupVersionKind) string {
249 return "/mutate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
250 gvk.Version + "-" + strings.ToLower(gvk.Kind)
251 }
252
253 func generateValidatePath(gvk schema.GroupVersionKind) string {
254 return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
255 gvk.Version + "-" + strings.ToLower(gvk.Kind)
256 }
257
View as plain text