github.com/caseydavenport/controller-tools@v0.2.6-0.20200519183242-e8a18b1a6750/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:failurePolicy=<string>,matchPolicy=<string>,groups=<[]string>,resources=<[]string>,verbs=<[]string>,versions=<[]string>,name=<string>,path=<string>,mutating=<bool>,sideEffects=<string> 23 package webhook 24 25 import ( 26 "fmt" 27 "strings" 28 29 admissionreg "k8s.io/api/admissionregistration/v1beta1" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 32 "sigs.k8s.io/controller-tools/pkg/genall" 33 "sigs.k8s.io/controller-tools/pkg/markers" 34 ) 35 36 var ( 37 // ConfigDefinition s a marker for defining Webhook manifests. 38 // Call ToWebhook on the value to get a Kubernetes Webhook. 39 ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{})) 40 ) 41 42 // +controllertools:marker:generateHelp:category=Webhook 43 44 // Config specifies how a webhook should be served. 45 // 46 // It specifies only the details that are intrinsic to the application serving 47 // it (e.g. the resources it can handle, or the path it serves on). 48 type Config struct { 49 // Mutating marks this as a mutating webhook (it's validating only if false) 50 // 51 // Mutating webhooks are allowed to change the object in their response, 52 // and are called *before* all validating webhooks. Mutating webhooks may 53 // choose to reject an object, similarly to a validating webhook. 54 Mutating bool 55 // FailurePolicy specifies what should happen if the API server cannot reach the webhook. 56 // 57 // It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject 58 // the object in question). 59 FailurePolicy string 60 // MatchPolicy defines how the "rules" list is used to match incoming requests. 61 // Allowed values are "Exact" (match only if it exactly matches the specified rule) 62 // or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version). 63 MatchPolicy string `marker:",optional"` 64 // SideEffects specify whether calling the webhook will have side effects. 65 // This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then 66 // the API server will not call the webhook on a dry-run request and fails instead. 67 // If the value is "None", then the webhook has no side effects and the API server will call it on dry-run. 68 // If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the 69 // AdmissionReview sent in the request, and avoiding side effects if that value is "true." 70 SideEffects string `marker:",optional"` 71 72 // Groups specifies the API groups that this webhook receives requests for. 73 Groups []string 74 // Resources specifies the API resources that this webhook receives requests for. 75 Resources []string 76 // Verbs specifies the Kubernetes API verbs that this webhook receives requests for. 77 // 78 // Only modification-like verbs may be specified. 79 // May be "create", "update", "delete", "connect", or "*" (for all). 80 Verbs []string 81 // Versions specifies the API versions that this webhook receives requests for. 82 Versions []string 83 84 // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots 85 Name string 86 87 // Path specifies that path that the API server should connect to this webhook on. Must be 88 // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by 89 // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group 90 // are substituted for hyphens. For example, a validating webhook path for type 91 // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be 92 // /validate-batch-tutorial-kubebuilder-io-v1-cronjob 93 Path string 94 } 95 96 // verbToAPIVariant converts a marker's verb to the proper value for the API. 97 // Unrecognized verbs are passed through. 98 func verbToAPIVariant(verbRaw string) admissionreg.OperationType { 99 switch strings.ToLower(verbRaw) { 100 case strings.ToLower(string(admissionreg.Create)): 101 return admissionreg.Create 102 case strings.ToLower(string(admissionreg.Update)): 103 return admissionreg.Update 104 case strings.ToLower(string(admissionreg.Delete)): 105 return admissionreg.Delete 106 case strings.ToLower(string(admissionreg.Connect)): 107 return admissionreg.Connect 108 case strings.ToLower(string(admissionreg.OperationAll)): 109 return admissionreg.OperationAll 110 default: 111 return admissionreg.OperationType(verbRaw) 112 } 113 } 114 115 // ToMutatingWebhook converts this rule to its Kubernetes API form. 116 func (c Config) ToMutatingWebhook() (admissionreg.MutatingWebhook, error) { 117 if !c.Mutating { 118 return admissionreg.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name) 119 } 120 121 matchPolicy, err := c.matchPolicy() 122 if err != nil { 123 return admissionreg.MutatingWebhook{}, err 124 } 125 126 return admissionreg.MutatingWebhook{ 127 Name: c.Name, 128 Rules: c.rules(), 129 FailurePolicy: c.failurePolicy(), 130 MatchPolicy: matchPolicy, 131 ClientConfig: c.clientConfig(), 132 SideEffects: c.sideEffects(), 133 }, nil 134 } 135 136 // ToValidatingWebhook converts this rule to its Kubernetes API form. 137 func (c Config) ToValidatingWebhook() (admissionreg.ValidatingWebhook, error) { 138 if c.Mutating { 139 return admissionreg.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name) 140 } 141 142 matchPolicy, err := c.matchPolicy() 143 if err != nil { 144 return admissionreg.ValidatingWebhook{}, err 145 } 146 147 return admissionreg.ValidatingWebhook{ 148 Name: c.Name, 149 Rules: c.rules(), 150 FailurePolicy: c.failurePolicy(), 151 MatchPolicy: matchPolicy, 152 ClientConfig: c.clientConfig(), 153 SideEffects: c.sideEffects(), 154 }, nil 155 } 156 157 // rules returns the configuration of what operations on what 158 // resources/subresources a webhook should care about. 159 func (c Config) rules() []admissionreg.RuleWithOperations { 160 whConfig := admissionreg.RuleWithOperations{ 161 Rule: admissionreg.Rule{ 162 APIGroups: c.Groups, 163 APIVersions: c.Versions, 164 Resources: c.Resources, 165 }, 166 Operations: make([]admissionreg.OperationType, len(c.Verbs)), 167 } 168 169 for i, verbRaw := range c.Verbs { 170 whConfig.Operations[i] = verbToAPIVariant(verbRaw) 171 } 172 173 // fix the group names, since letting people type "core" is nice 174 for i, group := range whConfig.APIGroups { 175 if group == "core" { 176 whConfig.APIGroups[i] = "" 177 } 178 } 179 180 return []admissionreg.RuleWithOperations{whConfig} 181 } 182 183 // failurePolicy converts the string value to the proper value for the API. 184 // Unrecognized values are passed through. 185 func (c Config) failurePolicy() *admissionreg.FailurePolicyType { 186 var failurePolicy admissionreg.FailurePolicyType 187 switch strings.ToLower(c.FailurePolicy) { 188 case strings.ToLower(string(admissionreg.Ignore)): 189 failurePolicy = admissionreg.Ignore 190 case strings.ToLower(string(admissionreg.Fail)): 191 failurePolicy = admissionreg.Fail 192 default: 193 failurePolicy = admissionreg.FailurePolicyType(c.FailurePolicy) 194 } 195 return &failurePolicy 196 } 197 198 // matchPolicy converts the string value to the proper value for the API. 199 func (c Config) matchPolicy() (*admissionreg.MatchPolicyType, error) { 200 var matchPolicy admissionreg.MatchPolicyType 201 switch strings.ToLower(c.MatchPolicy) { 202 case strings.ToLower(string(admissionreg.Exact)): 203 matchPolicy = admissionreg.Exact 204 case strings.ToLower(string(admissionreg.Equivalent)): 205 matchPolicy = admissionreg.Equivalent 206 case "": 207 return nil, nil 208 default: 209 return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy) 210 } 211 return &matchPolicy, nil 212 } 213 214 // clientConfig returns the client config for a webhook. 215 func (c Config) clientConfig() admissionreg.WebhookClientConfig { 216 path := c.Path 217 return admissionreg.WebhookClientConfig{ 218 Service: &admissionreg.ServiceReference{ 219 Name: "webhook-service", 220 Namespace: "system", 221 Path: &path, 222 }, 223 // OpenAPI marks the field as required before 1.13 because of a bug that got fixed in 224 // https://github.com/kubernetes/api/commit/e7d9121e9ffd63cea0288b36a82bcc87b073bd1b 225 // Put "\n" as an placeholder as a workaround til 1.13+ is almost everywhere. 226 CABundle: []byte("\n"), 227 } 228 } 229 230 // sideEffects returns the sideEffects config for a webhook. 231 func (c Config) sideEffects() *admissionreg.SideEffectClass { 232 var sideEffects admissionreg.SideEffectClass 233 switch strings.ToLower(c.SideEffects) { 234 case strings.ToLower(string(admissionreg.SideEffectClassNone)): 235 sideEffects = admissionreg.SideEffectClassNone 236 case strings.ToLower(string(admissionreg.SideEffectClassNoneOnDryRun)): 237 sideEffects = admissionreg.SideEffectClassNoneOnDryRun 238 case strings.ToLower(string(admissionreg.SideEffectClassSome)): 239 sideEffects = admissionreg.SideEffectClassSome 240 case "": 241 return nil 242 default: 243 return nil 244 } 245 return &sideEffects 246 } 247 248 // +controllertools:marker:generateHelp 249 250 // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects. 251 type Generator struct{} 252 253 func (Generator) RegisterMarkers(into *markers.Registry) error { 254 if err := into.Register(ConfigDefinition); err != nil { 255 return err 256 } 257 into.AddHelp(ConfigDefinition, Config{}.Help()) 258 return nil 259 } 260 261 func (Generator) Generate(ctx *genall.GenerationContext) error { 262 var mutatingCfgs []admissionreg.MutatingWebhook 263 var validatingCfgs []admissionreg.ValidatingWebhook 264 for _, root := range ctx.Roots { 265 markerSet, err := markers.PackageMarkers(ctx.Collector, root) 266 if err != nil { 267 root.AddError(err) 268 } 269 270 for _, cfg := range markerSet[ConfigDefinition.Name] { 271 cfg := cfg.(Config) 272 if cfg.Mutating { 273 w, _ := cfg.ToMutatingWebhook() 274 mutatingCfgs = append(mutatingCfgs, w) 275 } else { 276 w, _ := cfg.ToValidatingWebhook() 277 validatingCfgs = append(validatingCfgs, w) 278 } 279 } 280 } 281 282 var objs []interface{} 283 if len(mutatingCfgs) > 0 { 284 objs = append(objs, &admissionreg.MutatingWebhookConfiguration{ 285 TypeMeta: metav1.TypeMeta{ 286 Kind: "MutatingWebhookConfiguration", 287 APIVersion: admissionreg.SchemeGroupVersion.String(), 288 }, 289 ObjectMeta: metav1.ObjectMeta{ 290 Name: "mutating-webhook-configuration", 291 }, 292 Webhooks: mutatingCfgs, 293 }) 294 } 295 296 if len(validatingCfgs) > 0 { 297 objs = append(objs, &admissionreg.ValidatingWebhookConfiguration{ 298 TypeMeta: metav1.TypeMeta{ 299 Kind: "ValidatingWebhookConfiguration", 300 APIVersion: admissionreg.SchemeGroupVersion.String(), 301 }, 302 ObjectMeta: metav1.ObjectMeta{ 303 Name: "validating-webhook-configuration", 304 }, 305 Webhooks: validatingCfgs, 306 }) 307 308 } 309 310 if len(objs) > 0 { 311 return ctx.WriteYAML("manifests.yaml", objs...) 312 } 313 314 return nil 315 }