parser.go 16 KB

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