Skip to content

Commit ded8c4d

Browse files
authored
Use controller-oriented RESTMapper in porch controllers (#3567)
We want to try this out, should be much faster and lighter on the apiserver.
1 parent 37f513e commit ded8c4d

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed

porch/controllers/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/GoogleContainerTools/kpt/porch/controllers/remoterootsyncsets/pkg/controllers/remoterootsyncset"
4444
"github.com/GoogleContainerTools/kpt/porch/controllers/rootsyncsets/pkg/controllers/rootsyncset"
4545
"github.com/GoogleContainerTools/kpt/porch/controllers/workloadidentitybindings/pkg/controllers/workloadidentitybinding"
46+
"github.com/GoogleContainerTools/kpt/porch/pkg/controllerrestmapper"
4647
//+kubebuilder:scaffold:imports
4748
)
4849

@@ -112,6 +113,7 @@ func run(ctx context.Context) error {
112113
LeaderElection: false,
113114
LeaderElectionID: "porch-operators.config.porch.kpt.dev",
114115
LeaderElectionResourceLock: resourcelock.LeasesResourceLock,
116+
MapperProvider: controllerrestmapper.New,
115117
}
116118

117119
ctrl.SetLogger(klogr.New())
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controllerrestmapper
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
"sync"
21+
22+
apierrors "k8s.io/apimachinery/pkg/api/errors"
23+
"k8s.io/apimachinery/pkg/api/meta"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
"k8s.io/client-go/discovery"
26+
"k8s.io/klog/v2"
27+
"sigs.k8s.io/controller-runtime/pkg/log"
28+
)
29+
30+
// cache is our cache of schema information.
31+
type cache struct {
32+
mutex sync.Mutex
33+
groupVersions map[schema.GroupVersion]*cachedGroupVersion
34+
}
35+
36+
// newCache is the constructor for a cache.
37+
func newCache() *cache {
38+
return &cache{
39+
groupVersions: make(map[schema.GroupVersion]*cachedGroupVersion),
40+
}
41+
}
42+
43+
// cachedGroupVersion caches (all) the resource information for a particular groupversion.
44+
type cachedGroupVersion struct {
45+
gv schema.GroupVersion
46+
mutex sync.Mutex
47+
kinds map[string]cachedGVR
48+
}
49+
50+
// cachedGVR caches the information for a particular resource.
51+
type cachedGVR struct {
52+
Resource string
53+
Scope meta.RESTScope
54+
}
55+
56+
// findRESTMapping returns the RESTMapping for the specified GVK, querying discovery if not cached.
57+
func (c *cache) findRESTMapping(discovery discovery.DiscoveryInterface, gv schema.GroupVersion, kind string) (*meta.RESTMapping, error) {
58+
c.mutex.Lock()
59+
cached := c.groupVersions[gv]
60+
if cached == nil {
61+
cached = &cachedGroupVersion{gv: gv}
62+
c.groupVersions[gv] = cached
63+
}
64+
c.mutex.Unlock()
65+
return cached.findRESTMapping(discovery, kind)
66+
}
67+
68+
// findRESTMapping returns the RESTMapping for the specified GVK, querying discovery if not cached.
69+
func (c *cachedGroupVersion) findRESTMapping(discovery discovery.DiscoveryInterface, kind string) (*meta.RESTMapping, error) {
70+
kinds, err := c.fetch(discovery)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
cached, found := kinds[kind]
76+
if !found {
77+
return nil, nil
78+
}
79+
return &meta.RESTMapping{
80+
Resource: c.gv.WithResource(cached.Resource),
81+
GroupVersionKind: c.gv.WithKind(kind),
82+
Scope: cached.Scope,
83+
}, nil
84+
}
85+
86+
// fetch returns the metadata, fetching it if not cached.
87+
func (c *cachedGroupVersion) fetch(discovery discovery.DiscoveryInterface) (map[string]cachedGVR, error) {
88+
log := log.Log
89+
90+
c.mutex.Lock()
91+
defer c.mutex.Unlock()
92+
93+
if c.kinds != nil {
94+
return c.kinds, nil
95+
}
96+
97+
log.Info("discovering server resources for group/version", "gv", c.gv.String())
98+
resourceList, err := discovery.ServerResourcesForGroupVersion(c.gv.String())
99+
if err != nil {
100+
// We treat "no match" as an empty result, but any other error percolates back up
101+
if meta.IsNoMatchError(err) || apierrors.IsNotFound(err) {
102+
return nil, nil
103+
} else {
104+
klog.Infof("unexpected error from ServerResourcesForGroupVersion(%v): %w", c.gv, err)
105+
return nil, fmt.Errorf("error from ServerResourcesForGroupVersion(%v): %w", c.gv, err)
106+
}
107+
}
108+
109+
kinds := make(map[string]cachedGVR)
110+
for i := range resourceList.APIResources {
111+
resource := resourceList.APIResources[i]
112+
113+
// if we have a slash, then this is a subresource and we shouldn't create mappings for those.
114+
if strings.Contains(resource.Name, "/") {
115+
continue
116+
}
117+
118+
scope := meta.RESTScopeRoot
119+
if resource.Namespaced {
120+
scope = meta.RESTScopeNamespace
121+
}
122+
kinds[resource.Kind] = cachedGVR{
123+
Resource: resource.Name,
124+
Scope: scope,
125+
}
126+
}
127+
c.kinds = kinds
128+
return kinds, nil
129+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package controllerrestmapper
16+
17+
import (
18+
"fmt"
19+
20+
"k8s.io/apimachinery/pkg/api/meta"
21+
"k8s.io/apimachinery/pkg/runtime/schema"
22+
"k8s.io/client-go/discovery"
23+
"k8s.io/client-go/rest"
24+
)
25+
26+
// New is the constructor for a ControllerRESTMapper
27+
func New(cfg *rest.Config) (meta.RESTMapper, error) {
28+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(cfg)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
return &ControllerRESTMapper{
34+
uncached: discoveryClient,
35+
cache: newCache(),
36+
}, nil
37+
}
38+
39+
// ControllerRESTMapper is a meta.RESTMapper that is optimized for controllers.
40+
// It caches results in memory, and minimizes discovery because we don't need shortnames etc in controllers.
41+
// Controllers primarily need to map from GVK -> GVR.
42+
type ControllerRESTMapper struct {
43+
uncached discovery.DiscoveryInterface
44+
cache *cache
45+
}
46+
47+
var _ meta.RESTMapper = &ControllerRESTMapper{}
48+
49+
// KindFor takes a partial resource and returns the single match. Returns an error if there are multiple matches
50+
func (m *ControllerRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
51+
return schema.GroupVersionKind{}, fmt.Errorf("ControllerRESTMaper does not support KindFor operation")
52+
}
53+
54+
// KindsFor takes a partial resource and returns the list of potential kinds in priority order
55+
func (m *ControllerRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
56+
return nil, fmt.Errorf("ControllerRESTMaper does not support KindsFor operation")
57+
}
58+
59+
// ResourceFor takes a partial resource and returns the single match. Returns an error if there are multiple matches
60+
func (m *ControllerRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
61+
return schema.GroupVersionResource{}, fmt.Errorf("ControllerRESTMaper does not support ResourceFor operation")
62+
}
63+
64+
// ResourcesFor takes a partial resource and returns the list of potential resource in priority order
65+
func (m *ControllerRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
66+
return nil, fmt.Errorf("ControllerRESTMaper does not support ResourcesFor operation")
67+
}
68+
69+
// RESTMapping identifies a preferred resource mapping for the provided group kind.
70+
func (m *ControllerRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
71+
for _, version := range versions {
72+
gv := schema.GroupVersion{Group: gk.Group, Version: version}
73+
mapping, err := m.cache.findRESTMapping(m.uncached, gv, gk.Kind)
74+
if err != nil {
75+
return nil, err
76+
}
77+
if mapping != nil {
78+
return mapping, nil
79+
}
80+
}
81+
82+
return nil, &meta.NoKindMatchError{GroupKind: gk, SearchedVersions: versions}
83+
}
84+
85+
// RESTMappings returns all resource mappings for the provided group kind if no
86+
// version search is provided. Otherwise identifies a preferred resource mapping for
87+
// the provided version(s).
88+
func (m *ControllerRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
89+
return nil, fmt.Errorf("ControllerRESTMaper does not support RESTMappings operation")
90+
}
91+
92+
func (m *ControllerRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
93+
return "", fmt.Errorf("ControllerRESTMaper does not support ResourceSingularizer operation")
94+
}

0 commit comments

Comments
 (0)