...

Source file src/sigs.k8s.io/controller-runtime/pkg/client/apiutil/restmapper_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/client/apiutil

     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  }
   734  

View as plain text