github.com/minio/controller-tools@v0.4.7/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> 23 package webhook 24 25 import ( 26 "fmt" 27 "strings" 28 29 admissionregv1 "k8s.io/api/admissionregistration/v1" 30 "k8s.io/apimachinery/pkg/runtime/schema" 31 "k8s.io/apimachinery/pkg/util/sets" 32 33 "github.com/minio/controller-tools/pkg/genall" 34 "github.com/minio/controller-tools/pkg/markers" 35 ) 36 37 // The default {Mutating,Validating}WebhookConfiguration version to generate. 38 const ( 39 defaultWebhookVersion = "v1" 40 ) 41 42 var ( 43 // ConfigDefinition s a marker for defining Webhook manifests. 44 // Call ToWebhook on the value to get a Kubernetes Webhook. 45 ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{})) 46 ) 47 48 // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration. 49 func supportedWebhookVersions() []string { 50 return []string{defaultWebhookVersion, "v1beta1"} 51 } 52 53 // +controllertools:marker:generateHelp:category=Webhook 54 55 // Config specifies how a webhook should be served. 56 // 57 // It specifies only the details that are intrinsic to the application serving 58 // it (e.g. the resources it can handle, or the path it serves on). 59 type Config struct { 60 // Mutating marks this as a mutating webhook (it's validating only if false) 61 // 62 // Mutating webhooks are allowed to change the object in their response, 63 // and are called *before* all validating webhooks. Mutating webhooks may 64 // choose to reject an object, similarly to a validating webhook. 65 Mutating bool 66 // FailurePolicy specifies what should happen if the API server cannot reach the webhook. 67 // 68 // It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject 69 // the object in question). 70 FailurePolicy string 71 // MatchPolicy defines how the "rules" list is used to match incoming requests. 72 // Allowed values are "Exact" (match only if it exactly matches the specified rule) 73 // or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version). 74 MatchPolicy string `marker:",optional"` 75 // SideEffects specify whether calling the webhook will have side effects. 76 // This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then 77 // the API server will not call the webhook on a dry-run request and fails instead. 78 // If the value is "None", then the webhook has no side effects and the API server will call it on dry-run. 79 // If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the 80 // AdmissionReview sent in the request, and avoiding side effects if that value is "true." 81 SideEffects string `marker:",optional"` 82 83 // Groups specifies the API groups that this webhook receives requests for. 84 Groups []string 85 // Resources specifies the API resources that this webhook receives requests for. 86 Resources []string 87 // Verbs specifies the Kubernetes API verbs that this webhook receives requests for. 88 // 89 // Only modification-like verbs may be specified. 90 // May be "create", "update", "delete", "connect", or "*" (for all). 91 Verbs []string 92 // Versions specifies the API versions that this webhook receives requests for. 93 Versions []string 94 95 // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots 96 Name string 97 98 // Path specifies that path that the API server should connect to this webhook on. Must be 99 // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by 100 // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group 101 // are substituted for hyphens. For example, a validating webhook path for type 102 // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be 103 // /validate-batch-tutorial-kubebuilder-io-v1-cronjob 104 Path string 105 106 // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects 107 // itself to generate. Defaults to v1. 108 WebhookVersions []string `marker:"webhookVersions,optional"` 109 110 // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview` 111 // versions the Webhook expects. 112 // For generating v1 {Mutating,Validating}WebhookConfiguration, this is mandatory. 113 // For generating v1beta1 {Mutating,Validating}WebhookConfiguration, this is optional, and default to v1beta1. 114 AdmissionReviewVersions []string `marker:"admissionReviewVersions,optional"` 115 } 116 117 // verbToAPIVariant converts a marker's verb to the proper value for the API. 118 // Unrecognized verbs are passed through. 119 func verbToAPIVariant(verbRaw string) admissionregv1.OperationType { 120 switch strings.ToLower(verbRaw) { 121 case strings.ToLower(string(admissionregv1.Create)): 122 return admissionregv1.Create 123 case strings.ToLower(string(admissionregv1.Update)): 124 return admissionregv1.Update 125 case strings.ToLower(string(admissionregv1.Delete)): 126 return admissionregv1.Delete 127 case strings.ToLower(string(admissionregv1.Connect)): 128 return admissionregv1.Connect 129 case strings.ToLower(string(admissionregv1.OperationAll)): 130 return admissionregv1.OperationAll 131 default: 132 return admissionregv1.OperationType(verbRaw) 133 } 134 } 135 136 // ToMutatingWebhook converts this rule to its Kubernetes API form. 137 func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { 138 if !c.Mutating { 139 return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name) 140 } 141 142 matchPolicy, err := c.matchPolicy() 143 if err != nil { 144 return admissionregv1.MutatingWebhook{}, err 145 } 146 147 return admissionregv1.MutatingWebhook{ 148 Name: c.Name, 149 Rules: c.rules(), 150 FailurePolicy: c.failurePolicy(), 151 MatchPolicy: matchPolicy, 152 ClientConfig: c.clientConfig(), 153 SideEffects: c.sideEffects(), 154 AdmissionReviewVersions: c.AdmissionReviewVersions, 155 }, nil 156 } 157 158 // ToValidatingWebhook converts this rule to its Kubernetes API form. 159 func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) { 160 if c.Mutating { 161 return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name) 162 } 163 164 matchPolicy, err := c.matchPolicy() 165 if err != nil { 166 return admissionregv1.ValidatingWebhook{}, err 167 } 168 169 return admissionregv1.ValidatingWebhook{ 170 Name: c.Name, 171 Rules: c.rules(), 172 FailurePolicy: c.failurePolicy(), 173 MatchPolicy: matchPolicy, 174 ClientConfig: c.clientConfig(), 175 SideEffects: c.sideEffects(), 176 AdmissionReviewVersions: c.AdmissionReviewVersions, 177 }, nil 178 } 179 180 // rules returns the configuration of what operations on what 181 // resources/subresources a webhook should care about. 182 func (c Config) rules() []admissionregv1.RuleWithOperations { 183 whConfig := admissionregv1.RuleWithOperations{ 184 Rule: admissionregv1.Rule{ 185 APIGroups: c.Groups, 186 APIVersions: c.Versions, 187 Resources: c.Resources, 188 }, 189 Operations: make([]admissionregv1.OperationType, len(c.Verbs)), 190 } 191 192 for i, verbRaw := range c.Verbs { 193 whConfig.Operations[i] = verbToAPIVariant(verbRaw) 194 } 195 196 // fix the group names, since letting people type "core" is nice 197 for i, group := range whConfig.APIGroups { 198 if group == "core" { 199 whConfig.APIGroups[i] = "" 200 } 201 } 202 203 return []admissionregv1.RuleWithOperations{whConfig} 204 } 205 206 // failurePolicy converts the string value to the proper value for the API. 207 // Unrecognized values are passed through. 208 func (c Config) failurePolicy() *admissionregv1.FailurePolicyType { 209 var failurePolicy admissionregv1.FailurePolicyType 210 switch strings.ToLower(c.FailurePolicy) { 211 case strings.ToLower(string(admissionregv1.Ignore)): 212 failurePolicy = admissionregv1.Ignore 213 case strings.ToLower(string(admissionregv1.Fail)): 214 failurePolicy = admissionregv1.Fail 215 default: 216 failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy) 217 } 218 return &failurePolicy 219 } 220 221 // matchPolicy converts the string value to the proper value for the API. 222 func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) { 223 var matchPolicy admissionregv1.MatchPolicyType 224 switch strings.ToLower(c.MatchPolicy) { 225 case strings.ToLower(string(admissionregv1.Exact)): 226 matchPolicy = admissionregv1.Exact 227 case strings.ToLower(string(admissionregv1.Equivalent)): 228 matchPolicy = admissionregv1.Equivalent 229 case "": 230 return nil, nil 231 default: 232 return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy) 233 } 234 return &matchPolicy, nil 235 } 236 237 // clientConfig returns the client config for a webhook. 238 func (c Config) clientConfig() admissionregv1.WebhookClientConfig { 239 path := c.Path 240 return admissionregv1.WebhookClientConfig{ 241 Service: &admissionregv1.ServiceReference{ 242 Name: "webhook-service", 243 Namespace: "system", 244 Path: &path, 245 }, 246 } 247 } 248 249 // sideEffects returns the sideEffects config for a webhook. 250 func (c Config) sideEffects() *admissionregv1.SideEffectClass { 251 var sideEffects admissionregv1.SideEffectClass 252 switch strings.ToLower(c.SideEffects) { 253 case strings.ToLower(string(admissionregv1.SideEffectClassNone)): 254 sideEffects = admissionregv1.SideEffectClassNone 255 case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)): 256 sideEffects = admissionregv1.SideEffectClassNoneOnDryRun 257 case strings.ToLower(string(admissionregv1.SideEffectClassSome)): 258 sideEffects = admissionregv1.SideEffectClassSome 259 case "": 260 return nil 261 default: 262 return nil 263 } 264 return &sideEffects 265 } 266 267 // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. 268 func (c Config) webhookVersions() ([]string, error) { 269 // If WebhookVersions is not specified, we default it to `v1`. 270 if len(c.WebhookVersions) == 0 { 271 return []string{defaultWebhookVersion}, nil 272 } 273 supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...) 274 for _, version := range c.WebhookVersions { 275 if !supportedWebhookVersions.Has(version) { 276 return nil, fmt.Errorf("unsupported webhook version: %s", version) 277 } 278 } 279 return sets.NewString(c.WebhookVersions...).UnsortedList(), nil 280 } 281 282 // +controllertools:marker:generateHelp 283 284 // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. 285 type Generator struct{} 286 287 func (Generator) RegisterMarkers(into *markers.Registry) error { 288 if err := into.Register(ConfigDefinition); err != nil { 289 return err 290 } 291 into.AddHelp(ConfigDefinition, Config{}.Help()) 292 return nil 293 } 294 295 func (Generator) Generate(ctx *genall.GenerationContext) error { 296 supportedWebhookVersions := supportedWebhookVersions() 297 mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions)) 298 validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions)) 299 for _, root := range ctx.Roots { 300 markerSet, err := markers.PackageMarkers(ctx.Collector, root) 301 if err != nil { 302 root.AddError(err) 303 } 304 305 for _, cfg := range markerSet[ConfigDefinition.Name] { 306 cfg := cfg.(Config) 307 webhookVersions, err := cfg.webhookVersions() 308 if err != nil { 309 return err 310 } 311 if cfg.Mutating { 312 w, err := cfg.ToMutatingWebhook() 313 if err != nil { 314 return err 315 } 316 for _, webhookVersion := range webhookVersions { 317 mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w) 318 } 319 } else { 320 w, err := cfg.ToValidatingWebhook() 321 if err != nil { 322 return err 323 } 324 for _, webhookVersion := range webhookVersions { 325 validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w) 326 } 327 } 328 } 329 } 330 331 versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions)) 332 for _, version := range supportedWebhookVersions { 333 if cfgs, ok := mutatingCfgs[version]; ok { 334 // All webhook config versions in supportedWebhookVersions have the same general form, with a few 335 // stricter requirements for v1. Since no conversion scheme exists for webhook configs, the v1 336 // type can be used for all versioned types in this context. 337 objRaw := &admissionregv1.MutatingWebhookConfiguration{} 338 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 339 Group: admissionregv1.SchemeGroupVersion.Group, 340 Version: version, 341 Kind: "MutatingWebhookConfiguration", 342 }) 343 objRaw.SetName("mutating-webhook-configuration") 344 objRaw.Webhooks = cfgs 345 switch version { 346 case admissionregv1.SchemeGroupVersion.Version: 347 for i := range objRaw.Webhooks { 348 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 349 // return an error 350 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 351 return err 352 } 353 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 354 // return an error 355 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 356 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 357 } 358 } 359 } 360 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 361 } 362 363 if cfgs, ok := validatingCfgs[version]; ok { 364 // All webhook config versions in supportedWebhookVersions have the same general form, with a few 365 // stricter requirements for v1. Since no conversion scheme exists for webhook configs, the v1 366 // type can be used for all versioned types in this context. 367 objRaw := &admissionregv1.ValidatingWebhookConfiguration{} 368 objRaw.SetGroupVersionKind(schema.GroupVersionKind{ 369 Group: admissionregv1.SchemeGroupVersion.Group, 370 Version: version, 371 Kind: "ValidatingWebhookConfiguration", 372 }) 373 objRaw.SetName("validating-webhook-configuration") 374 objRaw.Webhooks = cfgs 375 switch version { 376 case admissionregv1.SchemeGroupVersion.Version: 377 for i := range objRaw.Webhooks { 378 // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`, 379 // return an error 380 if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil { 381 return err 382 } 383 // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set, 384 // return an error 385 if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 { 386 return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration") 387 } 388 } 389 } 390 versionedWebhooks[version] = append(versionedWebhooks[version], objRaw) 391 } 392 } 393 394 for k, v := range versionedWebhooks { 395 var fileName string 396 if k == defaultWebhookVersion { 397 fileName = fmt.Sprintf("manifests.yaml") 398 } else { 399 fileName = fmt.Sprintf("manifests.%s.yaml", k) 400 } 401 if err := ctx.WriteYAML(fileName, v...); err != nil { 402 return err 403 } 404 } 405 return nil 406 } 407 408 func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error { 409 if sideEffects == nil { 410 return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration") 411 } 412 if *sideEffects == admissionregv1.SideEffectClassUnknown || 413 *sideEffects == admissionregv1.SideEffectClassSome { 414 return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration") 415 } 416 return nil 417 }