sigs.k8s.io/controller-runtime@v0.18.2/pkg/client/apiutil/restmapper_test.go (about) 1 /* 2 Copyright 2023 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 apiutil_test 18 19 import ( 20 "context" 21 "fmt" 22 "net/http" 23 "testing" 24 25 _ "github.com/onsi/ginkgo/v2" 26 gmg "github.com/onsi/gomega" 27 "github.com/onsi/gomega/format" 28 gomegatypes "github.com/onsi/gomega/types" 29 30 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 31 apierrors "k8s.io/apimachinery/pkg/api/errors" 32 "k8s.io/apimachinery/pkg/api/meta" 33 "k8s.io/apimachinery/pkg/runtime/schema" 34 "k8s.io/apimachinery/pkg/types" 35 "k8s.io/client-go/discovery" 36 "k8s.io/client-go/kubernetes/scheme" 37 "k8s.io/client-go/rest" 38 39 "sigs.k8s.io/controller-runtime/pkg/client" 40 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 41 "sigs.k8s.io/controller-runtime/pkg/envtest" 42 ) 43 44 // countingRoundTripper is used to count HTTP requests. 45 type countingRoundTripper struct { 46 roundTripper http.RoundTripper 47 requestCount int 48 } 49 50 func newCountingRoundTripper(rt http.RoundTripper) *countingRoundTripper { 51 return &countingRoundTripper{roundTripper: rt} 52 } 53 54 // RoundTrip implements http.RoundTripper.RoundTrip that additionally counts requests. 55 func (crt *countingRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 56 crt.requestCount++ 57 58 return crt.roundTripper.RoundTrip(r) 59 } 60 61 // GetRequestCount returns how many requests have been made. 62 func (crt *countingRoundTripper) GetRequestCount() int { 63 return crt.requestCount 64 } 65 66 // Reset sets the counter to 0. 67 func (crt *countingRoundTripper) Reset() { 68 crt.requestCount = 0 69 } 70 71 func setupEnvtest(t *testing.T) (*rest.Config, func(t *testing.T)) { 72 t.Log("Setup envtest") 73 74 g := gmg.NewWithT(t) 75 testEnv := &envtest.Environment{ 76 CRDDirectoryPaths: []string{"testdata"}, 77 } 78 79 cfg, err := testEnv.Start() 80 g.Expect(err).NotTo(gmg.HaveOccurred()) 81 g.Expect(cfg).NotTo(gmg.BeNil()) 82 83 teardownFunc := func(t *testing.T) { 84 t.Log("Stop envtest") 85 g.Expect(testEnv.Stop()).To(gmg.Succeed()) 86 } 87 88 return cfg, teardownFunc 89 } 90 91 func TestLazyRestMapperProvider(t *testing.T) { 92 restCfg, tearDownFn := setupEnvtest(t) 93 defer tearDownFn(t) 94 95 t.Run("LazyRESTMapper should fetch data based on the request", func(t *testing.T) { 96 g := gmg.NewWithT(t) 97 98 // For each new group it performs just one request to the API server: 99 // GET https://host/apis/<group>/<version> 100 101 httpClient, err := rest.HTTPClientFor(restCfg) 102 g.Expect(err).NotTo(gmg.HaveOccurred()) 103 104 crt := newCountingRoundTripper(httpClient.Transport) 105 httpClient.Transport = crt 106 107 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 108 g.Expect(err).NotTo(gmg.HaveOccurred()) 109 110 // There are no requests before any call 111 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 112 113 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "v1") 114 g.Expect(err).NotTo(gmg.HaveOccurred()) 115 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) 116 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 117 118 mappings, err := lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "pod"}, "v1") 119 g.Expect(err).NotTo(gmg.HaveOccurred()) 120 g.Expect(mappings).To(gmg.HaveLen(1)) 121 g.Expect(mappings[0].GroupVersionKind.Kind).To(gmg.Equal("pod")) 122 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 123 124 kind, err := lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}) 125 g.Expect(err).NotTo(gmg.HaveOccurred()) 126 g.Expect(kind.Kind).To(gmg.Equal("Ingress")) 127 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 128 129 kinds, err := lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "tokenreviews"}) 130 g.Expect(err).NotTo(gmg.HaveOccurred()) 131 g.Expect(kinds).To(gmg.HaveLen(1)) 132 g.Expect(kinds[0].Kind).To(gmg.Equal("TokenReview")) 133 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 134 135 resource, err := lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1", Resource: "priorityclasses"}) 136 g.Expect(err).NotTo(gmg.HaveOccurred()) 137 g.Expect(resource.Resource).To(gmg.Equal("priorityclasses")) 138 g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) 139 140 resources, err := lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "poddisruptionbudgets"}) 141 g.Expect(err).NotTo(gmg.HaveOccurred()) 142 g.Expect(resources).To(gmg.HaveLen(1)) 143 g.Expect(resources[0].Resource).To(gmg.Equal("poddisruptionbudgets")) 144 g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) 145 }) 146 147 t.Run("LazyRESTMapper should cache fetched data and doesn't perform any additional requests", func(t *testing.T) { 148 g := gmg.NewWithT(t) 149 150 httpClient, err := rest.HTTPClientFor(restCfg) 151 g.Expect(err).NotTo(gmg.HaveOccurred()) 152 153 crt := newCountingRoundTripper(httpClient.Transport) 154 httpClient.Transport = crt 155 156 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 157 g.Expect(err).NotTo(gmg.HaveOccurred()) 158 159 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 160 161 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}) 162 g.Expect(err).NotTo(gmg.HaveOccurred()) 163 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) 164 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 165 166 // Data taken from cache - there are no more additional requests. 167 168 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}) 169 g.Expect(err).NotTo(gmg.HaveOccurred()) 170 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) 171 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 172 173 kind, err := lazyRestMapper.KindFor((schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"})) 174 g.Expect(err).NotTo(gmg.HaveOccurred()) 175 g.Expect(kind.Kind).To(gmg.Equal("Deployment")) 176 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 177 178 resource, err := lazyRestMapper.ResourceFor((schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"})) 179 g.Expect(err).NotTo(gmg.HaveOccurred()) 180 g.Expect(resource.Resource).To(gmg.Equal("deployments")) 181 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 182 }) 183 184 t.Run("LazyRESTMapper should work correctly with empty versions list", func(t *testing.T) { 185 g := gmg.NewWithT(t) 186 187 httpClient, err := rest.HTTPClientFor(restCfg) 188 g.Expect(err).NotTo(gmg.HaveOccurred()) 189 190 crt := newCountingRoundTripper(httpClient.Transport) 191 httpClient.Transport = crt 192 193 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 194 g.Expect(err).NotTo(gmg.HaveOccurred()) 195 196 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 197 198 // crew.example.com has 2 versions: v1 and v2 199 200 // If no versions were provided by user, we fetch all of them. 201 // Here we expect 4 calls. 202 // To initialize: 203 // #1: GET https://host/api 204 // #2: GET https://host/apis 205 // Then, for each version it performs one request to the API server: 206 // #3: GET https://host/apis/crew.example.com/v1 207 // #4: GET https://host/apis/crew.example.com/v2 208 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) 209 g.Expect(err).NotTo(gmg.HaveOccurred()) 210 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 211 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 212 213 // All subsequent calls won't send requests to the server. 214 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) 215 g.Expect(err).NotTo(gmg.HaveOccurred()) 216 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 217 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 218 }) 219 220 t.Run("LazyRESTMapper should work correctly with multiple API group versions", func(t *testing.T) { 221 g := gmg.NewWithT(t) 222 223 httpClient, err := rest.HTTPClientFor(restCfg) 224 g.Expect(err).NotTo(gmg.HaveOccurred()) 225 226 crt := newCountingRoundTripper(httpClient.Transport) 227 httpClient.Transport = crt 228 229 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 230 g.Expect(err).NotTo(gmg.HaveOccurred()) 231 232 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 233 234 // We explicitly ask for 2 versions: v1 and v2. 235 // For each version it performs one request to the API server: 236 // #1: GET https://host/apis/crew.example.com/v1 237 // #2: GET https://host/apis/crew.example.com/v2 238 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1", "v2") 239 g.Expect(err).NotTo(gmg.HaveOccurred()) 240 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 241 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 242 243 // All subsequent calls won't send requests to the server as everything is stored in the cache. 244 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") 245 g.Expect(err).NotTo(gmg.HaveOccurred()) 246 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 247 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 248 249 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) 250 g.Expect(err).NotTo(gmg.HaveOccurred()) 251 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 252 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 253 }) 254 255 t.Run("LazyRESTMapper should work correctly with different API group versions", func(t *testing.T) { 256 g := gmg.NewWithT(t) 257 258 httpClient, err := rest.HTTPClientFor(restCfg) 259 g.Expect(err).NotTo(gmg.HaveOccurred()) 260 261 crt := newCountingRoundTripper(httpClient.Transport) 262 httpClient.Transport = crt 263 264 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 265 g.Expect(err).NotTo(gmg.HaveOccurred()) 266 267 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 268 269 // Now we want resources for crew.example.com/v1 version only. 270 // Here we expect 1 call: 271 // #1: GET https://host/apis/crew.example.com/v1 272 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") 273 g.Expect(err).NotTo(gmg.HaveOccurred()) 274 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 275 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 276 277 // Get additional resources from v2. 278 // It sends another request: 279 // #2: GET https://host/apis/crew.example.com/v2 280 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v2") 281 g.Expect(err).NotTo(gmg.HaveOccurred()) 282 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 283 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 284 285 // No subsequent calls require additional API requests. 286 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") 287 g.Expect(err).NotTo(gmg.HaveOccurred()) 288 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 289 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 290 291 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1", "v2") 292 g.Expect(err).NotTo(gmg.HaveOccurred()) 293 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 294 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 295 }) 296 297 t.Run("LazyRESTMapper should return an error if the group doesn't exist", func(t *testing.T) { 298 g := gmg.NewWithT(t) 299 300 // After initialization for each invalid group the mapper performs just 1 request to the API server. 301 302 httpClient, err := rest.HTTPClientFor(restCfg) 303 g.Expect(err).NotTo(gmg.HaveOccurred()) 304 305 crt := newCountingRoundTripper(httpClient.Transport) 306 httpClient.Transport = crt 307 308 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 309 g.Expect(err).NotTo(gmg.HaveOccurred()) 310 311 // A version is specified but the group doesn't exist. 312 // For each group, we expect 1 call to the version-specific discovery endpoint: 313 // #1: GET https://host/apis/<group>/<version> 314 315 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID1"}, "v1") 316 g.Expect(err).To(gmg.HaveOccurred()) 317 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 318 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 319 320 _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "INVALID2"}, "v1") 321 g.Expect(err).To(gmg.HaveOccurred()) 322 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 323 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 324 325 _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "INVALID3", Version: "v1"}) 326 g.Expect(err).To(gmg.HaveOccurred()) 327 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 328 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 329 330 _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "INVALID4", Version: "v1"}) 331 g.Expect(err).To(gmg.HaveOccurred()) 332 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 333 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 334 335 _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "INVALID5", Version: "v1"}) 336 g.Expect(err).To(gmg.HaveOccurred()) 337 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 338 g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) 339 340 _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "INVALID6", Version: "v1"}) 341 g.Expect(err).To(gmg.HaveOccurred()) 342 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 343 g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) 344 345 // No version is specified but the group doesn't exist. 346 // For each group, we expect 2 calls to discover all group versions: 347 // #1: GET https://host/api 348 // #2: GET https://host/apis 349 350 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID7"}) 351 g.Expect(err).To(beNoMatchError()) 352 g.Expect(crt.GetRequestCount()).To(gmg.Equal(8)) 353 354 _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "INVALID8"}) 355 g.Expect(err).To(beNoMatchError()) 356 g.Expect(crt.GetRequestCount()).To(gmg.Equal(10)) 357 358 _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "INVALID9"}) 359 g.Expect(err).To(beNoMatchError()) 360 g.Expect(crt.GetRequestCount()).To(gmg.Equal(12)) 361 362 _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "INVALID10"}) 363 g.Expect(err).To(beNoMatchError()) 364 g.Expect(crt.GetRequestCount()).To(gmg.Equal(14)) 365 366 _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "INVALID11"}) 367 g.Expect(err).To(beNoMatchError()) 368 g.Expect(crt.GetRequestCount()).To(gmg.Equal(16)) 369 370 _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "INVALID12"}) 371 g.Expect(err).To(beNoMatchError()) 372 g.Expect(crt.GetRequestCount()).To(gmg.Equal(18)) 373 }) 374 375 t.Run("LazyRESTMapper should return an error if a resource doesn't exist", func(t *testing.T) { 376 g := gmg.NewWithT(t) 377 378 // For each invalid resource the mapper performs just 1 request to the API server. 379 380 httpClient, err := rest.HTTPClientFor(restCfg) 381 g.Expect(err).NotTo(gmg.HaveOccurred()) 382 383 crt := newCountingRoundTripper(httpClient.Transport) 384 httpClient.Transport = crt 385 386 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 387 g.Expect(err).NotTo(gmg.HaveOccurred()) 388 389 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "INVALID"}, "v1") 390 g.Expect(err).To(gmg.HaveOccurred()) 391 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 392 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 393 394 _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "INVALID"}, "v1") 395 g.Expect(err).To(gmg.HaveOccurred()) 396 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 397 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 398 399 _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "INVALID"}) 400 g.Expect(err).To(gmg.HaveOccurred()) 401 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 402 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 403 404 _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "INVALID"}) 405 g.Expect(err).To(gmg.HaveOccurred()) 406 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 407 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 408 409 _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1", Resource: "INVALID"}) 410 g.Expect(err).To(gmg.HaveOccurred()) 411 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 412 g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) 413 414 _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "INVALID"}) 415 g.Expect(err).To(gmg.HaveOccurred()) 416 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 417 g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) 418 }) 419 420 t.Run("LazyRESTMapper should return an error if the version doesn't exist", func(t *testing.T) { 421 g := gmg.NewWithT(t) 422 423 // After initialization, for each invalid resource mapper performs 1 requests to the API server. 424 425 httpClient, err := rest.HTTPClientFor(restCfg) 426 g.Expect(err).NotTo(gmg.HaveOccurred()) 427 428 crt := newCountingRoundTripper(httpClient.Transport) 429 httpClient.Transport = crt 430 431 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 432 g.Expect(err).NotTo(gmg.HaveOccurred()) 433 434 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "INVALID") 435 g.Expect(err).To(gmg.HaveOccurred()) 436 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 437 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 438 439 _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "pod"}, "INVALID") 440 g.Expect(err).To(gmg.HaveOccurred()) 441 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 442 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 443 444 _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "INVALID", Resource: "ingresses"}) 445 g.Expect(err).To(gmg.HaveOccurred()) 446 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 447 g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) 448 449 _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "INVALID", Resource: "tokenreviews"}) 450 g.Expect(err).To(gmg.HaveOccurred()) 451 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 452 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 453 454 _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "INVALID", Resource: "priorityclasses"}) 455 g.Expect(err).To(gmg.HaveOccurred()) 456 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 457 g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) 458 459 _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "INVALID", Resource: "poddisruptionbudgets"}) 460 g.Expect(err).To(gmg.HaveOccurred()) 461 g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) 462 g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) 463 }) 464 465 t.Run("LazyRESTMapper should work correctly if the version isn't specified", func(t *testing.T) { 466 g := gmg.NewWithT(t) 467 468 httpClient, err := rest.HTTPClientFor(restCfg) 469 g.Expect(err).NotTo(gmg.HaveOccurred()) 470 471 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 472 g.Expect(err).NotTo(gmg.HaveOccurred()) 473 474 kind, err := lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Resource: "ingress"}) 475 g.Expect(err).NotTo(gmg.HaveOccurred()) 476 g.Expect(kind.Version).ToNot(gmg.BeEmpty()) 477 478 kinds, err := lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Resource: "tokenreviews"}) 479 g.Expect(err).NotTo(gmg.HaveOccurred()) 480 g.Expect(kinds).ToNot(gmg.BeEmpty()) 481 g.Expect(kinds[0].Version).ToNot(gmg.BeEmpty()) 482 483 resorce, err := lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Resource: "priorityclasses"}) 484 g.Expect(err).NotTo(gmg.HaveOccurred()) 485 g.Expect(resorce.Version).ToNot(gmg.BeEmpty()) 486 487 resorces, err := lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Resource: "poddisruptionbudgets"}) 488 g.Expect(err).NotTo(gmg.HaveOccurred()) 489 g.Expect(kinds).ToNot(gmg.BeEmpty()) 490 g.Expect(resorces[0].Version).ToNot(gmg.BeEmpty()) 491 }) 492 493 t.Run("LazyRESTMapper can fetch CRDs if they were created at runtime", func(t *testing.T) { 494 g := gmg.NewWithT(t) 495 496 // To fetch all versions mapper does 2 requests: 497 // GET https://host/api 498 // GET https://host/apis 499 // Then, for each version it performs just one request to the API server as usual: 500 // GET https://host/apis/<group>/<version> 501 502 httpClient, err := rest.HTTPClientFor(restCfg) 503 g.Expect(err).NotTo(gmg.HaveOccurred()) 504 505 crt := newCountingRoundTripper(httpClient.Transport) 506 httpClient.Transport = crt 507 508 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 509 g.Expect(err).NotTo(gmg.HaveOccurred()) 510 511 // There are no requests before any call 512 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 513 514 // Since we don't specify what version we expect, restmapper will fetch them all and search there. 515 // To fetch a list of available versions 516 // #1: GET https://host/api 517 // #2: GET https://host/apis 518 // Then, for each currently registered version: 519 // #3: GET https://host/apis/crew.example.com/v1 520 // #4: GET https://host/apis/crew.example.com/v2 521 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) 522 g.Expect(err).NotTo(gmg.HaveOccurred()) 523 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) 524 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 525 526 s := scheme.Scheme 527 err = apiextensionsv1.AddToScheme(s) 528 g.Expect(err).NotTo(gmg.HaveOccurred()) 529 530 c, err := client.New(restCfg, client.Options{Scheme: s}) 531 g.Expect(err).NotTo(gmg.HaveOccurred()) 532 533 // Register another CRD in runtime - "riders.crew.example.com". 534 createNewCRD(context.TODO(), g, c, "crew.example.com", "Rider", "riders") 535 536 // Wait a bit until the CRD is registered. 537 g.Eventually(func() error { 538 _, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "rider"}) 539 return err 540 }).Should(gmg.Succeed()) 541 542 // Since we don't specify what version we expect, restmapper will fetch them all and search there. 543 // To fetch a list of available versions 544 // #1: GET https://host/api 545 // #2: GET https://host/apis 546 // Then, for each currently registered version: 547 // #3: GET https://host/apis/crew.example.com/v1 548 // #4: GET https://host/apis/crew.example.com/v2 549 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "rider"}) 550 g.Expect(err).NotTo(gmg.HaveOccurred()) 551 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("rider")) 552 }) 553 554 t.Run("LazyRESTMapper should invalidate the group cache if a version is not found", func(t *testing.T) { 555 g := gmg.NewWithT(t) 556 ctx := context.Background() 557 558 httpClient, err := rest.HTTPClientFor(restCfg) 559 g.Expect(err).NotTo(gmg.HaveOccurred()) 560 561 crt := newCountingRoundTripper(httpClient.Transport) 562 httpClient.Transport = crt 563 564 lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) 565 g.Expect(err).NotTo(gmg.HaveOccurred()) 566 567 s := scheme.Scheme 568 err = apiextensionsv1.AddToScheme(s) 569 g.Expect(err).NotTo(gmg.HaveOccurred()) 570 571 c, err := client.New(restCfg, client.Options{Scheme: s}) 572 g.Expect(err).NotTo(gmg.HaveOccurred()) 573 574 // Register a new CRD ina new group to avoid collisions when deleting versions - "taxis.inventory.example.com". 575 group := "inventory.example.com" 576 kind := "Taxi" 577 plural := "taxis" 578 crdName := plural + "." + group 579 // Create a CRD with two versions: v1alpha1 and v1 where both are served and 580 // v1 is the storage version so we can easily remove v1alpha1 later. 581 crd := newCRD(ctx, g, c, group, kind, plural) 582 v1alpha1 := crd.Spec.Versions[0] 583 v1alpha1.Name = "v1alpha1" 584 v1alpha1.Storage = false 585 v1alpha1.Served = true 586 v1 := crd.Spec.Versions[0] 587 v1.Name = "v1" 588 v1.Storage = true 589 v1.Served = true 590 crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1alpha1, v1} 591 g.Expect(c.Create(ctx, crd)).To(gmg.Succeed()) 592 t.Cleanup(func() { 593 g.Expect(c.Delete(ctx, crd)).To(gmg.Succeed()) 594 }) 595 596 // Wait until the CRD is registered. 597 discHTTP, err := rest.HTTPClientFor(restCfg) 598 g.Expect(err).NotTo(gmg.HaveOccurred()) 599 discClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, discHTTP) 600 g.Expect(err).NotTo(gmg.HaveOccurred()) 601 g.Eventually(func(g gmg.Gomega) { 602 _, err = discClient.ServerResourcesForGroupVersion(group + "/v1") 603 g.Expect(err).NotTo(gmg.HaveOccurred()) 604 }).Should(gmg.Succeed(), "v1 should be available") 605 606 // There are no requests before any call 607 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 608 609 // Since we don't specify what version we expect, restmapper will fetch them all and search there. 610 // To fetch a list of available versions 611 // #1: GET https://host/api 612 // #2: GET https://host/apis 613 // Then, for all available versions: 614 // #3: GET https://host/apis/inventory.example.com/v1alpha1 615 // #4: GET https://host/apis/inventory.example.com/v1 616 // This should fill the cache for apiGroups and versions. 617 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}) 618 g.Expect(err).NotTo(gmg.HaveOccurred()) 619 g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal(kind)) 620 g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) 621 crt.Reset() // We reset the counter to check how many additional requests are made later. 622 623 // At this point v1alpha1 should be cached 624 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") 625 g.Expect(err).NotTo(gmg.HaveOccurred()) 626 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 627 628 // We update the CRD to only have v1 version. 629 g.Expect(c.Get(ctx, types.NamespacedName{Name: crdName}, crd)).To(gmg.Succeed()) 630 for _, version := range crd.Spec.Versions { 631 if version.Name == "v1" { 632 v1 = version 633 break 634 } 635 } 636 crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1} 637 g.Expect(c.Update(ctx, crd)).To(gmg.Succeed()) 638 639 // We wait until v1alpha1 is not available anymore. 640 g.Eventually(func(g gmg.Gomega) { 641 _, err = discClient.ServerResourcesForGroupVersion(group + "/v1alpha1") 642 g.Expect(apierrors.IsNotFound(err)).To(gmg.BeTrue(), "v1alpha1 should not be available anymore") 643 }).Should(gmg.Succeed()) 644 645 // Although v1alpha1 is not available anymore, the cache is not invalidated yet so it should return a mapping. 646 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") 647 g.Expect(err).NotTo(gmg.HaveOccurred()) 648 g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) 649 650 // We request Limo, which is not in the mapper because it doesn't exist. 651 // This will trigger a reload of the lazy mapper cache. 652 // Reloading the cache will read v2 again and since it's not available anymore, it should invalidate the cache. 653 // #1: GET https://host/apis/inventory.example.com/v1alpha1 654 // #2: GET https://host/apis/inventory.example.com/v1 655 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: "Limo"}) 656 g.Expect(err).To(beNoMatchError()) 657 g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) 658 crt.Reset() 659 660 // Now we request v1alpha1 again and it should return an error since the cache was invalidated. 661 // #1: GET https://host/apis/inventory.example.com/v1alpha1 662 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") 663 g.Expect(err).To(beNoMatchError()) 664 g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) 665 666 // Verify that when requesting the mapping without a version, it doesn't error 667 // and it returns v1. 668 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}) 669 g.Expect(err).NotTo(gmg.HaveOccurred()) 670 g.Expect(mapping.Resource.Version).To(gmg.Equal("v1")) 671 }) 672 } 673 674 // createNewCRD creates a new CRD with the given group, kind, and plural and returns it. 675 func createNewCRD(ctx context.Context, g gmg.Gomega, c client.Client, group, kind, plural string) *apiextensionsv1.CustomResourceDefinition { 676 newCRD := newCRD(ctx, g, c, group, kind, plural) 677 g.Expect(c.Create(ctx, newCRD)).To(gmg.Succeed()) 678 679 return newCRD 680 } 681 682 // newCRD returns a new CRD with the given group, kind, and plural. 683 func newCRD(ctx context.Context, g gmg.Gomega, c client.Client, group, kind, plural string) *apiextensionsv1.CustomResourceDefinition { 684 crd := &apiextensionsv1.CustomResourceDefinition{} 685 err := c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) 686 g.Expect(err).NotTo(gmg.HaveOccurred()) 687 g.Expect(crd.Spec.Names.Kind).To(gmg.Equal("Driver")) 688 689 newCRD := &apiextensionsv1.CustomResourceDefinition{} 690 crd.DeepCopyInto(newCRD) 691 newCRD.Spec.Group = group 692 newCRD.Name = plural + "." + group 693 newCRD.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{ 694 Kind: kind, 695 Plural: plural, 696 } 697 newCRD.ResourceVersion = "" 698 699 return newCRD 700 } 701 702 func beNoMatchError() gomegatypes.GomegaMatcher { 703 return &errorMatcher{ 704 checkFunc: meta.IsNoMatchError, 705 message: "NoMatch", 706 } 707 } 708 709 type errorMatcher struct { 710 checkFunc func(error) bool 711 message string 712 } 713 714 func (e *errorMatcher) Match(actual interface{}) (success bool, err error) { 715 if actual == nil { 716 return false, nil 717 } 718 719 actualErr, actualOk := actual.(error) 720 if !actualOk { 721 return false, fmt.Errorf("expected an error-type. got:\n%s", format.Object(actual, 1)) 722 } 723 724 return e.checkFunc(actualErr), nil 725 } 726 727 func (e *errorMatcher) FailureMessage(actual interface{}) (message string) { 728 return format.Message(actual, fmt.Sprintf("to be %s error", e.message)) 729 } 730 731 func (e *errorMatcher) NegatedFailureMessage(actual interface{}) (message string) { 732 return format.Message(actual, fmt.Sprintf("not to be %s error", e.message)) 733 }