Skip to content

Commit b743ef2

Browse files
committed
pkg/client/apiutil: add a dynamic RESTMapper will reload the delegated
meta.RESTMapper on a cache miss. API calls will return ErrRateLimited if a rate limit is hit. pkg/manager: use dynamic RESTMapper as default.
1 parent 084b73e commit b743ef2

File tree

5 files changed

+247
-3
lines changed

5 files changed

+247
-3
lines changed

Gopkg.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ require (
3333
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac // indirect
3434
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be // indirect
3535
golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect
36-
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect
36+
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2
37+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7
3738
gomodules.xyz/jsonpatch/v2 v2.0.0
3839
google.golang.org/appengine v1.1.0 // indirect
3940
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect

go.sum

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
8787
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
8888
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
8989
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
90-
gomodules.xyz/jsonpatch/v2 v2.0.0 h1:OyHbl+7IOECpPKfVK42oFr6N7+Y2dR+Jsb/IiDV3hOo=
90+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
91+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
92+
gomodules.xyz/jsonpatch/v2 v2.0.0 h1:lHNQverf0+Gm1TbSbVIDWVXOhZ2FpZopxRqpr2uIjs4=
9193
gomodules.xyz/jsonpatch/v2 v2.0.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU=
9294
google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs=
9395
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/*
2+
Copyright 2019 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
18+
19+
import (
20+
"time"
21+
22+
"golang.org/x/time/rate"
23+
"golang.org/x/xerrors"
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
27+
"k8s.io/client-go/discovery"
28+
"k8s.io/client-go/rest"
29+
"k8s.io/client-go/restmapper"
30+
)
31+
32+
// ErrRateLimited is returned by a dynamicRESTMapper method if the number
33+
// of API calls has exceeded a limit within a certain time period.
34+
type ErrRateLimited struct {
35+
// Duration to wait until the next API call can be made.
36+
Delay time.Duration
37+
}
38+
39+
func (e ErrRateLimited) Error() string {
40+
return "too many API calls to the dynamicRESTMapper within a timeframe"
41+
}
42+
43+
// DelayIfRateLimited returns the delay time until the next API call is
44+
// allowed and true if err is of type ErrRateLimited. The zero
45+
// time.Duration value and false are returned if err is not a ErrRateLimited.
46+
func DelayIfRateLimited(err error) (time.Duration, bool) {
47+
var rlerr ErrRateLimited
48+
if xerrors.As(err, &rlerr) {
49+
return rlerr.Delay, true
50+
}
51+
return 0, false
52+
}
53+
54+
// dynamicRESTMapper is a RESTMapper that dynamically discovers resource
55+
// types at runtime.
56+
type dynamicRESTMapper struct {
57+
client discovery.DiscoveryInterface
58+
staticMapper meta.RESTMapper
59+
limiter *dynamicLimiter
60+
lazy bool
61+
}
62+
63+
// WithLimiter sets the RESTMapper's underlying limiter to lim.
64+
func WithLimiter(lim *rate.Limiter) func(*dynamicRESTMapper) error {
65+
return func(drm *dynamicRESTMapper) error {
66+
drm.limiter = &dynamicLimiter{lim}
67+
return nil
68+
}
69+
}
70+
71+
// WithLazyDiscovery prevents the RESTMapper from discovering REST mappings
72+
// until an API call is made.
73+
var WithLazyDiscovery = func(drm *dynamicRESTMapper) error {
74+
drm.lazy = true
75+
return nil
76+
}
77+
78+
// NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic
79+
// RESTMapper dynamically discovers resource types at runtime. opts
80+
// configure the RESTMapper.
81+
func NewDynamicRESTMapper(cfg *rest.Config, opts ...func(*dynamicRESTMapper) error) (meta.RESTMapper, error) {
82+
client, err := discovery.NewDiscoveryClientForConfig(cfg)
83+
if err != nil {
84+
return nil, err
85+
}
86+
drm := &dynamicRESTMapper{
87+
client: client,
88+
limiter: &dynamicLimiter{
89+
rate.NewLimiter(rate.Limit(defaultLimitRate), defaultLimitSize),
90+
},
91+
}
92+
for _, opt := range opts {
93+
if err = opt(drm); err != nil {
94+
return nil, err
95+
}
96+
}
97+
if !drm.lazy {
98+
if err := drm.setStaticMapper(); err != nil {
99+
return nil, err
100+
}
101+
}
102+
return drm, nil
103+
}
104+
105+
var (
106+
// defaultLimitRate is the number of RESTMapper API calls allowed
107+
// per second assuming the rate of API calls <= defaultLimitRate.
108+
defaultLimitRate = 600
109+
// defaultLimitSize is the maximum number of simultaneous RESTMapper
110+
// API calls allowed.
111+
defaultLimitSize = 5
112+
)
113+
114+
// setStaticMapper sets drm's staticMapper by querying its client.
115+
func (drm *dynamicRESTMapper) setStaticMapper() error {
116+
groupResources, err := restmapper.GetAPIGroupResources(drm.client)
117+
if err != nil {
118+
return err
119+
}
120+
drm.staticMapper = restmapper.NewDiscoveryRESTMapper(groupResources)
121+
return nil
122+
}
123+
124+
// reload reloads the static RESTMapper, and will return an error only
125+
// if a rate limit has been hit.
126+
func (drm *dynamicRESTMapper) reload() error {
127+
if err := drm.limiter.checkRate(); err != nil {
128+
return err
129+
}
130+
if err := drm.setStaticMapper(); err != nil {
131+
utilruntime.HandleError(err)
132+
}
133+
return nil
134+
}
135+
136+
func (drm *dynamicRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
137+
gvk, err := drm.staticMapper.KindFor(resource)
138+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
139+
if rerr := drm.reload(); rerr != nil {
140+
return schema.GroupVersionKind{}, rerr
141+
}
142+
gvk, err = drm.staticMapper.KindFor(resource)
143+
}
144+
return gvk, err
145+
}
146+
147+
func (drm *dynamicRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
148+
gvks, err := drm.staticMapper.KindsFor(resource)
149+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
150+
if rerr := drm.reload(); rerr != nil {
151+
return nil, rerr
152+
}
153+
gvks, err = drm.staticMapper.KindsFor(resource)
154+
}
155+
return gvks, err
156+
}
157+
158+
func (drm *dynamicRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
159+
gvr, err := drm.staticMapper.ResourceFor(input)
160+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
161+
if rerr := drm.reload(); rerr != nil {
162+
return schema.GroupVersionResource{}, rerr
163+
}
164+
gvr, err = drm.staticMapper.ResourceFor(input)
165+
}
166+
return gvr, err
167+
}
168+
169+
func (drm *dynamicRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
170+
gvrs, err := drm.staticMapper.ResourcesFor(input)
171+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
172+
if rerr := drm.reload(); rerr != nil {
173+
return nil, rerr
174+
}
175+
gvrs, err = drm.staticMapper.ResourcesFor(input)
176+
}
177+
return gvrs, err
178+
}
179+
180+
func (drm *dynamicRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
181+
mapping, err := drm.staticMapper.RESTMapping(gk, versions...)
182+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
183+
if rerr := drm.reload(); rerr != nil {
184+
return nil, rerr
185+
}
186+
mapping, err = drm.staticMapper.RESTMapping(gk, versions...)
187+
}
188+
return mapping, err
189+
}
190+
191+
func (drm *dynamicRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
192+
mappings, err := drm.staticMapper.RESTMappings(gk, versions...)
193+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
194+
if rerr := drm.reload(); rerr != nil {
195+
return nil, rerr
196+
}
197+
mappings, err = drm.staticMapper.RESTMappings(gk, versions...)
198+
}
199+
return mappings, err
200+
}
201+
202+
func (drm *dynamicRESTMapper) ResourceSingularizer(resource string) (string, error) {
203+
singular, err := drm.staticMapper.ResourceSingularizer(resource)
204+
if xerrors.Is(err, &meta.NoKindMatchError{}) {
205+
if rerr := drm.reload(); rerr != nil {
206+
return "", rerr
207+
}
208+
singular, err = drm.staticMapper.ResourceSingularizer(resource)
209+
}
210+
return singular, err
211+
}
212+
213+
// dynamicLimiter holds a rate limiter used to throttle chatty RESTMapper users.
214+
type dynamicLimiter struct {
215+
*rate.Limiter
216+
}
217+
218+
// checkRate returns an ErrRateLimited if too many API calls have been made
219+
// within the set limit.
220+
func (b *dynamicLimiter) checkRate() error {
221+
res := b.Reserve()
222+
if res.Delay() == 0 {
223+
return nil
224+
}
225+
return ErrRateLimited{res.Delay()}
226+
}

pkg/manager/manager.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,9 @@ func setOptionsDefaults(options Options) Options {
303303
}
304304

305305
if options.MapperProvider == nil {
306-
options.MapperProvider = apiutil.NewDiscoveryRESTMapper
306+
options.MapperProvider = func(c *rest.Config) (meta.RESTMapper, error) {
307+
return apiutil.NewDynamicRESTMapper(c)
308+
}
307309
}
308310

309311
// Allow newClient to be mocked

0 commit comments

Comments
 (0)