k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/testing/webhook_server.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package testing 18 19 import ( 20 "crypto/tls" 21 "crypto/x509" 22 "encoding/json" 23 "net/http" 24 "net/http/httptest" 25 "regexp" 26 "strconv" 27 "strings" 28 "testing" 29 "time" 30 31 "k8s.io/api/admission/v1beta1" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts" 34 testingclock "k8s.io/utils/clock/testing" 35 ) 36 37 // NewTestServerWithHandler returns a webhook test HTTPS server 38 // which uses given handler function to handle requests 39 func NewTestServerWithHandler(t testing.TB, handler func(http.ResponseWriter, *http.Request)) *httptest.Server { 40 // Create the test webhook server 41 sCert, err := tls.X509KeyPair(testcerts.ServerCert, testcerts.ServerKey) 42 if err != nil { 43 t.Error(err) 44 t.FailNow() 45 } 46 rootCAs := x509.NewCertPool() 47 rootCAs.AppendCertsFromPEM(testcerts.CACert) 48 testServer := httptest.NewUnstartedServer(http.HandlerFunc(handler)) 49 testServer.TLS = &tls.Config{ 50 Certificates: []tls.Certificate{sCert}, 51 ClientCAs: rootCAs, 52 ClientAuth: tls.RequireAndVerifyClientCert, 53 } 54 return testServer 55 } 56 57 // NewTestServer returns a webhook test HTTPS server with fixed webhook test certs. 58 func NewTestServer(t testing.TB) *httptest.Server { 59 return NewTestServerWithHandler(t, webhookHandler) 60 } 61 62 func webhookHandler(w http.ResponseWriter, r *http.Request) { 63 // fmt.Printf("got req: %v\n", r.URL.Path) 64 switch r.URL.Path { 65 case "/internalErr": 66 http.Error(w, "webhook internal server error", http.StatusInternalServerError) 67 return 68 case "/invalidReq": 69 w.WriteHeader(http.StatusSwitchingProtocols) 70 w.Write([]byte("webhook invalid request")) 71 return 72 case "/invalidResp": 73 w.Header().Set("Content-Type", "application/json") 74 w.Write([]byte("webhook invalid response")) 75 case "/disallow": 76 w.Header().Set("Content-Type", "application/json") 77 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 78 Response: &v1beta1.AdmissionResponse{ 79 Allowed: false, 80 Result: &metav1.Status{ 81 Code: http.StatusForbidden, 82 }, 83 }, 84 }) 85 case "/disallowReason": 86 w.Header().Set("Content-Type", "application/json") 87 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 88 Response: &v1beta1.AdmissionResponse{ 89 Allowed: false, 90 Result: &metav1.Status{ 91 Message: "you shall not pass", 92 Code: http.StatusForbidden, 93 }, 94 }, 95 }) 96 case "/shouldNotBeCalled": 97 w.Header().Set("Content-Type", "application/json") 98 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 99 Response: &v1beta1.AdmissionResponse{ 100 Allowed: false, 101 Result: &metav1.Status{ 102 Message: "doesn't expect labels to match object selector", 103 Code: http.StatusForbidden, 104 }, 105 }, 106 }) 107 case "/allow": 108 w.Header().Set("Content-Type", "application/json") 109 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 110 Response: &v1beta1.AdmissionResponse{ 111 Allowed: true, 112 AuditAnnotations: map[string]string{ 113 "key1": "value1", 114 }, 115 }, 116 }) 117 case "/removeLabel": 118 w.Header().Set("Content-Type", "application/json") 119 pt := v1beta1.PatchTypeJSONPatch 120 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 121 Response: &v1beta1.AdmissionResponse{ 122 Allowed: true, 123 PatchType: &pt, 124 Patch: []byte(`[{"op": "remove", "path": "/metadata/labels/remove"}]`), 125 AuditAnnotations: map[string]string{ 126 "key1": "value1", 127 }, 128 }, 129 }) 130 case "/addLabel": 131 w.Header().Set("Content-Type", "application/json") 132 pt := v1beta1.PatchTypeJSONPatch 133 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 134 Response: &v1beta1.AdmissionResponse{ 135 Allowed: true, 136 PatchType: &pt, 137 Patch: []byte(`[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`), 138 }, 139 }) 140 case "/invalidMutation": 141 w.Header().Set("Content-Type", "application/json") 142 pt := v1beta1.PatchTypeJSONPatch 143 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 144 Response: &v1beta1.AdmissionResponse{ 145 Allowed: true, 146 PatchType: &pt, 147 Patch: []byte(`[{"op": "add", "CORRUPTED_KEY":}]`), 148 }, 149 }) 150 case "/nilResponse": 151 w.Header().Set("Content-Type", "application/json") 152 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{}) 153 case "/invalidAnnotation": 154 w.Header().Set("Content-Type", "application/json") 155 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 156 Response: &v1beta1.AdmissionResponse{ 157 Allowed: true, 158 AuditAnnotations: map[string]string{ 159 "invalid*key": "value1", 160 }, 161 }, 162 }) 163 case "/noop": 164 w.Header().Set("Content-Type", "application/json") 165 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 166 Response: &v1beta1.AdmissionResponse{ 167 Allowed: true, 168 }, 169 }) 170 default: 171 http.NotFound(w, r) 172 } 173 } 174 175 // ClockSteppingWebhookHandler given a fakeClock returns a request handler 176 // that moves time in given clock by an amount specified in the webhook request 177 func ClockSteppingWebhookHandler(t testing.TB, fakeClock *testingclock.FakeClock) func(http.ResponseWriter, *http.Request) { 178 return func(w http.ResponseWriter, r *http.Request) { 179 path := r.URL.Path 180 validPath := regexp.MustCompile(`^/(?:allow|disallow)/(\d{1,10})$`) 181 182 if !validPath.MatchString(path) { 183 t.Errorf("error in test case, wrong webhook url path: '%q' expected to match: '%q'", path, validPath.String()) 184 t.FailNow() 185 } 186 187 delay, _ := strconv.ParseInt(validPath.FindStringSubmatch(path)[1], 0, 64) 188 fakeClock.Step(time.Duration(delay)) 189 w.Header().Set("Content-Type", "application/json") 190 191 if strings.HasPrefix(path, "/allow/") { 192 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 193 Response: &v1beta1.AdmissionResponse{ 194 Allowed: true, 195 AuditAnnotations: map[string]string{ 196 "key1": "value1", 197 }, 198 }, 199 }) 200 return 201 } 202 json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{ 203 Response: &v1beta1.AdmissionResponse{ 204 Allowed: false, 205 Result: &metav1.Status{ 206 Code: http.StatusForbidden, 207 }, 208 }, 209 }) 210 } 211 }