webhookconfig_test.go 9.4 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. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/types"
  21. pointer "k8s.io/utils/ptr"
  22. "github.com/external-secrets/external-secrets/pkg/constants"
  23. . "github.com/onsi/ginkgo/v2"
  24. . "github.com/onsi/gomega"
  25. )
  26. const defaultCACert = `-----BEGIN CERTIFICATE-----
  27. MIIDRjCCAi6gAwIBAgIBADANBgkqhkiG9w0BAQsFADA2MRkwFwYDVQQKExBleHRl
  28. cm5hbC1zZWNyZXRzMRkwFwYDVQQDExBleHRlcm5hbC1zZWNyZXRzMB4XDTIyMDIx
  29. NzEwMDYxMFoXDTMyMDIxNTExMDYxMFowNjEZMBcGA1UEChMQZXh0ZXJuYWwtc2Vj
  30. cmV0czEZMBcGA1UEAxMQZXh0ZXJuYWwtc2VjcmV0czCCASIwDQYJKoZIhvcNAQEB
  31. BQADggEPADCCAQoCggEBAKSINgqU2dBdX8JpPjRHWSdpxuoltGl6xXmQHOhbTXAt
  32. /STDu7oi6eiFgepQ2QHuWLGwZgbbYnEhtLvw4dUwPcLyv6WIdeiUSA4pdFxL7asc
  33. WV4tjiRkRTJVrixJTxXpry/CsPqXBlvnu1YGESkrLOYCmA2xnDH8voEBbwYvXXB9
  34. 3g5rOJncSh/7g+H55ZFFyWrIPyDUnfwE3CREjZXpsagFhRYpkuRlXTnU6t0OTEEh
  35. qLHlZ+ebUzL8NaegEgEHD32PrQPXpls1yurIrsA+I6McWkXGykykYHVK+1a1pL1g
  36. e+PBkegtwtX+EmB2ux7PVVeB4TTYqzCKbnObW4mJLZkCAwEAAaNfMF0wDgYDVR0P
  37. AQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHgSu/Im2gyu4TU0
  38. AWrMSFbtoVokMBsGA1UdEQQUMBKCEGV4dGVybmFsLXNlY3JldHMwDQYJKoZIhvcN
  39. AQELBQADggEBAJU88jCcPsAHN8DKLu+QMCoKYbeftX4gXxyoijGSde2w2O8NPtMP
  40. awu4Y5x3LNTwyIIxXi78UD0RI53GbUgHvS+X9v6CC2IZMS65xqKR+EsjzEh7Ldbm
  41. vZoF4ZDnfb2s5SK6MeYf67BE7XWpGfbHmjt6h80xsYjL6ovcik+dlu/AixMyLslS
  42. tDbMybAR8kR0zdQLYcZq7XEX5QsOO8qBn5rTfD6MiYik8ZrP7FqUMHyVpHiBuNio
  43. krnSOvynvuA9mlf2F2727dMt2Ij9uER+9QnhWBQex1h8CwALmm2k9G5Gt+RjB8oe
  44. lNjvmHAXUfOE/cbD7EP++X17kWt41FjmePc=
  45. -----END CERTIFICATE-----
  46. `
  47. type testCase struct {
  48. vwc *admissionregistration.ValidatingWebhookConfiguration
  49. service *corev1.Service
  50. endpoints *corev1.Endpoints
  51. secret *corev1.Secret
  52. assert func()
  53. }
  54. var _ = Describe("ValidatingWebhookConfig reconcile", Ordered, func() {
  55. var test *testCase
  56. BeforeEach(func() {
  57. test = makeDefaultTestcase()
  58. })
  59. AfterEach(func() {
  60. ctx := context.Background()
  61. k8sClient.Delete(ctx, test.vwc)
  62. k8sClient.Delete(ctx, test.secret)
  63. k8sClient.Delete(ctx, test.service)
  64. k8sClient.Delete(ctx, test.endpoints)
  65. })
  66. // Should patch VWC
  67. PatchAndReady := func(tc *testCase) {
  68. tc.endpoints.Subsets = nil
  69. // endpoints become ready in a moment
  70. go func() {
  71. <-time.After(time.Second * 4)
  72. eps := makeEndpoints()
  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.endpoints)
  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 makeEndpoints() *corev1.Endpoints {
  284. return &corev1.Endpoints{
  285. ObjectMeta: metav1.ObjectMeta{
  286. Name: ctrlSvcName,
  287. Namespace: ctrlSvcNamespace,
  288. },
  289. Subsets: []corev1.EndpointSubset{
  290. {
  291. Addresses: []corev1.EndpointAddress{
  292. {
  293. IP: "1.2.3.4",
  294. },
  295. },
  296. },
  297. },
  298. }
  299. }
  300. func makeDefaultTestcase() *testCase {
  301. return &testCase{
  302. assert: func() {
  303. // this is a noop by default
  304. },
  305. vwc: makeValidatingWebhookConfig(),
  306. secret: makeSecret(),
  307. service: makeService(),
  308. endpoints: makeEndpoints(),
  309. }
  310. }