parser.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  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>,timeoutSeconds=<int>,admissionReviewVersions=<[]string>,reinvocationPolicy=<string>
  19. package webhook
  20. import (
  21. "fmt"
  22. "sort"
  23. "strings"
  24. admissionregv1 "k8s.io/api/admissionregistration/v1"
  25. "k8s.io/apimachinery/pkg/runtime/schema"
  26. "k8s.io/apimachinery/pkg/util/sets"
  27. "sigs.k8s.io/controller-tools/pkg/genall"
  28. "sigs.k8s.io/controller-tools/pkg/markers"
  29. )
  30. // The default {Mutating,Validating}WebhookConfiguration version to generate.
  31. const (
  32. v1 = "v1"
  33. defaultWebhookVersion = v1
  34. )
  35. var (
  36. // ConfigDefinition s a marker for defining Webhook manifests.
  37. // Call ToWebhook on the value to get a Kubernetes Webhook.
  38. ConfigDefinition = markers.Must(markers.MakeDefinition("kubebuilder:webhook", markers.DescribesPackage, Config{}))
  39. )
  40. // supportedWebhookVersions returns currently supported API version of {Mutating,Validating}WebhookConfiguration.
  41. func supportedWebhookVersions() []string {
  42. return []string{defaultWebhookVersion}
  43. }
  44. // +controllertools:marker:generateHelp:category=Webhook
  45. // Config specifies how a webhook should be served.
  46. //
  47. // It specifies only the details that are intrinsic to the application serving
  48. // it (e.g. the resources it can handle, or the path it serves on).
  49. type Config struct {
  50. // Mutating marks this as a mutating webhook (it's validating only if false)
  51. //
  52. // Mutating webhooks are allowed to change the object in their response,
  53. // and are called *before* all validating webhooks. Mutating webhooks may
  54. // choose to reject an object, similarly to a validating webhook.
  55. Mutating bool
  56. // FailurePolicy specifies what should happen if the API server cannot reach the webhook.
  57. //
  58. // It may be either "ignore" (to skip the webhook and continue on) or "fail" (to reject
  59. // the object in question).
  60. FailurePolicy string
  61. // MatchPolicy defines how the "rules" list is used to match incoming requests.
  62. // Allowed values are "Exact" (match only if it exactly matches the specified rule)
  63. // or "Equivalent" (match a request if it modifies a resource listed in rules, even via another API group or version).
  64. MatchPolicy string `marker:",optional"`
  65. // SideEffects specify whether calling the webhook will have side effects.
  66. // This has an impact on dry runs and `kubectl diff`: if the sideEffect is "Unknown" (the default) or "Some", then
  67. // the API server will not call the webhook on a dry-run request and fails instead.
  68. // If the value is "None", then the webhook has no side effects and the API server will call it on dry-run.
  69. // If the value is "NoneOnDryRun", then the webhook is responsible for inspecting the "dryRun" property of the
  70. // AdmissionReview sent in the request, and avoiding side effects if that value is "true."
  71. SideEffects string `marker:",optional"`
  72. // TimeoutSeconds allows configuring how long the API server should wait for a webhook to respond before treating the call as a failure.
  73. // If the timeout expires before the webhook responds, the webhook call will be ignored or the API call will be rejected based on the failure policy.
  74. // The timeout value must be between 1 and 30 seconds.
  75. // The timeout for an admission webhook defaults to 10 seconds.
  76. TimeoutSeconds int `marker:",optional"`
  77. // Groups specifies the API groups that this webhook receives requests for.
  78. Groups []string
  79. // Resources specifies the API resources that this webhook receives requests for.
  80. Resources []string
  81. // Verbs specifies the Kubernetes API verbs that this webhook receives requests for.
  82. //
  83. // Only modification-like verbs may be specified.
  84. // May be "create", "update", "delete", "connect", or "*" (for all).
  85. Verbs []string
  86. // Versions specifies the API versions that this webhook receives requests for.
  87. Versions []string
  88. // Name indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
  89. Name string
  90. // Path specifies that path that the API server should connect to this webhook on. Must be
  91. // prefixed with a '/validate-' or '/mutate-' depending on the type, and followed by
  92. // $GROUP-$VERSION-$KIND where all values are lower-cased and the periods in the group
  93. // are substituted for hyphens. For example, a validating webhook path for type
  94. // batch.tutorial.kubebuilder.io/v1,Kind=CronJob would be
  95. // /validate-batch-tutorial-kubebuilder-io-v1-cronjob
  96. Path string `marker:"path,optional"`
  97. // WebhookVersions specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
  98. // itself to generate. The only supported value is v1. Defaults to v1.
  99. WebhookVersions []string `marker:"webhookVersions,optional"`
  100. // AdmissionReviewVersions is an ordered list of preferred `AdmissionReview`
  101. // versions the Webhook expects.
  102. AdmissionReviewVersions []string `marker:"admissionReviewVersions"`
  103. // ReinvocationPolicy allows mutating webhooks to request reinvocation after other mutations
  104. //
  105. // To allow mutating admission plugins to observe changes made by other plugins,
  106. // built-in mutating admission plugins are re-run if a mutating webhook modifies
  107. // an object, and mutating webhooks can specify a reinvocationPolicy to control
  108. // whether they are reinvoked as well.
  109. ReinvocationPolicy string `marker:"reinvocationPolicy,optional"`
  110. // URL allows mutating webhooks configuration to specify an external URL when generating
  111. // the manifests, instead of using the internal service communication. Should be in format of
  112. // https://address:port/path
  113. // When this option is specified, the serviceConfig.Service is removed from webhook the manifest.
  114. // The URL configuration should be between quotes.
  115. // `url` cannot be specified when `path` is specified.
  116. URL string `marker:"url,optional"`
  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. // 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. matchPolicy, err := c.matchPolicy()
  142. if err != nil {
  143. return admissionregv1.MutatingWebhook{}, err
  144. }
  145. clientConfig, err := c.clientConfig()
  146. if err != nil {
  147. return admissionregv1.MutatingWebhook{}, err
  148. }
  149. return admissionregv1.MutatingWebhook{
  150. Name: c.Name,
  151. Rules: c.rules(),
  152. FailurePolicy: c.failurePolicy(),
  153. MatchPolicy: matchPolicy,
  154. ClientConfig: clientConfig,
  155. SideEffects: c.sideEffects(),
  156. TimeoutSeconds: c.timeoutSeconds(),
  157. AdmissionReviewVersions: c.AdmissionReviewVersions,
  158. ReinvocationPolicy: c.reinvocationPolicy(),
  159. }, nil
  160. }
  161. // ToValidatingWebhook converts this rule to its Kubernetes API form.
  162. func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) {
  163. if c.Mutating {
  164. return admissionregv1.ValidatingWebhook{}, fmt.Errorf("%s is a mutating webhook", c.Name)
  165. }
  166. matchPolicy, err := c.matchPolicy()
  167. if err != nil {
  168. return admissionregv1.ValidatingWebhook{}, err
  169. }
  170. clientConfig, err := c.clientConfig()
  171. if err != nil {
  172. return admissionregv1.ValidatingWebhook{}, err
  173. }
  174. return admissionregv1.ValidatingWebhook{
  175. Name: c.Name,
  176. Rules: c.rules(),
  177. FailurePolicy: c.failurePolicy(),
  178. MatchPolicy: matchPolicy,
  179. ClientConfig: clientConfig,
  180. SideEffects: c.sideEffects(),
  181. TimeoutSeconds: c.timeoutSeconds(),
  182. AdmissionReviewVersions: c.AdmissionReviewVersions,
  183. }, nil
  184. }
  185. // rules returns the configuration of what operations on what
  186. // resources/subresources a webhook should care about.
  187. func (c Config) rules() []admissionregv1.RuleWithOperations {
  188. whConfig := admissionregv1.RuleWithOperations{
  189. Rule: admissionregv1.Rule{
  190. APIGroups: c.Groups,
  191. APIVersions: c.Versions,
  192. Resources: c.Resources,
  193. },
  194. Operations: make([]admissionregv1.OperationType, len(c.Verbs)),
  195. }
  196. for i, verbRaw := range c.Verbs {
  197. whConfig.Operations[i] = verbToAPIVariant(verbRaw)
  198. }
  199. // fix the group names, since letting people type "core" is nice
  200. for i, group := range whConfig.APIGroups {
  201. if group == "core" {
  202. whConfig.APIGroups[i] = ""
  203. }
  204. }
  205. return []admissionregv1.RuleWithOperations{whConfig}
  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. // 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. // clientConfig returns the client config for a webhook.
  237. func (c Config) clientConfig() (admissionregv1.WebhookClientConfig, error) {
  238. if (c.Path != "" && c.URL != "") || (c.Path == "" && c.URL == "") {
  239. return admissionregv1.WebhookClientConfig{}, fmt.Errorf("`url` or `path` markers are required and mutually exclusive")
  240. }
  241. path := c.Path
  242. if path != "" {
  243. return admissionregv1.WebhookClientConfig{
  244. Service: &admissionregv1.ServiceReference{
  245. Name: "webhook-service",
  246. Namespace: "system",
  247. Path: &path,
  248. },
  249. }, nil
  250. }
  251. url := c.URL
  252. return admissionregv1.WebhookClientConfig{
  253. URL: &url,
  254. }, nil
  255. }
  256. // sideEffects returns the sideEffects config for a webhook.
  257. func (c Config) sideEffects() *admissionregv1.SideEffectClass {
  258. var sideEffects admissionregv1.SideEffectClass
  259. switch strings.ToLower(c.SideEffects) {
  260. case strings.ToLower(string(admissionregv1.SideEffectClassNone)):
  261. sideEffects = admissionregv1.SideEffectClassNone
  262. case strings.ToLower(string(admissionregv1.SideEffectClassNoneOnDryRun)):
  263. sideEffects = admissionregv1.SideEffectClassNoneOnDryRun
  264. case strings.ToLower(string(admissionregv1.SideEffectClassSome)):
  265. sideEffects = admissionregv1.SideEffectClassSome
  266. case "":
  267. return nil
  268. default:
  269. return nil
  270. }
  271. return &sideEffects
  272. }
  273. // timeoutSeconds returns the timeoutSeconds config for a webhook.
  274. func (c Config) timeoutSeconds() *int32 {
  275. if c.TimeoutSeconds != 0 {
  276. timeoutSeconds := int32(c.TimeoutSeconds)
  277. return &timeoutSeconds
  278. }
  279. return nil
  280. }
  281. // reinvocationPolicy returns the reinvocationPolicy config for a mutating webhook.
  282. func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType {
  283. var reinvocationPolicy admissionregv1.ReinvocationPolicyType
  284. switch strings.ToLower(c.ReinvocationPolicy) {
  285. case strings.ToLower(string(admissionregv1.NeverReinvocationPolicy)):
  286. reinvocationPolicy = admissionregv1.NeverReinvocationPolicy
  287. case strings.ToLower(string(admissionregv1.IfNeededReinvocationPolicy)):
  288. reinvocationPolicy = admissionregv1.IfNeededReinvocationPolicy
  289. default:
  290. return nil
  291. }
  292. return &reinvocationPolicy
  293. }
  294. // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook.
  295. func (c Config) webhookVersions() ([]string, error) {
  296. // If WebhookVersions is not specified, we default it to `v1`.
  297. if len(c.WebhookVersions) == 0 {
  298. return []string{defaultWebhookVersion}, nil
  299. }
  300. supportedWebhookVersions := sets.NewString(supportedWebhookVersions()...)
  301. for _, version := range c.WebhookVersions {
  302. if !supportedWebhookVersions.Has(version) {
  303. return nil, fmt.Errorf("unsupported webhook version: %s", version)
  304. }
  305. }
  306. return sets.NewString(c.WebhookVersions...).UnsortedList(), nil
  307. }
  308. // +controllertools:marker:generateHelp
  309. // Generator generates (partial) {Mutating,Validating}WebhookConfiguration objects.
  310. type Generator struct {
  311. // HeaderFile specifies the header text (e.g. license) to prepend to generated files.
  312. HeaderFile string `marker:",optional"`
  313. // Year specifies the year to substitute for " YEAR" in the header file.
  314. Year string `marker:",optional"`
  315. }
  316. func (Generator) RegisterMarkers(into *markers.Registry) error {
  317. if err := into.Register(ConfigDefinition); err != nil {
  318. return err
  319. }
  320. into.AddHelp(ConfigDefinition, Config{}.Help())
  321. return nil
  322. }
  323. func (g Generator) Generate(ctx *genall.GenerationContext) error {
  324. supportedWebhookVersions := supportedWebhookVersions()
  325. mutatingCfgs := make(map[string][]admissionregv1.MutatingWebhook, len(supportedWebhookVersions))
  326. validatingCfgs := make(map[string][]admissionregv1.ValidatingWebhook, len(supportedWebhookVersions))
  327. for _, root := range ctx.Roots {
  328. markerSet, err := markers.PackageMarkers(ctx.Collector, root)
  329. if err != nil {
  330. root.AddError(err)
  331. }
  332. cfgs := markerSet[ConfigDefinition.Name]
  333. sort.SliceStable(cfgs, func(i, j int) bool {
  334. return cfgs[i].(Config).Name < cfgs[j].(Config).Name
  335. })
  336. for _, cfg := range cfgs {
  337. cfg := cfg.(Config)
  338. webhookVersions, err := cfg.webhookVersions()
  339. if err != nil {
  340. return err
  341. }
  342. if cfg.Mutating {
  343. w, err := cfg.ToMutatingWebhook()
  344. if err != nil {
  345. return err
  346. }
  347. for _, webhookVersion := range webhookVersions {
  348. mutatingCfgs[webhookVersion] = append(mutatingCfgs[webhookVersion], w)
  349. }
  350. } else {
  351. w, err := cfg.ToValidatingWebhook()
  352. if err != nil {
  353. return err
  354. }
  355. for _, webhookVersion := range webhookVersions {
  356. validatingCfgs[webhookVersion] = append(validatingCfgs[webhookVersion], w)
  357. }
  358. }
  359. }
  360. }
  361. versionedWebhooks := make(map[string][]interface{}, len(supportedWebhookVersions))
  362. for _, version := range supportedWebhookVersions {
  363. if cfgs, ok := mutatingCfgs[version]; ok {
  364. // The only possible version in supportedWebhookVersions is v1,
  365. // so use it for all versioned types in this context.
  366. objRaw := &admissionregv1.MutatingWebhookConfiguration{}
  367. objRaw.SetGroupVersionKind(schema.GroupVersionKind{
  368. Group: admissionregv1.SchemeGroupVersion.Group,
  369. Version: version,
  370. Kind: "MutatingWebhookConfiguration",
  371. })
  372. objRaw.SetName("mutating-webhook-configuration")
  373. objRaw.Webhooks = cfgs
  374. for i := range objRaw.Webhooks {
  375. // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
  376. // return an error
  377. if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
  378. return err
  379. }
  380. // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise,
  381. // return an error
  382. if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil {
  383. return err
  384. }
  385. // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
  386. // return an error
  387. if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
  388. return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
  389. }
  390. }
  391. versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
  392. }
  393. if cfgs, ok := validatingCfgs[version]; ok {
  394. // The only possible version in supportedWebhookVersions is v1,
  395. // so use it for all versioned types in this context.
  396. objRaw := &admissionregv1.ValidatingWebhookConfiguration{}
  397. objRaw.SetGroupVersionKind(schema.GroupVersionKind{
  398. Group: admissionregv1.SchemeGroupVersion.Group,
  399. Version: version,
  400. Kind: "ValidatingWebhookConfiguration",
  401. })
  402. objRaw.SetName("validating-webhook-configuration")
  403. objRaw.Webhooks = cfgs
  404. for i := range objRaw.Webhooks {
  405. // SideEffects is required in admissionregistration/v1, if this is not set or set to `Some` or `Known`,
  406. // return an error
  407. if err := checkSideEffectsForV1(objRaw.Webhooks[i].SideEffects); err != nil {
  408. return err
  409. }
  410. // TimeoutSeconds must be nil or between 1 and 30 seconds, otherwise,
  411. // return an error
  412. if err := checkTimeoutSeconds(objRaw.Webhooks[i].TimeoutSeconds); err != nil {
  413. return err
  414. }
  415. // AdmissionReviewVersions is required in admissionregistration/v1, if this is not set,
  416. // return an error
  417. if len(objRaw.Webhooks[i].AdmissionReviewVersions) == 0 {
  418. return fmt.Errorf("AdmissionReviewVersions is mandatory for v1 {Mutating,Validating}WebhookConfiguration")
  419. }
  420. }
  421. versionedWebhooks[version] = append(versionedWebhooks[version], objRaw)
  422. }
  423. }
  424. var headerText string
  425. if g.HeaderFile != "" {
  426. headerBytes, err := ctx.ReadFile(g.HeaderFile)
  427. if err != nil {
  428. return err
  429. }
  430. headerText = string(headerBytes)
  431. }
  432. headerText = strings.ReplaceAll(headerText, " YEAR", " "+g.Year)
  433. for k, v := range versionedWebhooks {
  434. var fileName string
  435. if k == defaultWebhookVersion {
  436. fileName = fmt.Sprintf("manifests.yaml")
  437. } else {
  438. fileName = fmt.Sprintf("manifests.%s.yaml", k)
  439. }
  440. if err := ctx.WriteYAML(fileName, headerText, v, genall.WithTransform(genall.TransformRemoveCreationTimestamp)); err != nil {
  441. return err
  442. }
  443. }
  444. return nil
  445. }
  446. func checkSideEffectsForV1(sideEffects *admissionregv1.SideEffectClass) error {
  447. if sideEffects == nil {
  448. return fmt.Errorf("SideEffects is required for creating v1 {Mutating,Validating}WebhookConfiguration")
  449. }
  450. if *sideEffects == admissionregv1.SideEffectClassUnknown ||
  451. *sideEffects == admissionregv1.SideEffectClassSome {
  452. return fmt.Errorf("SideEffects should not be set to `Some` or `Unknown` for v1 {Mutating,Validating}WebhookConfiguration")
  453. }
  454. return nil
  455. }
  456. func checkTimeoutSeconds(timeoutSeconds *int32) error {
  457. if timeoutSeconds != nil && (*timeoutSeconds < 1 || *timeoutSeconds > 30) {
  458. return fmt.Errorf("TimeoutSeconds must be between 1 and 30 seconds")
  459. }
  460. return nil
  461. }