istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/webhooks/validation/server/server_test.go (about) 1 // Copyright Istio Authors 2 // 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 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package server 16 17 import ( 18 "bytes" 19 "encoding/json" 20 "fmt" 21 "io" 22 "net/http" 23 "net/http/httptest" 24 "os" 25 "path/filepath" 26 "strconv" 27 "strings" 28 "testing" 29 30 admissionv1 "k8s.io/api/admission/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 33 "k8s.io/apimachinery/pkg/runtime" 34 35 "istio.io/istio/pkg/config/schema/collections" 36 "istio.io/istio/pkg/kube" 37 "istio.io/istio/pkg/test/config" 38 "istio.io/istio/pkg/testcerts" 39 ) 40 41 const ( 42 // testDomainSuffix is the default DNS domain suffix for Istio 43 // CRD resources. 44 testDomainSuffix = "local.cluster" 45 ) 46 47 func TestArgs_String(t *testing.T) { 48 p := DefaultArgs() 49 // Should not crash 50 _ = p.String() 51 } 52 53 func createTestWebhook(t testing.TB) *Webhook { 54 t.Helper() 55 dir := t.TempDir() 56 57 var ( 58 certFile = filepath.Join(dir, "cert-file.yaml") 59 keyFile = filepath.Join(dir, "key-file.yaml") 60 port = uint(0) 61 ) 62 63 // cert 64 if err := os.WriteFile(certFile, testcerts.ServerCert, 0o644); err != nil { // nolint: vetshadow 65 t.Fatalf("WriteFile(%v) failed: %v", certFile, err) 66 } 67 // key 68 if err := os.WriteFile(keyFile, testcerts.ServerKey, 0o644); err != nil { // nolint: vetshadow 69 t.Fatalf("WriteFile(%v) failed: %v", keyFile, err) 70 } 71 72 options := Options{ 73 Port: port, 74 DomainSuffix: testDomainSuffix, 75 Schemas: collections.Mocks, 76 Mux: http.NewServeMux(), 77 } 78 wh, err := New(options) 79 if err != nil { 80 t.Fatalf("New() failed: %v", err) 81 } 82 83 return wh 84 } 85 86 func makePilotConfig(t *testing.T, i int, validConfig bool, includeBogusKey bool) []byte { // nolint: unparam 87 t.Helper() 88 89 var key string 90 if validConfig { 91 key = "key" 92 } 93 94 name := fmt.Sprintf("%s%d", "mock-config", i) 95 96 r := collections.Mock 97 var un unstructured.Unstructured 98 un.SetGroupVersionKind(r.GroupVersionKind().Kubernetes()) 99 un.SetName(name) 100 un.SetLabels(map[string]string{"key": name}) 101 un.SetAnnotations(map[string]string{"annotationKey": name}) 102 un.Object["spec"] = &config.MockConfig{ 103 Key: key, 104 Pairs: []*config.ConfigPair{{ 105 Key: key, 106 Value: strconv.Itoa(i), 107 }}, 108 } 109 raw, err := json.Marshal(&un) 110 if err != nil { 111 t.Fatalf("Marshal(%v) failed: %v", name, err) 112 } 113 if includeBogusKey { 114 trial := make(map[string]any) 115 if err := json.Unmarshal(raw, &trial); err != nil { 116 t.Fatalf("Unmarshal(%v) failed: %v", name, err) 117 } 118 trial["unexpected_key"] = "any value" 119 if raw, err = json.Marshal(&trial); err != nil { 120 t.Fatalf("re-Marshal(%v) failed: %v", name, err) 121 } 122 } 123 return raw 124 } 125 126 func TestAdmitPilot(t *testing.T) { 127 valid := makePilotConfig(t, 0, true, false) 128 invalidConfig := makePilotConfig(t, 0, false, false) 129 extraKeyConfig := makePilotConfig(t, 0, true, true) 130 131 wh := createTestWebhook(t) 132 133 cases := []struct { 134 name string 135 admit admitFunc 136 in *kube.AdmissionRequest 137 allowed bool 138 }{ 139 { 140 name: "valid create", 141 admit: wh.validate, 142 in: &kube.AdmissionRequest{ 143 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 144 Object: runtime.RawExtension{Raw: valid}, 145 Operation: kube.Create, 146 }, 147 allowed: true, 148 }, 149 { 150 name: "valid update", 151 admit: wh.validate, 152 in: &kube.AdmissionRequest{ 153 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 154 Object: runtime.RawExtension{Raw: valid}, 155 Operation: kube.Update, 156 }, 157 allowed: true, 158 }, 159 { 160 name: "unsupported operation", 161 admit: wh.validate, 162 in: &kube.AdmissionRequest{ 163 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 164 Object: runtime.RawExtension{Raw: valid}, 165 Operation: kube.Delete, 166 }, 167 allowed: true, 168 }, 169 { 170 name: "invalid spec", 171 admit: wh.validate, 172 in: &kube.AdmissionRequest{ 173 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 174 Object: runtime.RawExtension{Raw: invalidConfig}, 175 Operation: kube.Create, 176 }, 177 allowed: false, 178 }, 179 { 180 name: "corrupt object", 181 admit: wh.validate, 182 in: &kube.AdmissionRequest{ 183 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 184 Object: runtime.RawExtension{Raw: append([]byte("---"), valid...)}, 185 Operation: kube.Create, 186 }, 187 allowed: false, 188 }, 189 { 190 name: "invalid extra key create", 191 admit: wh.validate, 192 in: &kube.AdmissionRequest{ 193 Kind: metav1.GroupVersionKind{Kind: collections.Mock.Kind()}, 194 Object: runtime.RawExtension{Raw: extraKeyConfig}, 195 Operation: kube.Create, 196 }, 197 allowed: false, 198 }, 199 } 200 201 for i, c := range cases { 202 t.Run(fmt.Sprintf("[%d] %s", i, c.name), func(t *testing.T) { 203 got := wh.validate(c.in) 204 if got.Allowed != c.allowed { 205 t.Fatalf("got %v want %v", got.Allowed, c.allowed) 206 } 207 }) 208 } 209 } 210 211 func makeTestReview(t *testing.T, valid bool, apiVersion string) []byte { 212 t.Helper() 213 review := admissionv1.AdmissionReview{ 214 TypeMeta: metav1.TypeMeta{ 215 Kind: "AdmissionReview", 216 APIVersion: fmt.Sprintf("admission.k8s.io/%s", apiVersion), 217 }, 218 Request: &admissionv1.AdmissionRequest{ 219 Kind: metav1.GroupVersionKind{ 220 Group: admissionv1.GroupName, 221 Version: apiVersion, 222 Kind: "AdmissionRequest", 223 }, 224 Object: runtime.RawExtension{ 225 Raw: makePilotConfig(t, 0, valid, false), 226 }, 227 Operation: admissionv1.Create, 228 }, 229 } 230 reviewJSON, err := json.Marshal(review) 231 if err != nil { 232 t.Fatalf("Failed to create AdmissionReview: %v", err) 233 } 234 return reviewJSON 235 } 236 237 func TestServe(t *testing.T) { 238 _ = createTestWebhook(t) 239 stop := make(chan struct{}) 240 defer func() { 241 close(stop) 242 }() 243 244 validReview := makeTestReview(t, true, "v1beta1") 245 validReviewV1 := makeTestReview(t, true, "v1") 246 invalidReview := makeTestReview(t, false, "v1beta1") 247 248 cases := []struct { 249 name string 250 body []byte 251 contentType string 252 wantStatusCode int 253 wantAllowed bool 254 allowedResponse bool 255 }{ 256 { 257 name: "valid", 258 body: validReview, 259 contentType: "application/json", 260 wantAllowed: true, 261 wantStatusCode: http.StatusOK, 262 allowedResponse: true, 263 }, 264 { 265 name: "valid(v1 version)", 266 body: validReviewV1, 267 contentType: "application/json", 268 wantAllowed: true, 269 wantStatusCode: http.StatusOK, 270 allowedResponse: true, 271 }, 272 { 273 name: "invalid", 274 body: invalidReview, 275 contentType: "application/json", 276 wantAllowed: false, 277 wantStatusCode: http.StatusOK, 278 }, 279 { 280 name: "wrong content-type", 281 body: validReview, 282 contentType: "application/yaml", 283 wantAllowed: false, 284 wantStatusCode: http.StatusUnsupportedMediaType, 285 }, 286 { 287 name: "bad content", 288 body: []byte{0, 1, 2, 3, 4, 5}, // random data 289 contentType: "application/json", 290 wantAllowed: false, 291 wantStatusCode: http.StatusOK, 292 }, 293 { 294 name: "no content", 295 body: []byte{}, 296 contentType: "application/json", 297 wantAllowed: false, 298 wantStatusCode: http.StatusBadRequest, 299 }, 300 } 301 302 for i, c := range cases { 303 t.Run(fmt.Sprintf("[%d] %s", i, c.name), func(t *testing.T) { 304 req := httptest.NewRequest("POST", "http://validator", bytes.NewReader(c.body)) 305 req.Header.Add("Content-Type", c.contentType) 306 w := httptest.NewRecorder() 307 308 serve(w, req, func(*kube.AdmissionRequest) *kube.AdmissionResponse { 309 return &kube.AdmissionResponse{Allowed: c.allowedResponse} 310 }) 311 312 res := w.Result() 313 314 if res.StatusCode != c.wantStatusCode { 315 t.Fatalf("%v: wrong status code: \ngot %v \nwant %v", c.name, res.StatusCode, c.wantStatusCode) 316 } 317 318 if res.StatusCode != http.StatusOK { 319 return 320 } 321 322 gotBody, err := io.ReadAll(res.Body) 323 if err != nil { 324 t.Fatalf("%v: could not read body: %v", c.name, err) 325 } 326 var gotReview admissionv1.AdmissionReview 327 if err := json.Unmarshal(gotBody, &gotReview); err != nil { 328 t.Fatalf("%v: could not decode response body: %v", c.name, err) 329 } 330 if gotReview.Response.Allowed != c.wantAllowed { 331 t.Fatalf("%v: AdmissionReview.Response.Allowed is wrong : got %v want %v", 332 c.name, gotReview.Response.Allowed, c.wantAllowed) 333 } 334 }) 335 } 336 } 337 338 // scenario is a common struct used by many tests in this context. 339 type scenario struct { 340 wrapFunc func(*Options) 341 expectedError string 342 } 343 344 func TestValidate(t *testing.T) { 345 scenarios := map[string]scenario{ 346 "valid": { 347 wrapFunc: func(args *Options) {}, 348 expectedError: "", 349 }, 350 "invalid port": { 351 wrapFunc: func(args *Options) { args.Port = 100000 }, 352 expectedError: "port number 100000 must be in the range 1..65535", 353 }, 354 } 355 356 for name, scenario := range scenarios { 357 t.Run(name, func(tt *testing.T) { 358 runTestCode(name, tt, scenario) 359 }) 360 } 361 } 362 363 func runTestCode(name string, t *testing.T, test scenario) { 364 args := DefaultArgs() 365 366 test.wrapFunc(&args) 367 err := args.Validate() 368 if err == nil && test.expectedError != "" { 369 t.Errorf("Test %q failed: expected error: %q, got nil", name, test.expectedError) 370 } 371 if err != nil { 372 if test.expectedError == "" { 373 t.Errorf("Test %q failed: expected nil error, got %v", name, err) 374 } 375 if !strings.Contains(err.Error(), test.expectedError) { 376 t.Errorf("Test %q failed: expected error: %q, got %q", name, test.expectedError, err.Error()) 377 } 378 } 379 }