github.com/alex123012/deckhouse-controller-tools@v0.0.0-20230510090815-d594daf1af8c/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>,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 85 // Groups specifies the API groups that this webhook receives requests for. 86 Groups []string 87 // Resources specifies the API resources that this webhook receives requests for. 88 Resources []string 89 // Verbs specifies the Kubernetes API verbs that this webhook receives requests for. 90 // 91 // Only modification-like verbs may be specified. 92 // May be "create", "update", "delete", "connect", or "*" (for all). 93 Verbs []string 94 // Versions specifies the API versions that this webhook receives requests for. 95 Versions []string 96 97 // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots 98 Name string 99 100 // Path specifies that path that the API server should connect to this webhook on. Must be 101 // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by 102 // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group 103 // are substituted for hyphens. For example, a validating webhook path for type 104 // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be 105 // /validate-batch-tutorial-kubebuilder-io-v1-cronjob 106 Path string 107 108 // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects 109 // itself to generate. The only supported value is v1. Defaults to v1. 110 WebhookVersions []string `marker:"webhookVersions,optional"` 111 112 // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` 113 // versions the Webhook expects. 114 AdmissionReviewVersions []string `marker:"admissionReviewVersions"` 115 116 // ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations 117 // 118 // To allow mutating admission plugins to observe changes made by other plugins, 119 // built-in mutating admission plugins are re-run if a mutating webhook modifies 120 // an object, and mutating webhooks can specify a reinvocationPolicy to control 121 // whether they are reinvoked as well. 122 ReinvocationPolicy string `marker:"reinvocationPolicy,optional"` 123 } 124 125 // verbToAPIVariant converts a marker's verb to the proper value for the API. 126 // Unrecognized verbs are passed through. 127 func verbToAPIVariant(verbRaw string) admissionregv1.OperationType { 128 switch strings.ToLower(verbRaw) { 129 case strings.ToLower(string(admissionregv1.Create)): 130 return admissionregv1.Create 131 case strings.ToLower(string(admissionregv1.Update)): 132 return admissionregv1.Update 133 case strings.ToLower(string(admissionregv1.Delete)): 134 return admissionregv1.Delete 135 case strings.ToLower(string(admissionregv1.Connect)): 136 return admissionregv1.Connect 137 case strings.ToLower(string(admissionregv1.OperationAll)): 138 return admissionregv1.OperationAll 139 default: 140 return admissionregv1.OperationType(verbRaw) 141 } 142 } 143 144 // ToMutatingWebhook converts this rule to its Kubernetes API form. 145 func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { 146 if !c.Mutating { 147 return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name) 148 } 149 150 matchPolicy, err := c.matchPolicy() 151 if err != nil { 152 return admissionregv1.MutatingWebhook{}, err 153 } 154 155 return admissionregv1.MutatingWebhook{ 156 Name: c.Name, 157 Rules: c.rules(), 158 FailurePolicy: c.failurePolicy(), 159 MatchPolicy: matchPolicy, 160 ClientConfig: c.clientConfig(), 161 SideEffects: c.sideEffects(), 162 AdmissionReviewVersions: c.AdmissionReviewVersions, 163 ReinvocationPolicy: c.reinvocationPolicy(), 164 }, nil 165 } 166 167 // ToValidatingWebhook converts this rule to its Kubernetes API form. 168 func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) { 169 if c.Mutating { 170 return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name) 171 } 172 173 matchPolicy, err := c.matchPolicy() 174 if err != nil { 175 return admissionregv1.ValidatingWebhook{}, err 176 } 177 178 return admissionregv1.ValidatingWebhook{ 179 Name: c.Name, 180 Rules: c.rules(), 181 FailurePolicy: c.failurePolicy(), 182 MatchPolicy: matchPolicy, 183 ClientConfig: c.clientConfig(), 184 SideEffects: c.sideEffects(), 185 AdmissionReviewVersions: c.AdmissionReviewVersions, 186 }, nil 187 } 188 189 // rules returns the configuration of what operations on what 190 // resources/subresources a webhook should care about. 191 func (c Config) rules() []admissionregv1.RuleWithOperations { 192 whConfig := admissionregv1.RuleWithOperations{ 193 Rule: admissionregv1.Rule{ 194 APIGroups: c.Groups, 195 APIVersions: c.Versions, 196 Resources: c.Resources, 197 }, 198 Operations: make([]admissionregv1.OperationType, len(c.Verbs)), 199 } 200 201 for i, verbRaw := range c.Verbs { 202 whConfig.Operations[i] = verbToAPIVariant(verbRaw) 203 } 204 205 // fix the group names, since letting people type "core" is nice 206 for i, group := range whConfig.APIGroups { 207 if group == "core" { 208 whConfig.APIGroups[i] = "" 209 } 210 } 211 212 return []admissionregv1.RuleWithOperations{whConfig} 213 } 214 215 // failurePolicy converts the string value to the proper value for the API. 216 // Unrecognized values are passed through. 217 func (c Config) failurePolicy() *admissionregv1.FailurePolicyType { 218 var failurePolicy admissionregv1.FailurePolicyType 219 switch strings.ToLower(c.FailurePolicy) { 220 case strings.ToLower(string(admissionregv1.Ignore)): 221 failurePolicy = admissionregv1.Ignore 222 case strings.ToLower(string(admissionregv1.Fail)): 223 failurePolicy = admissionregv1.Fail 224 default: 225 failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy) 226 } 227 return &failurePolicy 228 } 229 230 // matchPolicy converts the string value to the proper value for the API. 231 func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) { 232 var matchPolicy admissionregv1.MatchPolicyType 233 switch strings.ToLower(c.MatchPolicy) { 234 case strings.ToLower(string(admissionregv1.Exact)): 235 matchPolicy = admissionregv1.Exact 236 case strings.ToLower(string(admissionregv1.Equivalent)): 237 matchPolicy = admissionregv1.Equivalent 238 case "": 239 return nil, nil 240 default: 241 return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy) 242 } 243 return &matchPolicy, nil 244 } 245 246 // clientConfig returns the client config for a webhook. 247 func (c Config) clientConfig() admissionregv1.WebhookClientConfig { 248 path := c.Path 249 return admissionregv1.WebhookClientConfig{ 250 Service: &admissionregv1.ServiceReference{ 251 Name: "webhook-service", 252 Namespace: "system", 253 Path: &path, 254 }, 255 } 256 } 257 258 // sideEffects returns the sideEffects config for a webhook. 259 func (c Config) sideEffects() *admissionregv1.SideEffectClass { 260 var sideEffects admissionregv1.SideEffectClass 261 switch strings.ToLower(c.SideEffects) { 262 case strings.ToLower(string(admissionregv1.SideEffectClassNone)): 263 sideEffects = admissionregv1.SideEffectClassNone 264 case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)): 265 sideEffects = admissionregv1.SideEffectClassNoneOnDryRun 266 case strings.ToLower(string(admissionregv1.SideEffectClassSome)): 267 sideEffects = admissionregv1.SideEffectClassSome 268 case "": 269 return nil 270 default: 271 return nil 272 } 273 return &sideEffects 274 } 275 276 // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook. 277 func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType { 278 var reinvocationPolicy admissionregv1.ReinvocationPolicyType 279 switch strings.ToLower(c.ReinvocationPolicy) { 280 case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)): 281 reinvocationPolicy = admissionregv1.NeverReinvocationPolicy 282 case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)): 283 reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy 284 default: 285 return nil 286 } 287 return &reinvocationPolicy 288 } 289 290 // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. 291 func (c Config) webhookVersions() ([]string, error) { 292 // If WebhookVersions is not specified, we default it to `v1`. 293 if len(c.WebhookVersions) == 0 { 294 return []string{defaultWebhookVersion}, nil 295 } 296 supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...) 297 for _, version := range c.WebhookVersions { 298 if !supportedWebhookVersions.Has(version) { 299 return nil, fmt.Errorf("unsupported webhook version: %s", version) 300 } 301 } 302 return sets.NewString(c.WebhookVersions...).UnsortedList(), nil 303 } 304 305 // +controllertools:marker:generateHelp 306 307 // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. 308 type Generator struct { 309 // HeaderFile specifies the header text (e.g. license) to prepend to generated files. 310 HeaderFile string `marker:",optional"` 311 312 // Year specifies the year to substitute for " YEAR" in the header file. 313 Year string `marker:",optional"` 314 } 315 316 func (Generator) RegisterMarkers(into *markers.Registry) error { 317 if err := into.Register(ConfigDefinition); err != nil { 318 return err 319 } 320 into.AddHelp(ConfigDefinition, Config{}.Help()) 321 return nil 322 } 323 324 func (g Generator) Generate(ctx *genall.GenerationContext) error { 325 supportedWebhookVersions := supportedWebhookVersions() 326 mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions)) 327 validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions)) 328 for _, root := range ctx.Roots { 329 markerSet, err := markers.PackageMarkers(ctx.Collector, root) 330 if err != nil { 331 root.AddError(err) 332 } 333 334 cfgs := markerSet[ConfigDefinition.Name] 335 sort.SliceStable(cfgs, func(i, j int) bool { 336 return cfgs[i].(Config).Name < cfgs[j].(Config).Name 337 }) 338 339 for _, cfg := range cfgs { 340 cfg := cfg.(Config) 341 webhookVersions, err := cfg.webhookVersions() 342 if err != nil { 343 return err 344 } 345 if cfg.Mutating { 346 w, err := cfg.ToMutatingWebhook() 347 if err != nil { 348 return err 349 } 350 for _, webhookVersion := range webhookVersions { 351 mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w) 352 } 353 } else { 354 w, err := cfg.ToValidatingWebhook() 355 if err != nil { 356 return err 357 } 358 for _, webhookVersion := range webhookVersions { 359 validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w) 360 } 361 } 362 } 363 } 364 365 versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions)) 366 for _, version := range supportedWebhookVersions { 367 if cfgs, ok := mutatingCfgs[version]; ok { 368 // The only possible version in supportedWebhookVersions is v1, 369 // so use it for all versioned types in this context. 370 objRaw := &admissionregv1.MutatingWebhookConfiguration{} 371 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 372 Group: admissionregv1.SchemeGroupVersion.Group, 373 Version: version, 374 Kind: "MutatingWebhookConfiguration", 375 }) 376 objRaw.SetName("mutating-webhook-configuration") 377 objRaw.Webhooks = cfgs 378 for i := range objRaw.Webhooks { 379 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 380 // return an error 381 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 382 return err 383 } 384 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 385 // return an error 386 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 387 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 388 } 389 } 390 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 391 } 392 393 if cfgs, ok := validatingCfgs[version]; ok { 394 // The only possible version in supportedWebhookVersions is v1, 395 // so use it for all versioned types in this context. 396 objRaw := &admissionregv1.ValidatingWebhookConfiguration{} 397 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 398 Group: admissionregv1.SchemeGroupVersion.Group, 399 Version: version, 400 Kind: "ValidatingWebhookConfiguration", 401 }) 402 objRaw.SetName("validating-webhook-configuration") 403 objRaw.Webhooks = cfgs 404 for i := range objRaw.Webhooks { 405 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 406 // return an error 407 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 408 return err 409 } 410 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 411 // return an error 412 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 413 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 414 } 415 } 416 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 417 } 418 } 419 420 var headerText string 421 if g.HeaderFile != "" { 422 headerBytes, err := ctx.ReadFile(g.HeaderFile) 423 if err != nil { 424 return err 425 } 426 headerText = string(headerBytes) 427 } 428 headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year) 429 430 for k, v := range versionedWebhooks { 431 var fileName string 432 if k == defaultWebhookVersion { 433 fileName = fmt.Sprintf("manifests.yaml") 434 } else { 435 fileName = fmt.Sprintf("manifests.%s.yaml", k) 436 } 437 if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil { 438 return err 439 } 440 } 441 return nil 442 } 443 444 func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error { 445 if sideEffects == nil { 446 return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration") 447 } 448 if *sideEffects == admissionregv1.SideEffectClassUnknown || 449 *sideEffects == admissionregv1.SideEffectClassSome { 450 return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration") 451 } 452 return nil 453 }