validation.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /*
  2. Copyright 2017 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 validation
  14. import (
  15. "fmt"
  16. "strings"
  17. genericvalidation "k8s.io/apimachinery/pkg/api/validation"
  18. metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
  19. "k8s.io/apimachinery/pkg/util/sets"
  20. utilvalidation "k8s.io/apimachinery/pkg/util/validation"
  21. "k8s.io/apimachinery/pkg/util/validation/field"
  22. "k8s.io/apiserver/pkg/util/webhook"
  23. "k8s.io/kubernetes/pkg/apis/admissionregistration"
  24. "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
  25. )
  26. func hasWildcard(slice []string) bool {
  27. for _, s := range slice {
  28. if s == "*" {
  29. return true
  30. }
  31. }
  32. return false
  33. }
  34. func validateResources(resources []string, fldPath *field.Path) field.ErrorList {
  35. var allErrors field.ErrorList
  36. if len(resources) == 0 {
  37. allErrors = append(allErrors, field.Required(fldPath, ""))
  38. }
  39. // */x
  40. resourcesWithWildcardSubresoures := sets.String{}
  41. // x/*
  42. subResourcesWithWildcardResource := sets.String{}
  43. // */*
  44. hasDoubleWildcard := false
  45. // *
  46. hasSingleWildcard := false
  47. // x
  48. hasResourceWithoutSubresource := false
  49. for i, resSub := range resources {
  50. if resSub == "" {
  51. allErrors = append(allErrors, field.Required(fldPath.Index(i), ""))
  52. continue
  53. }
  54. if resSub == "*/*" {
  55. hasDoubleWildcard = true
  56. }
  57. if resSub == "*" {
  58. hasSingleWildcard = true
  59. }
  60. parts := strings.SplitN(resSub, "/", 2)
  61. if len(parts) == 1 {
  62. hasResourceWithoutSubresource = resSub != "*"
  63. continue
  64. }
  65. res, sub := parts[0], parts[1]
  66. if _, ok := resourcesWithWildcardSubresoures[res]; ok {
  67. allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '%s/*' is present, must not specify %s", res, resSub)))
  68. }
  69. if _, ok := subResourcesWithWildcardResource[sub]; ok {
  70. allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resSub, fmt.Sprintf("if '*/%s' is present, must not specify %s", sub, resSub)))
  71. }
  72. if sub == "*" {
  73. resourcesWithWildcardSubresoures[res] = struct{}{}
  74. }
  75. if res == "*" {
  76. subResourcesWithWildcardResource[sub] = struct{}{}
  77. }
  78. }
  79. if len(resources) > 1 && hasDoubleWildcard {
  80. allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*/*' is present, must not specify other resources"))
  81. }
  82. if hasSingleWildcard && hasResourceWithoutSubresource {
  83. allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources without subresources"))
  84. }
  85. return allErrors
  86. }
  87. func validateResourcesNoSubResources(resources []string, fldPath *field.Path) field.ErrorList {
  88. var allErrors field.ErrorList
  89. if len(resources) == 0 {
  90. allErrors = append(allErrors, field.Required(fldPath, ""))
  91. }
  92. for i, resource := range resources {
  93. if resource == "" {
  94. allErrors = append(allErrors, field.Required(fldPath.Index(i), ""))
  95. }
  96. if strings.Contains(resource, "/") {
  97. allErrors = append(allErrors, field.Invalid(fldPath.Index(i), resource, "must not specify subresources"))
  98. }
  99. }
  100. if len(resources) > 1 && hasWildcard(resources) {
  101. allErrors = append(allErrors, field.Invalid(fldPath, resources, "if '*' is present, must not specify other resources"))
  102. }
  103. return allErrors
  104. }
  105. var validScopes = sets.NewString(
  106. string(admissionregistration.ClusterScope),
  107. string(admissionregistration.NamespacedScope),
  108. string(admissionregistration.AllScopes),
  109. )
  110. func validateRule(rule *admissionregistration.Rule, fldPath *field.Path, allowSubResource bool) field.ErrorList {
  111. var allErrors field.ErrorList
  112. if len(rule.APIGroups) == 0 {
  113. allErrors = append(allErrors, field.Required(fldPath.Child("apiGroups"), ""))
  114. }
  115. if len(rule.APIGroups) > 1 && hasWildcard(rule.APIGroups) {
  116. allErrors = append(allErrors, field.Invalid(fldPath.Child("apiGroups"), rule.APIGroups, "if '*' is present, must not specify other API groups"))
  117. }
  118. // Note: group could be empty, e.g., the legacy "v1" API
  119. if len(rule.APIVersions) == 0 {
  120. allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions"), ""))
  121. }
  122. if len(rule.APIVersions) > 1 && hasWildcard(rule.APIVersions) {
  123. allErrors = append(allErrors, field.Invalid(fldPath.Child("apiVersions"), rule.APIVersions, "if '*' is present, must not specify other API versions"))
  124. }
  125. for i, version := range rule.APIVersions {
  126. if version == "" {
  127. allErrors = append(allErrors, field.Required(fldPath.Child("apiVersions").Index(i), ""))
  128. }
  129. }
  130. if allowSubResource {
  131. allErrors = append(allErrors, validateResources(rule.Resources, fldPath.Child("resources"))...)
  132. } else {
  133. allErrors = append(allErrors, validateResourcesNoSubResources(rule.Resources, fldPath.Child("resources"))...)
  134. }
  135. if rule.Scope != nil && !validScopes.Has(string(*rule.Scope)) {
  136. allErrors = append(allErrors, field.NotSupported(fldPath.Child("scope"), *rule.Scope, validScopes.List()))
  137. }
  138. return allErrors
  139. }
  140. var AcceptedAdmissionReviewVersions = []string{v1beta1.SchemeGroupVersion.Version}
  141. func isAcceptedAdmissionReviewVersion(v string) bool {
  142. for _, version := range AcceptedAdmissionReviewVersions {
  143. if v == version {
  144. return true
  145. }
  146. }
  147. return false
  148. }
  149. func validateAdmissionReviewVersions(versions []string, requireRecognizedVersion bool, fldPath *field.Path) field.ErrorList {
  150. allErrors := field.ErrorList{}
  151. // Currently only v1beta1 accepted in AdmissionReviewVersions
  152. if len(versions) < 1 {
  153. allErrors = append(allErrors, field.Required(fldPath, ""))
  154. } else {
  155. seen := map[string]bool{}
  156. hasAcceptedVersion := false
  157. for i, v := range versions {
  158. if seen[v] {
  159. allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, "duplicate version"))
  160. continue
  161. }
  162. seen[v] = true
  163. for _, errString := range utilvalidation.IsDNS1035Label(v) {
  164. allErrors = append(allErrors, field.Invalid(fldPath.Index(i), v, errString))
  165. }
  166. if isAcceptedAdmissionReviewVersion(v) {
  167. hasAcceptedVersion = true
  168. }
  169. }
  170. if requireRecognizedVersion && !hasAcceptedVersion {
  171. allErrors = append(allErrors, field.Invalid(
  172. fldPath, versions,
  173. fmt.Sprintf("none of the versions accepted by this server. accepted version(s) are %v",
  174. strings.Join(AcceptedAdmissionReviewVersions, ", "))))
  175. }
  176. }
  177. return allErrors
  178. }
  179. func ValidateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList {
  180. return validateValidatingWebhookConfiguration(e, true)
  181. }
  182. func validateValidatingWebhookConfiguration(e *admissionregistration.ValidatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList {
  183. allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
  184. for i, hook := range e.Webhooks {
  185. allErrors = append(allErrors, validateValidatingWebhook(&hook, field.NewPath("webhooks").Index(i))...)
  186. allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
  187. }
  188. return allErrors
  189. }
  190. func ValidateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration) field.ErrorList {
  191. return validateMutatingWebhookConfiguration(e, true)
  192. }
  193. func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, requireRecognizedVersion bool) field.ErrorList {
  194. allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
  195. for i, hook := range e.Webhooks {
  196. allErrors = append(allErrors, validateMutatingWebhook(&hook, field.NewPath("webhooks").Index(i))...)
  197. allErrors = append(allErrors, validateAdmissionReviewVersions(hook.AdmissionReviewVersions, requireRecognizedVersion, field.NewPath("webhooks").Index(i).Child("admissionReviewVersions"))...)
  198. }
  199. return allErrors
  200. }
  201. func validateValidatingWebhook(hook *admissionregistration.ValidatingWebhook, fldPath *field.Path) field.ErrorList {
  202. var allErrors field.ErrorList
  203. // hook.Name must be fully qualified
  204. allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...)
  205. for i, rule := range hook.Rules {
  206. allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...)
  207. }
  208. if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) {
  209. allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
  210. }
  211. if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) {
  212. allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List()))
  213. }
  214. if hook.SideEffects != nil && !supportedSideEffectClasses.Has(string(*hook.SideEffects)) {
  215. allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, supportedSideEffectClasses.List()))
  216. }
  217. if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) {
  218. allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds"))
  219. }
  220. if hook.NamespaceSelector != nil {
  221. allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
  222. }
  223. if hook.ObjectSelector != nil {
  224. allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, fldPath.Child("objectSelector"))...)
  225. }
  226. cc := hook.ClientConfig
  227. switch {
  228. case (cc.URL == nil) == (cc.Service == nil):
  229. allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required"))
  230. case cc.URL != nil:
  231. allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...)
  232. case cc.Service != nil:
  233. allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...)
  234. }
  235. return allErrors
  236. }
  237. func validateMutatingWebhook(hook *admissionregistration.MutatingWebhook, fldPath *field.Path) field.ErrorList {
  238. var allErrors field.ErrorList
  239. // hook.Name must be fully qualified
  240. allErrors = append(allErrors, utilvalidation.IsFullyQualifiedName(fldPath.Child("name"), hook.Name)...)
  241. for i, rule := range hook.Rules {
  242. allErrors = append(allErrors, validateRuleWithOperations(&rule, fldPath.Child("rules").Index(i))...)
  243. }
  244. if hook.FailurePolicy != nil && !supportedFailurePolicies.Has(string(*hook.FailurePolicy)) {
  245. allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *hook.FailurePolicy, supportedFailurePolicies.List()))
  246. }
  247. if hook.MatchPolicy != nil && !supportedMatchPolicies.Has(string(*hook.MatchPolicy)) {
  248. allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchPolicy"), *hook.MatchPolicy, supportedMatchPolicies.List()))
  249. }
  250. if hook.SideEffects != nil && !supportedSideEffectClasses.Has(string(*hook.SideEffects)) {
  251. allErrors = append(allErrors, field.NotSupported(fldPath.Child("sideEffects"), *hook.SideEffects, supportedSideEffectClasses.List()))
  252. }
  253. if hook.TimeoutSeconds != nil && (*hook.TimeoutSeconds > 30 || *hook.TimeoutSeconds < 1) {
  254. allErrors = append(allErrors, field.Invalid(fldPath.Child("timeoutSeconds"), *hook.TimeoutSeconds, "the timeout value must be between 1 and 30 seconds"))
  255. }
  256. if hook.NamespaceSelector != nil {
  257. allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.NamespaceSelector, fldPath.Child("namespaceSelector"))...)
  258. }
  259. if hook.ObjectSelector != nil {
  260. allErrors = append(allErrors, metav1validation.ValidateLabelSelector(hook.ObjectSelector, fldPath.Child("objectSelector"))...)
  261. }
  262. if hook.ReinvocationPolicy != nil && !supportedReinvocationPolicies.Has(string(*hook.ReinvocationPolicy)) {
  263. allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), *hook.ReinvocationPolicy, supportedReinvocationPolicies.List()))
  264. }
  265. cc := hook.ClientConfig
  266. switch {
  267. case (cc.URL == nil) == (cc.Service == nil):
  268. allErrors = append(allErrors, field.Required(fldPath.Child("clientConfig"), "exactly one of url or service is required"))
  269. case cc.URL != nil:
  270. allErrors = append(allErrors, webhook.ValidateWebhookURL(fldPath.Child("clientConfig").Child("url"), *cc.URL, true)...)
  271. case cc.Service != nil:
  272. allErrors = append(allErrors, webhook.ValidateWebhookService(fldPath.Child("clientConfig").Child("service"), cc.Service.Name, cc.Service.Namespace, cc.Service.Path, cc.Service.Port)...)
  273. }
  274. return allErrors
  275. }
  276. var supportedFailurePolicies = sets.NewString(
  277. string(admissionregistration.Ignore),
  278. string(admissionregistration.Fail),
  279. )
  280. var supportedMatchPolicies = sets.NewString(
  281. string(admissionregistration.Exact),
  282. string(admissionregistration.Equivalent),
  283. )
  284. var supportedSideEffectClasses = sets.NewString(
  285. string(admissionregistration.SideEffectClassUnknown),
  286. string(admissionregistration.SideEffectClassNone),
  287. string(admissionregistration.SideEffectClassSome),
  288. string(admissionregistration.SideEffectClassNoneOnDryRun),
  289. )
  290. var supportedOperations = sets.NewString(
  291. string(admissionregistration.OperationAll),
  292. string(admissionregistration.Create),
  293. string(admissionregistration.Update),
  294. string(admissionregistration.Delete),
  295. string(admissionregistration.Connect),
  296. )
  297. var supportedReinvocationPolicies = sets.NewString(
  298. string(admissionregistration.NeverReinvocationPolicy),
  299. string(admissionregistration.IfNeededReinvocationPolicy),
  300. )
  301. func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
  302. for _, o := range operations {
  303. if o == admissionregistration.OperationAll {
  304. return true
  305. }
  306. }
  307. return false
  308. }
  309. func validateRuleWithOperations(ruleWithOperations *admissionregistration.RuleWithOperations, fldPath *field.Path) field.ErrorList {
  310. var allErrors field.ErrorList
  311. if len(ruleWithOperations.Operations) == 0 {
  312. allErrors = append(allErrors, field.Required(fldPath.Child("operations"), ""))
  313. }
  314. if len(ruleWithOperations.Operations) > 1 && hasWildcardOperation(ruleWithOperations.Operations) {
  315. allErrors = append(allErrors, field.Invalid(fldPath.Child("operations"), ruleWithOperations.Operations, "if '*' is present, must not specify other operations"))
  316. }
  317. for i, operation := range ruleWithOperations.Operations {
  318. if !supportedOperations.Has(string(operation)) {
  319. allErrors = append(allErrors, field.NotSupported(fldPath.Child("operations").Index(i), operation, supportedOperations.List()))
  320. }
  321. }
  322. allowSubResource := true
  323. allErrors = append(allErrors, validateRule(&ruleWithOperations.Rule, fldPath, allowSubResource)...)
  324. return allErrors
  325. }
  326. // mutatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one
  327. // admission review version this apiserver accepts.
  328. func mutatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.MutatingWebhook) bool {
  329. for _, hook := range webhooks {
  330. hasRecognizedVersion := false
  331. for _, version := range hook.AdmissionReviewVersions {
  332. if isAcceptedAdmissionReviewVersion(version) {
  333. hasRecognizedVersion = true
  334. break
  335. }
  336. }
  337. if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 {
  338. return false
  339. }
  340. }
  341. return true
  342. }
  343. // validatingHasAcceptedAdmissionReviewVersions returns true if all webhooks have at least one
  344. // admission review version this apiserver accepts.
  345. func validatingHasAcceptedAdmissionReviewVersions(webhooks []admissionregistration.ValidatingWebhook) bool {
  346. for _, hook := range webhooks {
  347. hasRecognizedVersion := false
  348. for _, version := range hook.AdmissionReviewVersions {
  349. if isAcceptedAdmissionReviewVersion(version) {
  350. hasRecognizedVersion = true
  351. break
  352. }
  353. }
  354. if !hasRecognizedVersion && len(hook.AdmissionReviewVersions) > 0 {
  355. return false
  356. }
  357. }
  358. return true
  359. }
  360. func ValidateValidatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.ValidatingWebhookConfiguration) field.ErrorList {
  361. return validateValidatingWebhookConfiguration(newC, validatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks))
  362. }
  363. func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistration.MutatingWebhookConfiguration) field.ErrorList {
  364. return validateMutatingWebhookConfiguration(newC, mutatingHasAcceptedAdmissionReviewVersions(oldC.Webhooks))
  365. }