parser.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  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. v1 = "v1"
  32. defaultWebhookVersion = v1
  33. )
  34. var (
  35. // ConfigDefinition s a marker for defining Webhook manifests.
  36. // Call ToWebhook on the value to get a Kubernetes Webhook.
  37. ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{}))
  38. )
  39. // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration.
  40. func supportedWebhookVersions() []string {
  41. return []string{defaultWebhookVersion}
  42. }
  43. // +controllertools:marker:generateHelp:category=Webhook
  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. // Groups specifies the API groups that this webhook receives requests for.
  72. Groups []string
  73. // Resources specifies the API resources that this webhook receives requests for.
  74. Resources []string
  75. // Verbs specifies the Kubernetes API verbs that this webhook receives requests for.
  76. //
  77. // Only modification-like verbs may be specified.
  78. // May be "create", "update", "delete", "connect", or "*" (for all).
  79. Verbs []string
  80. // Versions specifies the API versions that this webhook receives requests for.
  81. Versions []string
  82. // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
  83. Name string
  84. // Path specifies that path that the API server should connect to this webhook on. Must be
  85. // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
  86. // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
  87. // are substituted for hyphens. For example, a validating webhook path for type
  88. // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
  89. // /validate-batch-tutorial-kubebuilder-io-v1-cronjob
  90. Path string
  91. // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
  92. // itself to generate. The only supported value is v1. Defaults to v1.
  93. WebhookVersions []string `marker:"webhookVersions,optional"`
  94. // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
  95. // versions the Webhook expects.
  96. AdmissionReviewVersions []string `marker:"admissionReviewVersions"`
  97. }
  98. // verbToAPIVariant converts a marker's verb to the proper value for the API.
  99. // Unrecognized verbs are passed through.
  100. func verbToAPIVariant(verbRaw string) admissionregv1.OperationType {
  101. switch strings.ToLower(verbRaw) {
  102. case strings.ToLower(string(admissionregv1.Create)):
  103. return admissionregv1.Create
  104. case strings.ToLower(string(admissionregv1.Update)):
  105. return admissionregv1.Update
  106. case strings.ToLower(string(admissionregv1.Delete)):
  107. return admissionregv1.Delete
  108. case strings.ToLower(string(admissionregv1.Connect)):
  109. return admissionregv1.Connect
  110. case strings.ToLower(string(admissionregv1.OperationAll)):
  111. return admissionregv1.OperationAll
  112. default:
  113. return admissionregv1.OperationType(verbRaw)
  114. }
  115. }
  116. // ToMutatingWebhook converts this rule to its Kubernetes API form.
  117. func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) {
  118. if !c.Mutating {
  119. return admissionregv1.MutatingWebhook{}, fmt.Errorf("%s is a validating webhook", c.Name)
  120. }
  121. matchPolicy, err := c.matchPolicy()
  122. if err != nil {
  123. return admissionregv1.MutatingWebhook{}, err
  124. }
  125. return admissionregv1.MutatingWebhook{
  126. Name: c.Name,
  127. Rules: c.rules(),
  128. FailurePolicy: c.failurePolicy(),
  129. MatchPolicy: matchPolicy,
  130. ClientConfig: c.clientConfig(),
  131. SideEffects: c.sideEffects(),
  132. AdmissionReviewVersions: c.AdmissionReviewVersions,
  133. }, nil
  134. }
  135. // ToValidatingWebhook converts this rule to its Kubernetes API form.
  136. func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
  137. if c.Mutating {
  138. return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
  139. }
  140. matchPolicy, err := c.matchPolicy()
  141. if err != nil {
  142. return admissionregv1.ValidatingWebhook{}, err
  143. }
  144. return admissionregv1.ValidatingWebhook{
  145. Name: c.Name,
  146. Rules: c.rules(),
  147. FailurePolicy: c.failurePolicy(),
  148. MatchPolicy: matchPolicy,
  149. ClientConfig: c.clientConfig(),
  150. SideEffects: c.sideEffects(),
  151. AdmissionReviewVersions: c.AdmissionReviewVersions,
  152. }, nil
  153. }
  154. // rules returns the configuration of what operations on what
  155. // resources/subresources a webhook should care about.
  156. func (c Config) rules() []admissionregv1.RuleWithOperations {
  157. whConfig := admissionregv1.RuleWithOperations{
  158. Rule: admissionregv1.Rule{
  159. APIGroups: c.Groups,
  160. APIVersions: c.Versions,
  161. Resources: c.Resources,
  162. },
  163. Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
  164. }
  165. for i, verbRaw := range c.Verbs {
  166. whConfig.Operations[i] = verbToAPIVariant(verbRaw)
  167. }
  168. // fix the group names, since letting people type "core" is nice
  169. for i, group := range whConfig.APIGroups {
  170. if group == "core" {
  171. whConfig.APIGroups[i] = ""
  172. }
  173. }
  174. return []admissionregv1.RuleWithOperations{whConfig}
  175. }
  176. // failurePolicy converts the string value to the proper value for the API.
  177. // Unrecognized values are passed through.
  178. func (c Config) failurePolicy() *admissionregv1.FailurePolicyType {
  179. var failurePolicy admissionregv1.FailurePolicyType
  180. switch strings.ToLower(c.FailurePolicy) {
  181. case strings.ToLower(string(admissionregv1.Ignore)):
  182. failurePolicy = admissionregv1.Ignore
  183. case strings.ToLower(string(admissionregv1.Fail)):
  184. failurePolicy = admissionregv1.Fail
  185. default:
  186. failurePolicy = admissionregv1.FailurePolicyType(c.FailurePolicy)
  187. }
  188. return &failurePolicy
  189. }
  190. // matchPolicy converts the string value to the proper value for the API.
  191. func (c Config) matchPolicy() (*admissionregv1.MatchPolicyType, error) {
  192. var matchPolicy admissionregv1.MatchPolicyType
  193. switch strings.ToLower(c.MatchPolicy) {
  194. case strings.ToLower(string(admissionregv1.Exact)):
  195. matchPolicy = admissionregv1.Exact
  196. case strings.ToLower(string(admissionregv1.Equivalent)):
  197. matchPolicy = admissionregv1.Equivalent
  198. case "":
  199. return nil, nil
  200. default:
  201. return nil, fmt.Errorf("unknown value %q for matchPolicy", c.MatchPolicy)
  202. }
  203. return &matchPolicy, nil
  204. }
  205. // clientConfig returns the client config for a webhook.
  206. func (c Config) clientConfig() admissionregv1.WebhookClientConfig {
  207. path := c.Path
  208. return admissionregv1.WebhookClientConfig{
  209. Service: &admissionregv1.ServiceReference{
  210. Name: "webhook-service",
  211. Namespace: "system",
  212. Path: &path,
  213. },
  214. }
  215. }
  216. // sideEffects returns the sideEffects config for a webhook.
  217. func (c Config) sideEffects() *admissionregv1.SideEffectClass {
  218. var sideEffects admissionregv1.SideEffectClass
  219. switch strings.ToLower(c.SideEffects) {
  220. case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
  221. sideEffects = admissionregv1.SideEffectClassNone
  222. case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
  223. sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
  224. case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
  225. sideEffects = admissionregv1.SideEffectClassSome
  226. case "":
  227. return nil
  228. default:
  229. return nil
  230. }
  231. return &sideEffects
  232. }
  233. // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
  234. func (c Config) webhookVersions() ([]string, error) {
  235. // If WebhookVersions is not specified, we default it to `v1`.
  236. if len(c.WebhookVersions) == 0 {
  237. return []string{defaultWebhookVersion}, nil
  238. }
  239. supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
  240. for _, version := range c.WebhookVersions {
  241. if !supportedWebhookVersions.Has(version) {
  242. return nil, fmt.Errorf("unsupported webhook version: %s", version)
  243. }
  244. }
  245. return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
  246. }
  247. // +controllertools:marker:generateHelp
  248. // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
  249. type Generator struct{}
  250. func (Generator) RegisterMarkers(into *markers.Registry) error {
  251. if err := into.Register(ConfigDefinition); err != nil {
  252. return err
  253. }
  254. into.AddHelp(ConfigDefinition, Config{}.Help())
  255. return nil
  256. }
  257. func (Generator) Generate(ctx *genall.GenerationContext) error {
  258. supportedWebhookVersions := supportedWebhookVersions()
  259. mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
  260. validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
  261. for _, root := range ctx.Roots {
  262. markerSet, err := markers.PackageMarkers(ctx.Collector, root)
  263. if err != nil {
  264. root.AddError(err)
  265. }
  266. for _, cfg := range markerSet[ConfigDefinition.Name] {
  267. cfg := cfg.(Config)
  268. webhookVersions, err := cfg.webhookVersions()
  269. if err != nil {
  270. return err
  271. }
  272. if cfg.Mutating {
  273. w, err := cfg.ToMutatingWebhook()
  274. if err != nil {
  275. return err
  276. }
  277. for _, webhookVersion := range webhookVersions {
  278. mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
  279. }
  280. } else {
  281. w, err := cfg.ToValidatingWebhook()
  282. if err != nil {
  283. return err
  284. }
  285. for _, webhookVersion := range webhookVersions {
  286. validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
  287. }
  288. }
  289. }
  290. }
  291. versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
  292. for _, version := range supportedWebhookVersions {
  293. if cfgs, ok := mutatingCfgs[version]; ok {
  294. // The only possible version in supportedWebhookVersions is v1,
  295. // so use it for all versioned types in this context.
  296. objRaw := &admissionregv1.MutatingWebhookConfiguration{}
  297. objRaw.SetGroupVersionKind(schema.GroupVersionKind{
  298. Group: admissionregv1.SchemeGroupVersion.Group,
  299. Version: version,
  300. Kind: "MutatingWebhookConfiguration",
  301. })
  302. objRaw.SetName("mutating-webhook-configuration")
  303. objRaw.Webhooks = cfgs
  304. for i := range objRaw.Webhooks {
  305. // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
  306. // return an error
  307. if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
  308. return err
  309. }
  310. // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
  311. // return an error
  312. if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
  313. return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
  314. }
  315. }
  316. versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
  317. }
  318. if cfgs, ok := validatingCfgs[version]; ok {
  319. // The only possible version in supportedWebhookVersions is v1,
  320. // so use it for all versioned types in this context.
  321. objRaw := &admissionregv1.ValidatingWebhookConfiguration{}
  322. objRaw.SetGroupVersionKind(schema.GroupVersionKind{
  323. Group: admissionregv1.SchemeGroupVersion.Group,
  324. Version: version,
  325. Kind: "ValidatingWebhookConfiguration",
  326. })
  327. objRaw.SetName("validating-webhook-configuration")
  328. objRaw.Webhooks = cfgs
  329. for i := range objRaw.Webhooks {
  330. // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
  331. // return an error
  332. if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
  333. return err
  334. }
  335. // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
  336. // return an error
  337. if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
  338. return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
  339. }
  340. }
  341. versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
  342. }
  343. }
  344. for k, v := range versionedWebhooks {
  345. var fileName string
  346. if k == defaultWebhookVersion {
  347. fileName = fmt.Sprintf("manifests.yaml")
  348. } else {
  349. fileName = fmt.Sprintf("manifests.%s.yaml", k)
  350. }
  351. if err := ctx.WriteYAML(fileName, v...); err != nil {
  352. return err
  353. }
  354. }
  355. return nil
  356. }
  357. func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
  358. if sideEffects == nil {
  359. return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
  360. }
  361. if *sideEffects == admissionregv1.SideEffectClassUnknown ||
  362. *sideEffects == admissionregv1.SideEffectClassSome {
  363. return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
  364. }
  365. return nil
  366. }