Skip to content

Commit 40b41df

Browse files
g-gastonk8s-infra-cherrypick-robot
authored andcommitted
Clean restmapper cache if a version is notFound
This avoids: - Extra calls to https://host/apis/<group>/<version> when a version seen before and cached in apiGroups is deleted or marked as not served. - Returnning a valid mapping for a cached version that is deleted or not served anymore.
1 parent 11e5a5e commit 40b41df

File tree

2 files changed

+154
-22
lines changed

2 files changed

+154
-22
lines changed

pkg/client/apiutil/restmapper.go

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,23 +182,26 @@ func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) er
182182
Group: metav1.APIGroup{Name: groupName},
183183
VersionedResources: make(map[string][]metav1.APIResource),
184184
}
185-
if _, ok := m.knownGroups[groupName]; ok {
186-
groupResources = m.knownGroups[groupName]
187-
}
188185

189186
// Update information for group resources about versioned resources.
190187
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
191188
groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...)
192189
if err != nil {
193190
return fmt.Errorf("failed to get API group resources: %w", err)
194191
}
192+
193+
if _, ok := m.knownGroups[groupName]; ok {
194+
groupResources = m.knownGroups[groupName]
195+
}
196+
195197
for version, resources := range groupVersionResources {
196198
groupResources.VersionedResources[version.Version] = resources.APIResources
197199
}
198200

199201
// Update information for group resources about the API group by adding new versions.
200202
// Ignore the versions that are already registered.
201-
for _, version := range versions {
203+
for groupVersion := range groupVersionResources {
204+
version := groupVersion.Version
202205
found := false
203206
for _, v := range groupResources.Group.Versions {
204207
if v.Version == version {
@@ -266,6 +269,7 @@ func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error)
266269
}
267270

268271
// fetchGroupVersionResources fetches the resources for the specified group and its versions.
272+
// This method might modify the cache so it needs to be called under the lock.
269273
func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
270274
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
271275
failedGroups := make(map[schema.GroupVersion]error)
@@ -274,9 +278,15 @@ func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string
274278
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
275279

276280
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
277-
if err != nil && !apierrors.IsNotFound(err) {
281+
if apierrors.IsNotFound(err) && m.isGroupVersionCached(groupVersion) {
282+
// If the version is not found, we remove the group from the cache
283+
// so it gets refreshed on the next call.
284+
delete(m.apiGroups, groupName)
285+
delete(m.knownGroups, groupName)
286+
} else if err != nil {
278287
failedGroups[groupVersion] = err
279288
}
289+
280290
if apiResourceList != nil {
281291
// even in case of error, some fallback might have been returned.
282292
groupVersionResources[groupVersion] = apiResourceList
@@ -290,3 +300,13 @@ func (m *mapper) fetchGroupVersionResources(groupName string, versions ...string
290300

291301
return groupVersionResources, nil
292302
}
303+
304+
// isGroupVersionCached checks if a version for a group is cached in the known groups cache.
305+
func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool {
306+
if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
307+
_, cached := cachedGroup.VersionedResources[gv.Version]
308+
return cached
309+
}
310+
311+
return false
312+
}

pkg/client/apiutil/restmapper_test.go

Lines changed: 129 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ import (
2828
gomegatypes "github.com/onsi/gomega/types"
2929

3030
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
31+
apierrors "k8s.io/apimachinery/pkg/api/errors"
3132
"k8s.io/apimachinery/pkg/api/meta"
3233
"k8s.io/apimachinery/pkg/runtime/schema"
3334
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/client-go/discovery"
3436
"k8s.io/client-go/kubernetes/scheme"
3537
"k8s.io/client-go/rest"
3638

@@ -529,23 +531,7 @@ func TestLazyRestMapperProvider(t *testing.T) {
529531
g.Expect(err).NotTo(gmg.HaveOccurred())
530532

531533
// Register another CRD in runtime - "riders.crew.example.com".
532-
533-
crd := &apiextensionsv1.CustomResourceDefinition{}
534-
err = c.Get(context.TODO(), types.NamespacedName{Name: "drivers.crew.example.com"}, crd)
535-
g.Expect(err).NotTo(gmg.HaveOccurred())
536-
g.Expect(crd.Spec.Names.Kind).To(gmg.Equal("Driver"))
537-
538-
newCRD := &apiextensionsv1.CustomResourceDefinition{}
539-
crd.DeepCopyInto(newCRD)
540-
newCRD.Name = "riders.crew.example.com"
541-
newCRD.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{
542-
Kind: "Rider",
543-
Plural: "riders",
544-
}
545-
newCRD.ResourceVersion = ""
546-
547-
// Create the new CRD.
548-
g.Expect(c.Create(context.TODO(), newCRD)).To(gmg.Succeed())
534+
createNewCRD(context.TODO(), g, c, "crew.example.com", "Rider", "riders")
549535

550536
// Wait a bit until the CRD is registered.
551537
g.Eventually(func() error {
@@ -564,6 +550,131 @@ func TestLazyRestMapperProvider(t *testing.T) {
564550
g.Expect(err).NotTo(gmg.HaveOccurred())
565551
g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("rider"))
566552
})
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 - "taxi.inventory.example.com".
575+
group := "inventory.example.com"
576+
kind := "Taxi"
577+
plural := "taxis"
578+
crdName := plural + "." + group
579+
crd := createNewCRD(ctx, g, c, group, kind, plural)
580+
t.Cleanup(func() {
581+
g.Expect(c.Delete(ctx, crd)).To(gmg.Succeed())
582+
})
583+
584+
// Wait until the CRD is registered.
585+
discHTTP, err := rest.HTTPClientFor(restCfg)
586+
g.Expect(err).NotTo(gmg.HaveOccurred())
587+
discClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, discHTTP)
588+
g.Expect(err).NotTo(gmg.HaveOccurred())
589+
g.Eventually(func(g gmg.Gomega) {
590+
_, err = discClient.ServerResourcesForGroupVersion(group + "/v1")
591+
g.Expect(err).NotTo(gmg.HaveOccurred())
592+
}).Should(gmg.Succeed(), "v1 should be available")
593+
594+
// There are no requests before any call
595+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(0))
596+
597+
// Since we don't specify what version we expect, restmapper will fetch them all and search there.
598+
// To fetch a list of available versions
599+
// #1: GET https://host/api
600+
// #2: GET https://host/apis
601+
// Then, for all available versions:
602+
// #3: GET https://host/apis/inventory.example.com/v1
603+
// #4: GET https://host/apis/inventory.example.com/v2
604+
// This should fill the cache for apiGroups and versions.
605+
mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind})
606+
g.Expect(err).NotTo(gmg.HaveOccurred())
607+
g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal(kind))
608+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(4))
609+
crt.Reset() // We reset the counter to check how many additional requests are made later.
610+
611+
// At this point v2 should be cached
612+
_, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v2")
613+
g.Expect(err).NotTo(gmg.HaveOccurred())
614+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(0))
615+
616+
// We update the CRD to only have v1 version.
617+
g.Expect(c.Get(ctx, types.NamespacedName{Name: crdName}, crd)).To(gmg.Succeed())
618+
var v1 apiextensionsv1.CustomResourceDefinitionVersion
619+
for i, version := range crd.Spec.Versions {
620+
if version.Name == "v1" {
621+
crd.Spec.Versions[i].Storage = true
622+
v1 = version
623+
v1.Storage = true
624+
}
625+
}
626+
crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1}
627+
g.Expect(c.Update(ctx, crd)).To(gmg.Succeed())
628+
629+
// We wait until v2 is not available anymore.
630+
g.Eventually(func(g gmg.Gomega) {
631+
_, err = discClient.ServerResourcesForGroupVersion(group + "/v2")
632+
g.Expect(apierrors.IsNotFound(err)).To(gmg.BeTrue(), "v2 should not be available anymore")
633+
}).Should(gmg.Succeed())
634+
635+
// Although v2 is not available anymore, the cache is not invalidated yet so it should return a mapping.
636+
_, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v2")
637+
g.Expect(err).NotTo(gmg.HaveOccurred())
638+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(0))
639+
640+
// We request Limo, which is not in the mapper because it doesn't exist.
641+
// This will trigger a reload of the lazy mapper cache.
642+
// Reloading the cache will read v2 again and since it's not available anymore, it should invalidate the cache.
643+
// #1: GET https://host/apis/inventory.example.com/v1
644+
// #2: GET https://host/apis/inventory.example.com/v2
645+
_, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: "Limo"})
646+
g.Expect(err).To(beNoMatchError())
647+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(2))
648+
crt.Reset()
649+
650+
// Now we request v2 again and it should return an error since the cache was invalidated.
651+
// #1: GET https://host/apis/inventory.example.com/v2
652+
_, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v2")
653+
g.Expect(err).To(beNoMatchError())
654+
g.Expect(crt.GetRequestCount()).To(gmg.Equal(1))
655+
})
656+
}
657+
658+
func createNewCRD(ctx context.Context, g gmg.Gomega, c client.Client, group, kind, plural string) *apiextensionsv1.CustomResourceDefinition {
659+
crd := &apiextensionsv1.CustomResourceDefinition{}
660+
err := c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd)
661+
g.Expect(err).NotTo(gmg.HaveOccurred())
662+
g.Expect(crd.Spec.Names.Kind).To(gmg.Equal("Driver"))
663+
664+
newCRD := &apiextensionsv1.CustomResourceDefinition{}
665+
crd.DeepCopyInto(newCRD)
666+
newCRD.Spec.Group = group
667+
newCRD.Name = plural + "." + group
668+
newCRD.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{
669+
Kind: kind,
670+
Plural: plural,
671+
}
672+
newCRD.ResourceVersion = ""
673+
674+
// Create the new CRD.
675+
g.Expect(c.Create(ctx, newCRD)).To(gmg.Succeed())
676+
677+
return newCRD
567678
}
568679

569680
func beNoMatchError() gomegatypes.GomegaMatcher {
@@ -594,6 +705,7 @@ func (e *errorMatcher) Match(actual interface{}) (success bool, err error) {
594705
func (e *errorMatcher) FailureMessage(actual interface{}) (message string) {
595706
return format.Message(actual, fmt.Sprintf("to be %s error", e.message))
596707
}
708+
597709
func (e *errorMatcher) NegatedFailureMessage(actual interface{}) (message string) {
598710
return format.Message(actual, fmt.Sprintf("not to be %s error", e.message))
599711
}

0 commit comments

Comments
 (0)