1
16
17 package admissionwebhook
18
19 import (
20 "context"
21 "crypto/tls"
22 "crypto/x509"
23 "encoding/json"
24 "io"
25 "net/http"
26 "net/http/httptest"
27 "sync"
28 "testing"
29 "time"
30
31 admissionv1 "k8s.io/api/admission/v1"
32 admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
33 corev1 "k8s.io/api/core/v1"
34 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
35 apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
36 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
38 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
39 "k8s.io/apimachinery/pkg/runtime"
40 "k8s.io/apimachinery/pkg/runtime/schema"
41 "k8s.io/apimachinery/pkg/runtime/serializer"
42 "k8s.io/apimachinery/pkg/types"
43 "k8s.io/apimachinery/pkg/util/wait"
44 "k8s.io/client-go/dynamic"
45 clientset "k8s.io/client-go/kubernetes"
46 apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
47 "k8s.io/kubernetes/test/integration/etcd"
48 "k8s.io/kubernetes/test/integration/framework"
49 )
50
51 var (
52 runtimeSchemeGVKTest = runtime.NewScheme()
53 codecFactoryGVKTest = serializer.NewCodecFactory(runtimeSchemeGVKTest)
54 deserializerGVKTest = codecFactoryGVKTest.UniversalDeserializer()
55 )
56
57 type admissionTypeChecker struct {
58 mu sync.Mutex
59 upCh chan struct{}
60 upOnce sync.Once
61 requests []*admissionv1.AdmissionRequest
62 }
63
64 func (r *admissionTypeChecker) Reset() chan struct{} {
65 r.mu.Lock()
66 defer r.mu.Unlock()
67 r.upCh = make(chan struct{})
68 r.upOnce = sync.Once{}
69 r.requests = []*admissionv1.AdmissionRequest{}
70 return r.upCh
71 }
72
73 func (r *admissionTypeChecker) TypeCheck(req *admissionv1.AdmissionRequest, version string) *admissionv1.AdmissionResponse {
74 r.mu.Lock()
75 defer r.mu.Unlock()
76 r.requests = append(r.requests, req)
77 raw := req.Object.Raw
78 var into runtime.Object
79 if _, gvk, err := deserializerGVKTest.Decode(raw, nil, into); err != nil {
80 if gvk.Version != version {
81 return &admissionv1.AdmissionResponse{
82 UID: req.UID,
83 Allowed: false,
84 }
85 }
86 }
87
88 return &admissionv1.AdmissionResponse{
89 UID: req.UID,
90 Allowed: true,
91 }
92 }
93
94 func (r *admissionTypeChecker) MarkerReceived() {
95 r.mu.Lock()
96 defer r.mu.Unlock()
97 r.upOnce.Do(func() {
98 close(r.upCh)
99 })
100 }
101
102 func newAdmissionTypeCheckerHandler(recorder *admissionTypeChecker) http.Handler {
103 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104 defer r.Body.Close()
105 data, err := io.ReadAll(r.Body)
106 if err != nil {
107 http.Error(w, err.Error(), 400)
108 }
109 review := admissionv1.AdmissionReview{}
110 if err := json.Unmarshal(data, &review); err != nil {
111 http.Error(w, err.Error(), 400)
112 }
113
114 switch r.URL.Path {
115 case "/marker":
116 recorder.MarkerReceived()
117 return
118 case "/v1":
119 review.Response = recorder.TypeCheck(review.Request, "v1")
120 case "/v2":
121 review.Response = recorder.TypeCheck(review.Request, "v2")
122 }
123
124 w.Header().Set("Content-Type", "application/json")
125 if err := json.NewEncoder(w).Encode(review); err != nil {
126 http.Error(w, err.Error(), 400)
127 return
128 }
129
130 })
131 }
132
133
134 func Test_MutatingWebhookConvertsGVKWithMatchPolicyEquivalent(t *testing.T) {
135
136 roots := x509.NewCertPool()
137 if !roots.AppendCertsFromPEM(localhostCert) {
138 t.Fatal("Failed to append Cert from PEM")
139 }
140 cert, err := tls.X509KeyPair(localhostCert, localhostKey)
141 if err != nil {
142 t.Fatalf("Failed to build cert with error: %+v", err)
143 }
144
145 typeChecker := &admissionTypeChecker{}
146
147 webhookServer := httptest.NewUnstartedServer(newAdmissionTypeCheckerHandler(typeChecker))
148 webhookServer.TLS = &tls.Config{
149 RootCAs: roots,
150 Certificates: []tls.Certificate{cert},
151 }
152 webhookServer.StartTLS()
153 defer webhookServer.Close()
154
155 upCh := typeChecker.Reset()
156 server, err := apiservertesting.StartTestServer(t, nil, []string{
157 "--disable-admission-plugins=ServiceAccount",
158 }, framework.SharedEtcd())
159 if err != nil {
160 t.Fatal(err)
161 }
162 defer server.TearDownFn()
163
164 etcd.CreateTestCRDs(t, apiextensionsclientset.NewForConfigOrDie(server.ClientConfig), false, versionedCustomResourceDefinition())
165 if err != nil {
166 t.Fatal(err)
167 }
168
169 config := server.ClientConfig
170
171 client, err := clientset.NewForConfig(config)
172 if err != nil {
173 t.Fatal(err)
174 }
175
176
177 markerNs := "marker"
178 _, err = client.CoreV1().Namespaces().Create(context.TODO(), &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: markerNs}}, metav1.CreateOptions{})
179 if err != nil {
180 t.Fatal(err)
181 }
182
183
184 marker, err := client.CoreV1().Pods(markerNs).Create(context.TODO(), newMarkerPodGVKConversion(markerNs), metav1.CreateOptions{})
185 if err != nil {
186 t.Fatal(err)
187 }
188
189 equivalent := admissionregistrationv1.Equivalent
190 ignore := admissionregistrationv1.Ignore
191
192 v1Endpoint := webhookServer.URL + "/v1"
193 markerEndpoint := webhookServer.URL + "/marker"
194 v2Endpoint := webhookServer.URL + "/v2"
195 mutatingWebhook := &admissionregistrationv1.MutatingWebhookConfiguration{
196 ObjectMeta: metav1.ObjectMeta{
197 Name: "admission.integration.test",
198 },
199 Webhooks: []admissionregistrationv1.MutatingWebhook{
200 {
201 Name: "admission.integration.test.v2",
202 Rules: []admissionregistrationv1.RuleWithOperations{{
203 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
204 Rule: admissionregistrationv1.Rule{
205 APIGroups: []string{"awesome.example.com"},
206 APIVersions: []string{"v2"},
207 Resources: []string{"*/*"},
208 },
209 }},
210 MatchPolicy: &equivalent,
211 ClientConfig: admissionregistrationv1.WebhookClientConfig{
212 URL: &v2Endpoint,
213 CABundle: localhostCert,
214 },
215 FailurePolicy: &ignore,
216 SideEffects: &noSideEffects,
217 AdmissionReviewVersions: []string{"v1"},
218 MatchConditions: []admissionregistrationv1.MatchCondition{
219 {
220 Name: "test-v2",
221 Expression: "object.apiVersion == 'awesome.example.com/v2'",
222 },
223 },
224 },
225 {
226 Name: "admission.integration.test",
227 Rules: []admissionregistrationv1.RuleWithOperations{{
228 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.Create},
229 Rule: admissionregistrationv1.Rule{
230 APIGroups: []string{"awesome.example.com"},
231 APIVersions: []string{"v1"},
232 Resources: []string{"*/*"},
233 },
234 }},
235 MatchPolicy: &equivalent,
236 ClientConfig: admissionregistrationv1.WebhookClientConfig{
237 URL: &v1Endpoint,
238 CABundle: localhostCert,
239 },
240 SideEffects: &noSideEffects,
241 AdmissionReviewVersions: []string{"v1"},
242 MatchConditions: []admissionregistrationv1.MatchCondition{
243 {
244 Name: "test-v1",
245 Expression: "object.apiVersion == 'awesome.example.com/v1'",
246 },
247 },
248 },
249 {
250 Name: "admission.integration.test.marker",
251 Rules: []admissionregistrationv1.RuleWithOperations{{
252 Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
253 Rule: admissionregistrationv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
254 }},
255 ClientConfig: admissionregistrationv1.WebhookClientConfig{
256 URL: &markerEndpoint,
257 CABundle: localhostCert,
258 },
259 NamespaceSelector: &metav1.LabelSelector{MatchLabels: map[string]string{
260 corev1.LabelMetadataName: "marker",
261 }},
262 ObjectSelector: &metav1.LabelSelector{MatchLabels: map[string]string{"marker": "true"}},
263 SideEffects: &noSideEffects,
264 AdmissionReviewVersions: []string{"v1"},
265 },
266 },
267 }
268
269 mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), mutatingWebhook, metav1.CreateOptions{})
270 if err != nil {
271 t.Fatal(err)
272 }
273 defer func() {
274 err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{})
275 if err != nil {
276 t.Fatal(err)
277 }
278 }()
279
280
281 if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
282 _, err = client.CoreV1().Pods(markerNs).Patch(context.TODO(), marker.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
283 select {
284 case <-upCh:
285 return true, nil
286 default:
287 t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
288 return false, nil
289 }
290 }); err != nil {
291 t.Fatal(err)
292 }
293 dynamicClient, err := dynamic.NewForConfig(config)
294 if err != nil {
295 t.Fatal(err)
296 }
297
298 v1Resource := &unstructured.Unstructured{
299 Object: map[string]interface{}{
300 "apiVersion": "awesome.example.com" + "/" + "v1",
301 "kind": "Panda",
302 "metadata": map[string]interface{}{
303 "name": "v1-bears",
304 },
305 },
306 }
307
308 v2Resource := &unstructured.Unstructured{
309 Object: map[string]interface{}{
310 "apiVersion": "awesome.example.com" + "/" + "v2",
311 "kind": "Panda",
312 "metadata": map[string]interface{}{
313 "name": "v2-bears",
314 },
315 },
316 }
317
318 _, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v1", Resource: "pandas"}).Create(context.TODO(), v1Resource, metav1.CreateOptions{})
319 if err != nil {
320 t.Errorf("error1 %v", err.Error())
321 }
322
323 _, err = dynamicClient.Resource(schema.GroupVersionResource{Group: "awesome.example.com", Version: "v2", Resource: "pandas"}).Create(context.TODO(), v2Resource, metav1.CreateOptions{})
324 if err != nil {
325 t.Errorf("error2 %v", err.Error())
326 }
327
328 if len(typeChecker.requests) != 4 {
329 t.Errorf("expected 4 request got %v", len(typeChecker.requests))
330 }
331 }
332
333 func newMarkerPodGVKConversion(namespace string) *corev1.Pod {
334 return &corev1.Pod{
335 ObjectMeta: metav1.ObjectMeta{
336 Namespace: namespace,
337 Name: "marker",
338 Labels: map[string]string{
339 "marker": "true",
340 },
341 },
342 Spec: corev1.PodSpec{
343 Containers: []corev1.Container{{
344 Name: "fake-name",
345 Image: "fakeimage",
346 }},
347 },
348 }
349 }
350
351
352 func versionedCustomResourceDefinition() *apiextensionsv1.CustomResourceDefinition {
353 return &apiextensionsv1.CustomResourceDefinition{
354 ObjectMeta: metav1.ObjectMeta{
355 Name: "pandas.awesome.example.com",
356 },
357 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
358 Group: "awesome.example.com",
359 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
360 {
361 Name: "v1",
362 Served: true,
363 Storage: true,
364 Schema: fixtures.AllowAllSchema(),
365 Subresources: &apiextensionsv1.CustomResourceSubresources{
366 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
367 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
368 SpecReplicasPath: ".spec.replicas",
369 StatusReplicasPath: ".status.replicas",
370 LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(),
371 },
372 },
373 },
374 {
375 Name: "v2",
376 Served: true,
377 Storage: false,
378 Schema: fixtures.AllowAllSchema(),
379 Subresources: &apiextensionsv1.CustomResourceSubresources{
380 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
381 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
382 SpecReplicasPath: ".spec.replicas",
383 StatusReplicasPath: ".status.replicas",
384 LabelSelectorPath: func() *string { path := ".status.selector"; return &path }(),
385 },
386 },
387 },
388 },
389 Scope: apiextensionsv1.ClusterScoped,
390 Names: apiextensionsv1.CustomResourceDefinitionNames{
391 Plural: "pandas",
392 Kind: "Panda",
393 },
394 },
395 }
396 }
397
View as plain text