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