k8s.io/client-go@v0.31.1/discovery/discovery_client_test.go (about) 1 /* 2 Copyright 2014 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 discovery 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "net/http" 23 "net/http/httptest" 24 "reflect" 25 "testing" 26 "time" 27 28 "github.com/gogo/protobuf/proto" 29 openapi_v2 "github.com/google/gnostic-models/openapiv2" 30 openapi_v3 "github.com/google/gnostic-models/openapiv3" 31 "github.com/google/go-cmp/cmp" 32 "github.com/stretchr/testify/assert" 33 "github.com/stretchr/testify/require" 34 golangproto "google.golang.org/protobuf/proto" 35 apidiscovery "k8s.io/api/apidiscovery/v2" 36 apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1" 37 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 38 "k8s.io/apimachinery/pkg/runtime" 39 "k8s.io/apimachinery/pkg/runtime/schema" 40 "k8s.io/apimachinery/pkg/util/sets" 41 "k8s.io/apimachinery/pkg/version" 42 "k8s.io/client-go/openapi" 43 restclient "k8s.io/client-go/rest" 44 testutil "k8s.io/client-go/util/testing" 45 "k8s.io/kube-openapi/pkg/spec3" 46 ) 47 48 func TestGetServerVersion(t *testing.T) { 49 expect := version.Info{ 50 Major: "foo", 51 Minor: "bar", 52 GitCommit: "baz", 53 } 54 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 55 output, err := json.Marshal(expect) 56 if err != nil { 57 t.Errorf("unexpected encoding error: %v", err) 58 return 59 } 60 w.Header().Set("Content-Type", "application/json") 61 w.WriteHeader(http.StatusOK) 62 _, err = w.Write(output) 63 require.NoError(t, err) 64 })) 65 defer server.Close() 66 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 67 68 got, err := client.ServerVersion() 69 if err != nil { 70 t.Fatalf("unexpected encoding error: %v", err) 71 } 72 if e, a := expect, *got; !reflect.DeepEqual(e, a) { 73 t.Errorf("expected %v, got %v", e, a) 74 } 75 } 76 77 func TestGetServerGroupsWithV1Server(t *testing.T) { 78 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 79 var obj interface{} 80 switch req.URL.Path { 81 case "/api": 82 obj = &metav1.APIVersions{ 83 Versions: []string{ 84 "v1", 85 }, 86 } 87 case "/apis": 88 obj = &metav1.APIGroupList{ 89 Groups: []metav1.APIGroup{ 90 { 91 Name: "extensions", 92 Versions: []metav1.GroupVersionForDiscovery{ 93 {GroupVersion: "extensions/v1beta1"}, 94 }, 95 }, 96 }, 97 } 98 default: 99 w.WriteHeader(http.StatusNotFound) 100 return 101 } 102 output, err := json.Marshal(obj) 103 if err != nil { 104 t.Fatalf("unexpected encoding error: %v", err) 105 return 106 } 107 w.Header().Set("Content-Type", "application/json") 108 w.WriteHeader(http.StatusOK) 109 _, err = w.Write(output) 110 require.NoError(t, err) 111 })) 112 defer server.Close() 113 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 114 apiGroupList, err := client.ServerGroups() 115 if err != nil { 116 t.Fatalf("unexpected error: %v", err) 117 } 118 groupVersions := metav1.ExtractGroupVersions(apiGroupList) 119 if !reflect.DeepEqual(groupVersions, []string{"v1", "extensions/v1beta1"}) { 120 t.Errorf("expected: %q, got: %q", []string{"v1", "extensions/v1beta1"}, groupVersions) 121 } 122 } 123 124 func TestDiscoveryToleratesMissingCoreGroup(t *testing.T) { 125 // Discovery tolerates 404 from /api. Aggregated api servers can do this. 126 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 127 var obj interface{} 128 switch req.URL.Path { 129 case "/api": 130 w.WriteHeader(http.StatusNotFound) 131 case "/apis": 132 obj = &metav1.APIGroupList{ 133 Groups: []metav1.APIGroup{ 134 { 135 Name: "extensions", 136 Versions: []metav1.GroupVersionForDiscovery{ 137 {GroupVersion: "extensions/v1beta1"}, 138 }, 139 }, 140 }, 141 } 142 } 143 output, err := json.Marshal(obj) 144 if err != nil { 145 t.Fatalf("unexpected encoding error: %v", err) 146 return 147 } 148 w.Header().Set("Content-Type", "application/json") 149 w.WriteHeader(http.StatusOK) 150 _, err = w.Write(output) 151 require.NoError(t, err) 152 })) 153 defer server.Close() 154 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 155 // ServerGroups should not return an error even if server returns 404 at /api. 156 apiGroupList, err := client.ServerGroups() 157 if err != nil { 158 t.Fatalf("unexpected error: %v", err) 159 } 160 groupVersions := metav1.ExtractGroupVersions(apiGroupList) 161 if !reflect.DeepEqual(groupVersions, []string{"extensions/v1beta1"}) { 162 t.Errorf("expected: %q, got: %q", []string{"extensions/v1beta1"}, groupVersions) 163 } 164 } 165 166 func TestDiscoveryFailsWhenNonCoreGroupsMissing(t *testing.T) { 167 // Discovery fails when /apis returns 404. 168 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 169 var obj interface{} 170 switch req.URL.Path { 171 case "/api": 172 obj = &metav1.APIVersions{ 173 Versions: []string{ 174 "v1", 175 }, 176 } 177 case "/apis": 178 w.WriteHeader(http.StatusNotFound) 179 } 180 output, err := json.Marshal(obj) 181 if err != nil { 182 t.Fatalf("unexpected encoding error: %v", err) 183 return 184 } 185 w.Header().Set("Content-Type", "application/json") 186 w.WriteHeader(http.StatusOK) 187 _, err = w.Write(output) 188 require.NoError(t, err) 189 })) 190 defer server.Close() 191 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 192 _, err := client.ServerGroups() 193 if err == nil { 194 t.Fatal("expected error, received none") 195 } 196 } 197 198 func TestGetServerGroupsWithBrokenServer(t *testing.T) { 199 // 404 Not Found errors because discovery at /apis returns an error. 200 // 403 Forbidden errors because discovery at both /api and /apis returns error. 201 for _, statusCode := range []int{http.StatusNotFound, http.StatusForbidden} { 202 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 203 w.WriteHeader(statusCode) 204 })) 205 defer server.Close() 206 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 207 _, err := client.ServerGroups() 208 if err == nil { 209 t.Fatal("expected error, received none") 210 } 211 } 212 } 213 214 func TestTimeoutIsSet(t *testing.T) { 215 cfg := &restclient.Config{} 216 setDiscoveryDefaults(cfg) 217 assert.Equal(t, defaultTimeout, cfg.Timeout) 218 } 219 220 func TestGetServerResourcesForGroupVersion(t *testing.T) { 221 stable := metav1.APIResourceList{ 222 GroupVersion: "v1", 223 APIResources: []metav1.APIResource{ 224 {Name: "pods", Namespaced: true, Kind: "Pod"}, 225 {Name: "services", Namespaced: true, Kind: "Service"}, 226 {Name: "namespaces", Namespaced: false, Kind: "Namespace"}, 227 }, 228 } 229 beta := metav1.APIResourceList{ 230 GroupVersion: "extensions/v1beta1", 231 APIResources: []metav1.APIResource{ 232 {Name: "deployments", Namespaced: true, Kind: "Deployment"}, 233 {Name: "ingresses", Namespaced: true, Kind: "Ingress"}, 234 {Name: "jobs", Namespaced: true, Kind: "Job"}, 235 }, 236 } 237 beta2 := metav1.APIResourceList{ 238 GroupVersion: "extensions/v1beta2", 239 APIResources: []metav1.APIResource{ 240 {Name: "deployments", Namespaced: true, Kind: "Deployment"}, 241 {Name: "ingresses", Namespaced: true, Kind: "Ingress"}, 242 {Name: "jobs", Namespaced: true, Kind: "Job"}, 243 }, 244 } 245 extensionsbeta3 := metav1.APIResourceList{GroupVersion: "extensions/v1beta3", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 246 extensionsbeta4 := metav1.APIResourceList{GroupVersion: "extensions/v1beta4", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 247 extensionsbeta5 := metav1.APIResourceList{GroupVersion: "extensions/v1beta5", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 248 extensionsbeta6 := metav1.APIResourceList{GroupVersion: "extensions/v1beta6", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 249 extensionsbeta7 := metav1.APIResourceList{GroupVersion: "extensions/v1beta7", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 250 extensionsbeta8 := metav1.APIResourceList{GroupVersion: "extensions/v1beta8", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 251 extensionsbeta9 := metav1.APIResourceList{GroupVersion: "extensions/v1beta9", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 252 extensionsbeta10 := metav1.APIResourceList{GroupVersion: "extensions/v1beta10", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 253 254 appsbeta1 := metav1.APIResourceList{GroupVersion: "apps/v1beta1", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 255 appsbeta2 := metav1.APIResourceList{GroupVersion: "apps/v1beta2", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 256 appsbeta3 := metav1.APIResourceList{GroupVersion: "apps/v1beta3", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 257 appsbeta4 := metav1.APIResourceList{GroupVersion: "apps/v1beta4", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 258 appsbeta5 := metav1.APIResourceList{GroupVersion: "apps/v1beta5", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 259 appsbeta6 := metav1.APIResourceList{GroupVersion: "apps/v1beta6", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 260 appsbeta7 := metav1.APIResourceList{GroupVersion: "apps/v1beta7", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 261 appsbeta8 := metav1.APIResourceList{GroupVersion: "apps/v1beta8", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 262 appsbeta9 := metav1.APIResourceList{GroupVersion: "apps/v1beta9", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 263 appsbeta10 := metav1.APIResourceList{GroupVersion: "apps/v1beta10", APIResources: []metav1.APIResource{{Name: "deployments", Namespaced: true, Kind: "Deployment"}}} 264 265 tests := []struct { 266 resourcesList *metav1.APIResourceList 267 path string 268 request string 269 expectErr bool 270 }{ 271 { 272 resourcesList: &stable, 273 path: "/api/v1", 274 request: "v1", 275 expectErr: false, 276 }, 277 { 278 resourcesList: &beta, 279 path: "/apis/extensions/v1beta1", 280 request: "extensions/v1beta1", 281 expectErr: false, 282 }, 283 { 284 resourcesList: &stable, 285 path: "/api/v1", 286 request: "foobar", 287 expectErr: true, 288 }, 289 } 290 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 291 var list interface{} 292 switch req.URL.Path { 293 case "/api/v1": 294 list = &stable 295 case "/apis/extensions/v1beta1": 296 list = &beta 297 case "/apis/extensions/v1beta2": 298 list = &beta2 299 case "/apis/extensions/v1beta3": 300 list = &extensionsbeta3 301 case "/apis/extensions/v1beta4": 302 list = &extensionsbeta4 303 case "/apis/extensions/v1beta5": 304 list = &extensionsbeta5 305 case "/apis/extensions/v1beta6": 306 list = &extensionsbeta6 307 case "/apis/extensions/v1beta7": 308 list = &extensionsbeta7 309 case "/apis/extensions/v1beta8": 310 list = &extensionsbeta8 311 case "/apis/extensions/v1beta9": 312 list = &extensionsbeta9 313 case "/apis/extensions/v1beta10": 314 list = &extensionsbeta10 315 case "/apis/apps/v1beta1": 316 list = &appsbeta1 317 case "/apis/apps/v1beta2": 318 list = &appsbeta2 319 case "/apis/apps/v1beta3": 320 list = &appsbeta3 321 case "/apis/apps/v1beta4": 322 list = &appsbeta4 323 case "/apis/apps/v1beta5": 324 list = &appsbeta5 325 case "/apis/apps/v1beta6": 326 list = &appsbeta6 327 case "/apis/apps/v1beta7": 328 list = &appsbeta7 329 case "/apis/apps/v1beta8": 330 list = &appsbeta8 331 case "/apis/apps/v1beta9": 332 list = &appsbeta9 333 case "/apis/apps/v1beta10": 334 list = &appsbeta10 335 case "/api": 336 list = &metav1.APIVersions{ 337 Versions: []string{ 338 "v1", 339 }, 340 } 341 case "/apis": 342 list = &metav1.APIGroupList{ 343 Groups: []metav1.APIGroup{ 344 { 345 Name: "apps", 346 Versions: []metav1.GroupVersionForDiscovery{ 347 {GroupVersion: "apps/v1beta1", Version: "v1beta1"}, 348 {GroupVersion: "apps/v1beta2", Version: "v1beta2"}, 349 {GroupVersion: "apps/v1beta3", Version: "v1beta3"}, 350 {GroupVersion: "apps/v1beta4", Version: "v1beta4"}, 351 {GroupVersion: "apps/v1beta5", Version: "v1beta5"}, 352 {GroupVersion: "apps/v1beta6", Version: "v1beta6"}, 353 {GroupVersion: "apps/v1beta7", Version: "v1beta7"}, 354 {GroupVersion: "apps/v1beta8", Version: "v1beta8"}, 355 {GroupVersion: "apps/v1beta9", Version: "v1beta9"}, 356 {GroupVersion: "apps/v1beta10", Version: "v1beta10"}, 357 }, 358 }, 359 { 360 Name: "extensions", 361 Versions: []metav1.GroupVersionForDiscovery{ 362 {GroupVersion: "extensions/v1beta1", Version: "v1beta1"}, 363 {GroupVersion: "extensions/v1beta2", Version: "v1beta2"}, 364 {GroupVersion: "extensions/v1beta3", Version: "v1beta3"}, 365 {GroupVersion: "extensions/v1beta4", Version: "v1beta4"}, 366 {GroupVersion: "extensions/v1beta5", Version: "v1beta5"}, 367 {GroupVersion: "extensions/v1beta6", Version: "v1beta6"}, 368 {GroupVersion: "extensions/v1beta7", Version: "v1beta7"}, 369 {GroupVersion: "extensions/v1beta8", Version: "v1beta8"}, 370 {GroupVersion: "extensions/v1beta9", Version: "v1beta9"}, 371 {GroupVersion: "extensions/v1beta10", Version: "v1beta10"}, 372 }, 373 }, 374 }, 375 } 376 default: 377 t.Logf("unexpected request: %s", req.URL.Path) 378 w.WriteHeader(http.StatusNotFound) 379 return 380 } 381 output, err := json.Marshal(list) 382 if err != nil { 383 t.Errorf("unexpected encoding error: %v", err) 384 return 385 } 386 w.Header().Set("Content-Type", "application/json") 387 w.WriteHeader(http.StatusOK) 388 _, err = w.Write(output) 389 require.NoError(t, err) 390 })) 391 defer server.Close() 392 for _, test := range tests { 393 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 394 got, err := client.ServerResourcesForGroupVersion(test.request) 395 if test.expectErr { 396 if err == nil { 397 t.Error("unexpected non-error") 398 } 399 continue 400 } 401 if err != nil { 402 t.Errorf("unexpected error: %v", err) 403 continue 404 } 405 if !reflect.DeepEqual(got, test.resourcesList) { 406 t.Errorf("expected:\n%v\ngot:\n%v\n", test.resourcesList, got) 407 } 408 } 409 410 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 411 start := time.Now() 412 _, serverResources, err := client.ServerGroupsAndResources() 413 if err != nil { 414 t.Errorf("unexpected error: %v", err) 415 } 416 end := time.Now() 417 if d := end.Sub(start); d > time.Second { 418 t.Errorf("took too long to perform discovery: %s", d) 419 } 420 serverGroupVersions := groupVersions(serverResources) 421 expectedGroupVersions := []string{ 422 "v1", 423 "apps/v1beta1", 424 "apps/v1beta2", 425 "apps/v1beta3", 426 "apps/v1beta4", 427 "apps/v1beta5", 428 "apps/v1beta6", 429 "apps/v1beta7", 430 "apps/v1beta8", 431 "apps/v1beta9", 432 "apps/v1beta10", 433 "extensions/v1beta1", 434 "extensions/v1beta2", 435 "extensions/v1beta3", 436 "extensions/v1beta4", 437 "extensions/v1beta5", 438 "extensions/v1beta6", 439 "extensions/v1beta7", 440 "extensions/v1beta8", 441 "extensions/v1beta9", 442 "extensions/v1beta10", 443 } 444 if !reflect.DeepEqual(expectedGroupVersions, serverGroupVersions) { 445 t.Errorf("unexpected group versions: %v", cmp.Diff(expectedGroupVersions, serverGroupVersions)) 446 } 447 } 448 449 func returnedOpenAPI() *openapi_v2.Document { 450 return &openapi_v2.Document{ 451 Definitions: &openapi_v2.Definitions{ 452 AdditionalProperties: []*openapi_v2.NamedSchema{ 453 { 454 Name: "fake.type.1", 455 Value: &openapi_v2.Schema{ 456 Properties: &openapi_v2.Properties{ 457 AdditionalProperties: []*openapi_v2.NamedSchema{ 458 { 459 Name: "count", 460 Value: &openapi_v2.Schema{ 461 Type: &openapi_v2.TypeItem{ 462 Value: []string{"integer"}, 463 }, 464 }, 465 }, 466 }, 467 }, 468 }, 469 }, 470 { 471 Name: "fake.type.2", 472 Value: &openapi_v2.Schema{ 473 Properties: &openapi_v2.Properties{ 474 AdditionalProperties: []*openapi_v2.NamedSchema{ 475 { 476 Name: "count", 477 Value: &openapi_v2.Schema{ 478 Type: &openapi_v2.TypeItem{ 479 Value: []string{"array"}, 480 }, 481 Items: &openapi_v2.ItemsItem{ 482 Schema: []*openapi_v2.Schema{ 483 { 484 Type: &openapi_v2.TypeItem{ 485 Value: []string{"string"}, 486 }, 487 }, 488 }, 489 }, 490 }, 491 }, 492 }, 493 }, 494 }, 495 }, 496 }, 497 }, 498 } 499 } 500 501 func openapiSchemaFakeServer(t *testing.T) (*httptest.Server, error) { 502 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 503 if req.URL.Path != "/openapi/v2" { 504 errMsg := fmt.Sprintf("Unexpected url %v", req.URL) 505 w.WriteHeader(http.StatusNotFound) 506 w.Write([]byte(errMsg)) 507 t.Errorf("testing should fail as %s", errMsg) 508 return 509 } 510 if req.Method != "GET" { 511 errMsg := fmt.Sprintf("Unexpected method %v", req.Method) 512 w.WriteHeader(http.StatusMethodNotAllowed) 513 w.Write([]byte(errMsg)) 514 t.Errorf("testing should fail as %s", errMsg) 515 return 516 } 517 decipherableFormat := req.Header.Get("Accept") 518 if decipherableFormat != "application/com.github.proto-openapi.spec.v2@v1.0+protobuf" { 519 errMsg := fmt.Sprintf("Unexpected accept mime type %v", decipherableFormat) 520 w.WriteHeader(http.StatusUnsupportedMediaType) 521 w.Write([]byte(errMsg)) 522 t.Errorf("testing should fail as %s", errMsg) 523 return 524 } 525 526 output, err := proto.Marshal(returnedOpenAPI()) 527 if err != nil { 528 errMsg := fmt.Sprintf("Unexpected marshal error: %v", err) 529 w.WriteHeader(http.StatusInternalServerError) 530 w.Write([]byte(errMsg)) 531 t.Errorf("testing should fail as %s", errMsg) 532 return 533 } 534 w.WriteHeader(http.StatusOK) 535 w.Write(output) 536 })) 537 538 return server, nil 539 } 540 541 func openapiV3SchemaFakeServer(t *testing.T) (*httptest.Server, map[string]*spec3.OpenAPI, error) { 542 res, err := testutil.NewFakeOpenAPIV3Server("testdata") 543 if err != nil { 544 return nil, nil, err 545 } 546 return res.HttpServer, res.ServedDocuments, nil 547 } 548 549 func TestGetOpenAPISchema(t *testing.T) { 550 server, err := openapiSchemaFakeServer(t) 551 if err != nil { 552 t.Errorf("unexpected error starting fake server: %v", err) 553 } 554 defer server.Close() 555 556 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 557 got, err := client.OpenAPISchema() 558 if err != nil { 559 t.Fatalf("unexpected error getting openapi: %v", err) 560 } 561 if e, a := returnedOpenAPI(), got; !golangproto.Equal(e, a) { 562 t.Errorf("expected \n%v, got \n%v", e, a) 563 } 564 } 565 566 func TestGetOpenAPISchemaV3(t *testing.T) { 567 server, testV3Specs, err := openapiV3SchemaFakeServer(t) 568 if err != nil { 569 t.Errorf("unexpected error starting fake server: %v", err) 570 } 571 defer server.Close() 572 573 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 574 openapiClient := client.OpenAPIV3() 575 paths, err := openapiClient.Paths() 576 if err != nil { 577 t.Fatalf("unexpected error getting openapi: %v", err) 578 } 579 580 contentTypes := []string{ 581 runtime.ContentTypeJSON, openapi.ContentTypeOpenAPIV3PB, 582 } 583 584 for _, contentType := range contentTypes { 585 t.Run(contentType, func(t *testing.T) { 586 for k, v := range paths { 587 actual, err := v.Schema(contentType) 588 if err != nil { 589 t.Fatal(err) 590 } 591 592 expected := testV3Specs[k] 593 switch contentType { 594 595 case runtime.ContentTypeJSON: 596 var actualSpec spec3.OpenAPI 597 598 if err := json.Unmarshal(actual, &actualSpec); err != nil { 599 t.Fatal(err) 600 } 601 602 // Cannot use DeepEqual directly due to differences in how 603 // default key is being handled in gnostic vs kube-openapi 604 // Our test server parses the files in directly as gnostic 605 // which retains empty maps/lists, etc. 606 require.EqualValues(t, expected, &actualSpec) 607 case openapi.ContentTypeOpenAPIV3PB: 608 // Convert to JSON then to gnostic then to PB for comparison 609 expectedJSON, err := json.Marshal(expected) 610 if err != nil { 611 t.Fatal(err) 612 } 613 614 expectedGnostic, err := openapi_v3.ParseDocument(expectedJSON) 615 if err != nil { 616 t.Fatal(err) 617 } 618 619 expectedPB, err := golangproto.Marshal(expectedGnostic) 620 if err != nil { 621 t.Fatal(err) 622 } 623 if !reflect.DeepEqual(expectedPB, actual) { 624 t.Fatalf("expected equal values: %v", cmp.Diff(expectedPB, actual)) 625 } 626 default: 627 panic(fmt.Errorf("unrecognized content type: %v", contentType)) 628 } 629 630 // Ensure that fetching schema once again does not return same instance 631 actualAgain, err := v.Schema(contentType) 632 if err != nil { 633 t.Fatal(err) 634 } 635 636 if reflect.ValueOf(actual).Pointer() == reflect.ValueOf(actualAgain).Pointer() { 637 t.Fatal("expected schema not to be cached") 638 } 639 } 640 641 }) 642 } 643 } 644 645 func TestServerPreferredResources(t *testing.T) { 646 stable := metav1.APIResourceList{ 647 GroupVersion: "v1", 648 APIResources: []metav1.APIResource{ 649 {Name: "pods", Namespaced: true, Kind: "Pod"}, 650 {Name: "services", Namespaced: true, Kind: "Service"}, 651 {Name: "namespaces", Namespaced: false, Kind: "Namespace"}, 652 }, 653 } 654 tests := []struct { 655 resourcesList []*metav1.APIResourceList 656 response func(w http.ResponseWriter, req *http.Request) 657 expectErr func(err error) bool 658 }{ 659 { 660 resourcesList: []*metav1.APIResourceList{&stable}, 661 expectErr: IsGroupDiscoveryFailedError, 662 response: func(w http.ResponseWriter, req *http.Request) { 663 var list interface{} 664 switch req.URL.Path { 665 case "/apis/extensions/v1beta1": 666 w.WriteHeader(http.StatusInternalServerError) 667 return 668 case "/api/v1": 669 list = &stable 670 case "/api": 671 list = &metav1.APIVersions{ 672 Versions: []string{ 673 "v1", 674 }, 675 } 676 case "/apis": 677 list = &metav1.APIGroupList{ 678 Groups: []metav1.APIGroup{ 679 { 680 Versions: []metav1.GroupVersionForDiscovery{ 681 {GroupVersion: "extensions/v1beta1"}, 682 }, 683 }, 684 }, 685 } 686 default: 687 t.Logf("unexpected request: %s", req.URL.Path) 688 w.WriteHeader(http.StatusNotFound) 689 return 690 } 691 output, err := json.Marshal(list) 692 if err != nil { 693 t.Errorf("unexpected encoding error: %v", err) 694 return 695 } 696 w.Header().Set("Content-Type", "application/json") 697 w.WriteHeader(http.StatusOK) 698 w.Write(output) 699 }, 700 }, 701 { 702 resourcesList: nil, 703 expectErr: IsGroupDiscoveryFailedError, 704 response: func(w http.ResponseWriter, req *http.Request) { 705 var list interface{} 706 switch req.URL.Path { 707 case "/apis/extensions/v1beta1": 708 w.WriteHeader(http.StatusInternalServerError) 709 return 710 case "/api/v1": 711 w.WriteHeader(http.StatusInternalServerError) 712 case "/api": 713 list = &metav1.APIVersions{ 714 Versions: []string{ 715 "v1", 716 }, 717 } 718 case "/apis": 719 list = &metav1.APIGroupList{ 720 Groups: []metav1.APIGroup{ 721 { 722 Versions: []metav1.GroupVersionForDiscovery{ 723 {GroupVersion: "extensions/v1beta1"}, 724 }, 725 }, 726 }, 727 } 728 default: 729 t.Logf("unexpected request: %s", req.URL.Path) 730 w.WriteHeader(http.StatusNotFound) 731 return 732 } 733 output, err := json.Marshal(list) 734 if err != nil { 735 t.Errorf("unexpected encoding error: %v", err) 736 return 737 } 738 w.Header().Set("Content-Type", "application/json") 739 w.WriteHeader(http.StatusOK) 740 w.Write(output) 741 }, 742 }, 743 } 744 for _, test := range tests { 745 server := httptest.NewServer(http.HandlerFunc(test.response)) 746 defer server.Close() 747 748 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 749 resources, err := client.ServerPreferredResources() 750 if test.expectErr != nil { 751 if err == nil { 752 t.Error("unexpected non-error") 753 } 754 755 continue 756 } 757 if err != nil { 758 t.Errorf("unexpected error: %v", err) 759 continue 760 } 761 got, err := GroupVersionResources(resources) 762 if err != nil { 763 t.Errorf("unexpected error: %v", err) 764 continue 765 } 766 expected, _ := GroupVersionResources(test.resourcesList) 767 if !reflect.DeepEqual(got, expected) { 768 t.Errorf("expected:\n%v\ngot:\n%v\n", test.resourcesList, got) 769 } 770 server.Close() 771 } 772 } 773 774 func TestServerPreferredResourcesRetries(t *testing.T) { 775 stable := metav1.APIResourceList{ 776 GroupVersion: "v1", 777 APIResources: []metav1.APIResource{ 778 {Name: "pods", Namespaced: true, Kind: "Pod"}, 779 }, 780 } 781 beta := metav1.APIResourceList{ 782 GroupVersion: "extensions/v1", 783 APIResources: []metav1.APIResource{ 784 {Name: "deployments", Namespaced: true, Kind: "Deployment"}, 785 }, 786 } 787 788 response := func(numErrors int) http.HandlerFunc { 789 var i = 0 790 return func(w http.ResponseWriter, req *http.Request) { 791 var list interface{} 792 switch req.URL.Path { 793 case "/apis/extensions/v1beta1": 794 if i < numErrors { 795 i++ 796 w.WriteHeader(http.StatusInternalServerError) 797 return 798 } 799 list = &beta 800 case "/api/v1": 801 list = &stable 802 case "/api": 803 list = &metav1.APIVersions{ 804 Versions: []string{ 805 "v1", 806 }, 807 } 808 case "/apis": 809 list = &metav1.APIGroupList{ 810 Groups: []metav1.APIGroup{ 811 { 812 Name: "extensions", 813 Versions: []metav1.GroupVersionForDiscovery{ 814 {GroupVersion: "extensions/v1beta1", Version: "v1beta1"}, 815 }, 816 PreferredVersion: metav1.GroupVersionForDiscovery{ 817 GroupVersion: "extensions/v1beta1", 818 Version: "v1beta1", 819 }, 820 }, 821 }, 822 } 823 default: 824 t.Logf("unexpected request: %s", req.URL.Path) 825 w.WriteHeader(http.StatusNotFound) 826 return 827 } 828 output, err := json.Marshal(list) 829 if err != nil { 830 t.Errorf("unexpected encoding error: %v", err) 831 return 832 } 833 w.Header().Set("Content-Type", "application/json") 834 w.WriteHeader(http.StatusOK) 835 w.Write(output) 836 } 837 } 838 tests := []struct { 839 responseErrors int 840 expectResources int 841 expectedError func(err error) bool 842 }{ 843 { 844 responseErrors: 1, 845 expectResources: 2, 846 expectedError: func(err error) bool { 847 return err == nil 848 }, 849 }, 850 { 851 responseErrors: 2, 852 expectResources: 1, 853 expectedError: IsGroupDiscoveryFailedError, 854 }, 855 } 856 857 for i, tc := range tests { 858 server := httptest.NewServer(http.HandlerFunc(response(tc.responseErrors))) 859 defer server.Close() 860 861 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 862 resources, err := client.ServerPreferredResources() 863 if !tc.expectedError(err) { 864 t.Errorf("case %d: unexpected error: %v", i, err) 865 } 866 got, err := GroupVersionResources(resources) 867 if err != nil { 868 t.Errorf("case %d: unexpected error: %v", i, err) 869 } 870 if len(got) != tc.expectResources { 871 t.Errorf("case %d: expect %d resources, got %#v", i, tc.expectResources, got) 872 } 873 server.Close() 874 } 875 } 876 877 func TestServerPreferredNamespacedResources(t *testing.T) { 878 stable := metav1.APIResourceList{ 879 GroupVersion: "v1", 880 APIResources: []metav1.APIResource{ 881 {Name: "pods", Namespaced: true, Kind: "Pod"}, 882 {Name: "services", Namespaced: true, Kind: "Service"}, 883 {Name: "namespaces", Namespaced: false, Kind: "Namespace"}, 884 }, 885 } 886 batchv1 := metav1.APIResourceList{ 887 GroupVersion: "batch/v1", 888 APIResources: []metav1.APIResource{ 889 {Name: "jobs", Namespaced: true, Kind: "Job"}, 890 }, 891 } 892 batchv2alpha1 := metav1.APIResourceList{ 893 GroupVersion: "batch/v2alpha1", 894 APIResources: []metav1.APIResource{ 895 {Name: "jobs", Namespaced: true, Kind: "Job"}, 896 {Name: "cronjobs", Namespaced: true, Kind: "CronJob"}, 897 }, 898 } 899 batchv3alpha1 := metav1.APIResourceList{ 900 GroupVersion: "batch/v3alpha1", 901 APIResources: []metav1.APIResource{ 902 {Name: "jobs", Namespaced: true, Kind: "Job"}, 903 {Name: "cronjobs", Namespaced: true, Kind: "CronJob"}, 904 }, 905 } 906 tests := []struct { 907 response func(w http.ResponseWriter, req *http.Request) 908 expected map[schema.GroupVersionResource]struct{} 909 }{ 910 { 911 // Combines discovery for /api and /apis. 912 response: func(w http.ResponseWriter, req *http.Request) { 913 var list interface{} 914 switch req.URL.Path { 915 case "/api": 916 list = &metav1.APIVersions{ 917 Versions: []string{ 918 "v1", 919 }, 920 } 921 case "/api/v1": 922 list = &stable 923 case "/apis": 924 list = &metav1.APIGroupList{ 925 Groups: []metav1.APIGroup{ 926 { 927 Name: "batch", 928 Versions: []metav1.GroupVersionForDiscovery{ 929 {GroupVersion: "batch/v1", Version: "v1"}, 930 }, 931 PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: "batch/v1", Version: "v1"}, 932 }, 933 }, 934 } 935 case "/apis/batch/v1": 936 list = &batchv1 937 938 default: 939 t.Logf("unexpected request: %s", req.URL.Path) 940 w.WriteHeader(http.StatusNotFound) 941 return 942 } 943 output, err := json.Marshal(list) 944 if err != nil { 945 t.Errorf("unexpected encoding error: %v", err) 946 return 947 } 948 w.Header().Set("Content-Type", "application/json") 949 w.WriteHeader(http.StatusOK) 950 w.Write(output) 951 }, 952 expected: map[schema.GroupVersionResource]struct{}{ 953 {Group: "", Version: "v1", Resource: "pods"}: {}, 954 {Group: "", Version: "v1", Resource: "services"}: {}, 955 {Group: "batch", Version: "v1", Resource: "jobs"}: {}, 956 }, 957 }, 958 { 959 // Only return /apis (not legacy /api); does not error. 404 for legacy 960 // core/v1 at /api is tolerated. 961 response: func(w http.ResponseWriter, req *http.Request) { 962 var list interface{} 963 switch req.URL.Path { 964 case "/apis": 965 list = &metav1.APIGroupList{ 966 Groups: []metav1.APIGroup{ 967 { 968 Name: "batch", 969 Versions: []metav1.GroupVersionForDiscovery{ 970 {GroupVersion: "batch/v1", Version: "v1"}, 971 {GroupVersion: "batch/v2alpha1", Version: "v2alpha1"}, 972 {GroupVersion: "batch/v3alpha1", Version: "v3alpha1"}, 973 }, 974 PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: "batch/v1", Version: "v1"}, 975 }, 976 }, 977 } 978 case "/apis/batch/v1": 979 list = &batchv1 980 case "/apis/batch/v2alpha1": 981 list = &batchv2alpha1 982 case "/apis/batch/v3alpha1": 983 list = &batchv3alpha1 984 default: 985 t.Logf("unexpected request: %s", req.URL.Path) 986 w.WriteHeader(http.StatusNotFound) 987 return 988 } 989 output, err := json.Marshal(list) 990 if err != nil { 991 t.Errorf("unexpected encoding error: %v", err) 992 return 993 } 994 w.Header().Set("Content-Type", "application/json") 995 w.WriteHeader(http.StatusOK) 996 w.Write(output) 997 }, 998 expected: map[schema.GroupVersionResource]struct{}{ 999 {Group: "batch", Version: "v1", Resource: "jobs"}: {}, 1000 {Group: "batch", Version: "v2alpha1", Resource: "cronjobs"}: {}, 1001 }, 1002 }, 1003 { 1004 response: func(w http.ResponseWriter, req *http.Request) { 1005 var list interface{} 1006 switch req.URL.Path { 1007 case "/apis": 1008 list = &metav1.APIGroupList{ 1009 Groups: []metav1.APIGroup{ 1010 { 1011 Name: "batch", 1012 Versions: []metav1.GroupVersionForDiscovery{ 1013 {GroupVersion: "batch/v1", Version: "v1"}, 1014 {GroupVersion: "batch/v2alpha1", Version: "v2alpha1"}, 1015 {GroupVersion: "batch/v3alpha1", Version: "v3alpha1"}, 1016 }, 1017 PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: "batch/v2alpha", Version: "v2alpha1"}, 1018 }, 1019 }, 1020 } 1021 case "/apis/batch/v1": 1022 list = &batchv1 1023 case "/apis/batch/v2alpha1": 1024 list = &batchv2alpha1 1025 case "/apis/batch/v3alpha1": 1026 list = &batchv3alpha1 1027 default: 1028 t.Logf("unexpected request: %s", req.URL.Path) 1029 w.WriteHeader(http.StatusNotFound) 1030 return 1031 } 1032 output, err := json.Marshal(list) 1033 if err != nil { 1034 t.Errorf("unexpected encoding error: %v", err) 1035 return 1036 } 1037 w.Header().Set("Content-Type", "application/json") 1038 w.WriteHeader(http.StatusOK) 1039 w.Write(output) 1040 }, 1041 expected: map[schema.GroupVersionResource]struct{}{ 1042 {Group: "batch", Version: "v2alpha1", Resource: "jobs"}: {}, 1043 {Group: "batch", Version: "v2alpha1", Resource: "cronjobs"}: {}, 1044 }, 1045 }, 1046 } 1047 for i, test := range tests { 1048 server := httptest.NewServer(http.HandlerFunc(test.response)) 1049 defer server.Close() 1050 1051 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 1052 resources, err := client.ServerPreferredNamespacedResources() 1053 if err != nil { 1054 t.Errorf("[%d] unexpected error: %v", i, err) 1055 continue 1056 } 1057 got, err := GroupVersionResources(resources) 1058 if err != nil { 1059 t.Errorf("[%d] unexpected error: %v", i, err) 1060 continue 1061 } 1062 1063 if !reflect.DeepEqual(got, test.expected) { 1064 t.Errorf("[%d] expected:\n%v\ngot:\n%v\n", i, test.expected, got) 1065 } 1066 server.Close() 1067 } 1068 } 1069 1070 // Tests of the aggregated discovery format. 1071 func TestAggregatedServerGroups(t *testing.T) { 1072 tests := []struct { 1073 name string 1074 corev1 *apidiscovery.APIGroupDiscoveryList 1075 apis *apidiscovery.APIGroupDiscoveryList 1076 expectedGroupNames []string 1077 expectedGroupVersions []string 1078 expectedPreferredVersions []string 1079 }{ 1080 { 1081 name: "Aggregated discovery: 1 group/1 version at /api, 1 group/1 version at /apis", 1082 corev1: &apidiscovery.APIGroupDiscoveryList{ 1083 Items: []apidiscovery.APIGroupDiscovery{ 1084 { 1085 Versions: []apidiscovery.APIVersionDiscovery{ 1086 { 1087 Version: "v1", 1088 Resources: []apidiscovery.APIResourceDiscovery{ 1089 { 1090 Resource: "pods", 1091 ResponseKind: &metav1.GroupVersionKind{ 1092 Group: "", 1093 Version: "v1", 1094 Kind: "Pod", 1095 }, 1096 Scope: apidiscovery.ScopeNamespace, 1097 }, 1098 }, 1099 }, 1100 }, 1101 }, 1102 }, 1103 }, 1104 apis: &apidiscovery.APIGroupDiscoveryList{ 1105 Items: []apidiscovery.APIGroupDiscovery{ 1106 { 1107 ObjectMeta: metav1.ObjectMeta{ 1108 Name: "apps", 1109 }, 1110 Versions: []apidiscovery.APIVersionDiscovery{ 1111 { 1112 Version: "v1", 1113 Resources: []apidiscovery.APIResourceDiscovery{ 1114 { 1115 Resource: "deployments", 1116 ResponseKind: &metav1.GroupVersionKind{ 1117 Group: "apps", 1118 Version: "v1", 1119 Kind: "Deployment", 1120 }, 1121 Scope: apidiscovery.ScopeNamespace, 1122 }, 1123 }, 1124 }, 1125 }, 1126 }, 1127 }, 1128 }, 1129 expectedGroupNames: []string{"", "apps"}, 1130 expectedGroupVersions: []string{"v1", "apps/v1"}, 1131 expectedPreferredVersions: []string{"v1", "apps/v1"}, 1132 }, 1133 { 1134 name: "Aggregated discovery: 1 group/1 version at /api, 1 group/2 versions at /apis", 1135 corev1: &apidiscovery.APIGroupDiscoveryList{ 1136 Items: []apidiscovery.APIGroupDiscovery{ 1137 { 1138 ObjectMeta: metav1.ObjectMeta{ 1139 Name: "", 1140 }, 1141 Versions: []apidiscovery.APIVersionDiscovery{ 1142 { 1143 Version: "v1", 1144 Resources: []apidiscovery.APIResourceDiscovery{ 1145 { 1146 Resource: "pods", 1147 ResponseKind: &metav1.GroupVersionKind{ 1148 Group: "", 1149 Version: "v1", 1150 Kind: "Pod", 1151 }, 1152 Scope: apidiscovery.ScopeNamespace, 1153 }, 1154 }, 1155 }, 1156 }, 1157 }, 1158 }, 1159 }, 1160 apis: &apidiscovery.APIGroupDiscoveryList{ 1161 Items: []apidiscovery.APIGroupDiscovery{ 1162 { 1163 ObjectMeta: metav1.ObjectMeta{ 1164 Name: "apps", 1165 }, 1166 Versions: []apidiscovery.APIVersionDiscovery{ 1167 // v2 is preferred since it is first 1168 { 1169 Version: "v2", 1170 Resources: []apidiscovery.APIResourceDiscovery{ 1171 { 1172 Resource: "deployments", 1173 ResponseKind: &metav1.GroupVersionKind{ 1174 Group: "apps", 1175 Version: "v2", 1176 Kind: "Deployment", 1177 }, 1178 Scope: apidiscovery.ScopeNamespace, 1179 }, 1180 }, 1181 }, 1182 { 1183 Version: "v1", 1184 Resources: []apidiscovery.APIResourceDiscovery{ 1185 { 1186 Resource: "deployments", 1187 ResponseKind: &metav1.GroupVersionKind{ 1188 Group: "apps", 1189 Version: "v1", 1190 Kind: "Deployment", 1191 }, 1192 Scope: apidiscovery.ScopeNamespace, 1193 }, 1194 }, 1195 }, 1196 }, 1197 }, 1198 }, 1199 }, 1200 expectedGroupNames: []string{"", "apps"}, 1201 expectedGroupVersions: []string{"v1", "apps/v1", "apps/v2"}, 1202 expectedPreferredVersions: []string{"v1", "apps/v2"}, 1203 }, 1204 { 1205 name: "Aggregated discovery: /api returns nothing, 2 groups at /apis", 1206 corev1: &apidiscovery.APIGroupDiscoveryList{}, 1207 apis: &apidiscovery.APIGroupDiscoveryList{ 1208 Items: []apidiscovery.APIGroupDiscovery{ 1209 { 1210 ObjectMeta: metav1.ObjectMeta{ 1211 Name: "apps", 1212 }, 1213 Versions: []apidiscovery.APIVersionDiscovery{ 1214 { 1215 Version: "v1", 1216 Resources: []apidiscovery.APIResourceDiscovery{ 1217 { 1218 Resource: "deployments", 1219 ResponseKind: &metav1.GroupVersionKind{ 1220 Group: "apps", 1221 Version: "v1", 1222 Kind: "Deployment", 1223 }, 1224 Scope: apidiscovery.ScopeNamespace, 1225 }, 1226 { 1227 Resource: "statefulsets", 1228 ResponseKind: &metav1.GroupVersionKind{ 1229 Group: "apps", 1230 Version: "v1", 1231 Kind: "StatefulSet", 1232 }, 1233 Scope: apidiscovery.ScopeNamespace, 1234 }, 1235 }, 1236 }, 1237 }, 1238 }, 1239 { 1240 ObjectMeta: metav1.ObjectMeta{ 1241 Name: "batch", 1242 }, 1243 Versions: []apidiscovery.APIVersionDiscovery{ 1244 // v1 is preferred since it is first 1245 { 1246 Version: "v1", 1247 Resources: []apidiscovery.APIResourceDiscovery{ 1248 { 1249 Resource: "jobs", 1250 ResponseKind: &metav1.GroupVersionKind{ 1251 Group: "batch", 1252 Version: "v1", 1253 Kind: "Job", 1254 }, 1255 Scope: apidiscovery.ScopeNamespace, 1256 }, 1257 { 1258 Resource: "cronjobs", 1259 ResponseKind: &metav1.GroupVersionKind{ 1260 Group: "batch", 1261 Version: "v1", 1262 Kind: "CronJob", 1263 }, 1264 Scope: apidiscovery.ScopeNamespace, 1265 }, 1266 }, 1267 }, 1268 { 1269 Version: "v1beta1", 1270 Resources: []apidiscovery.APIResourceDiscovery{ 1271 { 1272 Resource: "jobs", 1273 ResponseKind: &metav1.GroupVersionKind{ 1274 Group: "batch", 1275 Version: "v1beta1", 1276 Kind: "Job", 1277 }, 1278 Scope: apidiscovery.ScopeNamespace, 1279 }, 1280 { 1281 Resource: "cronjobs", 1282 ResponseKind: &metav1.GroupVersionKind{ 1283 Group: "batch", 1284 Version: "v1beta1", 1285 Kind: "CronJob", 1286 }, 1287 Scope: apidiscovery.ScopeNamespace, 1288 }, 1289 }, 1290 }, 1291 }, 1292 }, 1293 }, 1294 }, 1295 expectedGroupNames: []string{"apps", "batch"}, 1296 expectedGroupVersions: []string{"apps/v1", "batch/v1", "batch/v1beta1"}, 1297 expectedPreferredVersions: []string{"apps/v1", "batch/v1"}, 1298 }, 1299 } 1300 1301 for _, test := range tests { 1302 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 1303 var output []byte 1304 var err error 1305 var agg *apidiscovery.APIGroupDiscoveryList 1306 switch req.URL.Path { 1307 case "/api": 1308 agg = test.corev1 1309 case "/apis": 1310 agg = test.apis 1311 default: 1312 w.WriteHeader(http.StatusNotFound) 1313 return 1314 } 1315 output, err = json.Marshal(agg) 1316 require.NoError(t, err) 1317 // Content-Type is "aggregated" discovery format. Add extra parameter 1318 // to ensure we are resilient to these extra parameters. 1319 w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") 1320 w.WriteHeader(http.StatusOK) 1321 _, err = w.Write(output) 1322 require.NoError(t, err) 1323 })) 1324 defer server.Close() 1325 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 1326 apiGroupList, err := client.ServerGroups() 1327 require.NoError(t, err) 1328 // Test the expected groups are returned for the aggregated format. 1329 expectedGroupNames := sets.NewString(test.expectedGroupNames...) 1330 actualGroupNames := sets.NewString(groupNamesFromList(apiGroupList)...) 1331 assert.True(t, expectedGroupNames.Equal(actualGroupNames), 1332 "%s: Expected groups (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List()) 1333 // Test the expected group versions for the aggregated discovery is correct. 1334 expectedGroupVersions := sets.NewString(test.expectedGroupVersions...) 1335 actualGroupVersions := sets.NewString(groupVersionsFromGroups(apiGroupList)...) 1336 assert.True(t, expectedGroupVersions.Equal(actualGroupVersions), 1337 "%s: Expected group/versions (%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List()) 1338 // Test the groups preferred version is correct. 1339 expectedPreferredVersions := sets.NewString(test.expectedPreferredVersions...) 1340 actualPreferredVersions := sets.NewString(preferredVersionsFromList(apiGroupList)...) 1341 assert.True(t, expectedPreferredVersions.Equal(actualPreferredVersions), 1342 "%s: Expected preferred group/version (%s), got (%s)", test.name, expectedPreferredVersions.List(), actualPreferredVersions.List()) 1343 } 1344 } 1345 1346 func TestAggregatedServerGroupsAndResources(t *testing.T) { 1347 tests := []struct { 1348 name string 1349 corev1 *apidiscovery.APIGroupDiscoveryList 1350 corev1DiscoveryBeta *apidiscoveryv2beta1.APIGroupDiscoveryList 1351 apis *apidiscovery.APIGroupDiscoveryList 1352 apisDiscoveryBeta *apidiscoveryv2beta1.APIGroupDiscoveryList 1353 expectedGroupNames []string 1354 expectedGroupVersions []string 1355 expectedGVKs []string 1356 expectedFailedGVs []string 1357 }{ 1358 { 1359 name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/1 resources at /apis", 1360 corev1: &apidiscovery.APIGroupDiscoveryList{ 1361 Items: []apidiscovery.APIGroupDiscovery{ 1362 { 1363 Versions: []apidiscovery.APIVersionDiscovery{ 1364 { 1365 Version: "v1", 1366 Resources: []apidiscovery.APIResourceDiscovery{ 1367 { 1368 Resource: "pods", 1369 ResponseKind: &metav1.GroupVersionKind{ 1370 Group: "", 1371 Version: "v1", 1372 Kind: "Pod", 1373 }, 1374 Scope: apidiscovery.ScopeNamespace, 1375 }, 1376 }, 1377 }, 1378 }, 1379 }, 1380 }, 1381 }, 1382 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1383 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1384 { 1385 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1386 { 1387 Version: "v1", 1388 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1389 { 1390 Resource: "pods", 1391 ResponseKind: &metav1.GroupVersionKind{ 1392 Group: "", 1393 Version: "v1", 1394 Kind: "Pod", 1395 }, 1396 Scope: apidiscoveryv2beta1.ScopeNamespace, 1397 }, 1398 }, 1399 }, 1400 }, 1401 }, 1402 }, 1403 }, 1404 apis: &apidiscovery.APIGroupDiscoveryList{ 1405 Items: []apidiscovery.APIGroupDiscovery{ 1406 { 1407 ObjectMeta: metav1.ObjectMeta{ 1408 Name: "apps", 1409 }, 1410 Versions: []apidiscovery.APIVersionDiscovery{ 1411 { 1412 Version: "v1", 1413 Resources: []apidiscovery.APIResourceDiscovery{ 1414 { 1415 Resource: "deployments", 1416 ResponseKind: &metav1.GroupVersionKind{ 1417 Group: "apps", 1418 Version: "v1", 1419 Kind: "Deployment", 1420 }, 1421 Scope: apidiscovery.ScopeNamespace, 1422 }, 1423 }, 1424 }, 1425 }, 1426 }, 1427 }, 1428 }, 1429 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1430 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1431 { 1432 ObjectMeta: metav1.ObjectMeta{ 1433 Name: "apps", 1434 }, 1435 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1436 { 1437 Version: "v1", 1438 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1439 { 1440 Resource: "deployments", 1441 ResponseKind: &metav1.GroupVersionKind{ 1442 Group: "apps", 1443 Version: "v1", 1444 Kind: "Deployment", 1445 }, 1446 Scope: apidiscoveryv2beta1.ScopeNamespace, 1447 }, 1448 }, 1449 }, 1450 }, 1451 }, 1452 }, 1453 }, 1454 expectedGroupNames: []string{"", "apps"}, 1455 expectedGroupVersions: []string{"v1", "apps/v1"}, 1456 expectedGVKs: []string{ 1457 "/v1/Pod", 1458 "apps/v1/Deployment", 1459 }, 1460 }, 1461 { 1462 name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/2 versions/1 resources at /apis", 1463 corev1: &apidiscovery.APIGroupDiscoveryList{ 1464 Items: []apidiscovery.APIGroupDiscovery{ 1465 { 1466 Versions: []apidiscovery.APIVersionDiscovery{ 1467 { 1468 Version: "v1", 1469 Resources: []apidiscovery.APIResourceDiscovery{ 1470 { 1471 Resource: "pods", 1472 ResponseKind: &metav1.GroupVersionKind{ 1473 Group: "", 1474 Version: "v1", 1475 Kind: "Pod", 1476 }, 1477 Scope: apidiscovery.ScopeNamespace, 1478 }, 1479 }, 1480 }, 1481 }, 1482 }, 1483 }, 1484 }, 1485 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1486 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1487 { 1488 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1489 { 1490 Version: "v1", 1491 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1492 { 1493 Resource: "pods", 1494 ResponseKind: &metav1.GroupVersionKind{ 1495 Group: "", 1496 Version: "v1", 1497 Kind: "Pod", 1498 }, 1499 Scope: apidiscoveryv2beta1.ScopeNamespace, 1500 }, 1501 }, 1502 }, 1503 }, 1504 }, 1505 }, 1506 }, 1507 apis: &apidiscovery.APIGroupDiscoveryList{ 1508 Items: []apidiscovery.APIGroupDiscovery{ 1509 { 1510 ObjectMeta: metav1.ObjectMeta{ 1511 Name: "apps", 1512 }, 1513 Versions: []apidiscovery.APIVersionDiscovery{ 1514 { 1515 Version: "v1", 1516 Resources: []apidiscovery.APIResourceDiscovery{ 1517 { 1518 Resource: "deployments", 1519 ResponseKind: &metav1.GroupVersionKind{ 1520 Group: "apps", 1521 Version: "v1", 1522 Kind: "Deployment", 1523 }, 1524 Scope: apidiscovery.ScopeNamespace, 1525 }, 1526 }, 1527 }, 1528 { 1529 Version: "v2", 1530 Resources: []apidiscovery.APIResourceDiscovery{ 1531 { 1532 Resource: "deployments", 1533 ResponseKind: &metav1.GroupVersionKind{ 1534 Group: "apps", 1535 Version: "v2", 1536 Kind: "Deployment", 1537 }, 1538 Scope: apidiscovery.ScopeNamespace, 1539 }, 1540 }, 1541 }, 1542 }, 1543 }, 1544 }, 1545 }, 1546 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1547 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1548 { 1549 ObjectMeta: metav1.ObjectMeta{ 1550 Name: "apps", 1551 }, 1552 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1553 { 1554 Version: "v1", 1555 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1556 { 1557 Resource: "deployments", 1558 ResponseKind: &metav1.GroupVersionKind{ 1559 Group: "apps", 1560 Version: "v1", 1561 Kind: "Deployment", 1562 }, 1563 Scope: apidiscoveryv2beta1.ScopeNamespace, 1564 }, 1565 }, 1566 }, 1567 { 1568 Version: "v2", 1569 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1570 { 1571 Resource: "deployments", 1572 ResponseKind: &metav1.GroupVersionKind{ 1573 Group: "apps", 1574 Version: "v2", 1575 Kind: "Deployment", 1576 }, 1577 Scope: apidiscoveryv2beta1.ScopeNamespace, 1578 }, 1579 }, 1580 }, 1581 }, 1582 }, 1583 }, 1584 }, 1585 expectedGroupNames: []string{"", "apps"}, 1586 expectedGroupVersions: []string{"v1", "apps/v1", "apps/v2"}, 1587 expectedGVKs: []string{ 1588 "/v1/Pod", 1589 "apps/v1/Deployment", 1590 "apps/v2/Deployment", 1591 }, 1592 }, 1593 { 1594 name: "Aggregated discovery: 1 group/1 resources at /api, 1 group/2 versions/1 resources at /apis", 1595 corev1: &apidiscovery.APIGroupDiscoveryList{ 1596 Items: []apidiscovery.APIGroupDiscovery{ 1597 { 1598 Versions: []apidiscovery.APIVersionDiscovery{ 1599 { 1600 Version: "v1", 1601 Resources: []apidiscovery.APIResourceDiscovery{ 1602 { 1603 Resource: "pods", 1604 ResponseKind: &metav1.GroupVersionKind{ 1605 Group: "", 1606 Version: "v1", 1607 Kind: "Pod", 1608 }, 1609 Scope: apidiscovery.ScopeNamespace, 1610 }, 1611 }, 1612 }, 1613 }, 1614 }, 1615 }, 1616 }, 1617 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1618 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1619 { 1620 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1621 { 1622 Version: "v1", 1623 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1624 { 1625 Resource: "pods", 1626 ResponseKind: &metav1.GroupVersionKind{ 1627 Group: "", 1628 Version: "v1", 1629 Kind: "Pod", 1630 }, 1631 Scope: apidiscoveryv2beta1.ScopeNamespace, 1632 }, 1633 }, 1634 }, 1635 }, 1636 }, 1637 }, 1638 }, 1639 apis: &apidiscovery.APIGroupDiscoveryList{ 1640 Items: []apidiscovery.APIGroupDiscovery{ 1641 { 1642 ObjectMeta: metav1.ObjectMeta{ 1643 Name: "apps", 1644 }, 1645 Versions: []apidiscovery.APIVersionDiscovery{ 1646 { 1647 Version: "v1", 1648 Resources: []apidiscovery.APIResourceDiscovery{ 1649 { 1650 Resource: "deployments", 1651 ResponseKind: &metav1.GroupVersionKind{ 1652 Group: "apps", 1653 Version: "v1", 1654 Kind: "Deployment", 1655 }, 1656 Scope: apidiscovery.ScopeNamespace, 1657 }, 1658 }, 1659 }, 1660 { 1661 Version: "v2", 1662 Resources: []apidiscovery.APIResourceDiscovery{ 1663 { 1664 Resource: "deployments", 1665 ResponseKind: &metav1.GroupVersionKind{ 1666 Group: "apps", 1667 Version: "v2", 1668 Kind: "Deployment", 1669 }, 1670 Scope: apidiscovery.ScopeNamespace, 1671 }, 1672 }, 1673 Freshness: apidiscovery.DiscoveryFreshnessStale, 1674 }, 1675 }, 1676 }, 1677 }, 1678 }, 1679 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1680 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1681 { 1682 ObjectMeta: metav1.ObjectMeta{ 1683 Name: "apps", 1684 }, 1685 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1686 { 1687 Version: "v1", 1688 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1689 { 1690 Resource: "deployments", 1691 ResponseKind: &metav1.GroupVersionKind{ 1692 Group: "apps", 1693 Version: "v1", 1694 Kind: "Deployment", 1695 }, 1696 Scope: apidiscoveryv2beta1.ScopeNamespace, 1697 }, 1698 }, 1699 }, 1700 { 1701 Version: "v2", 1702 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1703 { 1704 Resource: "deployments", 1705 ResponseKind: &metav1.GroupVersionKind{ 1706 Group: "apps", 1707 Version: "v2", 1708 Kind: "Deployment", 1709 }, 1710 Scope: apidiscoveryv2beta1.ScopeNamespace, 1711 }, 1712 }, 1713 Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, 1714 }, 1715 }, 1716 }, 1717 }, 1718 }, 1719 expectedGroupNames: []string{"", "apps"}, 1720 expectedGroupVersions: []string{"v1", "apps/v1"}, 1721 expectedGVKs: []string{ 1722 "/v1/Pod", 1723 "apps/v1/Deployment", 1724 }, 1725 expectedFailedGVs: []string{"apps/v2"}, 1726 }, 1727 { 1728 name: "Aggregated discovery: 1 group/2 resources at /api, 1 group/2 resources at /apis", 1729 corev1: &apidiscovery.APIGroupDiscoveryList{ 1730 Items: []apidiscovery.APIGroupDiscovery{ 1731 { 1732 Versions: []apidiscovery.APIVersionDiscovery{ 1733 { 1734 Version: "v1", 1735 Resources: []apidiscovery.APIResourceDiscovery{ 1736 { 1737 Resource: "pods", 1738 ResponseKind: &metav1.GroupVersionKind{ 1739 Group: "", 1740 Version: "v1", 1741 Kind: "Pod", 1742 }, 1743 Scope: apidiscovery.ScopeNamespace, 1744 }, 1745 { 1746 Resource: "services", 1747 ResponseKind: &metav1.GroupVersionKind{ 1748 Group: "", 1749 Version: "v1", 1750 Kind: "Service", 1751 }, 1752 Scope: apidiscovery.ScopeNamespace, 1753 }, 1754 }, 1755 }, 1756 }, 1757 }, 1758 }, 1759 }, 1760 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1761 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1762 { 1763 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1764 { 1765 Version: "v1", 1766 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1767 { 1768 Resource: "pods", 1769 ResponseKind: &metav1.GroupVersionKind{ 1770 Group: "", 1771 Version: "v1", 1772 Kind: "Pod", 1773 }, 1774 Scope: apidiscoveryv2beta1.ScopeNamespace, 1775 }, 1776 { 1777 Resource: "services", 1778 ResponseKind: &metav1.GroupVersionKind{ 1779 Group: "", 1780 Version: "v1", 1781 Kind: "Service", 1782 }, 1783 Scope: apidiscoveryv2beta1.ScopeNamespace, 1784 }, 1785 }, 1786 }, 1787 }, 1788 }, 1789 }, 1790 }, 1791 apis: &apidiscovery.APIGroupDiscoveryList{ 1792 Items: []apidiscovery.APIGroupDiscovery{ 1793 { 1794 ObjectMeta: metav1.ObjectMeta{ 1795 Name: "apps", 1796 }, 1797 Versions: []apidiscovery.APIVersionDiscovery{ 1798 // Stale "v2" version not included. 1799 { 1800 Version: "v2", 1801 Resources: []apidiscovery.APIResourceDiscovery{ 1802 { 1803 Resource: "deployments", 1804 ResponseKind: &metav1.GroupVersionKind{ 1805 Group: "apps", 1806 Version: "v2", 1807 Kind: "Deployment", 1808 }, 1809 Scope: apidiscovery.ScopeNamespace, 1810 }, 1811 { 1812 Resource: "statefulsets", 1813 ResponseKind: &metav1.GroupVersionKind{ 1814 Group: "apps", 1815 Version: "v2", 1816 Kind: "StatefulSet", 1817 }, 1818 Scope: apidiscovery.ScopeNamespace, 1819 }, 1820 }, 1821 Freshness: apidiscovery.DiscoveryFreshnessStale, 1822 }, 1823 { 1824 Version: "v1", 1825 Resources: []apidiscovery.APIResourceDiscovery{ 1826 { 1827 Resource: "deployments", 1828 ResponseKind: &metav1.GroupVersionKind{ 1829 Group: "apps", 1830 Version: "v1", 1831 Kind: "Deployment", 1832 }, 1833 Scope: apidiscovery.ScopeNamespace, 1834 }, 1835 { 1836 Resource: "statefulsets", 1837 ResponseKind: &metav1.GroupVersionKind{ 1838 Group: "apps", 1839 Version: "v1", 1840 Kind: "StatefulSet", 1841 }, 1842 Scope: apidiscovery.ScopeNamespace, 1843 }, 1844 }, 1845 }, 1846 }, 1847 }, 1848 }, 1849 }, 1850 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1851 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1852 { 1853 ObjectMeta: metav1.ObjectMeta{ 1854 Name: "apps", 1855 }, 1856 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1857 // Stale "v2" version not included. 1858 { 1859 Version: "v2", 1860 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1861 { 1862 Resource: "deployments", 1863 ResponseKind: &metav1.GroupVersionKind{ 1864 Group: "apps", 1865 Version: "v2", 1866 Kind: "Deployment", 1867 }, 1868 Scope: apidiscoveryv2beta1.ScopeNamespace, 1869 }, 1870 { 1871 Resource: "statefulsets", 1872 ResponseKind: &metav1.GroupVersionKind{ 1873 Group: "apps", 1874 Version: "v2", 1875 Kind: "StatefulSet", 1876 }, 1877 Scope: apidiscoveryv2beta1.ScopeNamespace, 1878 }, 1879 }, 1880 Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, 1881 }, 1882 { 1883 Version: "v1", 1884 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1885 { 1886 Resource: "deployments", 1887 ResponseKind: &metav1.GroupVersionKind{ 1888 Group: "apps", 1889 Version: "v1", 1890 Kind: "Deployment", 1891 }, 1892 Scope: apidiscoveryv2beta1.ScopeNamespace, 1893 }, 1894 { 1895 Resource: "statefulsets", 1896 ResponseKind: &metav1.GroupVersionKind{ 1897 Group: "apps", 1898 Version: "v1", 1899 Kind: "StatefulSet", 1900 }, 1901 Scope: apidiscoveryv2beta1.ScopeNamespace, 1902 }, 1903 }, 1904 }, 1905 }, 1906 }, 1907 }, 1908 }, 1909 expectedGroupNames: []string{"", "apps"}, 1910 expectedGroupVersions: []string{"v1", "apps/v1"}, 1911 expectedGVKs: []string{ 1912 "/v1/Pod", 1913 "/v1/Service", 1914 "apps/v1/Deployment", 1915 "apps/v1/StatefulSet", 1916 }, 1917 expectedFailedGVs: []string{"apps/v2"}, 1918 }, 1919 { 1920 name: "Aggregated discovery: 1 group/2 resources at /api, 2 group/2 resources/1 stale GV at /apis", 1921 corev1: &apidiscovery.APIGroupDiscoveryList{ 1922 Items: []apidiscovery.APIGroupDiscovery{ 1923 { 1924 Versions: []apidiscovery.APIVersionDiscovery{ 1925 { 1926 Version: "v1", 1927 Resources: []apidiscovery.APIResourceDiscovery{ 1928 { 1929 Resource: "pods", 1930 ResponseKind: &metav1.GroupVersionKind{ 1931 Group: "", 1932 Version: "v1", 1933 Kind: "Pod", 1934 }, 1935 Scope: apidiscovery.ScopeNamespace, 1936 }, 1937 { 1938 Resource: "services", 1939 ResponseKind: &metav1.GroupVersionKind{ 1940 Group: "", 1941 Version: "v1", 1942 Kind: "Service", 1943 }, 1944 Scope: apidiscovery.ScopeNamespace, 1945 }, 1946 }, 1947 }, 1948 }, 1949 }, 1950 }, 1951 }, 1952 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 1953 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 1954 { 1955 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 1956 { 1957 Version: "v1", 1958 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 1959 { 1960 Resource: "pods", 1961 ResponseKind: &metav1.GroupVersionKind{ 1962 Group: "", 1963 Version: "v1", 1964 Kind: "Pod", 1965 }, 1966 Scope: apidiscoveryv2beta1.ScopeNamespace, 1967 }, 1968 { 1969 Resource: "services", 1970 ResponseKind: &metav1.GroupVersionKind{ 1971 Group: "", 1972 Version: "v1", 1973 Kind: "Service", 1974 }, 1975 Scope: apidiscoveryv2beta1.ScopeNamespace, 1976 }, 1977 }, 1978 }, 1979 }, 1980 }, 1981 }, 1982 }, 1983 apis: &apidiscovery.APIGroupDiscoveryList{ 1984 Items: []apidiscovery.APIGroupDiscovery{ 1985 { 1986 ObjectMeta: metav1.ObjectMeta{ 1987 Name: "apps", 1988 }, 1989 Versions: []apidiscovery.APIVersionDiscovery{ 1990 { 1991 Version: "v1", 1992 Resources: []apidiscovery.APIResourceDiscovery{ 1993 { 1994 Resource: "deployments", 1995 ResponseKind: &metav1.GroupVersionKind{ 1996 Group: "apps", 1997 Version: "v1", 1998 Kind: "Deployment", 1999 }, 2000 Scope: apidiscovery.ScopeNamespace, 2001 }, 2002 { 2003 Resource: "statefulsets", 2004 ResponseKind: &metav1.GroupVersionKind{ 2005 Group: "apps", 2006 Version: "v1", 2007 Kind: "StatefulSet", 2008 }, 2009 Scope: apidiscovery.ScopeNamespace, 2010 }, 2011 }, 2012 }, 2013 }, 2014 }, 2015 { 2016 ObjectMeta: metav1.ObjectMeta{ 2017 Name: "batch", 2018 }, 2019 Versions: []apidiscovery.APIVersionDiscovery{ 2020 // Stale Group/Version is not included 2021 { 2022 Version: "v1", 2023 Resources: []apidiscovery.APIResourceDiscovery{ 2024 { 2025 Resource: "jobs", 2026 ResponseKind: &metav1.GroupVersionKind{ 2027 Group: "batch", 2028 Version: "v1", 2029 Kind: "Job", 2030 }, 2031 Scope: apidiscovery.ScopeNamespace, 2032 }, 2033 { 2034 Resource: "cronjobs", 2035 ResponseKind: &metav1.GroupVersionKind{ 2036 Group: "batch", 2037 Version: "v1", 2038 Kind: "CronJob", 2039 }, 2040 Scope: apidiscovery.ScopeNamespace, 2041 }, 2042 }, 2043 Freshness: apidiscovery.DiscoveryFreshnessStale, 2044 }, 2045 { 2046 Version: "v1beta1", 2047 Resources: []apidiscovery.APIResourceDiscovery{ 2048 { 2049 Resource: "jobs", 2050 ResponseKind: &metav1.GroupVersionKind{ 2051 Group: "batch", 2052 Version: "v1beta1", 2053 Kind: "Job", 2054 }, 2055 Scope: apidiscovery.ScopeNamespace, 2056 }, 2057 { 2058 Resource: "cronjobs", 2059 ResponseKind: &metav1.GroupVersionKind{ 2060 Group: "batch", 2061 Version: "v1beta1", 2062 Kind: "CronJob", 2063 }, 2064 Scope: apidiscovery.ScopeNamespace, 2065 }, 2066 }, 2067 }, 2068 }, 2069 }, 2070 }, 2071 }, 2072 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 2073 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 2074 { 2075 ObjectMeta: metav1.ObjectMeta{ 2076 Name: "apps", 2077 }, 2078 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 2079 { 2080 Version: "v1", 2081 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2082 { 2083 Resource: "deployments", 2084 ResponseKind: &metav1.GroupVersionKind{ 2085 Group: "apps", 2086 Version: "v1", 2087 Kind: "Deployment", 2088 }, 2089 Scope: apidiscoveryv2beta1.ScopeNamespace, 2090 }, 2091 { 2092 Resource: "statefulsets", 2093 ResponseKind: &metav1.GroupVersionKind{ 2094 Group: "apps", 2095 Version: "v1", 2096 Kind: "StatefulSet", 2097 }, 2098 Scope: apidiscoveryv2beta1.ScopeNamespace, 2099 }, 2100 }, 2101 }, 2102 }, 2103 }, 2104 { 2105 ObjectMeta: metav1.ObjectMeta{ 2106 Name: "batch", 2107 }, 2108 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 2109 // Stale Group/Version is not included 2110 { 2111 Version: "v1", 2112 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2113 { 2114 Resource: "jobs", 2115 ResponseKind: &metav1.GroupVersionKind{ 2116 Group: "batch", 2117 Version: "v1", 2118 Kind: "Job", 2119 }, 2120 Scope: apidiscoveryv2beta1.ScopeNamespace, 2121 }, 2122 { 2123 Resource: "cronjobs", 2124 ResponseKind: &metav1.GroupVersionKind{ 2125 Group: "batch", 2126 Version: "v1", 2127 Kind: "CronJob", 2128 }, 2129 Scope: apidiscoveryv2beta1.ScopeNamespace, 2130 }, 2131 }, 2132 Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, 2133 }, 2134 { 2135 Version: "v1beta1", 2136 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2137 { 2138 Resource: "jobs", 2139 ResponseKind: &metav1.GroupVersionKind{ 2140 Group: "batch", 2141 Version: "v1beta1", 2142 Kind: "Job", 2143 }, 2144 Scope: apidiscoveryv2beta1.ScopeNamespace, 2145 }, 2146 { 2147 Resource: "cronjobs", 2148 ResponseKind: &metav1.GroupVersionKind{ 2149 Group: "batch", 2150 Version: "v1beta1", 2151 Kind: "CronJob", 2152 }, 2153 Scope: apidiscoveryv2beta1.ScopeNamespace, 2154 }, 2155 }, 2156 }, 2157 }, 2158 }, 2159 }, 2160 }, 2161 expectedGroupNames: []string{"", "apps", "batch"}, 2162 expectedGroupVersions: []string{"v1", "apps/v1", "batch/v1beta1"}, 2163 expectedGVKs: []string{ 2164 "/v1/Pod", 2165 "/v1/Service", 2166 "apps/v1/Deployment", 2167 "apps/v1/StatefulSet", 2168 "batch/v1beta1/Job", 2169 "batch/v1beta1/CronJob", 2170 }, 2171 expectedFailedGVs: []string{"batch/v1"}, 2172 }, 2173 { 2174 name: "Aggregated discovery: /api returns nothing, 2 groups/2 resources at /apis", 2175 corev1: &apidiscovery.APIGroupDiscoveryList{}, 2176 corev1DiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{}, 2177 apis: &apidiscovery.APIGroupDiscoveryList{ 2178 Items: []apidiscovery.APIGroupDiscovery{ 2179 { 2180 ObjectMeta: metav1.ObjectMeta{ 2181 Name: "apps", 2182 }, 2183 Versions: []apidiscovery.APIVersionDiscovery{ 2184 { 2185 Version: "v1", 2186 Resources: []apidiscovery.APIResourceDiscovery{ 2187 { 2188 Resource: "deployments", 2189 ResponseKind: &metav1.GroupVersionKind{ 2190 Group: "apps", 2191 Version: "v1", 2192 Kind: "Deployment", 2193 }, 2194 Scope: apidiscovery.ScopeNamespace, 2195 }, 2196 { 2197 Resource: "statefulsets", 2198 ResponseKind: &metav1.GroupVersionKind{ 2199 Group: "apps", 2200 Version: "v1", 2201 Kind: "StatefulSet", 2202 }, 2203 Scope: apidiscovery.ScopeNamespace, 2204 }, 2205 }, 2206 }, 2207 }, 2208 }, 2209 { 2210 ObjectMeta: metav1.ObjectMeta{ 2211 Name: "batch", 2212 }, 2213 Versions: []apidiscovery.APIVersionDiscovery{ 2214 { 2215 Version: "v1", 2216 Resources: []apidiscovery.APIResourceDiscovery{ 2217 { 2218 Resource: "jobs", 2219 ResponseKind: &metav1.GroupVersionKind{ 2220 Group: "batch", 2221 Version: "v1", 2222 Kind: "Job", 2223 }, 2224 Scope: apidiscovery.ScopeNamespace, 2225 }, 2226 { 2227 Resource: "cronjobs", 2228 ResponseKind: &metav1.GroupVersionKind{ 2229 Group: "batch", 2230 Version: "v1", 2231 Kind: "CronJob", 2232 }, 2233 Scope: apidiscovery.ScopeNamespace, 2234 }, 2235 }, 2236 }, 2237 { 2238 // Stale "v1beta1" not included. 2239 Version: "v1beta1", 2240 Resources: []apidiscovery.APIResourceDiscovery{ 2241 { 2242 Resource: "jobs", 2243 ResponseKind: &metav1.GroupVersionKind{ 2244 Group: "batch", 2245 Version: "v1beta1", 2246 Kind: "Job", 2247 }, 2248 Scope: apidiscovery.ScopeNamespace, 2249 }, 2250 { 2251 Resource: "cronjobs", 2252 ResponseKind: &metav1.GroupVersionKind{ 2253 Group: "batch", 2254 Version: "v1beta1", 2255 Kind: "CronJob", 2256 }, 2257 Scope: apidiscovery.ScopeNamespace, 2258 }, 2259 }, 2260 Freshness: apidiscovery.DiscoveryFreshnessStale, 2261 }, 2262 }, 2263 }, 2264 }, 2265 }, 2266 apisDiscoveryBeta: &apidiscoveryv2beta1.APIGroupDiscoveryList{ 2267 Items: []apidiscoveryv2beta1.APIGroupDiscovery{ 2268 { 2269 ObjectMeta: metav1.ObjectMeta{ 2270 Name: "apps", 2271 }, 2272 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 2273 { 2274 Version: "v1", 2275 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2276 { 2277 Resource: "deployments", 2278 ResponseKind: &metav1.GroupVersionKind{ 2279 Group: "apps", 2280 Version: "v1", 2281 Kind: "Deployment", 2282 }, 2283 Scope: apidiscoveryv2beta1.ScopeNamespace, 2284 }, 2285 { 2286 Resource: "statefulsets", 2287 ResponseKind: &metav1.GroupVersionKind{ 2288 Group: "apps", 2289 Version: "v1", 2290 Kind: "StatefulSet", 2291 }, 2292 Scope: apidiscoveryv2beta1.ScopeNamespace, 2293 }, 2294 }, 2295 }, 2296 }, 2297 }, 2298 { 2299 ObjectMeta: metav1.ObjectMeta{ 2300 Name: "batch", 2301 }, 2302 Versions: []apidiscoveryv2beta1.APIVersionDiscovery{ 2303 { 2304 Version: "v1", 2305 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2306 { 2307 Resource: "jobs", 2308 ResponseKind: &metav1.GroupVersionKind{ 2309 Group: "batch", 2310 Version: "v1", 2311 Kind: "Job", 2312 }, 2313 Scope: apidiscoveryv2beta1.ScopeNamespace, 2314 }, 2315 { 2316 Resource: "cronjobs", 2317 ResponseKind: &metav1.GroupVersionKind{ 2318 Group: "batch", 2319 Version: "v1", 2320 Kind: "CronJob", 2321 }, 2322 Scope: apidiscoveryv2beta1.ScopeNamespace, 2323 }, 2324 }, 2325 }, 2326 { 2327 // Stale "v1beta1" not included. 2328 Version: "v1beta1", 2329 Resources: []apidiscoveryv2beta1.APIResourceDiscovery{ 2330 { 2331 Resource: "jobs", 2332 ResponseKind: &metav1.GroupVersionKind{ 2333 Group: "batch", 2334 Version: "v1beta1", 2335 Kind: "Job", 2336 }, 2337 Scope: apidiscoveryv2beta1.ScopeNamespace, 2338 }, 2339 { 2340 Resource: "cronjobs", 2341 ResponseKind: &metav1.GroupVersionKind{ 2342 Group: "batch", 2343 Version: "v1beta1", 2344 Kind: "CronJob", 2345 }, 2346 Scope: apidiscoveryv2beta1.ScopeNamespace, 2347 }, 2348 }, 2349 Freshness: apidiscoveryv2beta1.DiscoveryFreshnessStale, 2350 }, 2351 }, 2352 }, 2353 }, 2354 }, 2355 expectedGroupNames: []string{"apps", "batch"}, 2356 expectedGroupVersions: []string{"apps/v1", "batch/v1"}, 2357 expectedGVKs: []string{ 2358 "apps/v1/Deployment", 2359 "apps/v1/StatefulSet", 2360 "batch/v1/Job", 2361 "batch/v1/CronJob", 2362 }, 2363 expectedFailedGVs: []string{"batch/v1beta1"}, 2364 }, 2365 } 2366 2367 // Ensure that client can parse both V2Beta1 and V2 types from server 2368 serverAccepts := []string{AcceptV2Beta1, AcceptV2} 2369 for _, test := range tests { 2370 for _, accept := range serverAccepts { 2371 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 2372 var output []byte 2373 var err error 2374 if accept == AcceptV2 { 2375 var agg *apidiscovery.APIGroupDiscoveryList 2376 switch req.URL.Path { 2377 case "/api": 2378 agg = test.corev1 2379 case "/apis": 2380 agg = test.apis 2381 default: 2382 w.WriteHeader(http.StatusNotFound) 2383 return 2384 } 2385 output, err = json.Marshal(agg) 2386 require.NoError(t, err) 2387 } else { 2388 var agg *apidiscoveryv2beta1.APIGroupDiscoveryList 2389 switch req.URL.Path { 2390 case "/api": 2391 agg = test.corev1DiscoveryBeta 2392 case "/apis": 2393 agg = test.apisDiscoveryBeta 2394 default: 2395 w.WriteHeader(http.StatusNotFound) 2396 return 2397 } 2398 output, err = json.Marshal(&agg) 2399 require.NoError(t, err) 2400 } 2401 // Content-Type is "aggregated" discovery format. Add extra parameter 2402 // to ensure we are resilient to these extra parameters. 2403 w.Header().Set("Content-Type", accept+"; charset=utf-8") 2404 w.WriteHeader(http.StatusOK) 2405 _, err = w.Write(output) 2406 require.NoError(t, err) 2407 2408 })) 2409 defer server.Close() 2410 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 2411 apiGroups, resources, err := client.ServerGroupsAndResources() 2412 if len(test.expectedFailedGVs) > 0 { 2413 require.Error(t, err) 2414 expectedFailedGVs := sets.NewString(test.expectedFailedGVs...) 2415 actualFailedGVs := sets.NewString(failedGroupVersions(err)...) 2416 assert.True(t, expectedFailedGVs.Equal(actualFailedGVs), 2417 "%s: Expected Failed GVs (%s), got (%s)", test.name, expectedFailedGVs, actualFailedGVs) 2418 } else { 2419 require.NoError(t, err) 2420 } 2421 // Test the expected groups are returned for the aggregated format. 2422 expectedGroupNames := sets.NewString(test.expectedGroupNames...) 2423 actualGroupNames := sets.NewString(groupNames(apiGroups)...) 2424 assert.True(t, expectedGroupNames.Equal(actualGroupNames), 2425 "%s: Expected GVKs (%s), got (%s)", test.name, expectedGroupNames.List(), actualGroupNames.List()) 2426 // If the core V1 group is returned from /api, it should be the first group. 2427 if expectedGroupNames.Has("") { 2428 assert.True(t, len(apiGroups) > 0) 2429 actualFirstGroup := apiGroups[0] 2430 assert.True(t, len(actualFirstGroup.Versions) > 0) 2431 actualFirstGroupVersion := actualFirstGroup.Versions[0].GroupVersion 2432 assert.Equal(t, "v1", actualFirstGroupVersion) 2433 } 2434 // Test the expected group/versions are returned from the aggregated discovery. 2435 expectedGroupVersions := sets.NewString(test.expectedGroupVersions...) 2436 actualGroupVersions := sets.NewString(groupVersions(resources)...) 2437 assert.True(t, expectedGroupVersions.Equal(actualGroupVersions), 2438 "%s: Expected GroupVersions(%s), got (%s)", test.name, expectedGroupVersions.List(), actualGroupVersions.List()) 2439 // Test the expected GVKs are returned from the aggregated discovery. 2440 expectedGVKs := sets.NewString(test.expectedGVKs...) 2441 actualGVKs := sets.NewString(groupVersionKinds(resources)...) 2442 assert.True(t, expectedGVKs.Equal(actualGVKs), 2443 "%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List()) 2444 } 2445 } 2446 } 2447 2448 func TestAggregatedServerGroupsAndResourcesWithErrors(t *testing.T) { 2449 tests := []struct { 2450 name string 2451 corev1 *apidiscovery.APIGroupDiscoveryList 2452 coreHttpStatus int 2453 apis *apidiscovery.APIGroupDiscoveryList 2454 apisHttpStatus int 2455 expectedGroups []string 2456 expectedResources []string 2457 expectedErr bool 2458 }{ 2459 { 2460 name: "Aggregated Discovery: 404 for core/v1 is tolerated", 2461 corev1: &apidiscovery.APIGroupDiscoveryList{}, 2462 coreHttpStatus: http.StatusNotFound, 2463 apis: &apidiscovery.APIGroupDiscoveryList{ 2464 Items: []apidiscovery.APIGroupDiscovery{ 2465 { 2466 ObjectMeta: metav1.ObjectMeta{ 2467 Name: "apps", 2468 }, 2469 Versions: []apidiscovery.APIVersionDiscovery{ 2470 { 2471 Version: "v1", 2472 Resources: []apidiscovery.APIResourceDiscovery{ 2473 { 2474 Resource: "deployments", 2475 ResponseKind: &metav1.GroupVersionKind{ 2476 Group: "apps", 2477 Version: "v1", 2478 Kind: "Deployment", 2479 }, 2480 Scope: apidiscovery.ScopeNamespace, 2481 }, 2482 { 2483 Resource: "daemonsets", 2484 ResponseKind: &metav1.GroupVersionKind{ 2485 Group: "apps", 2486 Version: "v1", 2487 Kind: "DaemonSet", 2488 }, 2489 Scope: apidiscovery.ScopeNamespace, 2490 }, 2491 }, 2492 }, 2493 }, 2494 }, 2495 }, 2496 }, 2497 apisHttpStatus: http.StatusOK, 2498 expectedGroups: []string{"apps"}, 2499 expectedResources: []string{"apps/v1/Deployment", "apps/v1/DaemonSet"}, 2500 expectedErr: false, 2501 }, 2502 { 2503 name: "Aggregated Discovery: 403 for core/v1 causes error", 2504 corev1: &apidiscovery.APIGroupDiscoveryList{}, 2505 coreHttpStatus: http.StatusForbidden, 2506 apis: &apidiscovery.APIGroupDiscoveryList{}, 2507 apisHttpStatus: http.StatusOK, 2508 expectedErr: true, 2509 }, 2510 { 2511 name: "Aggregated Discovery: 404 for /apis causes error", 2512 corev1: &apidiscovery.APIGroupDiscoveryList{}, 2513 coreHttpStatus: http.StatusOK, 2514 apis: &apidiscovery.APIGroupDiscoveryList{}, 2515 apisHttpStatus: http.StatusNotFound, 2516 expectedErr: true, 2517 }, 2518 { 2519 name: "Aggregated Discovery: 403 for /apis causes error", 2520 corev1: &apidiscovery.APIGroupDiscoveryList{}, 2521 coreHttpStatus: http.StatusOK, 2522 apis: &apidiscovery.APIGroupDiscoveryList{}, 2523 apisHttpStatus: http.StatusForbidden, 2524 expectedErr: true, 2525 }, 2526 } 2527 2528 for _, test := range tests { 2529 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 2530 var output []byte 2531 var err error 2532 var status int 2533 var agg *apidiscovery.APIGroupDiscoveryList 2534 switch req.URL.Path { 2535 case "/api": 2536 agg = test.corev1 2537 status = test.coreHttpStatus 2538 case "/apis": 2539 agg = test.apis 2540 status = test.apisHttpStatus 2541 default: 2542 w.WriteHeader(http.StatusNotFound) 2543 return 2544 } 2545 output, err = json.Marshal(agg) 2546 require.NoError(t, err) 2547 // Content-Type is "aggregated" discovery format. Add extra parameter 2548 // to ensure we are resilient to these extra parameters. 2549 w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") 2550 w.WriteHeader(status) 2551 _, err = w.Write(output) 2552 require.NoError(t, err) 2553 })) 2554 defer server.Close() 2555 2556 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 2557 apiGroups, resources, err := client.ServerGroupsAndResources() 2558 if test.expectedErr { 2559 require.Error(t, err) 2560 require.Nil(t, apiGroups) 2561 require.Nil(t, resources) 2562 continue 2563 } 2564 require.NoError(t, err) 2565 // First check the returned groups 2566 expectedGroups := sets.NewString(test.expectedGroups...) 2567 actualGroups := sets.NewString(groupNames(apiGroups)...) 2568 assert.True(t, expectedGroups.Equal(actualGroups), 2569 "%s: Expected GVKs (%s), got (%s)", test.name, expectedGroups.List(), actualGroups.List()) 2570 // Next check the returned resources 2571 expectedGVKs := sets.NewString(test.expectedResources...) 2572 actualGVKs := sets.NewString(groupVersionKinds(resources)...) 2573 assert.True(t, expectedGVKs.Equal(actualGVKs), 2574 "%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List()) 2575 } 2576 } 2577 2578 func TestAggregatedServerPreferredResources(t *testing.T) { 2579 tests := []struct { 2580 name string 2581 corev1 *apidiscovery.APIGroupDiscoveryList 2582 apis *apidiscovery.APIGroupDiscoveryList 2583 expectedGVKs []string 2584 expectedFailedGVs []string 2585 }{ 2586 { 2587 name: "Aggregated discovery: basic corev1 and apps/v1 preferred resources returned", 2588 corev1: &apidiscovery.APIGroupDiscoveryList{ 2589 Items: []apidiscovery.APIGroupDiscovery{ 2590 { 2591 Versions: []apidiscovery.APIVersionDiscovery{ 2592 { 2593 Version: "v1", 2594 Resources: []apidiscovery.APIResourceDiscovery{ 2595 { 2596 Resource: "pods", 2597 ResponseKind: &metav1.GroupVersionKind{ 2598 Group: "", 2599 Version: "v1", 2600 Kind: "Pod", 2601 }, 2602 Scope: apidiscovery.ScopeNamespace, 2603 }, 2604 }, 2605 }, 2606 }, 2607 }, 2608 }, 2609 }, 2610 apis: &apidiscovery.APIGroupDiscoveryList{ 2611 Items: []apidiscovery.APIGroupDiscovery{ 2612 { 2613 ObjectMeta: metav1.ObjectMeta{ 2614 Name: "apps", 2615 }, 2616 Versions: []apidiscovery.APIVersionDiscovery{ 2617 { 2618 Version: "v1", 2619 Resources: []apidiscovery.APIResourceDiscovery{ 2620 { 2621 Resource: "deployments", 2622 ResponseKind: &metav1.GroupVersionKind{ 2623 Group: "apps", 2624 Version: "v1", 2625 Kind: "Deployment", 2626 }, 2627 Scope: apidiscovery.ScopeNamespace, 2628 }, 2629 }, 2630 }, 2631 }, 2632 }, 2633 }, 2634 }, 2635 expectedGVKs: []string{ 2636 "/v1/Pod", 2637 "apps/v1/Deployment", 2638 }, 2639 }, 2640 { 2641 name: "Aggregated discovery: only resources from preferred apps/v2 group/version", 2642 corev1: &apidiscovery.APIGroupDiscoveryList{ 2643 Items: []apidiscovery.APIGroupDiscovery{ 2644 { 2645 Versions: []apidiscovery.APIVersionDiscovery{ 2646 { 2647 Version: "v1", 2648 Resources: []apidiscovery.APIResourceDiscovery{ 2649 { 2650 Resource: "pods", 2651 ResponseKind: &metav1.GroupVersionKind{ 2652 Group: "", 2653 Version: "v1", 2654 Kind: "Pod", 2655 }, 2656 Scope: apidiscovery.ScopeNamespace, 2657 }, 2658 }, 2659 }, 2660 }, 2661 }, 2662 }, 2663 }, 2664 apis: &apidiscovery.APIGroupDiscoveryList{ 2665 Items: []apidiscovery.APIGroupDiscovery{ 2666 { 2667 ObjectMeta: metav1.ObjectMeta{ 2668 Name: "apps", 2669 }, 2670 Versions: []apidiscovery.APIVersionDiscovery{ 2671 // v2 is "preferred version since it is first 2672 { 2673 Version: "v2", 2674 Resources: []apidiscovery.APIResourceDiscovery{ 2675 { 2676 Resource: "deployments", 2677 ResponseKind: &metav1.GroupVersionKind{ 2678 Group: "apps", 2679 Version: "v2", 2680 Kind: "Deployment", 2681 }, 2682 Scope: apidiscovery.ScopeNamespace, 2683 }, 2684 }, 2685 }, 2686 { 2687 Version: "v1", 2688 Resources: []apidiscovery.APIResourceDiscovery{ 2689 { 2690 Resource: "deployments", 2691 ResponseKind: &metav1.GroupVersionKind{ 2692 Group: "apps", 2693 Version: "v1", 2694 Kind: "Deployment", 2695 }, 2696 Scope: apidiscovery.ScopeNamespace, 2697 }, 2698 }, 2699 }, 2700 }, 2701 }, 2702 }, 2703 }, 2704 // Only v2 resources from apps group, since v2 is preferred version. 2705 expectedGVKs: []string{ 2706 "/v1/Pod", 2707 "apps/v2/Deployment", 2708 }, 2709 }, 2710 { 2711 name: "Aggregated discovery: stale Group/Version can not produce preferred version", 2712 corev1: &apidiscovery.APIGroupDiscoveryList{ 2713 Items: []apidiscovery.APIGroupDiscovery{ 2714 { 2715 Versions: []apidiscovery.APIVersionDiscovery{ 2716 { 2717 Version: "v1", 2718 Resources: []apidiscovery.APIResourceDiscovery{ 2719 { 2720 Resource: "pods", 2721 ResponseKind: &metav1.GroupVersionKind{ 2722 Group: "", 2723 Version: "v1", 2724 Kind: "Pod", 2725 }, 2726 Scope: apidiscovery.ScopeNamespace, 2727 }, 2728 }, 2729 }, 2730 }, 2731 }, 2732 }, 2733 }, 2734 apis: &apidiscovery.APIGroupDiscoveryList{ 2735 Items: []apidiscovery.APIGroupDiscovery{ 2736 { 2737 ObjectMeta: metav1.ObjectMeta{ 2738 Name: "apps", 2739 }, 2740 Versions: []apidiscovery.APIVersionDiscovery{ 2741 // v2 is "stale", so it can not be "preferred". 2742 { 2743 Version: "v2", 2744 Resources: []apidiscovery.APIResourceDiscovery{ 2745 { 2746 Resource: "deployments", 2747 ResponseKind: &metav1.GroupVersionKind{ 2748 Group: "apps", 2749 Version: "v2", 2750 Kind: "Deployment", 2751 }, 2752 Scope: apidiscovery.ScopeNamespace, 2753 }, 2754 }, 2755 Freshness: apidiscovery.DiscoveryFreshnessStale, 2756 }, 2757 { 2758 Version: "v1", 2759 Resources: []apidiscovery.APIResourceDiscovery{ 2760 { 2761 Resource: "deployments", 2762 ResponseKind: &metav1.GroupVersionKind{ 2763 Group: "apps", 2764 Version: "v1", 2765 Kind: "Deployment", 2766 }, 2767 Scope: apidiscovery.ScopeNamespace, 2768 }, 2769 }, 2770 }, 2771 }, 2772 }, 2773 }, 2774 }, 2775 // Only v1 resources from apps group; v2 would be preferred but it is "stale". 2776 expectedGVKs: []string{ 2777 "/v1/Pod", 2778 "apps/v1/Deployment", 2779 }, 2780 expectedFailedGVs: []string{"apps/v2"}, 2781 }, 2782 { 2783 name: "Aggregated discovery: preferred multiple resources from multiple group/versions", 2784 corev1: &apidiscovery.APIGroupDiscoveryList{ 2785 Items: []apidiscovery.APIGroupDiscovery{ 2786 { 2787 Versions: []apidiscovery.APIVersionDiscovery{ 2788 { 2789 Version: "v1", 2790 Resources: []apidiscovery.APIResourceDiscovery{ 2791 { 2792 Resource: "pods", 2793 ResponseKind: &metav1.GroupVersionKind{ 2794 Group: "", 2795 Version: "v1", 2796 Kind: "Pod", 2797 }, 2798 Scope: apidiscovery.ScopeNamespace, 2799 }, 2800 { 2801 Resource: "services", 2802 ResponseKind: &metav1.GroupVersionKind{ 2803 Group: "", 2804 Version: "v1", 2805 Kind: "Service", 2806 }, 2807 Scope: apidiscovery.ScopeNamespace, 2808 }, 2809 }, 2810 }, 2811 }, 2812 }, 2813 }, 2814 }, 2815 apis: &apidiscovery.APIGroupDiscoveryList{ 2816 Items: []apidiscovery.APIGroupDiscovery{ 2817 { 2818 ObjectMeta: metav1.ObjectMeta{ 2819 Name: "apps", 2820 }, 2821 Versions: []apidiscovery.APIVersionDiscovery{ 2822 { 2823 Version: "v1", 2824 Resources: []apidiscovery.APIResourceDiscovery{ 2825 { 2826 Resource: "deployments", 2827 ResponseKind: &metav1.GroupVersionKind{ 2828 Group: "apps", 2829 Version: "v1", 2830 Kind: "Deployment", 2831 }, 2832 Scope: apidiscovery.ScopeNamespace, 2833 }, 2834 { 2835 Resource: "statefulsets", 2836 ResponseKind: &metav1.GroupVersionKind{ 2837 Group: "apps", 2838 Version: "v1", 2839 Kind: "StatefulSet", 2840 }, 2841 Scope: apidiscovery.ScopeNamespace, 2842 }, 2843 }, 2844 }, 2845 { 2846 Version: "v1beta1", 2847 Resources: []apidiscovery.APIResourceDiscovery{ 2848 { 2849 Resource: "deployments", 2850 ResponseKind: &metav1.GroupVersionKind{ 2851 Group: "apps", 2852 Version: "v1beta1", 2853 Kind: "Deployment", 2854 }, 2855 Scope: apidiscovery.ScopeNamespace, 2856 }, 2857 { 2858 Resource: "statefulsets", 2859 ResponseKind: &metav1.GroupVersionKind{ 2860 Group: "apps", 2861 Version: "v1beta1", 2862 Kind: "StatefulSet", 2863 }, 2864 Scope: apidiscovery.ScopeNamespace, 2865 }, 2866 }, 2867 Freshness: apidiscovery.DiscoveryFreshnessStale, 2868 }, 2869 }, 2870 }, 2871 }, 2872 }, 2873 expectedGVKs: []string{ 2874 "/v1/Pod", 2875 "/v1/Service", 2876 "apps/v1/Deployment", 2877 "apps/v1/StatefulSet", 2878 }, 2879 expectedFailedGVs: []string{"apps/v1beta1"}, 2880 }, 2881 { 2882 name: "Aggregated discovery: resources from multiple preferred group versions at /apis", 2883 corev1: &apidiscovery.APIGroupDiscoveryList{ 2884 Items: []apidiscovery.APIGroupDiscovery{ 2885 { 2886 Versions: []apidiscovery.APIVersionDiscovery{ 2887 { 2888 Version: "v1", 2889 Resources: []apidiscovery.APIResourceDiscovery{ 2890 { 2891 Resource: "pods", 2892 ResponseKind: &metav1.GroupVersionKind{ 2893 Group: "", 2894 Version: "v1", 2895 Kind: "Pod", 2896 }, 2897 Scope: apidiscovery.ScopeNamespace, 2898 }, 2899 { 2900 Resource: "services", 2901 ResponseKind: &metav1.GroupVersionKind{ 2902 Group: "", 2903 Version: "v1", 2904 Kind: "Service", 2905 }, 2906 Scope: apidiscovery.ScopeNamespace, 2907 }, 2908 }, 2909 }, 2910 }, 2911 }, 2912 }, 2913 }, 2914 apis: &apidiscovery.APIGroupDiscoveryList{ 2915 Items: []apidiscovery.APIGroupDiscovery{ 2916 { 2917 ObjectMeta: metav1.ObjectMeta{ 2918 Name: "apps", 2919 }, 2920 Versions: []apidiscovery.APIVersionDiscovery{ 2921 { 2922 Version: "v1", 2923 Resources: []apidiscovery.APIResourceDiscovery{ 2924 { 2925 Resource: "deployments", 2926 ResponseKind: &metav1.GroupVersionKind{ 2927 Group: "apps", 2928 Version: "v1", 2929 Kind: "Deployment", 2930 }, 2931 Scope: apidiscovery.ScopeNamespace, 2932 }, 2933 { 2934 Resource: "statefulsets", 2935 ResponseKind: &metav1.GroupVersionKind{ 2936 Group: "apps", 2937 Version: "v1", 2938 Kind: "StatefulSet", 2939 }, 2940 Scope: apidiscovery.ScopeNamespace, 2941 }, 2942 }, 2943 }, 2944 { 2945 // Not included because "v1" is preferred. 2946 Version: "v1beta1", 2947 Resources: []apidiscovery.APIResourceDiscovery{ 2948 { 2949 Resource: "deployments", 2950 ResponseKind: &metav1.GroupVersionKind{ 2951 Group: "apps", 2952 Version: "v1beta1", 2953 Kind: "Deployment", 2954 }, 2955 Scope: apidiscovery.ScopeNamespace, 2956 }, 2957 { 2958 Resource: "statefulsets", 2959 ResponseKind: &metav1.GroupVersionKind{ 2960 Group: "apps", 2961 Version: "v1beta1", 2962 Kind: "StatefulSet", 2963 }, 2964 Scope: apidiscovery.ScopeNamespace, 2965 }, 2966 }, 2967 }, 2968 }, 2969 }, 2970 { 2971 ObjectMeta: metav1.ObjectMeta{ 2972 Name: "batch", 2973 }, 2974 Versions: []apidiscovery.APIVersionDiscovery{ 2975 { 2976 Version: "v1", 2977 Resources: []apidiscovery.APIResourceDiscovery{ 2978 { 2979 Resource: "jobs", 2980 ResponseKind: &metav1.GroupVersionKind{ 2981 Group: "batch", 2982 Version: "v1", 2983 Kind: "Job", 2984 }, 2985 Scope: apidiscovery.ScopeNamespace, 2986 }, 2987 { 2988 Resource: "cronjobs", 2989 ResponseKind: &metav1.GroupVersionKind{ 2990 Group: "batch", 2991 Version: "v1", 2992 Kind: "CronJob", 2993 }, 2994 Scope: apidiscovery.ScopeNamespace, 2995 }, 2996 }, 2997 }, 2998 }, 2999 }, 3000 }, 3001 }, 3002 expectedGVKs: []string{ 3003 "/v1/Pod", 3004 "/v1/Service", 3005 "apps/v1/Deployment", 3006 "apps/v1/StatefulSet", 3007 "batch/v1/Job", 3008 "batch/v1/CronJob", 3009 }, 3010 }, 3011 { 3012 name: "Aggregated discovery: resources from only preferred group versions for batch group", 3013 corev1: &apidiscovery.APIGroupDiscoveryList{ 3014 Items: []apidiscovery.APIGroupDiscovery{ 3015 { 3016 Versions: []apidiscovery.APIVersionDiscovery{ 3017 { 3018 Version: "v1", 3019 Resources: []apidiscovery.APIResourceDiscovery{ 3020 { 3021 Resource: "pods", 3022 ResponseKind: &metav1.GroupVersionKind{ 3023 Group: "", 3024 Version: "v1", 3025 Kind: "Pod", 3026 }, 3027 Scope: apidiscovery.ScopeNamespace, 3028 }, 3029 { 3030 Resource: "services", 3031 ResponseKind: &metav1.GroupVersionKind{ 3032 Group: "", 3033 Version: "v1", 3034 Kind: "Service", 3035 }, 3036 Scope: apidiscovery.ScopeNamespace, 3037 }, 3038 }, 3039 }, 3040 }, 3041 }, 3042 }, 3043 }, 3044 apis: &apidiscovery.APIGroupDiscoveryList{ 3045 Items: []apidiscovery.APIGroupDiscovery{ 3046 { 3047 ObjectMeta: metav1.ObjectMeta{ 3048 Name: "apps", 3049 }, 3050 Versions: []apidiscovery.APIVersionDiscovery{ 3051 { 3052 Version: "v1", 3053 Resources: []apidiscovery.APIResourceDiscovery{ 3054 { 3055 Resource: "deployments", 3056 ResponseKind: &metav1.GroupVersionKind{ 3057 Group: "apps", 3058 Version: "v1", 3059 Kind: "Deployment", 3060 }, 3061 Scope: apidiscovery.ScopeNamespace, 3062 }, 3063 { 3064 Resource: "statefulsets", 3065 ResponseKind: &metav1.GroupVersionKind{ 3066 Group: "apps", 3067 Version: "v1", 3068 Kind: "StatefulSet", 3069 }, 3070 Scope: apidiscovery.ScopeNamespace, 3071 }, 3072 }, 3073 }, 3074 }, 3075 }, 3076 { 3077 ObjectMeta: metav1.ObjectMeta{ 3078 Name: "batch", 3079 }, 3080 Versions: []apidiscovery.APIVersionDiscovery{ 3081 { 3082 Version: "v1", 3083 Resources: []apidiscovery.APIResourceDiscovery{ 3084 { 3085 Resource: "jobs", 3086 ResponseKind: &metav1.GroupVersionKind{ 3087 Group: "batch", 3088 Version: "v1", 3089 Kind: "Job", 3090 }, 3091 Scope: apidiscovery.ScopeNamespace, 3092 }, 3093 { 3094 Resource: "cronjobs", 3095 ResponseKind: &metav1.GroupVersionKind{ 3096 Group: "batch", 3097 Version: "v1", 3098 Kind: "CronJob", 3099 }, 3100 Scope: apidiscovery.ScopeNamespace, 3101 }, 3102 }, 3103 }, 3104 { 3105 // Not included, since "v1" is preferred. 3106 Version: "v1beta1", 3107 Resources: []apidiscovery.APIResourceDiscovery{ 3108 { 3109 Resource: "jobs", 3110 ResponseKind: &metav1.GroupVersionKind{ 3111 Group: "batch", 3112 Version: "v1beta1", 3113 Kind: "Job", 3114 }, 3115 Scope: apidiscovery.ScopeNamespace, 3116 }, 3117 { 3118 Resource: "cronjobs", 3119 ResponseKind: &metav1.GroupVersionKind{ 3120 Group: "batch", 3121 Version: "v1beta1", 3122 Kind: "CronJob", 3123 }, 3124 Scope: apidiscovery.ScopeNamespace, 3125 }, 3126 }, 3127 }, 3128 }, 3129 }, 3130 }, 3131 }, 3132 // Only preferred resources expected--not batch/v1beta1 resources. 3133 expectedGVKs: []string{ 3134 "/v1/Pod", 3135 "/v1/Service", 3136 "apps/v1/Deployment", 3137 "apps/v1/StatefulSet", 3138 "batch/v1/Job", 3139 "batch/v1/CronJob", 3140 }, 3141 }, 3142 } 3143 3144 for _, test := range tests { 3145 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 3146 var output []byte 3147 var err error 3148 var agg *apidiscovery.APIGroupDiscoveryList 3149 switch req.URL.Path { 3150 case "/api": 3151 agg = test.corev1 3152 case "/apis": 3153 agg = test.apis 3154 default: 3155 w.WriteHeader(http.StatusNotFound) 3156 return 3157 } 3158 output, err = json.Marshal(agg) 3159 require.NoError(t, err) 3160 // Content-Type is "aggregated" discovery format. Add extra parameter 3161 // to ensure we are resilient to these extra parameters. 3162 w.Header().Set("Content-Type", AcceptV2+"; charset=utf-8") 3163 w.WriteHeader(http.StatusOK) 3164 _, err = w.Write(output) 3165 require.NoError(t, err) 3166 })) 3167 defer server.Close() 3168 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 3169 resources, err := client.ServerPreferredResources() 3170 if len(test.expectedFailedGVs) > 0 { 3171 require.Error(t, err) 3172 expectedFailedGVs := sets.NewString(test.expectedFailedGVs...) 3173 actualFailedGVs := sets.NewString(failedGroupVersions(err)...) 3174 assert.True(t, expectedFailedGVs.Equal(actualFailedGVs), 3175 "%s: Expected Failed GVs (%s), got (%s)", test.name, expectedFailedGVs, actualFailedGVs) 3176 } else { 3177 require.NoError(t, err) 3178 } 3179 // Test the expected preferred GVKs are returned from the aggregated discovery. 3180 expectedGVKs := sets.NewString(test.expectedGVKs...) 3181 actualGVKs := sets.NewString(groupVersionKinds(resources)...) 3182 assert.True(t, expectedGVKs.Equal(actualGVKs), 3183 "%s: Expected GVKs (%s), got (%s)", test.name, expectedGVKs.List(), actualGVKs.List()) 3184 } 3185 } 3186 3187 func TestDiscoveryContentTypeVersion(t *testing.T) { 3188 v2 := schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2", Kind: "APIGroupDiscoveryList"} 3189 tests := []struct { 3190 contentType string 3191 gvk schema.GroupVersionKind 3192 match bool 3193 expectErr bool 3194 }{ 3195 { 3196 contentType: "application/json; g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList", 3197 gvk: v2, 3198 match: true, 3199 expectErr: false, 3200 }, 3201 { 3202 // content-type parameters are not in correct order, but comparison ignores order. 3203 contentType: "application/json; v=v2;as=APIGroupDiscoveryList;g=apidiscovery.k8s.io", 3204 gvk: v2, 3205 match: true, 3206 expectErr: false, 3207 }, 3208 { 3209 // content-type parameters are not in correct order, but comparison ignores order. 3210 contentType: "application/json; as=APIGroupDiscoveryList;g=apidiscovery.k8s.io;v=v2", 3211 gvk: v2, 3212 match: true, 3213 expectErr: false, 3214 }, 3215 { 3216 // Ignores extra parameter "charset=utf-8" 3217 contentType: "application/json; g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList;charset=utf-8", 3218 gvk: v2, 3219 match: true, 3220 expectErr: false, 3221 }, 3222 { 3223 contentType: "application/json", 3224 gvk: v2, 3225 match: false, 3226 expectErr: false, 3227 }, 3228 { 3229 contentType: "application/json; charset=UTF-8", 3230 gvk: v2, 3231 match: false, 3232 expectErr: false, 3233 }, 3234 { 3235 contentType: "text/json", 3236 gvk: v2, 3237 match: false, 3238 expectErr: false, 3239 }, 3240 { 3241 contentType: "text/html", 3242 gvk: v2, 3243 match: false, 3244 expectErr: false, 3245 }, 3246 { 3247 contentType: "", 3248 gvk: v2, 3249 match: false, 3250 expectErr: true, 3251 }, 3252 } 3253 3254 for _, test := range tests { 3255 match, err := ContentTypeIsGVK(test.contentType, test.gvk) 3256 assert.Equal(t, test.expectErr, err != nil) 3257 assert.Equal(t, test.match, match) 3258 } 3259 } 3260 3261 func TestUseLegacyDiscovery(t *testing.T) { 3262 // Default client sends aggregated discovery accept format (first) as well as legacy format. 3263 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 3264 acceptHeader := req.Header.Get("Accept") 3265 assert.Equal(t, acceptDiscoveryFormats, acceptHeader) 3266 })) 3267 defer server.Close() 3268 client := NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 3269 client.ServerGroups() 3270 // When "UseLegacyDiscovery" field is set, only the legacy discovery format is requested. 3271 server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 3272 acceptHeader := req.Header.Get("Accept") 3273 assert.Equal(t, AcceptV1, acceptHeader) 3274 })) 3275 defer server.Close() 3276 client = NewDiscoveryClientForConfigOrDie(&restclient.Config{Host: server.URL}) 3277 client.UseLegacyDiscovery = true 3278 client.ServerGroups() 3279 } 3280 3281 func groupNames(groups []*metav1.APIGroup) []string { 3282 result := []string{} 3283 for _, group := range groups { 3284 result = append(result, group.Name) 3285 } 3286 return result 3287 } 3288 3289 func groupNamesFromList(groups *metav1.APIGroupList) []string { 3290 result := []string{} 3291 for _, group := range groups.Groups { 3292 result = append(result, group.Name) 3293 } 3294 return result 3295 } 3296 3297 func preferredVersionsFromList(groups *metav1.APIGroupList) []string { 3298 result := []string{} 3299 for _, group := range groups.Groups { 3300 preferredGV := group.PreferredVersion.GroupVersion 3301 result = append(result, preferredGV) 3302 } 3303 return result 3304 } 3305 3306 func groupVersions(resources []*metav1.APIResourceList) []string { 3307 result := []string{} 3308 for _, resourceList := range resources { 3309 result = append(result, resourceList.GroupVersion) 3310 } 3311 return result 3312 } 3313 3314 func groupVersionsFromGroups(groups *metav1.APIGroupList) []string { 3315 result := []string{} 3316 for _, group := range groups.Groups { 3317 for _, version := range group.Versions { 3318 result = append(result, version.GroupVersion) 3319 } 3320 } 3321 return result 3322 } 3323 3324 func groupVersionKinds(resources []*metav1.APIResourceList) []string { 3325 result := []string{} 3326 for _, resourceList := range resources { 3327 for _, resource := range resourceList.APIResources { 3328 gvk := fmt.Sprintf("%s/%s/%s", resource.Group, resource.Version, resource.Kind) 3329 result = append(result, gvk) 3330 } 3331 } 3332 return result 3333 } 3334 3335 func failedGroupVersions(err error) []string { 3336 result := []string{} 3337 ferr, ok := err.(*ErrGroupDiscoveryFailed) 3338 if !ok { 3339 return result 3340 } 3341 for gv := range ferr.Groups { 3342 result = append(result, gv.String()) 3343 } 3344 return result 3345 }