webhook.go 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734
  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 apimachinery
  14. import (
  15. "fmt"
  16. "reflect"
  17. "strings"
  18. "time"
  19. "k8s.io/api/admissionregistration/v1beta1"
  20. apps "k8s.io/api/apps/v1"
  21. v1 "k8s.io/api/core/v1"
  22. rbacv1beta1 "k8s.io/api/rbac/v1beta1"
  23. apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
  24. crdclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
  25. "k8s.io/apimachinery/pkg/api/errors"
  26. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  27. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  28. "k8s.io/apimachinery/pkg/types"
  29. "k8s.io/apimachinery/pkg/util/intstr"
  30. utilversion "k8s.io/apimachinery/pkg/util/version"
  31. "k8s.io/apimachinery/pkg/util/wait"
  32. "k8s.io/client-go/dynamic"
  33. clientset "k8s.io/client-go/kubernetes"
  34. "k8s.io/kubernetes/test/e2e/framework"
  35. e2edeploy "k8s.io/kubernetes/test/e2e/framework/deployment"
  36. e2elog "k8s.io/kubernetes/test/e2e/framework/log"
  37. "k8s.io/kubernetes/test/utils/crd"
  38. imageutils "k8s.io/kubernetes/test/utils/image"
  39. "k8s.io/utils/pointer"
  40. "github.com/onsi/ginkgo"
  41. "github.com/onsi/gomega"
  42. // ensure libs have a chance to initialize
  43. _ "github.com/stretchr/testify/assert"
  44. )
  45. const (
  46. secretName = "sample-webhook-secret"
  47. deploymentName = "sample-webhook-deployment"
  48. serviceName = "e2e-test-webhook"
  49. servicePort = 8443
  50. roleBindingName = "webhook-auth-reader"
  51. // The webhook configuration names should not be reused between test instances.
  52. crWebhookConfigName = "e2e-test-webhook-config-cr"
  53. webhookConfigName = "e2e-test-webhook-config"
  54. attachingPodWebhookConfigName = "e2e-test-webhook-config-attaching-pod"
  55. mutatingWebhookConfigName = "e2e-test-mutating-webhook-config"
  56. podMutatingWebhookConfigName = "e2e-test-mutating-webhook-pod"
  57. webhookFailClosedConfigName = "e2e-test-webhook-fail-closed"
  58. validatingWebhookForWebhooksConfigName = "e2e-test-validating-webhook-for-webhooks-config"
  59. mutatingWebhookForWebhooksConfigName = "e2e-test-mutating-webhook-for-webhooks-config"
  60. dummyValidatingWebhookConfigName = "e2e-test-dummy-validating-webhook-config"
  61. dummyMutatingWebhookConfigName = "e2e-test-dummy-mutating-webhook-config"
  62. crdWebhookConfigName = "e2e-test-webhook-config-crd"
  63. slowWebhookConfigName = "e2e-test-webhook-config-slow"
  64. skipNamespaceLabelKey = "skip-webhook-admission"
  65. skipNamespaceLabelValue = "yes"
  66. skippedNamespaceName = "exempted-namesapce"
  67. disallowedPodName = "disallowed-pod"
  68. toBeAttachedPodName = "to-be-attached-pod"
  69. hangingPodName = "hanging-pod"
  70. disallowedConfigMapName = "disallowed-configmap"
  71. nonDeletableConfigmapName = "nondeletable-configmap"
  72. allowedConfigMapName = "allowed-configmap"
  73. failNamespaceLabelKey = "fail-closed-webhook"
  74. failNamespaceLabelValue = "yes"
  75. failNamespaceName = "fail-closed-namesapce"
  76. addedLabelKey = "added-label"
  77. addedLabelValue = "yes"
  78. )
  79. var serverWebhookVersion = utilversion.MustParseSemantic("v1.8.0")
  80. var _ = SIGDescribe("AdmissionWebhook", func() {
  81. var context *certContext
  82. f := framework.NewDefaultFramework("webhook")
  83. var client clientset.Interface
  84. var namespaceName string
  85. ginkgo.BeforeEach(func() {
  86. client = f.ClientSet
  87. namespaceName = f.Namespace.Name
  88. // Make sure the relevant provider supports admission webhook
  89. framework.SkipUnlessServerVersionGTE(serverWebhookVersion, f.ClientSet.Discovery())
  90. framework.SkipUnlessProviderIs("gce", "gke", "local")
  91. _, err := f.ClientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().List(metav1.ListOptions{})
  92. if errors.IsNotFound(err) {
  93. framework.Skipf("dynamic configuration of webhooks requires the admissionregistration.k8s.io group to be enabled")
  94. }
  95. ginkgo.By("Setting up server cert")
  96. context = setupServerCert(namespaceName, serviceName)
  97. createAuthReaderRoleBinding(f, namespaceName)
  98. // Note that in 1.9 we will have backwards incompatible change to
  99. // admission webhooks, so the image will be updated to 1.9 sometime in
  100. // the development 1.9 cycle.
  101. deployWebhookAndService(f, imageutils.GetE2EImage(imageutils.AdmissionWebhook), context)
  102. })
  103. ginkgo.AfterEach(func() {
  104. cleanWebhookTest(client, namespaceName)
  105. })
  106. ginkgo.It("Should be able to deny pod and configmap creation", func() {
  107. webhookCleanup := registerWebhook(f, context)
  108. defer webhookCleanup()
  109. testWebhook(f)
  110. })
  111. ginkgo.It("Should be able to deny attaching pod", func() {
  112. webhookCleanup := registerWebhookForAttachingPod(f, context)
  113. defer webhookCleanup()
  114. testAttachingPodWebhook(f)
  115. })
  116. ginkgo.It("Should be able to deny custom resource creation and deletion", func() {
  117. testcrd, err := crd.CreateTestCRD(f)
  118. if err != nil {
  119. return
  120. }
  121. defer testcrd.CleanUp()
  122. webhookCleanup := registerWebhookForCustomResource(f, context, testcrd)
  123. defer webhookCleanup()
  124. testCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"])
  125. testBlockingCustomResourceDeletion(f, testcrd.Crd, testcrd.DynamicClients["v1"])
  126. })
  127. ginkgo.It("Should unconditionally reject operations on fail closed webhook", func() {
  128. webhookCleanup := registerFailClosedWebhook(f, context)
  129. defer webhookCleanup()
  130. testFailClosedWebhook(f)
  131. })
  132. ginkgo.It("Should mutate configmap", func() {
  133. webhookCleanup := registerMutatingWebhookForConfigMap(f, context)
  134. defer webhookCleanup()
  135. testMutatingConfigMapWebhook(f)
  136. })
  137. ginkgo.It("Should mutate pod and apply defaults after mutation", func() {
  138. webhookCleanup := registerMutatingWebhookForPod(f, context)
  139. defer webhookCleanup()
  140. testMutatingPodWebhook(f)
  141. })
  142. ginkgo.It("Should not be able to mutate or prevent deletion of webhook configuration objects", func() {
  143. validatingWebhookCleanup := registerValidatingWebhookForWebhookConfigurations(f, context)
  144. defer validatingWebhookCleanup()
  145. mutatingWebhookCleanup := registerMutatingWebhookForWebhookConfigurations(f, context)
  146. defer mutatingWebhookCleanup()
  147. testWebhooksForWebhookConfigurations(f)
  148. })
  149. ginkgo.It("Should mutate custom resource", func() {
  150. testcrd, err := crd.CreateTestCRD(f)
  151. if err != nil {
  152. return
  153. }
  154. defer testcrd.CleanUp()
  155. webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
  156. defer webhookCleanup()
  157. testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], false)
  158. })
  159. ginkgo.It("Should deny crd creation", func() {
  160. crdWebhookCleanup := registerValidatingWebhookForCRD(f, context)
  161. defer crdWebhookCleanup()
  162. testCRDDenyWebhook(f)
  163. })
  164. ginkgo.It("Should mutate custom resource with different stored version", func() {
  165. testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f)
  166. if err != nil {
  167. return
  168. }
  169. defer testcrd.CleanUp()
  170. webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
  171. defer webhookCleanup()
  172. testMultiVersionCustomResourceWebhook(f, testcrd)
  173. })
  174. ginkgo.It("Should mutate custom resource with pruning", func() {
  175. const prune = true
  176. testcrd, err := createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f, func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
  177. crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
  178. crd.Spec.Validation = &apiextensionsv1beta1.CustomResourceValidation{
  179. OpenAPIV3Schema: &apiextensionsv1beta1.JSONSchemaProps{
  180. Type: "object",
  181. Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
  182. "data": {
  183. Type: "object",
  184. Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
  185. "mutation-start": {Type: "string"},
  186. "mutation-stage-1": {Type: "string"},
  187. // mutation-stage-2 is intentionally missing such that it is pruned
  188. },
  189. },
  190. },
  191. },
  192. }
  193. })
  194. if err != nil {
  195. return
  196. }
  197. defer testcrd.CleanUp()
  198. webhookCleanup := registerMutatingWebhookForCustomResource(f, context, testcrd)
  199. defer webhookCleanup()
  200. testMutatingCustomResourceWebhook(f, testcrd.Crd, testcrd.DynamicClients["v1"], prune)
  201. })
  202. ginkgo.It("Should honor timeout", func() {
  203. policyFail := v1beta1.Fail
  204. policyIgnore := v1beta1.Ignore
  205. ginkgo.By("Setting timeout (1s) shorter than webhook latency (5s)")
  206. slowWebhookCleanup := registerSlowWebhook(f, context, &policyFail, pointer.Int32Ptr(1))
  207. testSlowWebhookTimeoutFailEarly(f)
  208. slowWebhookCleanup()
  209. ginkgo.By("Having no error when timeout is shorter than webhook latency and failure policy is ignore")
  210. slowWebhookCleanup = registerSlowWebhook(f, context, &policyIgnore, pointer.Int32Ptr(1))
  211. testSlowWebhookTimeoutNoError(f)
  212. slowWebhookCleanup()
  213. ginkgo.By("Having no error when timeout is longer than webhook latency")
  214. slowWebhookCleanup = registerSlowWebhook(f, context, &policyFail, pointer.Int32Ptr(10))
  215. testSlowWebhookTimeoutNoError(f)
  216. slowWebhookCleanup()
  217. ginkgo.By("Having no error when timeout is empty (defaulted to 10s in v1beta1)")
  218. slowWebhookCleanup = registerSlowWebhook(f, context, &policyFail, nil)
  219. testSlowWebhookTimeoutNoError(f)
  220. slowWebhookCleanup()
  221. })
  222. // TODO: add more e2e tests for mutating webhooks
  223. // 1. mutating webhook that mutates pod
  224. // 2. mutating webhook that sends empty patch
  225. // 2.1 and sets status.allowed=true
  226. // 2.2 and sets status.allowed=false
  227. // 3. mutating webhook that sends patch, but also sets status.allowed=false
  228. // 4. mutating webhook that fail-open v.s. fail-closed
  229. })
  230. func createAuthReaderRoleBinding(f *framework.Framework, namespace string) {
  231. ginkgo.By("Create role binding to let webhook read extension-apiserver-authentication")
  232. client := f.ClientSet
  233. // Create the role binding to allow the webhook read the extension-apiserver-authentication configmap
  234. _, err := client.RbacV1beta1().RoleBindings("kube-system").Create(&rbacv1beta1.RoleBinding{
  235. ObjectMeta: metav1.ObjectMeta{
  236. Name: roleBindingName,
  237. Annotations: map[string]string{
  238. rbacv1beta1.AutoUpdateAnnotationKey: "true",
  239. },
  240. },
  241. RoleRef: rbacv1beta1.RoleRef{
  242. APIGroup: "",
  243. Kind: "Role",
  244. Name: "extension-apiserver-authentication-reader",
  245. },
  246. // Webhook uses the default service account.
  247. Subjects: []rbacv1beta1.Subject{
  248. {
  249. Kind: "ServiceAccount",
  250. Name: "default",
  251. Namespace: namespace,
  252. },
  253. },
  254. })
  255. if err != nil && errors.IsAlreadyExists(err) {
  256. e2elog.Logf("role binding %s already exists", roleBindingName)
  257. } else {
  258. framework.ExpectNoError(err, "creating role binding %s:webhook to access configMap", namespace)
  259. }
  260. }
  261. func deployWebhookAndService(f *framework.Framework, image string, context *certContext) {
  262. ginkgo.By("Deploying the webhook pod")
  263. client := f.ClientSet
  264. // Creating the secret that contains the webhook's cert.
  265. secret := &v1.Secret{
  266. ObjectMeta: metav1.ObjectMeta{
  267. Name: secretName,
  268. },
  269. Type: v1.SecretTypeOpaque,
  270. Data: map[string][]byte{
  271. "tls.crt": context.cert,
  272. "tls.key": context.key,
  273. },
  274. }
  275. namespace := f.Namespace.Name
  276. _, err := client.CoreV1().Secrets(namespace).Create(secret)
  277. framework.ExpectNoError(err, "creating secret %q in namespace %q", secretName, namespace)
  278. // Create the deployment of the webhook
  279. podLabels := map[string]string{"app": "sample-webhook", "webhook": "true"}
  280. replicas := int32(1)
  281. zero := int64(0)
  282. mounts := []v1.VolumeMount{
  283. {
  284. Name: "webhook-certs",
  285. ReadOnly: true,
  286. MountPath: "/webhook.local.config/certificates",
  287. },
  288. }
  289. volumes := []v1.Volume{
  290. {
  291. Name: "webhook-certs",
  292. VolumeSource: v1.VolumeSource{
  293. Secret: &v1.SecretVolumeSource{SecretName: secretName},
  294. },
  295. },
  296. }
  297. containers := []v1.Container{
  298. {
  299. Name: "sample-webhook",
  300. VolumeMounts: mounts,
  301. Args: []string{
  302. "--tls-cert-file=/webhook.local.config/certificates/tls.crt",
  303. "--tls-private-key-file=/webhook.local.config/certificates/tls.key",
  304. "--alsologtostderr",
  305. "-v=4",
  306. "2>&1",
  307. },
  308. Image: image,
  309. },
  310. }
  311. d := &apps.Deployment{
  312. ObjectMeta: metav1.ObjectMeta{
  313. Name: deploymentName,
  314. Labels: podLabels,
  315. },
  316. Spec: apps.DeploymentSpec{
  317. Replicas: &replicas,
  318. Selector: &metav1.LabelSelector{
  319. MatchLabels: podLabels,
  320. },
  321. Strategy: apps.DeploymentStrategy{
  322. Type: apps.RollingUpdateDeploymentStrategyType,
  323. },
  324. Template: v1.PodTemplateSpec{
  325. ObjectMeta: metav1.ObjectMeta{
  326. Labels: podLabels,
  327. },
  328. Spec: v1.PodSpec{
  329. TerminationGracePeriodSeconds: &zero,
  330. Containers: containers,
  331. Volumes: volumes,
  332. },
  333. },
  334. },
  335. }
  336. deployment, err := client.AppsV1().Deployments(namespace).Create(d)
  337. framework.ExpectNoError(err, "creating deployment %s in namespace %s", deploymentName, namespace)
  338. ginkgo.By("Wait for the deployment to be ready")
  339. err = e2edeploy.WaitForDeploymentRevisionAndImage(client, namespace, deploymentName, "1", image)
  340. framework.ExpectNoError(err, "waiting for the deployment of image %s in %s in %s to complete", image, deploymentName, namespace)
  341. err = e2edeploy.WaitForDeploymentComplete(client, deployment)
  342. framework.ExpectNoError(err, "waiting for the deployment status valid", image, deploymentName, namespace)
  343. ginkgo.By("Deploying the webhook service")
  344. serviceLabels := map[string]string{"webhook": "true"}
  345. service := &v1.Service{
  346. ObjectMeta: metav1.ObjectMeta{
  347. Namespace: namespace,
  348. Name: serviceName,
  349. Labels: map[string]string{"test": "webhook"},
  350. },
  351. Spec: v1.ServiceSpec{
  352. Selector: serviceLabels,
  353. Ports: []v1.ServicePort{
  354. {
  355. Protocol: "TCP",
  356. Port: servicePort,
  357. TargetPort: intstr.FromInt(443),
  358. },
  359. },
  360. },
  361. }
  362. _, err = client.CoreV1().Services(namespace).Create(service)
  363. framework.ExpectNoError(err, "creating service %s in namespace %s", serviceName, namespace)
  364. ginkgo.By("Verifying the service has paired with the endpoint")
  365. err = framework.WaitForServiceEndpointsNum(client, namespace, serviceName, 1, 1*time.Second, 30*time.Second)
  366. framework.ExpectNoError(err, "waiting for service %s/%s have %d endpoint", namespace, serviceName, 1)
  367. }
  368. func strPtr(s string) *string { return &s }
  369. func registerWebhook(f *framework.Framework, context *certContext) func() {
  370. client := f.ClientSet
  371. ginkgo.By("Registering the webhook via the AdmissionRegistration API")
  372. namespace := f.Namespace.Name
  373. configName := webhookConfigName
  374. // A webhook that cannot talk to server, with fail-open policy
  375. failOpenHook := failingWebhook(namespace, "fail-open.k8s.io")
  376. policyIgnore := v1beta1.Ignore
  377. failOpenHook.FailurePolicy = &policyIgnore
  378. _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  379. ObjectMeta: metav1.ObjectMeta{
  380. Name: configName,
  381. },
  382. Webhooks: []v1beta1.ValidatingWebhook{
  383. {
  384. Name: "deny-unwanted-pod-container-name-and-label.k8s.io",
  385. Rules: []v1beta1.RuleWithOperations{{
  386. Operations: []v1beta1.OperationType{v1beta1.Create},
  387. Rule: v1beta1.Rule{
  388. APIGroups: []string{""},
  389. APIVersions: []string{"v1"},
  390. Resources: []string{"pods"},
  391. },
  392. }},
  393. ClientConfig: v1beta1.WebhookClientConfig{
  394. Service: &v1beta1.ServiceReference{
  395. Namespace: namespace,
  396. Name: serviceName,
  397. Path: strPtr("/pods"),
  398. Port: pointer.Int32Ptr(servicePort),
  399. },
  400. CABundle: context.signingCert,
  401. },
  402. },
  403. {
  404. Name: "deny-unwanted-configmap-data.k8s.io",
  405. Rules: []v1beta1.RuleWithOperations{{
  406. Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update, v1beta1.Delete},
  407. Rule: v1beta1.Rule{
  408. APIGroups: []string{""},
  409. APIVersions: []string{"v1"},
  410. Resources: []string{"configmaps"},
  411. },
  412. }},
  413. // The webhook skips the namespace that has label "skip-webhook-admission":"yes"
  414. NamespaceSelector: &metav1.LabelSelector{
  415. MatchExpressions: []metav1.LabelSelectorRequirement{
  416. {
  417. Key: skipNamespaceLabelKey,
  418. Operator: metav1.LabelSelectorOpNotIn,
  419. Values: []string{skipNamespaceLabelValue},
  420. },
  421. },
  422. },
  423. ClientConfig: v1beta1.WebhookClientConfig{
  424. Service: &v1beta1.ServiceReference{
  425. Namespace: namespace,
  426. Name: serviceName,
  427. Path: strPtr("/configmaps"),
  428. Port: pointer.Int32Ptr(servicePort),
  429. },
  430. CABundle: context.signingCert,
  431. },
  432. },
  433. // Server cannot talk to this webhook, so it always fails.
  434. // Because this webhook is configured fail-open, request should be admitted after the call fails.
  435. failOpenHook,
  436. },
  437. })
  438. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
  439. // The webhook configuration is honored in 10s.
  440. time.Sleep(10 * time.Second)
  441. return func() {
  442. client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  443. }
  444. }
  445. func registerWebhookForAttachingPod(f *framework.Framework, context *certContext) func() {
  446. client := f.ClientSet
  447. ginkgo.By("Registering the webhook via the AdmissionRegistration API")
  448. namespace := f.Namespace.Name
  449. configName := attachingPodWebhookConfigName
  450. // A webhook that cannot talk to server, with fail-open policy
  451. failOpenHook := failingWebhook(namespace, "fail-open.k8s.io")
  452. policyIgnore := v1beta1.Ignore
  453. failOpenHook.FailurePolicy = &policyIgnore
  454. _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  455. ObjectMeta: metav1.ObjectMeta{
  456. Name: configName,
  457. },
  458. Webhooks: []v1beta1.ValidatingWebhook{
  459. {
  460. Name: "deny-attaching-pod.k8s.io",
  461. Rules: []v1beta1.RuleWithOperations{{
  462. Operations: []v1beta1.OperationType{v1beta1.Connect},
  463. Rule: v1beta1.Rule{
  464. APIGroups: []string{""},
  465. APIVersions: []string{"v1"},
  466. Resources: []string{"pods/attach"},
  467. },
  468. }},
  469. ClientConfig: v1beta1.WebhookClientConfig{
  470. Service: &v1beta1.ServiceReference{
  471. Namespace: namespace,
  472. Name: serviceName,
  473. Path: strPtr("/pods/attach"),
  474. Port: pointer.Int32Ptr(servicePort),
  475. },
  476. CABundle: context.signingCert,
  477. },
  478. },
  479. },
  480. })
  481. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
  482. // The webhook configuration is honored in 10s.
  483. time.Sleep(10 * time.Second)
  484. return func() {
  485. client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  486. }
  487. }
  488. func registerMutatingWebhookForConfigMap(f *framework.Framework, context *certContext) func() {
  489. client := f.ClientSet
  490. ginkgo.By("Registering the mutating configmap webhook via the AdmissionRegistration API")
  491. namespace := f.Namespace.Name
  492. configName := mutatingWebhookConfigName
  493. _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
  494. ObjectMeta: metav1.ObjectMeta{
  495. Name: configName,
  496. },
  497. Webhooks: []v1beta1.MutatingWebhook{
  498. {
  499. Name: "adding-configmap-data-stage-1.k8s.io",
  500. Rules: []v1beta1.RuleWithOperations{{
  501. Operations: []v1beta1.OperationType{v1beta1.Create},
  502. Rule: v1beta1.Rule{
  503. APIGroups: []string{""},
  504. APIVersions: []string{"v1"},
  505. Resources: []string{"configmaps"},
  506. },
  507. }},
  508. ClientConfig: v1beta1.WebhookClientConfig{
  509. Service: &v1beta1.ServiceReference{
  510. Namespace: namespace,
  511. Name: serviceName,
  512. Path: strPtr("/mutating-configmaps"),
  513. Port: pointer.Int32Ptr(servicePort),
  514. },
  515. CABundle: context.signingCert,
  516. },
  517. },
  518. {
  519. Name: "adding-configmap-data-stage-2.k8s.io",
  520. Rules: []v1beta1.RuleWithOperations{{
  521. Operations: []v1beta1.OperationType{v1beta1.Create},
  522. Rule: v1beta1.Rule{
  523. APIGroups: []string{""},
  524. APIVersions: []string{"v1"},
  525. Resources: []string{"configmaps"},
  526. },
  527. }},
  528. ClientConfig: v1beta1.WebhookClientConfig{
  529. Service: &v1beta1.ServiceReference{
  530. Namespace: namespace,
  531. Name: serviceName,
  532. Path: strPtr("/mutating-configmaps"),
  533. Port: pointer.Int32Ptr(servicePort),
  534. },
  535. CABundle: context.signingCert,
  536. },
  537. },
  538. },
  539. })
  540. framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
  541. // The webhook configuration is honored in 10s.
  542. time.Sleep(10 * time.Second)
  543. return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
  544. }
  545. func testMutatingConfigMapWebhook(f *framework.Framework) {
  546. ginkgo.By("create a configmap that should be updated by the webhook")
  547. client := f.ClientSet
  548. configMap := toBeMutatedConfigMap(f)
  549. mutatedConfigMap, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configMap)
  550. gomega.Expect(err).To(gomega.BeNil())
  551. expectedConfigMapData := map[string]string{
  552. "mutation-start": "yes",
  553. "mutation-stage-1": "yes",
  554. "mutation-stage-2": "yes",
  555. }
  556. if !reflect.DeepEqual(expectedConfigMapData, mutatedConfigMap.Data) {
  557. framework.Failf("\nexpected %#v\n, got %#v\n", expectedConfigMapData, mutatedConfigMap.Data)
  558. }
  559. }
  560. func registerMutatingWebhookForPod(f *framework.Framework, context *certContext) func() {
  561. client := f.ClientSet
  562. ginkgo.By("Registering the mutating pod webhook via the AdmissionRegistration API")
  563. namespace := f.Namespace.Name
  564. configName := podMutatingWebhookConfigName
  565. _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
  566. ObjectMeta: metav1.ObjectMeta{
  567. Name: configName,
  568. },
  569. Webhooks: []v1beta1.MutatingWebhook{
  570. {
  571. Name: "adding-init-container.k8s.io",
  572. Rules: []v1beta1.RuleWithOperations{{
  573. Operations: []v1beta1.OperationType{v1beta1.Create},
  574. Rule: v1beta1.Rule{
  575. APIGroups: []string{""},
  576. APIVersions: []string{"v1"},
  577. Resources: []string{"pods"},
  578. },
  579. }},
  580. ClientConfig: v1beta1.WebhookClientConfig{
  581. Service: &v1beta1.ServiceReference{
  582. Namespace: namespace,
  583. Name: serviceName,
  584. Path: strPtr("/mutating-pods"),
  585. Port: pointer.Int32Ptr(servicePort),
  586. },
  587. CABundle: context.signingCert,
  588. },
  589. },
  590. },
  591. })
  592. framework.ExpectNoError(err, "registering mutating webhook config %s with namespace %s", configName, namespace)
  593. // The webhook configuration is honored in 10s.
  594. time.Sleep(10 * time.Second)
  595. return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
  596. }
  597. func testMutatingPodWebhook(f *framework.Framework) {
  598. ginkgo.By("create a pod that should be updated by the webhook")
  599. client := f.ClientSet
  600. configMap := toBeMutatedPod(f)
  601. mutatedPod, err := client.CoreV1().Pods(f.Namespace.Name).Create(configMap)
  602. gomega.Expect(err).To(gomega.BeNil())
  603. if len(mutatedPod.Spec.InitContainers) != 1 {
  604. framework.Failf("expect pod to have 1 init container, got %#v", mutatedPod.Spec.InitContainers)
  605. }
  606. if got, expected := mutatedPod.Spec.InitContainers[0].Name, "webhook-added-init-container"; got != expected {
  607. framework.Failf("expect the init container name to be %q, got %q", expected, got)
  608. }
  609. if got, expected := mutatedPod.Spec.InitContainers[0].TerminationMessagePolicy, v1.TerminationMessageReadFile; got != expected {
  610. framework.Failf("expect the init terminationMessagePolicy to be default to %q, got %q", expected, got)
  611. }
  612. }
  613. func toBeMutatedPod(f *framework.Framework) *v1.Pod {
  614. return &v1.Pod{
  615. ObjectMeta: metav1.ObjectMeta{
  616. Name: "webhook-to-be-mutated",
  617. },
  618. Spec: v1.PodSpec{
  619. Containers: []v1.Container{
  620. {
  621. Name: "example",
  622. Image: imageutils.GetPauseImageName(),
  623. },
  624. },
  625. },
  626. }
  627. }
  628. func testWebhook(f *framework.Framework) {
  629. ginkgo.By("create a pod that should be denied by the webhook")
  630. client := f.ClientSet
  631. // Creating the pod, the request should be rejected
  632. pod := nonCompliantPod(f)
  633. _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod)
  634. framework.ExpectError(err, "create pod %s in namespace %s should have been denied by webhook", pod.Name, f.Namespace.Name)
  635. expectedErrMsg1 := "the pod contains unwanted container name"
  636. if !strings.Contains(err.Error(), expectedErrMsg1) {
  637. framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error())
  638. }
  639. expectedErrMsg2 := "the pod contains unwanted label"
  640. if !strings.Contains(err.Error(), expectedErrMsg2) {
  641. framework.Failf("expect error contains %q, got %q", expectedErrMsg2, err.Error())
  642. }
  643. ginkgo.By("create a pod that causes the webhook to hang")
  644. client = f.ClientSet
  645. // Creating the pod, the request should be rejected
  646. pod = hangingPod(f)
  647. _, err = client.CoreV1().Pods(f.Namespace.Name).Create(pod)
  648. framework.ExpectError(err, "create pod %s in namespace %s should have caused webhook to hang", pod.Name, f.Namespace.Name)
  649. expectedTimeoutErr := "request did not complete within"
  650. if !strings.Contains(err.Error(), expectedTimeoutErr) {
  651. framework.Failf("expect timeout error %q, got %q", expectedTimeoutErr, err.Error())
  652. }
  653. ginkgo.By("create a configmap that should be denied by the webhook")
  654. // Creating the configmap, the request should be rejected
  655. configmap := nonCompliantConfigMap(f)
  656. _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap)
  657. framework.ExpectError(err, "create configmap %s in namespace %s should have been denied by the webhook", configmap.Name, f.Namespace.Name)
  658. expectedErrMsg := "the configmap contains unwanted key and value"
  659. if !strings.Contains(err.Error(), expectedErrMsg) {
  660. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  661. }
  662. ginkgo.By("create a configmap that should be admitted by the webhook")
  663. // Creating the configmap, the request should be admitted
  664. configmap = &v1.ConfigMap{
  665. ObjectMeta: metav1.ObjectMeta{
  666. Name: allowedConfigMapName,
  667. },
  668. Data: map[string]string{
  669. "admit": "this",
  670. },
  671. }
  672. _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap)
  673. framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
  674. ginkgo.By("update (PUT) the admitted configmap to a non-compliant one should be rejected by the webhook")
  675. toNonCompliantFn := func(cm *v1.ConfigMap) {
  676. if cm.Data == nil {
  677. cm.Data = map[string]string{}
  678. }
  679. cm.Data["webhook-e2e-test"] = "webhook-disallow"
  680. }
  681. _, err = updateConfigMap(client, f.Namespace.Name, allowedConfigMapName, toNonCompliantFn)
  682. framework.ExpectError(err, "update (PUT) admitted configmap %s in namespace %s to a non-compliant one should be rejected by webhook", allowedConfigMapName, f.Namespace.Name)
  683. if !strings.Contains(err.Error(), expectedErrMsg) {
  684. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  685. }
  686. ginkgo.By("update (PATCH) the admitted configmap to a non-compliant one should be rejected by the webhook")
  687. patch := nonCompliantConfigMapPatch()
  688. _, err = client.CoreV1().ConfigMaps(f.Namespace.Name).Patch(allowedConfigMapName, types.StrategicMergePatchType, []byte(patch))
  689. framework.ExpectError(err, "update admitted configmap %s in namespace %s by strategic merge patch to a non-compliant one should be rejected by webhook. Patch: %+v", allowedConfigMapName, f.Namespace.Name, patch)
  690. if !strings.Contains(err.Error(), expectedErrMsg) {
  691. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  692. }
  693. ginkgo.By("create a namespace that bypass the webhook")
  694. err = createNamespace(f, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{
  695. Name: skippedNamespaceName,
  696. Labels: map[string]string{
  697. skipNamespaceLabelKey: skipNamespaceLabelValue,
  698. },
  699. }})
  700. framework.ExpectNoError(err, "creating namespace %q", skippedNamespaceName)
  701. // clean up the namespace
  702. defer client.CoreV1().Namespaces().Delete(skippedNamespaceName, nil)
  703. ginkgo.By("create a configmap that violates the webhook policy but is in a whitelisted namespace")
  704. configmap = nonCompliantConfigMap(f)
  705. _, err = client.CoreV1().ConfigMaps(skippedNamespaceName).Create(configmap)
  706. framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, skippedNamespaceName)
  707. }
  708. func testBlockingConfigmapDeletion(f *framework.Framework) {
  709. ginkgo.By("create a configmap that should be denied by the webhook when deleting")
  710. client := f.ClientSet
  711. configmap := nonDeletableConfigmap(f)
  712. _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(configmap)
  713. framework.ExpectNoError(err, "failed to create configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
  714. ginkgo.By("deleting the configmap should be denied by the webhook")
  715. err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(configmap.Name, &metav1.DeleteOptions{})
  716. framework.ExpectError(err, "deleting configmap %s in namespace: %s should be denied", configmap.Name, f.Namespace.Name)
  717. expectedErrMsg1 := "the configmap cannot be deleted because it contains unwanted key and value"
  718. if !strings.Contains(err.Error(), expectedErrMsg1) {
  719. framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error())
  720. }
  721. ginkgo.By("remove the offending key and value from the configmap data")
  722. toCompliantFn := func(cm *v1.ConfigMap) {
  723. if cm.Data == nil {
  724. cm.Data = map[string]string{}
  725. }
  726. cm.Data["webhook-e2e-test"] = "webhook-allow"
  727. }
  728. _, err = updateConfigMap(client, f.Namespace.Name, configmap.Name, toCompliantFn)
  729. framework.ExpectNoError(err, "failed to update configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
  730. ginkgo.By("deleting the updated configmap should be successful")
  731. err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(configmap.Name, &metav1.DeleteOptions{})
  732. framework.ExpectNoError(err, "failed to delete configmap %s in namespace: %s", configmap.Name, f.Namespace.Name)
  733. }
  734. func testAttachingPodWebhook(f *framework.Framework) {
  735. ginkgo.By("create a pod")
  736. client := f.ClientSet
  737. pod := toBeAttachedPod(f)
  738. _, err := client.CoreV1().Pods(f.Namespace.Name).Create(pod)
  739. framework.ExpectNoError(err, "failed to create pod %s in namespace: %s", pod.Name, f.Namespace.Name)
  740. err = framework.WaitForPodNameRunningInNamespace(client, pod.Name, f.Namespace.Name)
  741. framework.ExpectNoError(err, "error while waiting for pod %s to go to Running phase in namespace: %s", pod.Name, f.Namespace.Name)
  742. ginkgo.By("'kubectl attach' the pod, should be denied by the webhook")
  743. timer := time.NewTimer(30 * time.Second)
  744. defer timer.Stop()
  745. _, err = framework.NewKubectlCommand("attach", fmt.Sprintf("--namespace=%v", f.Namespace.Name), pod.Name, "-i", "-c=container1").WithTimeout(timer.C).Exec()
  746. framework.ExpectError(err, "'kubectl attach' the pod, should be denied by the webhook")
  747. if e, a := "attaching to pod 'to-be-attached-pod' is not allowed", err.Error(); !strings.Contains(a, e) {
  748. framework.Failf("unexpected 'kubectl attach' error message. expected to contain %q, got %q", e, a)
  749. }
  750. }
  751. // failingWebhook returns a webhook with rule of create configmaps,
  752. // but with an invalid client config so that server cannot communicate with it
  753. func failingWebhook(namespace, name string) v1beta1.ValidatingWebhook {
  754. return v1beta1.ValidatingWebhook{
  755. Name: name,
  756. Rules: []v1beta1.RuleWithOperations{{
  757. Operations: []v1beta1.OperationType{v1beta1.Create},
  758. Rule: v1beta1.Rule{
  759. APIGroups: []string{""},
  760. APIVersions: []string{"v1"},
  761. Resources: []string{"configmaps"},
  762. },
  763. }},
  764. ClientConfig: v1beta1.WebhookClientConfig{
  765. Service: &v1beta1.ServiceReference{
  766. Namespace: namespace,
  767. Name: serviceName,
  768. Path: strPtr("/configmaps"),
  769. Port: pointer.Int32Ptr(servicePort),
  770. },
  771. // Without CA bundle, the call to webhook always fails
  772. CABundle: nil,
  773. },
  774. }
  775. }
  776. func registerFailClosedWebhook(f *framework.Framework, context *certContext) func() {
  777. client := f.ClientSet
  778. ginkgo.By("Registering a webhook that server cannot talk to, with fail closed policy, via the AdmissionRegistration API")
  779. namespace := f.Namespace.Name
  780. configName := webhookFailClosedConfigName
  781. // A webhook that cannot talk to server, with fail-closed policy
  782. policyFail := v1beta1.Fail
  783. hook := failingWebhook(namespace, "fail-closed.k8s.io")
  784. hook.FailurePolicy = &policyFail
  785. hook.NamespaceSelector = &metav1.LabelSelector{
  786. MatchExpressions: []metav1.LabelSelectorRequirement{
  787. {
  788. Key: failNamespaceLabelKey,
  789. Operator: metav1.LabelSelectorOpIn,
  790. Values: []string{failNamespaceLabelValue},
  791. },
  792. },
  793. }
  794. _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  795. ObjectMeta: metav1.ObjectMeta{
  796. Name: configName,
  797. },
  798. Webhooks: []v1beta1.ValidatingWebhook{
  799. // Server cannot talk to this webhook, so it always fails.
  800. // Because this webhook is configured fail-closed, request should be rejected after the call fails.
  801. hook,
  802. },
  803. })
  804. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
  805. // The webhook configuration is honored in 10s.
  806. time.Sleep(10 * time.Second)
  807. return func() {
  808. f.ClientSet.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  809. }
  810. }
  811. func testFailClosedWebhook(f *framework.Framework) {
  812. client := f.ClientSet
  813. ginkgo.By("create a namespace for the webhook")
  814. err := createNamespace(f, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{
  815. Name: failNamespaceName,
  816. Labels: map[string]string{
  817. failNamespaceLabelKey: failNamespaceLabelValue,
  818. },
  819. }})
  820. framework.ExpectNoError(err, "creating namespace %q", failNamespaceName)
  821. defer client.CoreV1().Namespaces().Delete(failNamespaceName, nil)
  822. ginkgo.By("create a configmap should be unconditionally rejected by the webhook")
  823. configmap := &v1.ConfigMap{
  824. ObjectMeta: metav1.ObjectMeta{
  825. Name: "foo",
  826. },
  827. }
  828. _, err = client.CoreV1().ConfigMaps(failNamespaceName).Create(configmap)
  829. framework.ExpectError(err, "create configmap in namespace: %s should be unconditionally rejected by the webhook", failNamespaceName)
  830. if !errors.IsInternalError(err) {
  831. framework.Failf("expect an internal error, got %#v", err)
  832. }
  833. }
  834. func registerValidatingWebhookForWebhookConfigurations(f *framework.Framework, context *certContext) func() {
  835. var err error
  836. client := f.ClientSet
  837. ginkgo.By("Registering a validating webhook on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects, via the AdmissionRegistration API")
  838. namespace := f.Namespace.Name
  839. configName := validatingWebhookForWebhooksConfigName
  840. failurePolicy := v1beta1.Fail
  841. // This webhook denies all requests to Delete validating webhook configuration and
  842. // mutating webhook configuration objects. It should never be called, however, because
  843. // dynamic admission webhooks should not be called on requests involving webhook configuration objects.
  844. _, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  845. ObjectMeta: metav1.ObjectMeta{
  846. Name: configName,
  847. },
  848. Webhooks: []v1beta1.ValidatingWebhook{
  849. {
  850. Name: "deny-webhook-configuration-deletions.k8s.io",
  851. Rules: []v1beta1.RuleWithOperations{{
  852. Operations: []v1beta1.OperationType{v1beta1.Delete},
  853. Rule: v1beta1.Rule{
  854. APIGroups: []string{"admissionregistration.k8s.io"},
  855. APIVersions: []string{"*"},
  856. Resources: []string{
  857. "validatingwebhookconfigurations",
  858. "mutatingwebhookconfigurations",
  859. },
  860. },
  861. }},
  862. ClientConfig: v1beta1.WebhookClientConfig{
  863. Service: &v1beta1.ServiceReference{
  864. Namespace: namespace,
  865. Name: serviceName,
  866. Path: strPtr("/always-deny"),
  867. Port: pointer.Int32Ptr(servicePort),
  868. },
  869. CABundle: context.signingCert,
  870. },
  871. FailurePolicy: &failurePolicy,
  872. },
  873. },
  874. })
  875. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
  876. // The webhook configuration is honored in 10s.
  877. time.Sleep(10 * time.Second)
  878. return func() {
  879. err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  880. framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", configName, namespace)
  881. }
  882. }
  883. func registerMutatingWebhookForWebhookConfigurations(f *framework.Framework, context *certContext) func() {
  884. var err error
  885. client := f.ClientSet
  886. ginkgo.By("Registering a mutating webhook on ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects, via the AdmissionRegistration API")
  887. namespace := f.Namespace.Name
  888. configName := mutatingWebhookForWebhooksConfigName
  889. failurePolicy := v1beta1.Fail
  890. // This webhook adds a label to all requests create to validating webhook configuration and
  891. // mutating webhook configuration objects. It should never be called, however, because
  892. // dynamic admission webhooks should not be called on requests involving webhook configuration objects.
  893. _, err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
  894. ObjectMeta: metav1.ObjectMeta{
  895. Name: configName,
  896. },
  897. Webhooks: []v1beta1.MutatingWebhook{
  898. {
  899. Name: "add-label-to-webhook-configurations.k8s.io",
  900. Rules: []v1beta1.RuleWithOperations{{
  901. Operations: []v1beta1.OperationType{v1beta1.Create},
  902. Rule: v1beta1.Rule{
  903. APIGroups: []string{"admissionregistration.k8s.io"},
  904. APIVersions: []string{"*"},
  905. Resources: []string{
  906. "validatingwebhookconfigurations",
  907. "mutatingwebhookconfigurations",
  908. },
  909. },
  910. }},
  911. ClientConfig: v1beta1.WebhookClientConfig{
  912. Service: &v1beta1.ServiceReference{
  913. Namespace: namespace,
  914. Name: serviceName,
  915. Path: strPtr("/add-label"),
  916. Port: pointer.Int32Ptr(servicePort),
  917. },
  918. CABundle: context.signingCert,
  919. },
  920. FailurePolicy: &failurePolicy,
  921. },
  922. },
  923. })
  924. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", configName, namespace)
  925. // The webhook configuration is honored in 10s.
  926. time.Sleep(10 * time.Second)
  927. return func() {
  928. err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil)
  929. framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", configName, namespace)
  930. }
  931. }
  932. // This test assumes that the deletion-rejecting webhook defined in
  933. // registerValidatingWebhookForWebhookConfigurations and the webhook-config-mutating
  934. // webhook defined in registerMutatingWebhookForWebhookConfigurations already exist.
  935. func testWebhooksForWebhookConfigurations(f *framework.Framework) {
  936. var err error
  937. client := f.ClientSet
  938. ginkgo.By("Creating a dummy validating-webhook-configuration object")
  939. namespace := f.Namespace.Name
  940. failurePolicy := v1beta1.Ignore
  941. mutatedValidatingWebhookConfiguration, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  942. ObjectMeta: metav1.ObjectMeta{
  943. Name: dummyValidatingWebhookConfigName,
  944. },
  945. Webhooks: []v1beta1.ValidatingWebhook{
  946. {
  947. Name: "dummy-validating-webhook.k8s.io",
  948. Rules: []v1beta1.RuleWithOperations{{
  949. Operations: []v1beta1.OperationType{v1beta1.Create},
  950. // This will not match any real resources so this webhook should never be called.
  951. Rule: v1beta1.Rule{
  952. APIGroups: []string{""},
  953. APIVersions: []string{"v1"},
  954. Resources: []string{"invalid"},
  955. },
  956. }},
  957. ClientConfig: v1beta1.WebhookClientConfig{
  958. Service: &v1beta1.ServiceReference{
  959. Namespace: namespace,
  960. Name: serviceName,
  961. // This path not recognized by the webhook service,
  962. // so the call to this webhook will always fail,
  963. // but because the failure policy is ignore, it will
  964. // have no effect on admission requests.
  965. Path: strPtr(""),
  966. Port: pointer.Int32Ptr(servicePort),
  967. },
  968. CABundle: nil,
  969. },
  970. FailurePolicy: &failurePolicy,
  971. },
  972. },
  973. })
  974. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", dummyValidatingWebhookConfigName, namespace)
  975. if mutatedValidatingWebhookConfiguration.ObjectMeta.Labels != nil && mutatedValidatingWebhookConfiguration.ObjectMeta.Labels[addedLabelKey] == addedLabelValue {
  976. framework.Failf("expected %s not to be mutated by mutating webhooks but it was", dummyValidatingWebhookConfigName)
  977. }
  978. // The webhook configuration is honored in 10s.
  979. time.Sleep(10 * time.Second)
  980. ginkgo.By("Deleting the validating-webhook-configuration, which should be possible to remove")
  981. err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(dummyValidatingWebhookConfigName, nil)
  982. framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", dummyValidatingWebhookConfigName, namespace)
  983. ginkgo.By("Creating a dummy mutating-webhook-configuration object")
  984. mutatedMutatingWebhookConfiguration, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
  985. ObjectMeta: metav1.ObjectMeta{
  986. Name: dummyMutatingWebhookConfigName,
  987. },
  988. Webhooks: []v1beta1.MutatingWebhook{
  989. {
  990. Name: "dummy-mutating-webhook.k8s.io",
  991. Rules: []v1beta1.RuleWithOperations{{
  992. Operations: []v1beta1.OperationType{v1beta1.Create},
  993. // This will not match any real resources so this webhook should never be called.
  994. Rule: v1beta1.Rule{
  995. APIGroups: []string{""},
  996. APIVersions: []string{"v1"},
  997. Resources: []string{"invalid"},
  998. },
  999. }},
  1000. ClientConfig: v1beta1.WebhookClientConfig{
  1001. Service: &v1beta1.ServiceReference{
  1002. Namespace: namespace,
  1003. Name: serviceName,
  1004. // This path not recognized by the webhook service,
  1005. // so the call to this webhook will always fail,
  1006. // but because the failure policy is ignore, it will
  1007. // have no effect on admission requests.
  1008. Path: strPtr(""),
  1009. Port: pointer.Int32Ptr(servicePort),
  1010. },
  1011. CABundle: nil,
  1012. },
  1013. FailurePolicy: &failurePolicy,
  1014. },
  1015. },
  1016. })
  1017. framework.ExpectNoError(err, "registering webhook config %s with namespace %s", dummyMutatingWebhookConfigName, namespace)
  1018. if mutatedMutatingWebhookConfiguration.ObjectMeta.Labels != nil && mutatedMutatingWebhookConfiguration.ObjectMeta.Labels[addedLabelKey] == addedLabelValue {
  1019. framework.Failf("expected %s not to be mutated by mutating webhooks but it was", dummyMutatingWebhookConfigName)
  1020. }
  1021. // The webhook configuration is honored in 10s.
  1022. time.Sleep(10 * time.Second)
  1023. ginkgo.By("Deleting the mutating-webhook-configuration, which should be possible to remove")
  1024. err = client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(dummyMutatingWebhookConfigName, nil)
  1025. framework.ExpectNoError(err, "deleting webhook config %s with namespace %s", dummyMutatingWebhookConfigName, namespace)
  1026. }
  1027. func createNamespace(f *framework.Framework, ns *v1.Namespace) error {
  1028. return wait.PollImmediate(100*time.Millisecond, 30*time.Second, func() (bool, error) {
  1029. _, err := f.ClientSet.CoreV1().Namespaces().Create(ns)
  1030. if err != nil {
  1031. if strings.HasPrefix(err.Error(), "object is being deleted:") {
  1032. return false, nil
  1033. }
  1034. return false, err
  1035. }
  1036. return true, nil
  1037. })
  1038. }
  1039. func nonCompliantPod(f *framework.Framework) *v1.Pod {
  1040. return &v1.Pod{
  1041. ObjectMeta: metav1.ObjectMeta{
  1042. Name: disallowedPodName,
  1043. Labels: map[string]string{
  1044. "webhook-e2e-test": "webhook-disallow",
  1045. },
  1046. },
  1047. Spec: v1.PodSpec{
  1048. Containers: []v1.Container{
  1049. {
  1050. Name: "webhook-disallow",
  1051. Image: imageutils.GetPauseImageName(),
  1052. },
  1053. },
  1054. },
  1055. }
  1056. }
  1057. func hangingPod(f *framework.Framework) *v1.Pod {
  1058. return &v1.Pod{
  1059. ObjectMeta: metav1.ObjectMeta{
  1060. Name: hangingPodName,
  1061. Labels: map[string]string{
  1062. "webhook-e2e-test": "wait-forever",
  1063. },
  1064. },
  1065. Spec: v1.PodSpec{
  1066. Containers: []v1.Container{
  1067. {
  1068. Name: "wait-forever",
  1069. Image: imageutils.GetPauseImageName(),
  1070. },
  1071. },
  1072. },
  1073. }
  1074. }
  1075. func toBeAttachedPod(f *framework.Framework) *v1.Pod {
  1076. return &v1.Pod{
  1077. ObjectMeta: metav1.ObjectMeta{
  1078. Name: toBeAttachedPodName,
  1079. },
  1080. Spec: v1.PodSpec{
  1081. Containers: []v1.Container{
  1082. {
  1083. Name: "container1",
  1084. Image: imageutils.GetPauseImageName(),
  1085. },
  1086. },
  1087. },
  1088. }
  1089. }
  1090. func nonCompliantConfigMap(f *framework.Framework) *v1.ConfigMap {
  1091. return &v1.ConfigMap{
  1092. ObjectMeta: metav1.ObjectMeta{
  1093. Name: disallowedConfigMapName,
  1094. },
  1095. Data: map[string]string{
  1096. "webhook-e2e-test": "webhook-disallow",
  1097. },
  1098. }
  1099. }
  1100. func nonDeletableConfigmap(f *framework.Framework) *v1.ConfigMap {
  1101. return &v1.ConfigMap{
  1102. ObjectMeta: metav1.ObjectMeta{
  1103. Name: nonDeletableConfigmapName,
  1104. },
  1105. Data: map[string]string{
  1106. "webhook-e2e-test": "webhook-nondeletable",
  1107. },
  1108. }
  1109. }
  1110. func toBeMutatedConfigMap(f *framework.Framework) *v1.ConfigMap {
  1111. return &v1.ConfigMap{
  1112. ObjectMeta: metav1.ObjectMeta{
  1113. Name: "to-be-mutated",
  1114. },
  1115. Data: map[string]string{
  1116. "mutation-start": "yes",
  1117. },
  1118. }
  1119. }
  1120. func nonCompliantConfigMapPatch() string {
  1121. return fmt.Sprint(`{"data":{"webhook-e2e-test":"webhook-disallow"}}`)
  1122. }
  1123. type updateConfigMapFn func(cm *v1.ConfigMap)
  1124. func updateConfigMap(c clientset.Interface, ns, name string, update updateConfigMapFn) (*v1.ConfigMap, error) {
  1125. var cm *v1.ConfigMap
  1126. pollErr := wait.PollImmediate(2*time.Second, 1*time.Minute, func() (bool, error) {
  1127. var err error
  1128. if cm, err = c.CoreV1().ConfigMaps(ns).Get(name, metav1.GetOptions{}); err != nil {
  1129. return false, err
  1130. }
  1131. update(cm)
  1132. if cm, err = c.CoreV1().ConfigMaps(ns).Update(cm); err == nil {
  1133. return true, nil
  1134. }
  1135. // Only retry update on conflict
  1136. if !errors.IsConflict(err) {
  1137. return false, err
  1138. }
  1139. return false, nil
  1140. })
  1141. return cm, pollErr
  1142. }
  1143. type updateCustomResourceFn func(cm *unstructured.Unstructured)
  1144. func updateCustomResource(c dynamic.ResourceInterface, ns, name string, update updateCustomResourceFn) (*unstructured.Unstructured, error) {
  1145. var cr *unstructured.Unstructured
  1146. pollErr := wait.PollImmediate(2*time.Second, 1*time.Minute, func() (bool, error) {
  1147. var err error
  1148. if cr, err = c.Get(name, metav1.GetOptions{}); err != nil {
  1149. return false, err
  1150. }
  1151. update(cr)
  1152. if cr, err = c.Update(cr, metav1.UpdateOptions{}); err == nil {
  1153. return true, nil
  1154. }
  1155. // Only retry update on conflict
  1156. if !errors.IsConflict(err) {
  1157. return false, err
  1158. }
  1159. return false, nil
  1160. })
  1161. return cr, pollErr
  1162. }
  1163. func cleanWebhookTest(client clientset.Interface, namespaceName string) {
  1164. _ = client.CoreV1().Services(namespaceName).Delete(serviceName, nil)
  1165. _ = client.AppsV1().Deployments(namespaceName).Delete(deploymentName, nil)
  1166. _ = client.CoreV1().Secrets(namespaceName).Delete(secretName, nil)
  1167. _ = client.RbacV1beta1().RoleBindings("kube-system").Delete(roleBindingName, nil)
  1168. }
  1169. func registerWebhookForCustomResource(f *framework.Framework, context *certContext, testcrd *crd.TestCrd) func() {
  1170. client := f.ClientSet
  1171. ginkgo.By("Registering the custom resource webhook via the AdmissionRegistration API")
  1172. namespace := f.Namespace.Name
  1173. configName := crWebhookConfigName
  1174. _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  1175. ObjectMeta: metav1.ObjectMeta{
  1176. Name: configName,
  1177. },
  1178. Webhooks: []v1beta1.ValidatingWebhook{
  1179. {
  1180. Name: "deny-unwanted-custom-resource-data.k8s.io",
  1181. Rules: []v1beta1.RuleWithOperations{{
  1182. Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update, v1beta1.Delete},
  1183. Rule: v1beta1.Rule{
  1184. APIGroups: []string{testcrd.Crd.Spec.Group},
  1185. APIVersions: servedAPIVersions(testcrd.Crd),
  1186. Resources: []string{testcrd.Crd.Spec.Names.Plural},
  1187. },
  1188. }},
  1189. ClientConfig: v1beta1.WebhookClientConfig{
  1190. Service: &v1beta1.ServiceReference{
  1191. Namespace: namespace,
  1192. Name: serviceName,
  1193. Path: strPtr("/custom-resource"),
  1194. Port: pointer.Int32Ptr(servicePort),
  1195. },
  1196. CABundle: context.signingCert,
  1197. },
  1198. },
  1199. },
  1200. })
  1201. framework.ExpectNoError(err, "registering custom resource webhook config %s with namespace %s", configName, namespace)
  1202. // The webhook configuration is honored in 10s.
  1203. time.Sleep(10 * time.Second)
  1204. return func() {
  1205. client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  1206. }
  1207. }
  1208. func registerMutatingWebhookForCustomResource(f *framework.Framework, context *certContext, testcrd *crd.TestCrd) func() {
  1209. client := f.ClientSet
  1210. ginkgo.By(fmt.Sprintf("Registering the mutating webhook for custom resource %s via the AdmissionRegistration API", testcrd.Crd.Name))
  1211. namespace := f.Namespace.Name
  1212. configName := f.UniqueName
  1213. _, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(&v1beta1.MutatingWebhookConfiguration{
  1214. ObjectMeta: metav1.ObjectMeta{
  1215. Name: configName,
  1216. },
  1217. Webhooks: []v1beta1.MutatingWebhook{
  1218. {
  1219. Name: "mutate-custom-resource-data-stage-1.k8s.io",
  1220. Rules: []v1beta1.RuleWithOperations{{
  1221. Operations: []v1beta1.OperationType{v1beta1.Create, v1beta1.Update},
  1222. Rule: v1beta1.Rule{
  1223. APIGroups: []string{testcrd.Crd.Spec.Group},
  1224. APIVersions: servedAPIVersions(testcrd.Crd),
  1225. Resources: []string{testcrd.Crd.Spec.Names.Plural},
  1226. },
  1227. }},
  1228. ClientConfig: v1beta1.WebhookClientConfig{
  1229. Service: &v1beta1.ServiceReference{
  1230. Namespace: namespace,
  1231. Name: serviceName,
  1232. Path: strPtr("/mutating-custom-resource"),
  1233. Port: pointer.Int32Ptr(servicePort),
  1234. },
  1235. CABundle: context.signingCert,
  1236. },
  1237. },
  1238. {
  1239. Name: "mutate-custom-resource-data-stage-2.k8s.io",
  1240. Rules: []v1beta1.RuleWithOperations{{
  1241. Operations: []v1beta1.OperationType{v1beta1.Create},
  1242. Rule: v1beta1.Rule{
  1243. APIGroups: []string{testcrd.Crd.Spec.Group},
  1244. APIVersions: servedAPIVersions(testcrd.Crd),
  1245. Resources: []string{testcrd.Crd.Spec.Names.Plural},
  1246. },
  1247. }},
  1248. ClientConfig: v1beta1.WebhookClientConfig{
  1249. Service: &v1beta1.ServiceReference{
  1250. Namespace: namespace,
  1251. Name: serviceName,
  1252. Path: strPtr("/mutating-custom-resource"),
  1253. Port: pointer.Int32Ptr(servicePort),
  1254. },
  1255. CABundle: context.signingCert,
  1256. },
  1257. },
  1258. },
  1259. })
  1260. framework.ExpectNoError(err, "registering custom resource webhook config %s with namespace %s", configName, namespace)
  1261. // The webhook configuration is honored in 10s.
  1262. time.Sleep(10 * time.Second)
  1263. return func() { client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(configName, nil) }
  1264. }
  1265. func testCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) {
  1266. ginkgo.By("Creating a custom resource that should be denied by the webhook")
  1267. crInstanceName := "cr-instance-1"
  1268. crInstance := &unstructured.Unstructured{
  1269. Object: map[string]interface{}{
  1270. "kind": crd.Spec.Names.Kind,
  1271. "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
  1272. "metadata": map[string]interface{}{
  1273. "name": crInstanceName,
  1274. "namespace": f.Namespace.Name,
  1275. },
  1276. "data": map[string]interface{}{
  1277. "webhook-e2e-test": "webhook-disallow",
  1278. },
  1279. },
  1280. }
  1281. _, err := customResourceClient.Create(crInstance, metav1.CreateOptions{})
  1282. framework.ExpectError(err, "create custom resource %s in namespace %s should be denied by webhook", crInstanceName, f.Namespace.Name)
  1283. expectedErrMsg := "the custom resource contains unwanted data"
  1284. if !strings.Contains(err.Error(), expectedErrMsg) {
  1285. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  1286. }
  1287. }
  1288. func testBlockingCustomResourceDeletion(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface) {
  1289. ginkgo.By("Creating a custom resource whose deletion would be denied by the webhook")
  1290. crInstanceName := "cr-instance-2"
  1291. crInstance := &unstructured.Unstructured{
  1292. Object: map[string]interface{}{
  1293. "kind": crd.Spec.Names.Kind,
  1294. "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
  1295. "metadata": map[string]interface{}{
  1296. "name": crInstanceName,
  1297. "namespace": f.Namespace.Name,
  1298. },
  1299. "data": map[string]interface{}{
  1300. "webhook-e2e-test": "webhook-nondeletable",
  1301. },
  1302. },
  1303. }
  1304. _, err := customResourceClient.Create(crInstance, metav1.CreateOptions{})
  1305. framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
  1306. ginkgo.By("Deleting the custom resource should be denied")
  1307. err = customResourceClient.Delete(crInstanceName, &metav1.DeleteOptions{})
  1308. framework.ExpectError(err, "deleting custom resource %s in namespace: %s should be denied", crInstanceName, f.Namespace.Name)
  1309. expectedErrMsg1 := "the custom resource cannot be deleted because it contains unwanted key and value"
  1310. if !strings.Contains(err.Error(), expectedErrMsg1) {
  1311. framework.Failf("expect error contains %q, got %q", expectedErrMsg1, err.Error())
  1312. }
  1313. ginkgo.By("Remove the offending key and value from the custom resource data")
  1314. toCompliantFn := func(cr *unstructured.Unstructured) {
  1315. if _, ok := cr.Object["data"]; !ok {
  1316. cr.Object["data"] = map[string]interface{}{}
  1317. }
  1318. data := cr.Object["data"].(map[string]interface{})
  1319. data["webhook-e2e-test"] = "webhook-allow"
  1320. }
  1321. _, err = updateCustomResource(customResourceClient, f.Namespace.Name, crInstanceName, toCompliantFn)
  1322. framework.ExpectNoError(err, "failed to update custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
  1323. ginkgo.By("Deleting the updated custom resource should be successful")
  1324. err = customResourceClient.Delete(crInstanceName, &metav1.DeleteOptions{})
  1325. framework.ExpectNoError(err, "failed to delete custom resource %s in namespace: %s", crInstanceName, f.Namespace.Name)
  1326. }
  1327. func testMutatingCustomResourceWebhook(f *framework.Framework, crd *apiextensionsv1beta1.CustomResourceDefinition, customResourceClient dynamic.ResourceInterface, prune bool) {
  1328. ginkgo.By("Creating a custom resource that should be mutated by the webhook")
  1329. crName := "cr-instance-1"
  1330. cr := &unstructured.Unstructured{
  1331. Object: map[string]interface{}{
  1332. "kind": crd.Spec.Names.Kind,
  1333. "apiVersion": crd.Spec.Group + "/" + crd.Spec.Version,
  1334. "metadata": map[string]interface{}{
  1335. "name": crName,
  1336. "namespace": f.Namespace.Name,
  1337. },
  1338. "data": map[string]interface{}{
  1339. "mutation-start": "yes",
  1340. },
  1341. },
  1342. }
  1343. mutatedCR, err := customResourceClient.Create(cr, metav1.CreateOptions{})
  1344. framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crName, f.Namespace.Name)
  1345. expectedCRData := map[string]interface{}{
  1346. "mutation-start": "yes",
  1347. "mutation-stage-1": "yes",
  1348. }
  1349. if !prune {
  1350. expectedCRData["mutation-stage-2"] = "yes"
  1351. }
  1352. if !reflect.DeepEqual(expectedCRData, mutatedCR.Object["data"]) {
  1353. framework.Failf("\nexpected %#v\n, got %#v\n", expectedCRData, mutatedCR.Object["data"])
  1354. }
  1355. }
  1356. func testMultiVersionCustomResourceWebhook(f *framework.Framework, testcrd *crd.TestCrd) {
  1357. customResourceClient := testcrd.DynamicClients["v1"]
  1358. ginkgo.By("Creating a custom resource while v1 is storage version")
  1359. crName := "cr-instance-1"
  1360. cr := &unstructured.Unstructured{
  1361. Object: map[string]interface{}{
  1362. "kind": testcrd.Crd.Spec.Names.Kind,
  1363. "apiVersion": testcrd.Crd.Spec.Group + "/" + testcrd.Crd.Spec.Version,
  1364. "metadata": map[string]interface{}{
  1365. "name": crName,
  1366. "namespace": f.Namespace.Name,
  1367. },
  1368. "data": map[string]interface{}{
  1369. "mutation-start": "yes",
  1370. },
  1371. },
  1372. }
  1373. _, err := customResourceClient.Create(cr, metav1.CreateOptions{})
  1374. framework.ExpectNoError(err, "failed to create custom resource %s in namespace: %s", crName, f.Namespace.Name)
  1375. ginkgo.By("Patching Custom Resource Definition to set v2 as storage")
  1376. apiVersionWithV2StoragePatch := fmt.Sprint(`{"spec": {"versions": [{"name": "v1", "storage": false, "served": true},{"name": "v2", "storage": true, "served": true}]}}`)
  1377. _, err = testcrd.APIExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Patch(testcrd.Crd.Name, types.StrategicMergePatchType, []byte(apiVersionWithV2StoragePatch))
  1378. framework.ExpectNoError(err, "failed to patch custom resource definition %s in namespace: %s", testcrd.Crd.Name, f.Namespace.Name)
  1379. ginkgo.By("Patching the custom resource while v2 is storage version")
  1380. crDummyPatch := fmt.Sprint(`[{ "op": "add", "path": "/dummy", "value": "test" }]`)
  1381. _, err = testcrd.DynamicClients["v2"].Patch(crName, types.JSONPatchType, []byte(crDummyPatch), metav1.PatchOptions{})
  1382. framework.ExpectNoError(err, "failed to patch custom resource %s in namespace: %s", crName, f.Namespace.Name)
  1383. }
  1384. func registerValidatingWebhookForCRD(f *framework.Framework, context *certContext) func() {
  1385. client := f.ClientSet
  1386. ginkgo.By("Registering the crd webhook via the AdmissionRegistration API")
  1387. namespace := f.Namespace.Name
  1388. configName := crdWebhookConfigName
  1389. // This webhook will deny the creation of CustomResourceDefinitions which have the
  1390. // label "webhook-e2e-test":"webhook-disallow"
  1391. // NOTE: Because tests are run in parallel and in an unpredictable order, it is critical
  1392. // that no other test attempts to create CRD with that label.
  1393. _, err := client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  1394. ObjectMeta: metav1.ObjectMeta{
  1395. Name: configName,
  1396. },
  1397. Webhooks: []v1beta1.ValidatingWebhook{
  1398. {
  1399. Name: "deny-crd-with-unwanted-label.k8s.io",
  1400. Rules: []v1beta1.RuleWithOperations{{
  1401. Operations: []v1beta1.OperationType{v1beta1.Create},
  1402. Rule: v1beta1.Rule{
  1403. APIGroups: []string{"apiextensions.k8s.io"},
  1404. APIVersions: []string{"*"},
  1405. Resources: []string{"customresourcedefinitions"},
  1406. },
  1407. }},
  1408. ClientConfig: v1beta1.WebhookClientConfig{
  1409. Service: &v1beta1.ServiceReference{
  1410. Namespace: namespace,
  1411. Name: serviceName,
  1412. Path: strPtr("/crd"),
  1413. Port: pointer.Int32Ptr(servicePort),
  1414. },
  1415. CABundle: context.signingCert,
  1416. },
  1417. },
  1418. },
  1419. })
  1420. framework.ExpectNoError(err, "registering crd webhook config %s with namespace %s", configName, namespace)
  1421. // The webhook configuration is honored in 10s.
  1422. time.Sleep(10 * time.Second)
  1423. return func() {
  1424. client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  1425. }
  1426. }
  1427. func testCRDDenyWebhook(f *framework.Framework) {
  1428. ginkgo.By("Creating a custom resource definition that should be denied by the webhook")
  1429. name := fmt.Sprintf("e2e-test-%s-%s-crd", f.BaseName, "deny")
  1430. kind := fmt.Sprintf("E2e-test-%s-%s-crd", f.BaseName, "deny")
  1431. group := fmt.Sprintf("%s-crd-test.k8s.io", f.BaseName)
  1432. apiVersions := []apiextensionsv1beta1.CustomResourceDefinitionVersion{
  1433. {
  1434. Name: "v1",
  1435. Served: true,
  1436. Storage: true,
  1437. },
  1438. }
  1439. // Creating a custom resource definition for use by assorted tests.
  1440. config, err := framework.LoadConfig()
  1441. if err != nil {
  1442. framework.Failf("failed to load config: %v", err)
  1443. return
  1444. }
  1445. apiExtensionClient, err := crdclientset.NewForConfig(config)
  1446. if err != nil {
  1447. framework.Failf("failed to initialize apiExtensionClient: %v", err)
  1448. return
  1449. }
  1450. crd := &apiextensionsv1beta1.CustomResourceDefinition{
  1451. ObjectMeta: metav1.ObjectMeta{
  1452. Name: name + "s." + group,
  1453. Labels: map[string]string{
  1454. "webhook-e2e-test": "webhook-disallow",
  1455. },
  1456. },
  1457. Spec: apiextensionsv1beta1.CustomResourceDefinitionSpec{
  1458. Group: group,
  1459. Versions: apiVersions,
  1460. Names: apiextensionsv1beta1.CustomResourceDefinitionNames{
  1461. Singular: name,
  1462. Kind: kind,
  1463. ListKind: kind + "List",
  1464. Plural: name + "s",
  1465. },
  1466. Scope: apiextensionsv1beta1.NamespaceScoped,
  1467. },
  1468. }
  1469. // create CRD
  1470. _, err = apiExtensionClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
  1471. framework.ExpectError(err, "create custom resource definition %s should be denied by webhook", crd.Name)
  1472. expectedErrMsg := "the crd contains unwanted label"
  1473. if !strings.Contains(err.Error(), expectedErrMsg) {
  1474. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  1475. }
  1476. }
  1477. func registerSlowWebhook(f *framework.Framework, context *certContext, policy *v1beta1.FailurePolicyType, timeout *int32) func() {
  1478. client := f.ClientSet
  1479. ginkgo.By("Registering slow webhook via the AdmissionRegistration API")
  1480. namespace := f.Namespace.Name
  1481. configName := slowWebhookConfigName
  1482. // Add a unique label to the namespace
  1483. ns, err := client.CoreV1().Namespaces().Get(namespace, metav1.GetOptions{})
  1484. framework.ExpectNoError(err, "error getting namespace %s", namespace)
  1485. if ns.Labels == nil {
  1486. ns.Labels = map[string]string{}
  1487. }
  1488. ns.Labels[slowWebhookConfigName] = namespace
  1489. _, err = client.CoreV1().Namespaces().Update(ns)
  1490. framework.ExpectNoError(err, "error labeling namespace %s", namespace)
  1491. _, err = client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Create(&v1beta1.ValidatingWebhookConfiguration{
  1492. ObjectMeta: metav1.ObjectMeta{
  1493. Name: configName,
  1494. },
  1495. Webhooks: []v1beta1.ValidatingWebhook{
  1496. {
  1497. Name: "allow-configmap-with-delay-webhook.k8s.io",
  1498. Rules: []v1beta1.RuleWithOperations{{
  1499. Operations: []v1beta1.OperationType{v1beta1.Create},
  1500. Rule: v1beta1.Rule{
  1501. APIGroups: []string{""},
  1502. APIVersions: []string{"v1"},
  1503. Resources: []string{"configmaps"},
  1504. },
  1505. }},
  1506. ClientConfig: v1beta1.WebhookClientConfig{
  1507. Service: &v1beta1.ServiceReference{
  1508. Namespace: namespace,
  1509. Name: serviceName,
  1510. Path: strPtr("/always-allow-delay-5s"),
  1511. Port: pointer.Int32Ptr(servicePort),
  1512. },
  1513. CABundle: context.signingCert,
  1514. },
  1515. // Scope the webhook to just this namespace
  1516. NamespaceSelector: &metav1.LabelSelector{
  1517. MatchLabels: ns.Labels,
  1518. },
  1519. FailurePolicy: policy,
  1520. TimeoutSeconds: timeout,
  1521. },
  1522. },
  1523. })
  1524. framework.ExpectNoError(err, "registering slow webhook config %s with namespace %s", configName, namespace)
  1525. // The webhook configuration is honored in 10s.
  1526. time.Sleep(10 * time.Second)
  1527. return func() {
  1528. client.AdmissionregistrationV1beta1().ValidatingWebhookConfigurations().Delete(configName, nil)
  1529. }
  1530. }
  1531. func testSlowWebhookTimeoutFailEarly(f *framework.Framework) {
  1532. ginkgo.By("Request fails when timeout (1s) is shorter than slow webhook latency (5s)")
  1533. client := f.ClientSet
  1534. name := "e2e-test-slow-webhook-configmap"
  1535. _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name}})
  1536. framework.ExpectError(err, "create configmap in namespace %s should have timed-out reaching slow webhook", f.Namespace.Name)
  1537. expectedErrMsg := `/always-allow-delay-5s?timeout=1s: context deadline exceeded`
  1538. if !strings.Contains(err.Error(), expectedErrMsg) {
  1539. framework.Failf("expect error contains %q, got %q", expectedErrMsg, err.Error())
  1540. }
  1541. }
  1542. func testSlowWebhookTimeoutNoError(f *framework.Framework) {
  1543. client := f.ClientSet
  1544. name := "e2e-test-slow-webhook-configmap"
  1545. _, err := client.CoreV1().ConfigMaps(f.Namespace.Name).Create(&v1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: name}})
  1546. gomega.Expect(err).To(gomega.BeNil())
  1547. err = client.CoreV1().ConfigMaps(f.Namespace.Name).Delete(name, &metav1.DeleteOptions{})
  1548. gomega.Expect(err).To(gomega.BeNil())
  1549. }
  1550. // createAdmissionWebhookMultiVersionTestCRDWithV1Storage creates a new CRD specifically
  1551. // for the admissin webhook calling test.
  1552. func createAdmissionWebhookMultiVersionTestCRDWithV1Storage(f *framework.Framework, opts ...crd.Option) (*crd.TestCrd, error) {
  1553. group := fmt.Sprintf("%s-multiversion-crd-test.k8s.io", f.BaseName)
  1554. return crd.CreateMultiVersionTestCRD(f, group, append([]crd.Option{func(crd *apiextensionsv1beta1.CustomResourceDefinition) {
  1555. crd.Spec.Versions = []apiextensionsv1beta1.CustomResourceDefinitionVersion{
  1556. {
  1557. Name: "v1",
  1558. Served: true,
  1559. Storage: true,
  1560. },
  1561. {
  1562. Name: "v2",
  1563. Served: true,
  1564. Storage: false,
  1565. },
  1566. }
  1567. }}, opts...)...)
  1568. }
  1569. // servedAPIVersions returns the API versions served by the CRD.
  1570. func servedAPIVersions(crd *apiextensionsv1beta1.CustomResourceDefinition) []string {
  1571. ret := []string{}
  1572. for _, v := range crd.Spec.Versions {
  1573. if v.Served {
  1574. ret = append(ret, v.Name)
  1575. }
  1576. }
  1577. return ret
  1578. }