github.com/TheSpiritXIII/controller-tools@v0.14.1/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 "github.com/TheSpiritXIII/controller-tools/pkg/genall" 35 "github.com/TheSpiritXIII/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 295 // sideEffects returns the sideEffects config for a webhook. 296 func (c Config) sideEffects() *admissionregv1.SideEffectClass { 297 var sideEffects admissionregv1.SideEffectClass 298 switch strings.ToLower(c.SideEffects) { 299 case strings.ToLower(string(admissionregv1.SideEffectClassNone)): 300 sideEffects = admissionregv1.SideEffectClassNone 301 case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)): 302 sideEffects = admissionregv1.SideEffectClassNoneOnDryRun 303 case strings.ToLower(string(admissionregv1.SideEffectClassSome)): 304 sideEffects = admissionregv1.SideEffectClassSome 305 case "": 306 return nil 307 default: 308 return nil 309 } 310 return &sideEffects 311 } 312 313 // timeoutSeconds returns the timeoutSeconds config for a webhook. 314 func (c Config) timeoutSeconds() *int32 { 315 if c.TimeoutSeconds != 0 { 316 timeoutSeconds := int32(c.TimeoutSeconds) 317 return &timeoutSeconds 318 } 319 return nil 320 } 321 322 // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook. 323 func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType { 324 var reinvocationPolicy admissionregv1.ReinvocationPolicyType 325 switch strings.ToLower(c.ReinvocationPolicy) { 326 case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)): 327 reinvocationPolicy = admissionregv1.NeverReinvocationPolicy 328 case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)): 329 reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy 330 default: 331 return nil 332 } 333 return &reinvocationPolicy 334 } 335 336 // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. 337 func (c Config) webhookVersions() ([]string, error) { 338 // If WebhookVersions is not specified, we default it to `v1`. 339 if len(c.WebhookVersions) == 0 { 340 return []string{defaultWebhookVersion}, nil 341 } 342 supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...) 343 for _, version := range c.WebhookVersions { 344 if !supportedWebhookVersions.Has(version) { 345 return nil, fmt.Errorf("unsupported webhook version: %s", version) 346 } 347 } 348 return sets.NewString(c.WebhookVersions...).UnsortedList(), nil 349 } 350 351 // +controllertools:marker:generateHelp 352 353 // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. 354 type Generator struct { 355 // HeaderFile specifies the header text (e.g. license) to prepend to generated files. 356 HeaderFile string `marker:",optional"` 357 358 // Year specifies the year to substitute for " YEAR" in the header file. 359 Year string `marker:",optional"` 360 } 361 362 func (Generator) RegisterMarkers(into *markers.Registry) error { 363 if err := into.Register(ConfigDefinition); err != nil { 364 return err 365 } 366 into.AddHelp(ConfigDefinition, Config{}.Help()) 367 return nil 368 } 369 370 func (g Generator) Generate(ctx *genall.GenerationContext) error { 371 supportedWebhookVersions := supportedWebhookVersions() 372 mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions)) 373 validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions)) 374 for _, root := range ctx.Roots { 375 markerSet, err := markers.PackageMarkers(ctx.Collector, root) 376 if err != nil { 377 root.AddError(err) 378 } 379 380 cfgs := markerSet[ConfigDefinition.Name] 381 sort.SliceStable(cfgs, func(i, j int) bool { 382 return cfgs[i].(Config).Name < cfgs[j].(Config).Name 383 }) 384 385 for _, cfg := range cfgs { 386 cfg := cfg.(Config) 387 webhookVersions, err := cfg.webhookVersions() 388 if err != nil { 389 return err 390 } 391 if cfg.Mutating { 392 w, err := cfg.ToMutatingWebhook() 393 if err != nil { 394 return err 395 } 396 for _, webhookVersion := range webhookVersions { 397 mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w) 398 } 399 } else { 400 w, err := cfg.ToValidatingWebhook() 401 if err != nil { 402 return err 403 } 404 for _, webhookVersion := range webhookVersions { 405 validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w) 406 } 407 } 408 } 409 } 410 411 versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions)) 412 for _, version := range supportedWebhookVersions { 413 if cfgs, ok := mutatingCfgs[version]; ok { 414 // The only possible version in supportedWebhookVersions is v1, 415 // so use it for all versioned types in this context. 416 objRaw := &admissionregv1.MutatingWebhookConfiguration{} 417 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 418 Group: admissionregv1.SchemeGroupVersion.Group, 419 Version: version, 420 Kind: "MutatingWebhookConfiguration", 421 }) 422 objRaw.SetName("mutating-webhook-configuration") 423 objRaw.Webhooks = cfgs 424 for i := range objRaw.Webhooks { 425 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 426 // return an error 427 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 428 return err 429 } 430 // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise, 431 // return an error 432 if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil { 433 return err 434 } 435 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 436 // return an error 437 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 438 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 439 } 440 } 441 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 442 } 443 444 if cfgs, ok := validatingCfgs[version]; ok { 445 // The only possible version in supportedWebhookVersions is v1, 446 // so use it for all versioned types in this context. 447 objRaw := &admissionregv1.ValidatingWebhookConfiguration{} 448 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 449 Group: admissionregv1.SchemeGroupVersion.Group, 450 Version: version, 451 Kind: "ValidatingWebhookConfiguration", 452 }) 453 objRaw.SetName("validating-webhook-configuration") 454 objRaw.Webhooks = cfgs 455 for i := range objRaw.Webhooks { 456 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 457 // return an error 458 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 459 return err 460 } 461 // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise, 462 // return an error 463 if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil { 464 return err 465 } 466 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 467 // return an error 468 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 469 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 470 } 471 } 472 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 473 } 474 } 475 476 var headerText string 477 if g.HeaderFile != "" { 478 headerBytes, err := ctx.ReadFile(g.HeaderFile) 479 if err != nil { 480 return err 481 } 482 headerText = string(headerBytes) 483 } 484 headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) 485 486 for k, v := range versionedWebhooks { 487 var fileName string 488 if k == defaultWebhookVersion { 489 fileName = fmt.Sprintf("manifests.yaml") 490 } else { 491 fileName = fmt.Sprintf("manifests.%s.yaml", k) 492 } 493 if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil { 494 return err 495 } 496 } 497 return nil 498 } 499 500 func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error { 501 if sideEffects == nil { 502 return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration") 503 } 504 if *sideEffects == admissionregv1.SideEffectClassUnknown || 505 *sideEffects == admissionregv1.SideEffectClassSome { 506 return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration") 507 } 508 return nil 509 } 510 511 func checkTimeoutSeconds(timeoutSeconds *int32) error { 512 if timeoutSeconds != nil && (*timeoutSeconds < 1 || *timeoutSeconds > 30) { 513 return fmt.Errorf("TimeoutSeconds must be between 1 and 30 seconds") 514 } 515 return nil 516 }