reinvocation_test.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. /*
  2. Copyright 2019 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 admissionwebhook
  14. import (
  15. "context"
  16. "crypto/tls"
  17. "crypto/x509"
  18. "encoding/json"
  19. "fmt"
  20. "io/ioutil"
  21. "net/http"
  22. "net/http/httptest"
  23. "os"
  24. "reflect"
  25. "strconv"
  26. "strings"
  27. "sync"
  28. "testing"
  29. "time"
  30. "k8s.io/api/admission/v1beta1"
  31. admissionv1beta1 "k8s.io/api/admissionregistration/v1beta1"
  32. registrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
  33. corev1 "k8s.io/api/core/v1"
  34. v1 "k8s.io/api/core/v1"
  35. schedulingv1 "k8s.io/api/scheduling/v1"
  36. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  37. "k8s.io/apimachinery/pkg/types"
  38. "k8s.io/apimachinery/pkg/util/wait"
  39. auditinternal "k8s.io/apiserver/pkg/apis/audit"
  40. auditv1 "k8s.io/apiserver/pkg/apis/audit/v1"
  41. clientset "k8s.io/client-go/kubernetes"
  42. "k8s.io/client-go/rest"
  43. kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
  44. "k8s.io/kubernetes/test/integration/framework"
  45. "k8s.io/kubernetes/test/utils"
  46. )
  47. const (
  48. testReinvocationClientUsername = "webhook-reinvocation-integration-client"
  49. auditPolicy = `
  50. apiVersion: audit.k8s.io/v1
  51. kind: Policy
  52. rules:
  53. - level: Request
  54. resources:
  55. - group: "" # core
  56. resources: ["pods"]
  57. `
  58. )
  59. // TestWebhookReinvocationPolicyWithWatchCache ensures that the admission webhook reinvocation policy is applied correctly with the watch cache enabled.
  60. func TestWebhookReinvocationPolicyWithWatchCache(t *testing.T) {
  61. testWebhookReinvocationPolicy(t, true)
  62. }
  63. // TestWebhookReinvocationPolicyWithoutWatchCache ensures that the admission webhook reinvocation policy is applied correctly without the watch cache enabled.
  64. func TestWebhookReinvocationPolicyWithoutWatchCache(t *testing.T) {
  65. testWebhookReinvocationPolicy(t, false)
  66. }
  67. func mutationAnnotationValue(configuration, webhook string, mutated bool) string {
  68. return fmt.Sprintf(`{"configuration":"%s","webhook":"%s","mutated":%t}`, configuration, webhook, mutated)
  69. }
  70. func patchAnnotationValue(configuration, webhook string, patch string) string {
  71. return strings.Replace(fmt.Sprintf(`{"configuration": "%s", "webhook": "%s", "patch": %s, "patchType": "JSONPatch"}`, configuration, webhook, patch), " ", "", -1)
  72. }
  73. // testWebhookReinvocationPolicy ensures that the admission webhook reinvocation policy is applied correctly.
  74. func testWebhookReinvocationPolicy(t *testing.T, watchCache bool) {
  75. reinvokeNever := registrationv1beta1.NeverReinvocationPolicy
  76. reinvokeIfNeeded := registrationv1beta1.IfNeededReinvocationPolicy
  77. type testWebhook struct {
  78. path string
  79. policy *registrationv1beta1.ReinvocationPolicyType
  80. objectSelector *metav1.LabelSelector
  81. }
  82. testCases := []struct {
  83. name string
  84. initialPriorityClass string
  85. webhooks []testWebhook
  86. expectLabels map[string]string
  87. expectInvocations map[string]int
  88. expectError bool
  89. errorContains string
  90. expectAuditMutationAnnotations map[string]string
  91. expectAuditPatchAnnotations map[string]string
  92. }{
  93. { // in-tree (mutation), webhook (no mutation), no reinvocation required
  94. name: "no reinvocation for in-tree only mutation",
  95. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  96. webhooks: []testWebhook{
  97. {path: "/noop", policy: &reinvokeIfNeeded},
  98. },
  99. expectInvocations: map[string]int{"/noop": 1},
  100. expectAuditMutationAnnotations: map[string]string{
  101. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-0", "admission.integration.test.0.noop", false),
  102. },
  103. },
  104. { // in-tree (mutation), webhook (mutation), reinvoke in-tree (no-mutation), no webhook reinvocation required
  105. name: "no webhook reinvocation for webhook when no in-tree reinvocation mutations",
  106. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  107. webhooks: []testWebhook{
  108. {path: "/addlabel", policy: &reinvokeIfNeeded},
  109. },
  110. expectInvocations: map[string]int{"/addlabel": 1},
  111. expectAuditPatchAnnotations: map[string]string{
  112. "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-1", "admission.integration.test.0.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
  113. },
  114. expectAuditMutationAnnotations: map[string]string{
  115. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-1", "admission.integration.test.0.addlabel", true),
  116. },
  117. },
  118. { // in-tree (mutation), webhook (mutation), reinvoke in-tree (mutation), webhook (no-mutation), both reinvoked
  119. name: "webhook is reinvoked after in-tree reinvocation",
  120. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  121. webhooks: []testWebhook{
  122. // Priority plugin is ordered to run before mutating webhooks
  123. {path: "/setpriority", policy: &reinvokeIfNeeded}, // trigger in-tree reinvoke mutation
  124. },
  125. expectInvocations: map[string]int{"/setpriority": 2},
  126. expectAuditPatchAnnotations: map[string]string{
  127. "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"},{"op": "remove", "path": "/spec/priority"}]`),
  128. },
  129. expectAuditMutationAnnotations: map[string]string{
  130. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", true),
  131. "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-2", "admission.integration.test.0.setpriority", false),
  132. },
  133. },
  134. { // in-tree (mutation), webhook A (mutation), webhook B (mutation), reinvoke in-tree (no-mutation), reinvoke webhook A (no-mutation), no reinvocation of webhook B required
  135. name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
  136. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  137. webhooks: []testWebhook{
  138. {path: "/addlabel", policy: &reinvokeIfNeeded},
  139. {path: "/conditionaladdlabel", policy: &reinvokeIfNeeded},
  140. },
  141. expectLabels: map[string]string{"x": "true", "a": "true", "b": "true"},
  142. expectInvocations: map[string]int{"/addlabel": 2, "/conditionaladdlabel": 1},
  143. expectAuditPatchAnnotations: map[string]string{
  144. "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
  145. "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-3", "admission.integration.test.1.conditionaladdlabel", `[{"op": "add", "path": "/metadata/labels/b", "value": "true"}]`),
  146. },
  147. expectAuditMutationAnnotations: map[string]string{
  148. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", true),
  149. "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.1.conditionaladdlabel", true),
  150. "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-3", "admission.integration.test.0.addlabel", false),
  151. },
  152. },
  153. { // in-tree (mutation), webhook A (mutation), webhook B (mutation), reinvoke in-tree (no-mutation), reinvoke webhook A (mutation), reinvoke webhook B (mutation), both webhooks reinvoked
  154. name: "all webhooks reinvoked when any webhook reinvocation causes mutation",
  155. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  156. webhooks: []testWebhook{
  157. {path: "/settrue", policy: &reinvokeIfNeeded},
  158. {path: "/setfalse", policy: &reinvokeIfNeeded},
  159. },
  160. expectLabels: map[string]string{"x": "true", "fight": "false"},
  161. expectInvocations: map[string]int{"/settrue": 2, "/setfalse": 2},
  162. expectAuditPatchAnnotations: map[string]string{
  163. "patch.webhook.admission.k8s.io/round_0_index_0": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`),
  164. "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`),
  165. "patch.webhook.admission.k8s.io/round_1_index_0": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`),
  166. "patch.webhook.admission.k8s.io/round_1_index_1": patchAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`),
  167. },
  168. expectAuditMutationAnnotations: map[string]string{
  169. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", true),
  170. "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", true),
  171. "mutation.webhook.admission.k8s.io/round_1_index_0": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.0.settrue", true),
  172. "mutation.webhook.admission.k8s.io/round_1_index_1": mutationAnnotationValue("admission.integration.test-4", "admission.integration.test.1.setfalse", true),
  173. },
  174. },
  175. { // in-tree (mutation), webhook A is SKIPPED due to objectSelector not matching, webhook B (mutation), reinvoke in-tree (no-mutation), webhook A is SKIPPED even though the labels match now, because it's not called in the first round. No reinvocation of webhook B required
  176. name: "no reinvocation of webhook B when in-tree or prior webhook mutations",
  177. initialPriorityClass: "low-priority", // trigger initial in-tree mutation
  178. webhooks: []testWebhook{
  179. {path: "/conditionaladdlabel", policy: &reinvokeIfNeeded, objectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "true"}}},
  180. {path: "/addlabel", policy: &reinvokeIfNeeded},
  181. },
  182. expectLabels: map[string]string{"x": "true", "a": "true"},
  183. expectInvocations: map[string]int{"/addlabel": 1, "/conditionaladdlabel": 0},
  184. expectAuditPatchAnnotations: map[string]string{
  185. "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-5", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
  186. },
  187. expectAuditMutationAnnotations: map[string]string{
  188. "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-5", "admission.integration.test.1.addlabel", true),
  189. },
  190. },
  191. {
  192. name: "invalid priority class set by webhook should result in error from in-tree priority plugin",
  193. webhooks: []testWebhook{
  194. // Priority plugin is ordered to run before mutating webhooks
  195. {path: "/setinvalidpriority", policy: &reinvokeIfNeeded},
  196. },
  197. expectError: true,
  198. errorContains: "no PriorityClass with name invalid was found",
  199. expectInvocations: map[string]int{"/setinvalidpriority": 1},
  200. },
  201. {
  202. name: "'reinvoke never' policy respected",
  203. webhooks: []testWebhook{
  204. {path: "/conditionaladdlabel", policy: &reinvokeNever},
  205. {path: "/addlabel", policy: &reinvokeNever},
  206. },
  207. expectLabels: map[string]string{"x": "true", "a": "true"},
  208. expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
  209. expectAuditPatchAnnotations: map[string]string{
  210. "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-7", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
  211. },
  212. expectAuditMutationAnnotations: map[string]string{
  213. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-7", "admission.integration.test.0.conditionaladdlabel", false),
  214. "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-7", "admission.integration.test.1.addlabel", true),
  215. },
  216. },
  217. {
  218. name: "'reinvoke never' (by default) policy respected",
  219. webhooks: []testWebhook{
  220. {path: "/conditionaladdlabel", policy: nil},
  221. {path: "/addlabel", policy: nil},
  222. },
  223. expectLabels: map[string]string{"x": "true", "a": "true"},
  224. expectInvocations: map[string]int{"/conditionaladdlabel": 1, "/addlabel": 1},
  225. expectAuditPatchAnnotations: map[string]string{
  226. "patch.webhook.admission.k8s.io/round_0_index_1": patchAnnotationValue("admission.integration.test-8", "admission.integration.test.1.addlabel", `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`),
  227. },
  228. expectAuditMutationAnnotations: map[string]string{
  229. "mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue("admission.integration.test-8", "admission.integration.test.0.conditionaladdlabel", false),
  230. "mutation.webhook.admission.k8s.io/round_0_index_1": mutationAnnotationValue("admission.integration.test-8", "admission.integration.test.1.addlabel", true),
  231. },
  232. },
  233. }
  234. roots := x509.NewCertPool()
  235. if !roots.AppendCertsFromPEM(localhostCert) {
  236. t.Fatal("Failed to append Cert from PEM")
  237. }
  238. cert, err := tls.X509KeyPair(localhostCert, localhostKey)
  239. if err != nil {
  240. t.Fatalf("Failed to build cert with error: %+v", err)
  241. }
  242. recorder := &invocationRecorder{counts: map[string]int{}}
  243. webhookServer := httptest.NewUnstartedServer(newReinvokeWebhookHandler(recorder))
  244. webhookServer.TLS = &tls.Config{
  245. RootCAs: roots,
  246. Certificates: []tls.Certificate{cert},
  247. }
  248. webhookServer.StartTLS()
  249. defer webhookServer.Close()
  250. // prepare audit policy file
  251. policyFile, err := ioutil.TempFile("", "audit-policy.yaml")
  252. if err != nil {
  253. t.Fatalf("Failed to create audit policy file: %v", err)
  254. }
  255. defer os.Remove(policyFile.Name())
  256. if _, err := policyFile.Write([]byte(auditPolicy)); err != nil {
  257. t.Fatalf("Failed to write audit policy file: %v", err)
  258. }
  259. if err := policyFile.Close(); err != nil {
  260. t.Fatalf("Failed to close audit policy file: %v", err)
  261. }
  262. // prepare audit log file
  263. logFile, err := ioutil.TempFile("", "audit.log")
  264. if err != nil {
  265. t.Fatalf("Failed to create audit log file: %v", err)
  266. }
  267. defer os.Remove(logFile.Name())
  268. s := kubeapiservertesting.StartTestServerOrDie(t, kubeapiservertesting.NewDefaultTestServerOptions(), []string{
  269. "--disable-admission-plugins=ServiceAccount",
  270. fmt.Sprintf("--watch-cache=%v", watchCache),
  271. "--audit-policy-file", policyFile.Name(),
  272. "--audit-log-version", "audit.k8s.io/v1",
  273. "--audit-log-mode", "blocking",
  274. "--audit-log-path", logFile.Name(),
  275. }, framework.SharedEtcd())
  276. defer s.TearDownFn()
  277. // Configure a client with a distinct user name so that it is easy to distinguish requests
  278. // made by the client from requests made by controllers. We use this to filter out requests
  279. // before recording them to ensure we don't accidentally mistake requests from controllers
  280. // as requests made by the client.
  281. clientConfig := rest.CopyConfig(s.ClientConfig)
  282. clientConfig.Impersonate.UserName = testReinvocationClientUsername
  283. clientConfig.Impersonate.Groups = []string{"system:masters", "system:authenticated"}
  284. client, err := clientset.NewForConfig(clientConfig)
  285. if err != nil {
  286. t.Fatalf("unexpected error: %v", err)
  287. }
  288. for priorityClass, priority := range map[string]int{"low-priority": 1, "high-priority": 10} {
  289. _, err = client.SchedulingV1().PriorityClasses().Create(context.TODO(), &schedulingv1.PriorityClass{ObjectMeta: metav1.ObjectMeta{Name: priorityClass}, Value: int32(priority)}, metav1.CreateOptions{})
  290. if err != nil {
  291. t.Fatal(err)
  292. }
  293. }
  294. for i, tt := range testCases {
  295. t.Run(tt.name, func(t *testing.T) {
  296. upCh := recorder.Reset()
  297. testCaseID := strconv.Itoa(i)
  298. ns := "reinvoke-" + testCaseID
  299. nsLabels := map[string]string{"test-case": testCaseID}
  300. _, err = client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: ns, Labels: nsLabels}}, metav1.CreateOptions{})
  301. if err != nil {
  302. t.Fatal(err)
  303. }
  304. // Write markers to a separate namespace to avoid cross-talk
  305. markerNs := ns + "-markers"
  306. markerNsLabels := map[string]string{"test-markers": testCaseID}
  307. _, err = client.CoreV1().Namespaces().Create(context.TODO(), &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs, Labels: markerNsLabels}}, metav1.CreateOptions{})
  308. if err != nil {
  309. t.Fatal(err)
  310. }
  311. // Create a maker object to use to check for the webhook configurations to be ready.
  312. marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newReinvocationMarkerFixture(markerNs), metav1.CreateOptions{})
  313. if err != nil {
  314. t.Fatal(err)
  315. }
  316. fail := admissionv1beta1.Fail
  317. webhooks := []admissionv1beta1.MutatingWebhook{}
  318. for j, webhook := range tt.webhooks {
  319. endpoint := webhookServer.URL + webhook.path
  320. name := fmt.Sprintf("admission.integration.test.%d.%s", j, strings.TrimPrefix(webhook.path, "/"))
  321. webhooks = append(webhooks, admissionv1beta1.MutatingWebhook{
  322. Name: name,
  323. ClientConfig: admissionv1beta1.WebhookClientConfig{
  324. URL: &endpoint,
  325. CABundle: localhostCert,
  326. },
  327. Rules: []admissionv1beta1.RuleWithOperations{{
  328. Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
  329. Rule: admissionv1beta1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
  330. }},
  331. ObjectSelector: webhook.objectSelector,
  332. NamespaceSelector: &metav1.LabelSelector{MatchLabels: nsLabels},
  333. FailurePolicy: &fail,
  334. ReinvocationPolicy: webhook.policy,
  335. AdmissionReviewVersions: []string{"v1beta1"},
  336. })
  337. }
  338. // Register a marker checking webhook with each set of webhook configurations
  339. markerEndpoint := webhookServer.URL + "/marker"
  340. webhooks = append(webhooks, admissionv1beta1.MutatingWebhook{
  341. Name: "admission.integration.test.marker",
  342. ClientConfig: admissionv1beta1.WebhookClientConfig{
  343. URL: &markerEndpoint,
  344. CABundle: localhostCert,
  345. },
  346. Rules: []admissionv1beta1.RuleWithOperations{{
  347. Operations: []admissionv1beta1.OperationType{admissionv1beta1.OperationAll},
  348. Rule: admissionv1beta1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
  349. }},
  350. NamespaceSelector: &metav1.LabelSelector{MatchLabels: markerNsLabels},
  351. ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
  352. AdmissionReviewVersions: []string{"v1beta1"},
  353. })
  354. cfg, err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1beta1.MutatingWebhookConfiguration{
  355. ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("admission.integration.test-%d", i)},
  356. Webhooks: webhooks,
  357. }, metav1.CreateOptions{})
  358. if err != nil {
  359. t.Fatal(err)
  360. }
  361. defer func() {
  362. err := client.AdmissionregistrationV1beta1().MutatingWebhookConfigurations().Delete(context.TODO(), cfg.GetName(), &metav1.DeleteOptions{})
  363. if err != nil {
  364. t.Fatal(err)
  365. }
  366. }()
  367. // wait until new webhook is called the first time
  368. if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
  369. _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
  370. select {
  371. case <-upCh:
  372. return true, nil
  373. default:
  374. t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
  375. return false, nil
  376. }
  377. }); err != nil {
  378. t.Fatal(err)
  379. }
  380. pod := &corev1.Pod{
  381. ObjectMeta: metav1.ObjectMeta{
  382. Namespace: ns,
  383. Name: "labeled",
  384. Labels: map[string]string{"x": "true"},
  385. },
  386. Spec: corev1.PodSpec{
  387. Containers: []v1.Container{{
  388. Name: "fake-name",
  389. Image: "fakeimage",
  390. }},
  391. },
  392. }
  393. if tt.initialPriorityClass != "" {
  394. pod.Spec.PriorityClassName = tt.initialPriorityClass
  395. }
  396. obj, err := client.CoreV1().Pods(ns).Create(context.TODO(), pod, metav1.CreateOptions{})
  397. if tt.expectError {
  398. if err == nil {
  399. t.Fatalf("expected error but got none")
  400. }
  401. if tt.errorContains != "" {
  402. if !strings.Contains(err.Error(), tt.errorContains) {
  403. t.Errorf("expected an error saying %q, but got: %v", tt.errorContains, err)
  404. }
  405. }
  406. return
  407. }
  408. if err != nil {
  409. t.Fatal(err)
  410. }
  411. if tt.expectLabels != nil {
  412. labels := obj.GetLabels()
  413. if !reflect.DeepEqual(tt.expectLabels, labels) {
  414. t.Errorf("expected labels '%v', but got '%v'", tt.expectLabels, labels)
  415. }
  416. }
  417. if tt.expectInvocations != nil {
  418. for k, v := range tt.expectInvocations {
  419. if recorder.GetCount(k) != v {
  420. t.Errorf("expected %d invocations of %s, but got %d", v, k, recorder.GetCount(k))
  421. }
  422. }
  423. }
  424. stream, err := os.OpenFile(logFile.Name(), os.O_RDWR, 0600)
  425. if err != nil {
  426. t.Errorf("unexpected error: %v", err)
  427. }
  428. defer stream.Close()
  429. missing, err := utils.CheckAuditLines(stream, expectedAuditEvents(tt.expectAuditMutationAnnotations, tt.expectAuditPatchAnnotations, ns), auditv1.SchemeGroupVersion)
  430. if err != nil {
  431. t.Errorf("unexpected error checking audit lines: %v", err)
  432. }
  433. if len(missing.MissingEvents) > 0 {
  434. t.Errorf("failed to get expected events -- missing: %s", missing)
  435. }
  436. if err := stream.Truncate(0); err != nil {
  437. t.Errorf("unexpected error truncate file: %v", err)
  438. }
  439. if _, err := stream.Seek(0, 0); err != nil {
  440. t.Errorf("unexpected error reset offset: %v", err)
  441. }
  442. })
  443. }
  444. }
  445. type invocationRecorder struct {
  446. mu sync.Mutex
  447. upCh chan struct{}
  448. upOnce sync.Once
  449. counts map[string]int
  450. }
  451. // Reset zeros out all counts and returns a channel that is closed when the first admission of the
  452. // marker object is received.
  453. func (i *invocationRecorder) Reset() chan struct{} {
  454. i.mu.Lock()
  455. defer i.mu.Unlock()
  456. i.counts = map[string]int{}
  457. i.upCh = make(chan struct{})
  458. i.upOnce = sync.Once{}
  459. return i.upCh
  460. }
  461. func (i *invocationRecorder) MarkerReceived() {
  462. i.mu.Lock()
  463. defer i.mu.Unlock()
  464. i.upOnce.Do(func() {
  465. close(i.upCh)
  466. })
  467. }
  468. func (i *invocationRecorder) GetCount(path string) int {
  469. i.mu.Lock()
  470. defer i.mu.Unlock()
  471. return i.counts[path]
  472. }
  473. func (i *invocationRecorder) IncrementCount(path string) {
  474. i.mu.Lock()
  475. defer i.mu.Unlock()
  476. i.counts[path]++
  477. }
  478. func newReinvokeWebhookHandler(recorder *invocationRecorder) http.Handler {
  479. patch := func(w http.ResponseWriter, patch string) {
  480. w.Header().Set("Content-Type", "application/json")
  481. pt := v1beta1.PatchTypeJSONPatch
  482. json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
  483. Response: &v1beta1.AdmissionResponse{
  484. Allowed: true,
  485. PatchType: &pt,
  486. Patch: []byte(patch),
  487. },
  488. })
  489. }
  490. allow := func(w http.ResponseWriter) {
  491. w.Header().Set("Content-Type", "application/json")
  492. json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
  493. Response: &v1beta1.AdmissionResponse{
  494. Allowed: true,
  495. },
  496. })
  497. }
  498. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  499. defer r.Body.Close()
  500. data, err := ioutil.ReadAll(r.Body)
  501. if err != nil {
  502. http.Error(w, err.Error(), 400)
  503. }
  504. review := v1beta1.AdmissionReview{}
  505. if err := json.Unmarshal(data, &review); err != nil {
  506. http.Error(w, err.Error(), 400)
  507. }
  508. if review.Request.UserInfo.Username != testReinvocationClientUsername {
  509. // skip requests not originating from this integration test's client
  510. allow(w)
  511. return
  512. }
  513. if len(review.Request.Object.Raw) == 0 {
  514. http.Error(w, err.Error(), 400)
  515. }
  516. pod := &corev1.Pod{}
  517. if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
  518. http.Error(w, err.Error(), 400)
  519. }
  520. recorder.IncrementCount(r.URL.Path)
  521. switch r.URL.Path {
  522. case "/marker":
  523. // When resetting between tests, a marker object is patched until this webhook
  524. // observes it, at which point it is considered ready.
  525. recorder.MarkerReceived()
  526. allow(w)
  527. return
  528. case "/noop":
  529. allow(w)
  530. case "/settrue":
  531. patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "true"}]`)
  532. case "/setfalse":
  533. patch(w, `[{"op": "replace", "path": "/metadata/labels/fight", "value": "false"}]`)
  534. case "/addlabel":
  535. labels := pod.GetLabels()
  536. if a, ok := labels["a"]; !ok || a != "true" {
  537. patch(w, `[{"op": "add", "path": "/metadata/labels/a", "value": "true"}]`)
  538. return
  539. }
  540. allow(w)
  541. case "/conditionaladdlabel": // if 'a' is set, set 'b' to true
  542. labels := pod.GetLabels()
  543. if _, ok := labels["a"]; ok {
  544. patch(w, `[{"op": "add", "path": "/metadata/labels/b", "value": "true"}]`)
  545. return
  546. }
  547. allow(w)
  548. case "/setpriority": // sets /spec/priorityClassName to high-priority if it is not already set
  549. if pod.Spec.PriorityClassName != "high-priority" {
  550. if pod.Spec.Priority != nil {
  551. patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"},{"op": "remove", "path": "/spec/priority"}]`)
  552. } else {
  553. patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "high-priority"}]`)
  554. }
  555. return
  556. }
  557. allow(w)
  558. case "/setinvalidpriority":
  559. patch(w, `[{"op": "add", "path": "/spec/priorityClassName", "value": "invalid"}]`)
  560. default:
  561. http.NotFound(w, r)
  562. }
  563. })
  564. }
  565. func expectedAuditEvents(webhookMutationAnnotations, webhookPatchAnnotations map[string]string, namespace string) []utils.AuditEvent {
  566. return []utils.AuditEvent{
  567. {
  568. Level: auditinternal.LevelRequest,
  569. Stage: auditinternal.StageResponseComplete,
  570. RequestURI: fmt.Sprintf("/api/v1/namespaces/%s/pods", namespace),
  571. Verb: "create",
  572. Code: 201,
  573. User: "system:apiserver",
  574. ImpersonatedUser: testReinvocationClientUsername,
  575. ImpersonatedGroups: "system:authenticated,system:masters",
  576. Resource: "pods",
  577. Namespace: namespace,
  578. AuthorizeDecision: "allow",
  579. RequestObject: true,
  580. ResponseObject: false,
  581. AdmissionWebhookMutationAnnotations: webhookMutationAnnotations,
  582. AdmissionWebhookPatchAnnotations: webhookPatchAnnotations,
  583. },
  584. }
  585. }
  586. func newReinvocationMarkerFixture(namespace string) *corev1.Pod {
  587. return &corev1.Pod{
  588. ObjectMeta: metav1.ObjectMeta{
  589. Namespace: namespace,
  590. Name: "marker",
  591. Labels: map[string]string{
  592. "marker": "true",
  593. },
  594. },
  595. Spec: corev1.PodSpec{
  596. Containers: []v1.Container{{
  597. Name: "fake-name",
  598. Image: "fakeimage",
  599. }},
  600. },
  601. }
  602. }