crd_conversion_webhook.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. /*
  2. Copyright 2018 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package apimachinery
  14. import (
  15. "time"
  16. "github.com/onsi/ginkgo"
  17. "github.com/onsi/gomega"
  18. apps "k8s.io/api/apps/v1"
  19. v1 "k8s.io/api/core/v1"
  20. rbacv1 "k8s.io/api/rbac/v1"
  21. "k8s.io/apimachinery/pkg/api/errors"
  22. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  23. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  24. "k8s.io/apimachinery/pkg/util/intstr"
  25. utilversion "k8s.io/apimachinery/pkg/util/version"
  26. "k8s.io/client-go/dynamic"
  27. clientset "k8s.io/client-go/kubernetes"
  28. "k8s.io/kubernetes/test/e2e/framework"
  29. e2edeploy "k8s.io/kubernetes/test/e2e/framework/deployment"
  30. e2elog "k8s.io/kubernetes/test/e2e/framework/log"
  31. "k8s.io/kubernetes/test/utils/crd"
  32. imageutils "k8s.io/kubernetes/test/utils/image"
  33. "k8s.io/utils/pointer"
  34. "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
  35. "k8s.io/apiextensions-apiserver/test/integration"
  36. // ensure libs have a chance to initialize
  37. _ "github.com/stretchr/testify/assert"
  38. )
  39. const (
  40. secretCRDName = "sample-custom-resource-conversion-webhook-secret"
  41. deploymentCRDName = "sample-crd-conversion-webhook-deployment"
  42. serviceCRDName = "e2e-test-crd-conversion-webhook"
  43. serviceCRDPort = 9443
  44. roleBindingCRDName = "crd-conversion-webhook-auth-reader"
  45. )
  46. var serverCRDConversionWebhookVersion = utilversion.MustParseSemantic("v1.13.0-alpha")
  47. var apiVersions = []v1beta1.CustomResourceDefinitionVersion{
  48. {
  49. Name: "v1",
  50. Served: true,
  51. Storage: true,
  52. Schema: &v1beta1.CustomResourceValidation{
  53. OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
  54. Type: "object",
  55. Properties: map[string]v1beta1.JSONSchemaProps{
  56. "hostPort": {Type: "string"},
  57. },
  58. },
  59. },
  60. },
  61. {
  62. Name: "v2",
  63. Served: true,
  64. Storage: false,
  65. Schema: &v1beta1.CustomResourceValidation{
  66. OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
  67. Type: "object",
  68. Properties: map[string]v1beta1.JSONSchemaProps{
  69. "host": {Type: "string"},
  70. "port": {Type: "string"},
  71. },
  72. },
  73. },
  74. },
  75. }
  76. var alternativeAPIVersions = []v1beta1.CustomResourceDefinitionVersion{
  77. {
  78. Name: "v1",
  79. Served: true,
  80. Storage: false,
  81. Schema: &v1beta1.CustomResourceValidation{
  82. OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
  83. Type: "object",
  84. Properties: map[string]v1beta1.JSONSchemaProps{
  85. "hostPort": {Type: "string"},
  86. },
  87. },
  88. },
  89. },
  90. {
  91. Name: "v2",
  92. Served: true,
  93. Storage: true,
  94. Schema: &v1beta1.CustomResourceValidation{
  95. OpenAPIV3Schema: &v1beta1.JSONSchemaProps{
  96. Type: "object",
  97. Properties: map[string]v1beta1.JSONSchemaProps{
  98. "host": {Type: "string"},
  99. "port": {Type: "string"},
  100. },
  101. },
  102. },
  103. },
  104. }
  105. var _ = SIGDescribe("CustomResourceConversionWebhook", func() {
  106. var context *certContext
  107. f := framework.NewDefaultFramework("crd-webhook")
  108. var client clientset.Interface
  109. var namespaceName string
  110. ginkgo.BeforeEach(func() {
  111. client = f.ClientSet
  112. namespaceName = f.Namespace.Name
  113. // Make sure the relevant provider supports conversion webhook
  114. framework.SkipUnlessServerVersionGTE(serverCRDConversionWebhookVersion, f.ClientSet.Discovery())
  115. ginkgo.By("Setting up server cert")
  116. context = setupServerCert(f.Namespace.Name, serviceCRDName)
  117. createAuthReaderRoleBindingForCRDConversion(f, f.Namespace.Name)
  118. deployCustomResourceWebhookAndService(f, imageutils.GetE2EImage(imageutils.CRDConversionWebhook), context)
  119. })
  120. ginkgo.AfterEach(func() {
  121. cleanCRDWebhookTest(client, namespaceName)
  122. })
  123. ginkgo.It("Should be able to convert from CR v1 to CR v2", func() {
  124. testcrd, err := crd.CreateMultiVersionTestCRD(f, "stable.example.com", func(crd *v1beta1.CustomResourceDefinition) {
  125. crd.Spec.Versions = apiVersions
  126. crd.Spec.Conversion = &v1beta1.CustomResourceConversion{
  127. Strategy: v1beta1.WebhookConverter,
  128. WebhookClientConfig: &v1beta1.WebhookClientConfig{
  129. CABundle: context.signingCert,
  130. Service: &v1beta1.ServiceReference{
  131. Namespace: f.Namespace.Name,
  132. Name: serviceCRDName,
  133. Path: pointer.StringPtr("/crdconvert"),
  134. Port: pointer.Int32Ptr(serviceCRDPort),
  135. },
  136. },
  137. }
  138. crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
  139. })
  140. if err != nil {
  141. return
  142. }
  143. defer testcrd.CleanUp()
  144. testCustomResourceConversionWebhook(f, testcrd.Crd, testcrd.DynamicClients)
  145. })
  146. ginkgo.It("Should be able to convert a non homogeneous list of CRs", func() {
  147. testcrd, err := crd.CreateMultiVersionTestCRD(f, "stable.example.com", func(crd *v1beta1.CustomResourceDefinition) {
  148. crd.Spec.Versions = apiVersions
  149. crd.Spec.Conversion = &v1beta1.CustomResourceConversion{
  150. Strategy: v1beta1.WebhookConverter,
  151. WebhookClientConfig: &v1beta1.WebhookClientConfig{
  152. CABundle: context.signingCert,
  153. Service: &v1beta1.ServiceReference{
  154. Namespace: f.Namespace.Name,
  155. Name: serviceCRDName,
  156. Path: pointer.StringPtr("/crdconvert"),
  157. Port: pointer.Int32Ptr(serviceCRDPort),
  158. },
  159. },
  160. }
  161. crd.Spec.PreserveUnknownFields = pointer.BoolPtr(false)
  162. })
  163. if err != nil {
  164. return
  165. }
  166. defer testcrd.CleanUp()
  167. testCRListConversion(f, testcrd)
  168. })
  169. })
  170. func cleanCRDWebhookTest(client clientset.Interface, namespaceName string) {
  171. _ = client.CoreV1().Services(namespaceName).Delete(serviceCRDName, nil)
  172. _ = client.AppsV1().Deployments(namespaceName).Delete(deploymentCRDName, nil)
  173. _ = client.CoreV1().Secrets(namespaceName).Delete(secretCRDName, nil)
  174. _ = client.RbacV1().RoleBindings("kube-system").Delete(roleBindingCRDName, nil)
  175. }
  176. func createAuthReaderRoleBindingForCRDConversion(f *framework.Framework, namespace string) {
  177. ginkgo.By("Create role binding to let cr conversion webhook read extension-apiserver-authentication")
  178. client := f.ClientSet
  179. // Create the role binding to allow the webhook read the extension-apiserver-authentication configmap
  180. _, err := client.RbacV1().RoleBindings("kube-system").Create(&rbacv1.RoleBinding{
  181. ObjectMeta: metav1.ObjectMeta{
  182. Name: roleBindingCRDName,
  183. },
  184. RoleRef: rbacv1.RoleRef{
  185. APIGroup: "",
  186. Kind: "Role",
  187. Name: "extension-apiserver-authentication-reader",
  188. },
  189. // Webhook uses the default service account.
  190. Subjects: []rbacv1.Subject{
  191. {
  192. Kind: "ServiceAccount",
  193. Name: "default",
  194. Namespace: namespace,
  195. },
  196. },
  197. })
  198. if err != nil && errors.IsAlreadyExists(err) {
  199. e2elog.Logf("role binding %s already exists", roleBindingCRDName)
  200. } else {
  201. framework.ExpectNoError(err, "creating role binding %s:webhook to access configMap", namespace)
  202. }
  203. }
  204. func deployCustomResourceWebhookAndService(f *framework.Framework, image string, context *certContext) {
  205. ginkgo.By("Deploying the custom resource conversion webhook pod")
  206. client := f.ClientSet
  207. // Creating the secret that contains the webhook's cert.
  208. secret := &v1.Secret{
  209. ObjectMeta: metav1.ObjectMeta{
  210. Name: secretCRDName,
  211. },
  212. Type: v1.SecretTypeOpaque,
  213. Data: map[string][]byte{
  214. "tls.crt": context.cert,
  215. "tls.key": context.key,
  216. },
  217. }
  218. namespace := f.Namespace.Name
  219. _, err := client.CoreV1().Secrets(namespace).Create(secret)
  220. framework.ExpectNoError(err, "creating secret %q in namespace %q", secretName, namespace)
  221. // Create the deployment of the webhook
  222. podLabels := map[string]string{"app": "sample-crd-conversion-webhook", "crd-webhook": "true"}
  223. replicas := int32(1)
  224. zero := int64(0)
  225. mounts := []v1.VolumeMount{
  226. {
  227. Name: "crd-conversion-webhook-certs",
  228. ReadOnly: true,
  229. MountPath: "/webhook.local.config/certificates",
  230. },
  231. }
  232. volumes := []v1.Volume{
  233. {
  234. Name: "crd-conversion-webhook-certs",
  235. VolumeSource: v1.VolumeSource{
  236. Secret: &v1.SecretVolumeSource{SecretName: secretCRDName},
  237. },
  238. },
  239. }
  240. containers := []v1.Container{
  241. {
  242. Name: "sample-crd-conversion-webhook",
  243. VolumeMounts: mounts,
  244. Args: []string{
  245. "--tls-cert-file=/webhook.local.config/certificates/tls.crt",
  246. "--tls-private-key-file=/webhook.local.config/certificates/tls.key",
  247. "--alsologtostderr",
  248. "-v=4",
  249. "2>&1",
  250. },
  251. Image: image,
  252. },
  253. }
  254. d := &apps.Deployment{
  255. ObjectMeta: metav1.ObjectMeta{
  256. Name: deploymentCRDName,
  257. Labels: podLabels,
  258. },
  259. Spec: apps.DeploymentSpec{
  260. Replicas: &replicas,
  261. Selector: &metav1.LabelSelector{
  262. MatchLabels: podLabels,
  263. },
  264. Strategy: apps.DeploymentStrategy{
  265. Type: apps.RollingUpdateDeploymentStrategyType,
  266. },
  267. Template: v1.PodTemplateSpec{
  268. ObjectMeta: metav1.ObjectMeta{
  269. Labels: podLabels,
  270. },
  271. Spec: v1.PodSpec{
  272. TerminationGracePeriodSeconds: &zero,
  273. Containers: containers,
  274. Volumes: volumes,
  275. },
  276. },
  277. },
  278. }
  279. deployment, err := client.AppsV1().Deployments(namespace).Create(d)
  280. framework.ExpectNoError(err, "creating deployment %s in namespace %s", deploymentCRDName, namespace)
  281. ginkgo.By("Wait for the deployment to be ready")
  282. err = e2edeploy.WaitForDeploymentRevisionAndImage(client, namespace, deploymentCRDName, "1", image)
  283. framework.ExpectNoError(err, "waiting for the deployment of image %s in %s in %s to complete", image, deploymentName, namespace)
  284. err = e2edeploy.WaitForDeploymentComplete(client, deployment)
  285. framework.ExpectNoError(err, "waiting for the deployment status valid", image, deploymentCRDName, namespace)
  286. ginkgo.By("Deploying the webhook service")
  287. serviceLabels := map[string]string{"crd-webhook": "true"}
  288. service := &v1.Service{
  289. ObjectMeta: metav1.ObjectMeta{
  290. Namespace: namespace,
  291. Name: serviceCRDName,
  292. Labels: map[string]string{"test": "crd-webhook"},
  293. },
  294. Spec: v1.ServiceSpec{
  295. Selector: serviceLabels,
  296. Ports: []v1.ServicePort{
  297. {
  298. Protocol: "TCP",
  299. Port: serviceCRDPort,
  300. TargetPort: intstr.FromInt(443),
  301. },
  302. },
  303. },
  304. }
  305. _, err = client.CoreV1().Services(namespace).Create(service)
  306. framework.ExpectNoError(err, "creating service %s in namespace %s", serviceCRDName, namespace)
  307. ginkgo.By("Verifying the service has paired with the endpoint")
  308. err = framework.WaitForServiceEndpointsNum(client, namespace, serviceCRDName, 1, 1*time.Second, 30*time.Second)
  309. framework.ExpectNoError(err, "waiting for service %s/%s have %d endpoint", namespace, serviceCRDName, 1)
  310. }
  311. func verifyV1Object(f *framework.Framework, crd *v1beta1.CustomResourceDefinition, obj *unstructured.Unstructured) {
  312. gomega.Expect(obj.GetAPIVersion()).To(gomega.BeEquivalentTo(crd.Spec.Group + "/v1"))
  313. hostPort, exists := obj.Object["hostPort"]
  314. gomega.Expect(exists).To(gomega.BeTrue())
  315. gomega.Expect(hostPort).To(gomega.BeEquivalentTo("localhost:8080"))
  316. _, hostExists := obj.Object["host"]
  317. gomega.Expect(hostExists).To(gomega.BeFalse())
  318. _, portExists := obj.Object["port"]
  319. gomega.Expect(portExists).To(gomega.BeFalse())
  320. }
  321. func verifyV2Object(f *framework.Framework, crd *v1beta1.CustomResourceDefinition, obj *unstructured.Unstructured) {
  322. gomega.Expect(obj.GetAPIVersion()).To(gomega.BeEquivalentTo(crd.Spec.Group + "/v2"))
  323. _, hostPortExists := obj.Object["hostPort"]
  324. gomega.Expect(hostPortExists).To(gomega.BeFalse())
  325. host, hostExists := obj.Object["host"]
  326. gomega.Expect(hostExists).To(gomega.BeTrue())
  327. gomega.Expect(host).To(gomega.BeEquivalentTo("localhost"))
  328. port, portExists := obj.Object["port"]
  329. gomega.Expect(portExists).To(gomega.BeTrue())
  330. gomega.Expect(port).To(gomega.BeEquivalentTo("8080"))
  331. }
  332. func testCustomResourceConversionWebhook(f *framework.Framework, crd *v1beta1.CustomResourceDefinition, customResourceClients map[string]dynamic.ResourceInterface) {
  333. name := "cr-instance-1"
  334. ginkgo.By("Creating a v1 custom resource")
  335. crInstance := &unstructured.Unstructured{
  336. Object: map[string]interface{}{
  337. "kind": crd.Spec.Names.Kind,
  338. "apiVersion": crd.Spec.Group + "/v1",
  339. "metadata": map[string]interface{}{
  340. "name": name,
  341. "namespace": f.Namespace.Name,
  342. },
  343. "hostPort": "localhost:8080",
  344. },
  345. }
  346. _, err := customResourceClients["v1"].Create(crInstance, metav1.CreateOptions{})
  347. gomega.Expect(err).To(gomega.BeNil())
  348. ginkgo.By("v2 custom resource should be converted")
  349. v2crd, err := customResourceClients["v2"].Get(name, metav1.GetOptions{})
  350. verifyV2Object(f, crd, v2crd)
  351. }
  352. func testCRListConversion(f *framework.Framework, testCrd *crd.TestCrd) {
  353. crd := testCrd.Crd
  354. customResourceClients := testCrd.DynamicClients
  355. name1 := "cr-instance-1"
  356. name2 := "cr-instance-2"
  357. ginkgo.By("Creating a v1 custom resource")
  358. crInstance := &unstructured.Unstructured{
  359. Object: map[string]interface{}{
  360. "kind": crd.Spec.Names.Kind,
  361. "apiVersion": crd.Spec.Group + "/v1",
  362. "metadata": map[string]interface{}{
  363. "name": name1,
  364. "namespace": f.Namespace.Name,
  365. },
  366. "hostPort": "localhost:8080",
  367. },
  368. }
  369. _, err := customResourceClients["v1"].Create(crInstance, metav1.CreateOptions{})
  370. gomega.Expect(err).To(gomega.BeNil())
  371. // Now cr-instance-1 is stored as v1. lets change storage version
  372. crd, err = integration.UpdateCustomResourceDefinitionWithRetry(testCrd.APIExtensionClient, crd.Name, func(c *v1beta1.CustomResourceDefinition) {
  373. c.Spec.Versions = alternativeAPIVersions
  374. })
  375. gomega.Expect(err).To(gomega.BeNil())
  376. ginkgo.By("Create a v2 custom resource")
  377. crInstance = &unstructured.Unstructured{
  378. Object: map[string]interface{}{
  379. "kind": crd.Spec.Names.Kind,
  380. "apiVersion": crd.Spec.Group + "/v1",
  381. "metadata": map[string]interface{}{
  382. "name": name2,
  383. "namespace": f.Namespace.Name,
  384. },
  385. "hostPort": "localhost:8080",
  386. },
  387. }
  388. // After changing a CRD, the resources for versions will be re-created that can be result in
  389. // cancelled connection (e.g. "grpc connection closed" or "context canceled").
  390. // Just retrying fixes that.
  391. //
  392. // TODO: we have to wait for the storage version to become effective. Storage version changes are not instant.
  393. for i := 0; i < 5; i++ {
  394. _, err = customResourceClients["v1"].Create(crInstance, metav1.CreateOptions{})
  395. if err == nil {
  396. break
  397. }
  398. }
  399. gomega.Expect(err).To(gomega.BeNil())
  400. // Now that we have a v1 and v2 object, both list operation in v1 and v2 should work as expected.
  401. ginkgo.By("List CRs in v1")
  402. list, err := customResourceClients["v1"].List(metav1.ListOptions{})
  403. gomega.Expect(err).To(gomega.BeNil())
  404. gomega.Expect(len(list.Items)).To(gomega.BeIdenticalTo(2))
  405. gomega.Expect((list.Items[0].GetName() == name1 && list.Items[1].GetName() == name2) ||
  406. (list.Items[0].GetName() == name2 && list.Items[1].GetName() == name1)).To(gomega.BeTrue())
  407. verifyV1Object(f, crd, &list.Items[0])
  408. verifyV1Object(f, crd, &list.Items[1])
  409. ginkgo.By("List CRs in v2")
  410. list, err = customResourceClients["v2"].List(metav1.ListOptions{})
  411. gomega.Expect(err).To(gomega.BeNil())
  412. gomega.Expect(len(list.Items)).To(gomega.BeIdenticalTo(2))
  413. gomega.Expect((list.Items[0].GetName() == name1 && list.Items[1].GetName() == name2) ||
  414. (list.Items[0].GetName() == name2 && list.Items[1].GetName() == name1)).To(gomega.BeTrue())
  415. verifyV2Object(f, crd, &list.Items[0])
  416. verifyV2Object(f, crd, &list.Items[1])
  417. }