webhookconfig_test.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package webhookconfig
  13. import (
  14. "bytes"
  15. "context"
  16. "time"
  17. admissionregistration "k8s.io/api/admissionregistration/v1"
  18. corev1 "k8s.io/api/core/v1"
  19. discoveryv1 "k8s.io/api/discovery/v1"
  20. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  21. "k8s.io/apimachinery/pkg/types"
  22. pointer "k8s.io/utils/ptr"
  23. "github.com/external-secrets/external-secrets/pkg/constants"
  24. . "github.com/onsi/ginkgo/v2"
  25. . "github.com/onsi/gomega"
  26. )
  27. const defaultCACert = `-----BEGIN CERTIFICATE-----
  28. MIIDRjCCAi6gAwIBAgIBADANBgkqhkiG9w0BAQsFADA2MRkwFwYDVQQKExBleHRl
  29. cm5hbC1zZWNyZXRzMRkwFwYDVQQDExBleHRlcm5hbC1zZWNyZXRzMB4XDTIyMDIx
  30. NzEwMDYxMFoXDTMyMDIxNTExMDYxMFowNjEZMBcGA1UEChMQZXh0ZXJuYWwtc2Vj
  31. cmV0czEZMBcGA1UEAxMQZXh0ZXJuYWwtc2VjcmV0czCCASIwDQYJKoZIhvcNAQEB
  32. BQADggEPADCCAQoCggEBAKSINgqU2dBdX8JpPjRHWSdpxuoltGl6xXmQHOhbTXAt
  33. /STDu7oi6eiFgepQ2QHuWLGwZgbbYnEhtLvw4dUwPcLyv6WIdeiUSA4pdFxL7asc
  34. WV4tjiRkRTJVrixJTxXpry/CsPqXBlvnu1YGESkrLOYCmA2xnDH8voEBbwYvXXB9
  35. 3g5rOJncSh/7g+H55ZFFyWrIPyDUnfwE3CREjZXpsagFhRYpkuRlXTnU6t0OTEEh
  36. qLHlZ+ebUzL8NaegEgEHD32PrQPXpls1yurIrsA+I6McWkXGykykYHVK+1a1pL1g
  37. e+PBkegtwtX+EmB2ux7PVVeB4TTYqzCKbnObW4mJLZkCAwEAAaNfMF0wDgYDVR0P
  38. AQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHgSu/Im2gyu4TU0
  39. AWrMSFbtoVokMBsGA1UdEQQUMBKCEGV4dGVybmFsLXNlY3JldHMwDQYJKoZIhvcN
  40. AQELBQADggEBAJU88jCcPsAHN8DKLu+QMCoKYbeftX4gXxyoijGSde2w2O8NPtMP
  41. awu4Y5x3LNTwyIIxXi78UD0RI53GbUgHvS+X9v6CC2IZMS65xqKR+EsjzEh7Ldbm
  42. vZoF4ZDnfb2s5SK6MeYf67BE7XWpGfbHmjt6h80xsYjL6ovcik+dlu/AixMyLslS
  43. tDbMybAR8kR0zdQLYcZq7XEX5QsOO8qBn5rTfD6MiYik8ZrP7FqUMHyVpHiBuNio
  44. krnSOvynvuA9mlf2F2727dMt2Ij9uER+9QnhWBQex1h8CwALmm2k9G5Gt+RjB8oe
  45. lNjvmHAXUfOE/cbD7EP++X17kWt41FjmePc=
  46. -----END CERTIFICATE-----
  47. `
  48. type testCase struct {
  49. vwc *admissionregistration.ValidatingWebhookConfiguration
  50. service *corev1.Service
  51. endpointSlice *discoveryv1.EndpointSlice
  52. secret *corev1.Secret
  53. assert func()
  54. }
  55. var _ = Describe("ValidatingWebhookConfig reconcile", Ordered, func() {
  56. var test *testCase
  57. BeforeEach(func() {
  58. test = makeDefaultTestcase()
  59. })
  60. AfterEach(func() {
  61. ctx := context.Background()
  62. k8sClient.Delete(ctx, test.vwc)
  63. k8sClient.Delete(ctx, test.secret)
  64. k8sClient.Delete(ctx, test.service)
  65. k8sClient.Delete(ctx, test.endpointSlice)
  66. })
  67. // Should patch VWC
  68. PatchAndReady := func(tc *testCase) {
  69. // endpoint slice becomes ready in a moment
  70. go func() {
  71. <-time.After(time.Second * 4)
  72. eps := makeEndpointSlice()
  73. err := k8sClient.Update(context.Background(), eps)
  74. Expect(err).ToNot(HaveOccurred())
  75. }()
  76. tc.assert = func() {
  77. Eventually(func() bool {
  78. // the controller should become ready at some point!
  79. err := reconciler.ReadyCheck(nil)
  80. return err == nil
  81. }).
  82. WithTimeout(time.Second * 10).
  83. WithPolling(time.Second).
  84. Should(BeTrue())
  85. Eventually(func() bool {
  86. var vwc admissionregistration.ValidatingWebhookConfiguration
  87. err := k8sClient.Get(context.Background(), types.NamespacedName{
  88. Name: tc.vwc.Name,
  89. }, &vwc)
  90. if err != nil {
  91. return false
  92. }
  93. for _, wc := range vwc.Webhooks {
  94. if !bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
  95. return false
  96. }
  97. if wc.ClientConfig.Service == nil {
  98. return false
  99. }
  100. if wc.ClientConfig.Service.Name != ctrlSvcName {
  101. return false
  102. }
  103. if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
  104. return false
  105. }
  106. }
  107. return true
  108. }).
  109. WithTimeout(time.Second * 10).
  110. WithPolling(time.Second).
  111. Should(BeTrue())
  112. }
  113. }
  114. IgnoreNoMatch := func(tc *testCase) {
  115. delete(tc.vwc.ObjectMeta.Labels, constants.WellKnownLabelKey)
  116. tc.assert = func() {
  117. Consistently(func() bool {
  118. var vwc admissionregistration.ValidatingWebhookConfiguration
  119. err := k8sClient.Get(context.Background(), types.NamespacedName{
  120. Name: tc.vwc.Name,
  121. }, &vwc)
  122. if err != nil {
  123. return false
  124. }
  125. for _, wc := range vwc.Webhooks {
  126. if bytes.Equal(wc.ClientConfig.CABundle, []byte(defaultCACert)) {
  127. return false
  128. }
  129. if wc.ClientConfig.Service.Name == ctrlSvcName {
  130. return false
  131. }
  132. if wc.ClientConfig.Service.Namespace == ctrlSvcNamespace {
  133. return false
  134. }
  135. }
  136. return true
  137. }).
  138. WithTimeout(time.Second * 10).
  139. WithPolling(time.Second).
  140. Should(BeTrue())
  141. }
  142. }
  143. // Should patch and update VWC after requeue duration has passed
  144. PatchAndUpdate := func(tc *testCase) {
  145. foobar := "new value"
  146. // ca cert will change after some time
  147. go func() {
  148. <-time.After(time.Second * 4)
  149. sec := makeSecret()
  150. sec.Data[caCertName] = []byte(foobar)
  151. err := k8sClient.Update(context.Background(), sec)
  152. Expect(err).ToNot(HaveOccurred())
  153. }()
  154. tc.assert = func() {
  155. var resourceVersion string
  156. Eventually(func() bool {
  157. var vwc admissionregistration.ValidatingWebhookConfiguration
  158. err := k8sClient.Get(context.Background(), types.NamespacedName{
  159. Name: tc.vwc.Name,
  160. }, &vwc)
  161. if err != nil {
  162. return false
  163. }
  164. for _, wc := range vwc.Webhooks {
  165. if !bytes.Equal(wc.ClientConfig.CABundle, []byte(foobar)) {
  166. return false
  167. }
  168. if wc.ClientConfig.Service == nil {
  169. return false
  170. }
  171. if wc.ClientConfig.Service.Name != ctrlSvcName {
  172. return false
  173. }
  174. if wc.ClientConfig.Service.Namespace != ctrlSvcNamespace {
  175. return false
  176. }
  177. }
  178. resourceVersion = vwc.ResourceVersion
  179. return true
  180. }).
  181. WithTimeout(time.Second * 10).
  182. WithPolling(time.Second).
  183. Should(BeTrue())
  184. // make sure no additional updates are made
  185. Consistently(func() bool {
  186. var vwc admissionregistration.ValidatingWebhookConfiguration
  187. err := k8sClient.Get(context.Background(), types.NamespacedName{Name: tc.vwc.Name}, &vwc)
  188. if err != nil {
  189. return false
  190. }
  191. return vwc.ResourceVersion == resourceVersion
  192. }).
  193. WithTimeout(time.Second * 10).
  194. WithPolling(time.Second).
  195. Should(BeTrue())
  196. }
  197. }
  198. DescribeTable("Controller Reconcile logic", func(muts ...func(tc *testCase)) {
  199. for _, mut := range muts {
  200. mut(test)
  201. }
  202. ctx := context.Background()
  203. err := k8sClient.Create(ctx, test.vwc)
  204. Expect(err).ToNot(HaveOccurred())
  205. err = k8sClient.Create(ctx, test.secret)
  206. Expect(err).ToNot(HaveOccurred())
  207. err = k8sClient.Create(ctx, test.service)
  208. Expect(err).ToNot(HaveOccurred())
  209. err = k8sClient.Create(ctx, test.endpointSlice)
  210. Expect(err).ToNot(HaveOccurred())
  211. test.assert()
  212. },
  213. Entry("should patch matching webhook configs", PatchAndReady),
  214. Entry("should update vwc with new ca cert after requeue duration", PatchAndUpdate),
  215. Entry("should ignore when vwc labels are missing", IgnoreNoMatch),
  216. )
  217. })
  218. func makeValidatingWebhookConfig() *admissionregistration.ValidatingWebhookConfiguration {
  219. return &admissionregistration.ValidatingWebhookConfiguration{
  220. ObjectMeta: metav1.ObjectMeta{
  221. Name: "name-shouldnt-matter",
  222. Labels: map[string]string{
  223. constants.WellKnownLabelKey: constants.WellKnownLabelValueWebhook,
  224. },
  225. },
  226. Webhooks: []admissionregistration.ValidatingWebhook{
  227. {
  228. Name: "secretstores.external-secrets.io",
  229. SideEffects: (*admissionregistration.SideEffectClass)(pointer.To(string(admissionregistration.SideEffectClassNone))),
  230. AdmissionReviewVersions: []string{"v1"},
  231. ClientConfig: admissionregistration.WebhookClientConfig{
  232. CABundle: []byte("Cg=="),
  233. Service: &admissionregistration.ServiceReference{
  234. Name: "noop",
  235. Namespace: "noop",
  236. Path: pointer.To("/validate-secretstore"),
  237. },
  238. },
  239. },
  240. {
  241. Name: "clustersecretstores.external-secrets.io",
  242. SideEffects: (*admissionregistration.SideEffectClass)(pointer.To(string(admissionregistration.SideEffectClassNone))),
  243. AdmissionReviewVersions: []string{"v1"},
  244. ClientConfig: admissionregistration.WebhookClientConfig{
  245. CABundle: []byte("Cg=="),
  246. Service: &admissionregistration.ServiceReference{
  247. Name: "noop",
  248. Namespace: "noop",
  249. Path: pointer.To("/validate-clustersecretstore"),
  250. },
  251. },
  252. },
  253. },
  254. }
  255. }
  256. func makeSecret() *corev1.Secret {
  257. return &corev1.Secret{
  258. ObjectMeta: metav1.ObjectMeta{
  259. Name: ctrlSecretName,
  260. Namespace: ctrlSecretNamespace,
  261. },
  262. Data: map[string][]byte{
  263. caCertName: []byte(defaultCACert),
  264. },
  265. }
  266. }
  267. func makeService() *corev1.Service {
  268. return &corev1.Service{
  269. ObjectMeta: metav1.ObjectMeta{
  270. Name: ctrlSvcName,
  271. Namespace: ctrlSvcNamespace,
  272. },
  273. Spec: corev1.ServiceSpec{
  274. Ports: []corev1.ServicePort{
  275. {
  276. Name: "http",
  277. Port: 80,
  278. },
  279. },
  280. },
  281. }
  282. }
  283. func makeEndpointSlice() *discoveryv1.EndpointSlice {
  284. return &discoveryv1.EndpointSlice{
  285. ObjectMeta: metav1.ObjectMeta{
  286. Name: ctrlSvcName + "-slice",
  287. Namespace: ctrlSvcNamespace,
  288. Labels: map[string]string{
  289. "kubernetes.io/service-name": ctrlSvcName,
  290. },
  291. },
  292. AddressType: discoveryv1.AddressTypeIPv4,
  293. Endpoints: []discoveryv1.Endpoint{
  294. {
  295. Addresses: []string{"1.2.3.4"},
  296. Conditions: discoveryv1.EndpointConditions{Ready: pointer.To(true)},
  297. },
  298. },
  299. }
  300. }
  301. func makeDefaultTestcase() *testCase {
  302. return &testCase{
  303. assert: func() {
  304. // this is a noop by default
  305. },
  306. vwc: makeValidatingWebhookConfig(),
  307. secret: makeSecret(),
  308. service: makeService(),
  309. endpointSlice: makeEndpointSlice(),
  310. }
  311. }