sigs.k8s.io/controller-runtime@v0.18.2/pkg/builder/webhook.go (about) 1 /* 2 Copyright 2019 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 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 // WebhookBuilder builds a Webhook. 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 // WebhookManagedBy returns a new webhook builder. 51 func WebhookManagedBy(m manager.Manager) *WebhookBuilder { 52 return &WebhookBuilder{mgr: m} 53 } 54 55 // TODO(droot): update the GoDoc for conversion. 56 57 // For takes a runtime.Object which should be a CR. 58 // If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type. 59 // If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type. 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 // WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook will be wired for this type. 69 func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter) *WebhookBuilder { 70 blder.customDefaulter = defaulter 71 return blder 72 } 73 74 // WithValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. 75 func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder { 76 blder.customValidator = validator 77 return blder 78 } 79 80 // WithLogConstructor overrides the webhook's LogConstructor. 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 // RecoverPanic indicates whether panics caused by the webhook should be recovered. 87 func (blder *WebhookBuilder) RecoverPanic() *WebhookBuilder { 88 blder.recoverPanic = true 89 return blder 90 } 91 92 // Complete builds the webhook. 93 func (blder *WebhookBuilder) Complete() error { 94 // Set the Config 95 blder.loadRestConfig() 96 97 // Configure the default LogConstructor 98 blder.setLogConstructor() 99 100 // Set the Webhook if needed 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 // Register webhook(s) for type 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 // registerDefaultingWebhook registers a defaulting webhook if necessary. 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 // Checking if the path is already registered. 160 // If so, just skip it. 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 // registerValidatingWebhook registers a validating webhook if necessary. 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 // Checking if the path is already registered. 191 // If so, just skip it. 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 }