sigs.k8s.io/gateway-api@v1.0.0/pkg/admission/server_test.go (about) 1 /* 2 Copyright 2021 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 admission 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "net/http" 24 "net/http/httptest" 25 "testing" 26 27 "github.com/lithammer/dedent" 28 "github.com/stretchr/testify/assert" 29 "github.com/stretchr/testify/require" 30 admission "k8s.io/api/admission/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 ) 33 34 var decoder = codecs.UniversalDeserializer() 35 36 func TestServeHTTPInvalidBody(t *testing.T) { 37 assert := assert.New(t) 38 res := httptest.NewRecorder() 39 handler := http.HandlerFunc(ServeHTTP) 40 req, err := http.NewRequest("POST", "", nil) 41 req = req.WithContext(context.Background()) 42 assert.Nil(err) 43 handler.ServeHTTP(res, req) 44 assert.Equal(400, res.Code) 45 assert.Equal("admission review object is missing\n", 46 res.Body.String()) 47 } 48 49 func TestServeHTTPInvalidMethod(t *testing.T) { 50 assert := assert.New(t) 51 res := httptest.NewRecorder() 52 handler := http.HandlerFunc(ServeHTTP) 53 req, err := http.NewRequest("GET", "", nil) 54 req = req.WithContext(context.Background()) 55 assert.Nil(err) 56 handler.ServeHTTP(res, req) 57 assert.Equal(http.StatusMethodNotAllowed, res.Code) 58 assert.Equal("invalid method GET, only POST requests are allowed\n", 59 res.Body.String()) 60 } 61 62 func TestServeHTTPSubmissions(t *testing.T) { 63 for _, apiVersion := range []string{ 64 "admission.k8s.io/v1", 65 "admission.k8s.io/v1", 66 } { 67 for _, tt := range []struct { 68 name string 69 reqBody string 70 71 wantRespCode int 72 wantSuccessResponse admission.AdmissionResponse 73 wantFailureMessage string 74 }{ 75 { 76 name: "malformed json missing colon at resource", 77 reqBody: dedent.Dedent(`{ 78 "kind": "AdmissionReview", 79 "apiVersion": "` + apiVersion + `", 80 "request": { 81 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 82 "resource": { 83 "group": "networking.x-k8s.io", 84 "version": "v1alpha1", 85 "resource" "httproutes" 86 }, 87 "object": { 88 "apiVersion": "networking.x-k8s.io/v1alpha1", 89 "kind": "HTTPRoute" 90 }, 91 "operation": "CREATE" 92 } 93 }`), 94 wantRespCode: http.StatusBadRequest, 95 wantFailureMessage: "invalid character '\"' after object key\n", 96 }, 97 { 98 name: "request with empty body", 99 wantRespCode: http.StatusBadRequest, 100 wantFailureMessage: "unexpected end of JSON input\n", 101 }, 102 { 103 name: "valid json but not of kind AdmissionReview", 104 reqBody: dedent.Dedent(`{ 105 "kind": "NotReviewYouAreLookingFor", 106 "apiVersion": "` + apiVersion + `", 107 "request": { 108 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 109 "resource": { 110 "group": "gateway.networking.k8s.io", 111 "version": "v1", 112 "resource": "httproutes" 113 }, 114 "object": { 115 "apiVersion": "gateway.networking.k8s.io/v1", 116 "kind": "HTTPRoute" 117 }, 118 "operation": "CREATE" 119 } 120 }`), 121 wantRespCode: http.StatusBadRequest, 122 wantFailureMessage: "submitted object is not of kind AdmissionReview\n", 123 }, 124 { 125 name: "valid v1 Gateway resource", 126 reqBody: dedent.Dedent(`{ 127 "kind": "AdmissionReview", 128 "apiVersion": "` + apiVersion + `", 129 "request": { 130 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 131 "resource": { 132 "group": "gateway.networking.k8s.io", 133 "version": "v1", 134 "resource": "gateways" 135 }, 136 "object": { 137 "kind": "Gateway", 138 "apiVersion": "gateway.networking.k8s.io/v1", 139 "metadata": { 140 "name": "gateway-1", 141 "labels": { 142 "app": "foo" 143 } 144 }, 145 "spec": { 146 "gatewayClassName": "contour-class", 147 "listeners": [ 148 { 149 "port": 80, 150 "protocol": "HTTP", 151 "hostname": "foo.com", 152 "routes": { 153 "group": "gateway.networking.k8s.io", 154 "kind": "HTTPRoute", 155 "namespaces": { 156 "from": "All" 157 } 158 } 159 } 160 ] 161 } 162 }, 163 "operation": "CREATE" 164 } 165 }`), 166 wantRespCode: http.StatusOK, 167 wantSuccessResponse: admission.AdmissionResponse{ 168 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 169 Allowed: true, 170 Result: &metav1.Status{}, 171 }, 172 }, 173 { 174 name: "valid v1 HTTPRoute resource", 175 reqBody: dedent.Dedent(`{ 176 "kind": "AdmissionReview", 177 "apiVersion": "` + apiVersion + `", 178 "request": { 179 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 180 "resource": { 181 "group": "gateway.networking.k8s.io", 182 "version": "v1", 183 "resource": "httproutes" 184 }, 185 "object": { 186 "kind": "HTTPRoute", 187 "apiVersion": "gateway.networking.k8s.io/v1", 188 "metadata": { 189 "name": "http-app-1", 190 "labels": { 191 "app": "foo" 192 } 193 }, 194 "spec": { 195 "hostnames": [ 196 "foo.com" 197 ], 198 "rules": [ 199 { 200 "matches": [ 201 { 202 "path": { 203 "type": "PathPrefix", 204 "value": "/bar" 205 } 206 } 207 ], 208 "filters": [ 209 { 210 "type": "RequestMirror", 211 "requestMirror": { 212 "serviceName": "my-service1-staging", 213 "port": 8080 214 } 215 } 216 ], 217 "forwardTo": [ 218 { 219 "serviceName": "my-service1", 220 "port": 8080 221 } 222 ] 223 } 224 ] 225 } 226 }, 227 "operation": "CREATE" 228 } 229 }`), 230 wantRespCode: http.StatusOK, 231 wantSuccessResponse: admission.AdmissionResponse{ 232 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 233 Allowed: true, 234 Result: &metav1.Status{}, 235 }, 236 }, 237 { 238 name: "valid v1 HTTPRoute resource with two request mirror filters", 239 reqBody: dedent.Dedent(`{ 240 "kind": "AdmissionReview", 241 "apiVersion": "` + apiVersion + `", 242 "request": { 243 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 244 "resource": { 245 "group": "gateway.networking.k8s.io", 246 "version": "v1", 247 "resource": "httproutes" 248 }, 249 "object": { 250 "kind": "HTTPRoute", 251 "apiVersion": "gateway.networking.k8s.io/v1", 252 "metadata": { 253 "name": "http-app-1", 254 "labels": { 255 "app": "foo" 256 } 257 }, 258 "spec": { 259 "hostnames": [ 260 "foo.com" 261 ], 262 "rules": [ 263 { 264 "matches": [ 265 { 266 "path": { 267 "type": "PathPrefix", 268 "value": "/bar" 269 } 270 } 271 ], 272 "filters": [ 273 { 274 "type": "RequestMirror", 275 "requestMirror": { 276 "serviceName": "my-service1-staging", 277 "port": 8080 278 } 279 }, 280 { 281 "type": "RequestMirror", 282 "requestMirror": { 283 "serviceName": "my-service2-staging", 284 "port": 8080 285 } 286 } 287 ], 288 "backendRefs": [ 289 { 290 "name": "RequestMirror", 291 "port": 8080 292 } 293 ] 294 } 295 ] 296 } 297 }, 298 "operation": "CREATE" 299 } 300 }`), 301 wantRespCode: http.StatusOK, 302 wantSuccessResponse: admission.AdmissionResponse{ 303 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 304 Allowed: true, 305 Result: &metav1.Status{}, 306 }, 307 }, 308 { 309 name: "v1a2 GatewayClass create events do not result in an error", 310 reqBody: dedent.Dedent(`{ 311 "kind": "AdmissionReview", 312 "apiVersion": "` + apiVersion + `", 313 "request": { 314 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 315 "resource": { 316 "group": "gateway.networking.k8s.io", 317 "version": "v1", 318 "resource": "gatewayclasses" 319 }, 320 "object": { 321 "kind": "GatewayClass", 322 "apiVersion": "gateway.networking.k8s.io/v1", 323 "metadata": { 324 "name": "gateway-class-1" 325 }, 326 "spec": { 327 "controller": "example.com/foo" 328 } 329 }, 330 "operation": "CREATE" 331 } 332 }`), 333 wantRespCode: http.StatusOK, 334 wantSuccessResponse: admission.AdmissionResponse{ 335 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 336 Allowed: true, 337 Result: &metav1.Status{}, 338 }, 339 }, 340 { 341 name: "update to v1 GatewayClass parameters field does" + 342 " not result in an error", 343 reqBody: dedent.Dedent(`{ 344 "kind": "AdmissionReview", 345 "apiVersion": "` + apiVersion + `", 346 "request": { 347 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 348 "resource": { 349 "group": "gateway.networking.k8s.io", 350 "version": "v1", 351 "resource": "gatewayclasses" 352 }, 353 "object": { 354 "kind": "GatewayClass", 355 "apiVersion": "gateway.networking.k8s.io/v1", 356 "metadata": { 357 "name": "gateway-class-1" 358 }, 359 "spec": { 360 "controllerName": "example.com/foo" 361 } 362 }, 363 "oldObject": { 364 "kind": "GatewayClass", 365 "apiVersion": "gateway.networking.k8s.io/v1", 366 "metadata": { 367 "name": "gateway-class-1" 368 }, 369 "spec": { 370 "controllerName": "example.com/foo", 371 "parametersRef": { 372 "name": "foo", 373 "namespace": "bar", 374 "scope": "Namespace", 375 "group": "example.com", 376 "kind": "ExampleConfig" 377 } 378 } 379 }, 380 "operation": "UPDATE" 381 } 382 }`), 383 wantRespCode: http.StatusOK, 384 wantSuccessResponse: admission.AdmissionResponse{ 385 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 386 Allowed: true, 387 Result: &metav1.Status{}, 388 }, 389 }, 390 { 391 name: "update to v1 GatewayClass controllerName field" + 392 " results in an error ", 393 reqBody: dedent.Dedent(`{ 394 "kind": "AdmissionReview", 395 "apiVersion": "` + apiVersion + `", 396 "request": { 397 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 398 "resource": { 399 "group": "gateway.networking.k8s.io", 400 "version": "v1", 401 "resource": "gatewayclasses" 402 }, 403 "object": { 404 "kind": "GatewayClass", 405 "apiVersion": "gateway.networking.k8s.io/v1", 406 "metadata": { 407 "name": "gateway-class-1" 408 }, 409 "spec": { 410 "controllerName": "example.com/foo" 411 } 412 }, 413 "oldObject": { 414 "kind": "GatewayClass", 415 "apiVersion": "gateway.networking.k8s.io/v1", 416 "metadata": { 417 "name": "gateway-class-1" 418 }, 419 "spec": { 420 "controllerName": "example.com/bar" 421 } 422 }, 423 "operation": "UPDATE" 424 } 425 }`), 426 wantRespCode: http.StatusOK, 427 wantSuccessResponse: admission.AdmissionResponse{ 428 UID: "7313cd05-eddc-4150-b88c-971a0d53b2ab", 429 Allowed: false, 430 Result: &metav1.Status{ 431 Code: 400, 432 Message: `spec.controllerName: Invalid value: "example.com/foo": cannot update an immutable field`, 433 }, 434 }, 435 }, 436 { 437 name: "unknown resource under networking.x-k8s.io", 438 reqBody: dedent.Dedent(`{ 439 "kind": "AdmissionReview", 440 "apiVersion": "` + apiVersion + `", 441 "request": { 442 "uid": "7313cd05-eddc-4150-b88c-971a0d53b2ab", 443 "resource": { 444 "group": "gateway.networking.k8s.io", 445 "version": "v1", 446 "resource": "brokenroutes" 447 }, 448 "object": { 449 "apiVersion": "gateway.networking.k8s.io/v1", 450 "kind": "HTTPRoute" 451 }, 452 "operation": "CREATE" 453 } 454 }`), 455 wantRespCode: http.StatusInternalServerError, 456 wantFailureMessage: "unknown resource 'brokenroutes'\n", 457 }, 458 } { 459 tt := tt 460 t.Run(fmt.Sprintf("%s/%s", apiVersion, tt.name), func(t *testing.T) { 461 assert := assert.New(t) 462 res := httptest.NewRecorder() 463 handler := http.HandlerFunc(ServeHTTP) 464 465 // send request 466 req, err := http.NewRequest("POST", "", bytes.NewBuffer([]byte(tt.reqBody))) 467 req = req.WithContext(context.Background()) 468 require.NoError(t, err) 469 handler.ServeHTTP(res, req) 470 471 // check response assertions 472 assert.Equal(tt.wantRespCode, res.Code) 473 if tt.wantRespCode == http.StatusOK { 474 var review admission.AdmissionReview 475 _, _, err = decoder.Decode(res.Body.Bytes(), nil, &review) 476 require.NoError(t, err) 477 assert.EqualValues(&tt.wantSuccessResponse, review.Response) 478 } else { 479 assert.Equal(res.Body.String(), tt.wantFailureMessage) 480 } 481 }) 482 } 483 } 484 }