sigs.k8s.io/controller-tools@v0.15.1-0.20240515195456-85686cb69316/pkg/webhook/parser.go (about) 1 /* 2 Copyright 2018 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 webhook contains libraries for generating webhookconfig manifests 18 // from markers in Go source files. 19 // 20 // The markers take the form: 21 // 22 // +kubebuilder:webhook:webhookVersions=<[]string>,failurePolicy=<string>,matchPolicy=<string>,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=<string>,path=<string>,mutating=<bool>,sideEffects=<string>,timeoutSeconds=<int>,admissionReviewVersions=<[]string>,reinvocationPolicy=<string> 23 package webhook 24 25 import ( 26 "fmt" 27 "sort" 28 "strings" 29 30 admissionregv1 "k8s.io/api/admissionregistration/v1" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/util/sets" 33 34 "sigs.k8s.io/controller-tools/pkg/genall" 35 "sigs.k8s.io/controller-tools/pkg/markers" 36 ) 37 38 // The default {Mutating,Validating}WebhookConfiguration version to generate. 39 const ( 40 v1 = "v1" 41 defaultWebhookVersion = v1 42 ) 43 44 var ( 45 // ConfigDefinition s a marker for defining Webhook manifests. 46 // Call ToWebhook on the value to get a Kubernetes Webhook. 47 ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{})) 48 ) 49 50 // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration. 51 func supportedWebhookVersions() []string { 52 return []string{defaultWebhookVersion} 53 } 54 55 // +controllertools:marker:generateHelp:category=Webhook 56 57 // Config specifies how a webhook should be served. 58 // 59 // It specifies only the details that are intrinsic to the application serving 60 // it (e.g. the resources it can handle, or the path it serves on). 61 type Config struct { 62 // Mutating marks this as a mutating webhook (it's validating only if false) 63 // 64 // Mutating webhooks are allowed to change the object in their response, 65 // and are called *before* all validating webhooks. Mutating webhooks may 66 // choose to reject an object, similarly to a validating webhook. 67 Mutating bool 68 // FailurePolicy specifies what should happen if the API server cannot reach the webhook. 69 // 70 // It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject 71 // the object in question). 72 FailurePolicy string 73 // MatchPolicy defines how the "rules" list is used to match incoming requests. 74 // Allowed values are "Exact" (match only if it exactly matches the specified rule) 75 // or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version). 76 MatchPolicy string `marker:",optional"` 77 // SideEffects specify whether calling the webhook will have side effects. 78 // This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then 79 // the API server will not call the webhook on a dry-run request and fails instead. 80 // If the value is "None", then the webhook has no side effects and the API server will call it on dry-run. 81 // If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the 82 // AdmissionReview sent in the request, and avoiding side effects if that value is "true." 83 SideEffects string `marker:",optional"` 84 // TimeoutSeconds allows configuring how long the API server should wait for a webhook to respond before treating the call as a failure. 85 // If the timeout expires before the webhook responds, the webhook call will be ignored or the API call will be rejected based on the failure policy. 86 // The timeout value must be between 1 and 30 seconds. 87 // The timeout for an admission webhook defaults to 10 seconds. 88 TimeoutSeconds int `marker:",optional"` 89 90 // Groups specifies the API groups that this webhook receives requests for. 91 Groups []string 92 // Resources specifies the API resources that this webhook receives requests for. 93 Resources []string 94 // Verbs specifies the Kubernetes API verbs that this webhook receives requests for. 95 // 96 // Only modification-like verbs may be specified. 97 // May be "create", "update", "delete", "connect", or "*" (for all). 98 Verbs []string 99 // Versions specifies the API versions that this webhook receives requests for. 100 Versions []string 101 102 // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots 103 Name string 104 105 // Path specifies that path that the API server should connect to this webhook on. Must be 106 // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by 107 // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group 108 // are substituted for hyphens. For example, a validating webhook path for type 109 // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be 110 // /validate-batch-tutorial-kubebuilder-io-v1-cronjob 111 Path string `marker:"path,optional"` 112 113 // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects 114 // itself to generate. The only supported value is v1. Defaults to v1. 115 WebhookVersions []string `marker:"webhookVersions,optional"` 116 117 // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` 118 // versions the Webhook expects. 119 AdmissionReviewVersions []string `marker:"admissionReviewVersions"` 120 121 // ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations 122 // 123 // To allow mutating admission plugins to observe changes made by other plugins, 124 // built-in mutating admission plugins are re-run if a mutating webhook modifies 125 // an object, and mutating webhooks can specify a reinvocationPolicy to control 126 // whether they are reinvoked as well. 127 ReinvocationPolicy string `marker:"reinvocationPolicy,optional"` 128 129 // URL allows mutating webhooks configuration to specify an external URL when generating 130 // the manifests, instead of using the internal service communication. Should be in format of 131 // https://address:port/path 132 // When this option is specified, the serviceConfig.Service is removed from webhook the manifest. 133 // The URL configuration should be between quotes. 134 // `url` cannot be specified when `path` is specified. 135 URL string `marker:"url,optional"` 136 } 137 138 // verbToAPIVariant converts a marker's verb to the proper value for the API. 139 // Unrecognized verbs are passed through. 140 func verbToAPIVariant(verbRaw string) admissionregv1.OperationType { 141 switch strings.ToLower(verbRaw) { 142 case strings.ToLower(string(admissionregv1.Create)): 143 return admissionregv1.Create 144 case strings.ToLower(string(admissionregv1.Update)): 145 return admissionregv1.Update 146 case strings.ToLower(string(admissionregv1.Delete)): 147 return admissionregv1.Delete 148 case strings.ToLower(string(admissionregv1.Connect)): 149 return admissionregv1.Connect 150 case strings.ToLower(string(admissionregv1.OperationAll)): 151 return admissionregv1.OperationAll 152 default: 153 return admissionregv1.OperationType(verbRaw) 154 } 155 } 156 157 // ToMutatingWebhook converts this rule to its Kubernetes API form. 158 func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { 159 if !c.Mutating { 160 return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name) 161 } 162 163 matchPolicy, err := c.matchPolicy() 164 if err != nil { 165 return admissionregv1.MutatingWebhook{}, err 166 } 167 168 clientConfig, err := c.clientConfig() 169 if err != nil { 170 return admissionregv1.MutatingWebhook{}, err 171 } 172 173 return admissionregv1.MutatingWebhook{ 174 Name: c.Name, 175 Rules: c.rules(), 176 FailurePolicy: c.failurePolicy(), 177 MatchPolicy: matchPolicy, 178 ClientConfig: clientConfig, 179 SideEffects: c.sideEffects(), 180 TimeoutSeconds: c.timeoutSeconds(), 181 AdmissionReviewVersions: c.AdmissionReviewVersions, 182 ReinvocationPolicy: c.reinvocationPolicy(), 183 }, nil 184 } 185 186 // ToValidatingWebhook converts this rule to its Kubernetes API form. 187 func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) { 188 if c.Mutating { 189 return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name) 190 } 191 192 matchPolicy, err := c.matchPolicy() 193 if err != nil { 194 return admissionregv1.ValidatingWebhook{}, err 195 } 196 197 clientConfig, err := c.clientConfig() 198 if err != nil { 199 return admissionregv1.ValidatingWebhook{}, err 200 } 201 202 return admissionregv1.ValidatingWebhook{ 203 Name: c.Name, 204 Rules: c.rules(), 205 FailurePolicy: c.failurePolicy(), 206 MatchPolicy: matchPolicy, 207 ClientConfig: clientConfig, 208 SideEffects: c.sideEffects(), 209 TimeoutSeconds: c.timeoutSeconds(), 210 AdmissionReviewVersions: c.AdmissionReviewVersions, 211 }, nil 212 } 213 214 // rules returns the configuration of what operations on what 215 // resources/subresources a webhook should care about. 216 func (c Config) rules() []admissionregv1.RuleWithOperations { 217 whConfig := admissionregv1.RuleWithOperations{ 218 Rule: admissionregv1.Rule{ 219 APIGroups: c.Groups, 220 APIVersions: c.Versions, 221 Resources: c.Resources, 222 }, 223 Operations: make([]admissionregv1.OperationType, len(c.Verbs)), 224 } 225 226 for i, verbRaw := range c.Verbs { 227 whConfig.Operations[i] = verbToAPIVariant(verbRaw) 228 } 229 230 // fix the group names, since letting people type "core" is nice 231 for i, group := range whConfig.APIGroups { 232 if group == "core" { 233 whConfig.APIGroups[i] = "" 234 } 235 } 236 237 return []admissionregv1.RuleWithOperations{whConfig} 238 } 239 240 // failurePolicy converts the string value to the proper value for the API. 241 // Unrecognized values are passed through. 242 func (c Config) failurePolicy() *admissionregv1.FailurePolicyType { 243 var failurePolicy admissionregv1.FailurePolicyType 244 switch strings.ToLower(c.FailurePolicy) { 245 case strings.ToLower(string(admissionregv1.Ignore)): 246 failurePolicy = admissionregv1.Ignore 247 case strings.ToLower(string(admissionregv1.Fail)): 248 failurePolicy = admissionregv1.Fail 249 default: 250 failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy) 251 } 252 return &failurePolicy 253 } 254 255 // matchPolicy converts the string value to the proper value for the API. 256 func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) { 257 var matchPolicy admissionregv1.MatchPolicyType 258 switch strings.ToLower(c.MatchPolicy) { 259 case strings.ToLower(string(admissionregv1.Exact)): 260 matchPolicy = admissionregv1.Exact 261 case strings.ToLower(string(admissionregv1.Equivalent)): 262 matchPolicy = admissionregv1.Equivalent 263 case "": 264 return nil, nil 265 default: 266 return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy) 267 } 268 return &matchPolicy, nil 269 } 270 271 // clientConfig returns the client config for a webhook. 272 func (c Config) clientConfig() (admissionregv1.WebhookClientConfig, error) { 273 if (c.Path != "" && c.URL != "") || (c.Path == "" && c.URL == "") { 274 return admissionregv1.WebhookClientConfig{}, fmt.Errorf("`url` or `path` markers are required and mutually exclusive") 275 } 276 277 path := c.Path 278 if path != "" { 279 return admissionregv1.WebhookClientConfig{ 280 Service: &admissionregv1.ServiceReference{ 281 Name: "webhook-service", 282 Namespace: "system", 283 Path: &path, 284 }, 285 }, nil 286 } 287 288 url := c.URL 289 return admissionregv1.WebhookClientConfig{ 290 URL: &url, 291 }, nil 292 } 293 294 // sideEffects returns the sideEffects config for a webhook. 295 func (c Config) sideEffects() *admissionregv1.SideEffectClass { 296 var sideEffects admissionregv1.SideEffectClass 297 switch strings.ToLower(c.SideEffects) { 298 case strings.ToLower(string(admissionregv1.SideEffectClassNone)): 299 sideEffects = admissionregv1.SideEffectClassNone 300 case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)): 301 sideEffects = admissionregv1.SideEffectClassNoneOnDryRun 302 case strings.ToLower(string(admissionregv1.SideEffectClassSome)): 303 sideEffects = admissionregv1.SideEffectClassSome 304 case "": 305 return nil 306 default: 307 return nil 308 } 309 return &sideEffects 310 } 311 312 // timeoutSeconds returns the timeoutSeconds config for a webhook. 313 func (c Config) timeoutSeconds() *int32 { 314 if c.TimeoutSeconds != 0 { 315 timeoutSeconds := int32(c.TimeoutSeconds) 316 return &timeoutSeconds 317 } 318 return nil 319 } 320 321 // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook. 322 func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType { 323 var reinvocationPolicy admissionregv1.ReinvocationPolicyType 324 switch strings.ToLower(c.ReinvocationPolicy) { 325 case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)): 326 reinvocationPolicy = admissionregv1.NeverReinvocationPolicy 327 case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)): 328 reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy 329 default: 330 return nil 331 } 332 return &reinvocationPolicy 333 } 334 335 // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. 336 func (c Config) webhookVersions() ([]string, error) { 337 // If WebhookVersions is not specified, we default it to `v1`. 338 if len(c.WebhookVersions) == 0 { 339 return []string{defaultWebhookVersion}, nil 340 } 341 supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...) 342 for _, version := range c.WebhookVersions { 343 if !supportedWebhookVersions.Has(version) { 344 return nil, fmt.Errorf("unsupported webhook version: %s", version) 345 } 346 } 347 return sets.NewString(c.WebhookVersions...).UnsortedList(), nil 348 } 349 350 // +controllertools:marker:generateHelp 351 352 // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. 353 type Generator struct { 354 // HeaderFile specifies the header text (e.g. license) to prepend to generated files. 355 HeaderFile string `marker:",optional"` 356 357 // Year specifies the year to substitute for " YEAR" in the header file. 358 Year string `marker:",optional"` 359 } 360 361 func (Generator) RegisterMarkers(into *markers.Registry) error { 362 if err := into.Register(ConfigDefinition); err != nil { 363 return err 364 } 365 into.AddHelp(ConfigDefinition, Config{}.Help()) 366 return nil 367 } 368 369 func (g Generator) Generate(ctx *genall.GenerationContext) error { 370 supportedWebhookVersions := supportedWebhookVersions() 371 mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions)) 372 validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions)) 373 for _, root := range ctx.Roots { 374 markerSet, err := markers.PackageMarkers(ctx.Collector, root) 375 if err != nil { 376 root.AddError(err) 377 } 378 379 cfgs := markerSet[ConfigDefinition.Name] 380 sort.SliceStable(cfgs, func(i, j int) bool { 381 return cfgs[i].(Config).Name < cfgs[j].(Config).Name 382 }) 383 384 for _, cfg := range cfgs { 385 cfg := cfg.(Config) 386 webhookVersions, err := cfg.webhookVersions() 387 if err != nil { 388 return err 389 } 390 if cfg.Mutating { 391 w, err := cfg.ToMutatingWebhook() 392 if err != nil { 393 return err 394 } 395 for _, webhookVersion := range webhookVersions { 396 mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w) 397 } 398 } else { 399 w, err := cfg.ToValidatingWebhook() 400 if err != nil { 401 return err 402 } 403 for _, webhookVersion := range webhookVersions { 404 validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w) 405 } 406 } 407 } 408 } 409 410 versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions)) 411 for _, version := range supportedWebhookVersions { 412 if cfgs, ok := mutatingCfgs[version]; ok { 413 // The only possible version in supportedWebhookVersions is v1, 414 // so use it for all versioned types in this context. 415 objRaw := &admissionregv1.MutatingWebhookConfiguration{} 416 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 417 Group: admissionregv1.SchemeGroupVersion.Group, 418 Version: version, 419 Kind: "MutatingWebhookConfiguration", 420 }) 421 objRaw.SetName("mutating-webhook-configuration") 422 objRaw.Webhooks = cfgs 423 for i := range objRaw.Webhooks { 424 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 425 // return an error 426 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 427 return err 428 } 429 // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise, 430 // return an error 431 if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil { 432 return err 433 } 434 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 435 // return an error 436 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 437 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 438 } 439 } 440 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 441 } 442 443 if cfgs, ok := validatingCfgs[version]; ok { 444 // The only possible version in supportedWebhookVersions is v1, 445 // so use it for all versioned types in this context. 446 objRaw := &admissionregv1.ValidatingWebhookConfiguration{} 447 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 448 Group: admissionregv1.SchemeGroupVersion.Group, 449 Version: version, 450 Kind: "ValidatingWebhookConfiguration", 451 }) 452 objRaw.SetName("validating-webhook-configuration") 453 objRaw.Webhooks = cfgs 454 for i := range objRaw.Webhooks { 455 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 456 // return an error 457 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 458 return err 459 } 460 // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise, 461 // return an error 462 if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil { 463 return err 464 } 465 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 466 // return an error 467 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 468 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 469 } 470 } 471 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 472 } 473 } 474 475 var headerText string 476 if g.HeaderFile != "" { 477 headerBytes, err := ctx.ReadFile(g.HeaderFile) 478 if err != nil { 479 return err 480 } 481 headerText = string(headerBytes) 482 } 483 headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) 484 485 for k, v := range versionedWebhooks { 486 var fileName string 487 if k == defaultWebhookVersion { 488 fileName = "manifests.yaml" 489 } else { 490 fileName = fmt.Sprintf("manifests.%s.yaml", k) 491 } 492 if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil { 493 return err 494 } 495 } 496 return nil 497 } 498 499 func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error { 500 if sideEffects == nil { 501 return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration") 502 } 503 if *sideEffects == admissionregv1.SideEffectClassUnknown || 504 *sideEffects == admissionregv1.SideEffectClassSome { 505 return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration") 506 } 507 return nil 508 } 509 510 func checkTimeoutSeconds(timeoutSeconds *int32) error { 511 if timeoutSeconds != nil && (*timeoutSeconds < 1 || *timeoutSeconds > 30) { 512 return fmt.Errorf("TimeoutSeconds must be between 1 and 30 seconds") 513 } 514 return nil 515 }