Skip to content

Commit 03c6359

Browse files
authored
Implement SharedFieldIndexer (#56)
SharedFieldIndexer allows sharing registrations and field indexations for different types, fields and functions. Created test cases for the most relevant scenarios.
1 parent 4fc5812 commit 03c6359

File tree

5 files changed

+431
-2
lines changed

5 files changed

+431
-2
lines changed

clientutils/fieldindexer.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2022 OnMetal authors
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 clientutils
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
28+
)
29+
30+
// SharedFieldIndexer allows registering and calling field index functions shared by different users.
31+
type SharedFieldIndexer struct {
32+
indexer client.FieldIndexer
33+
*sharedFieldIndexerMap
34+
}
35+
36+
// NewSharedFieldIndexer creates a new SharedFieldIndexer.
37+
func NewSharedFieldIndexer(indexer client.FieldIndexer, scheme *runtime.Scheme) *SharedFieldIndexer {
38+
return &SharedFieldIndexer{
39+
indexer: indexer,
40+
sharedFieldIndexerMap: newSharedFieldIndexerMap(scheme),
41+
}
42+
}
43+
44+
// Register registers the client.IndexerFunc for the given client.Object and field.
45+
func (s *SharedFieldIndexer) Register(obj client.Object, field string, extractValue client.IndexerFunc) error {
46+
updated, err := s.setIfNotPresent(obj, field, extractValue)
47+
if err != nil {
48+
return err
49+
}
50+
if !updated {
51+
return fmt.Errorf("indexer for type %T field %s already registered", obj, field)
52+
}
53+
return nil
54+
}
55+
56+
// MustRegister registers the client.IndexerFunc for the given client.Object and field.
57+
func (s *SharedFieldIndexer) MustRegister(obj client.Object, field string, extractValue client.IndexerFunc) {
58+
utilruntime.Must(s.Register(obj, field, extractValue))
59+
}
60+
61+
// IndexField calls a registered client.IndexerFunc for the given client.Object and field.
62+
// If the object / field is unknown or its GVK could not be determined, it errors.
63+
func (s *SharedFieldIndexer) IndexField(ctx context.Context, obj client.Object, field string) error {
64+
entry, err := s.get(obj, field)
65+
if err != nil {
66+
return err
67+
}
68+
69+
if entry == nil {
70+
return fmt.Errorf("unknown field %s for type %T", field, obj)
71+
}
72+
if entry.initialized {
73+
return nil
74+
}
75+
if err := s.indexer.IndexField(ctx, obj, field, entry.extractValue); err != nil {
76+
return err
77+
}
78+
entry.initialized = true
79+
return nil
80+
}
81+
82+
type sharedFieldIndexerMap struct {
83+
scheme *runtime.Scheme
84+
unstructured *specificSharedFieldIndexerMap
85+
metadata *specificSharedFieldIndexerMap
86+
structured *specificSharedFieldIndexerMap
87+
}
88+
89+
func newSharedFieldIndexerMap(scheme *runtime.Scheme) *sharedFieldIndexerMap {
90+
return &sharedFieldIndexerMap{
91+
scheme: scheme,
92+
unstructured: newSpecificSharedFieldIndexerMap(),
93+
metadata: newSpecificSharedFieldIndexerMap(),
94+
structured: newSpecificSharedFieldIndexerMap(),
95+
}
96+
}
97+
98+
type mapEntry struct {
99+
initialized bool
100+
extractValue client.IndexerFunc
101+
}
102+
103+
type specificSharedFieldIndexerMap struct {
104+
gvkToNameToEntry map[schema.GroupVersionKind]map[string]*mapEntry
105+
}
106+
107+
func newSpecificSharedFieldIndexerMap() *specificSharedFieldIndexerMap {
108+
return &specificSharedFieldIndexerMap{gvkToNameToEntry: make(map[schema.GroupVersionKind]map[string]*mapEntry)}
109+
}
110+
111+
func (s *specificSharedFieldIndexerMap) get(gvk schema.GroupVersionKind, name string) *mapEntry {
112+
return s.gvkToNameToEntry[gvk][name]
113+
}
114+
115+
func (s *specificSharedFieldIndexerMap) setIfNotPresent(gvk schema.GroupVersionKind, name string, extractValue client.IndexerFunc) (updated bool) {
116+
nameToEntry := s.gvkToNameToEntry[gvk]
117+
if nameToEntry == nil {
118+
nameToEntry = make(map[string]*mapEntry)
119+
s.gvkToNameToEntry[gvk] = nameToEntry
120+
}
121+
122+
if _, ok := nameToEntry[name]; ok {
123+
return false
124+
}
125+
nameToEntry[name] = &mapEntry{extractValue: extractValue}
126+
return true
127+
}
128+
129+
func (s *sharedFieldIndexerMap) mapFor(obj client.Object) (*specificSharedFieldIndexerMap, schema.GroupVersionKind, error) {
130+
gvk, err := apiutil.GVKForObject(obj, s.scheme)
131+
if err != nil {
132+
return nil, schema.GroupVersionKind{}, err
133+
}
134+
135+
switch obj.(type) {
136+
case *unstructured.Unstructured:
137+
return s.unstructured, gvk, nil
138+
case *metav1.PartialObjectMetadata:
139+
return s.metadata, gvk, nil
140+
default:
141+
return s.structured, gvk, nil
142+
}
143+
}
144+
145+
func (s *sharedFieldIndexerMap) get(obj client.Object, name string) (*mapEntry, error) {
146+
m, gvk, err := s.mapFor(obj)
147+
if err != nil {
148+
return nil, err
149+
}
150+
151+
return m.get(gvk, name), nil
152+
}
153+
154+
func (s *sharedFieldIndexerMap) setIfNotPresent(obj client.Object, name string, extractValue client.IndexerFunc) (updated bool, err error) {
155+
m, gvk, err := s.mapFor(obj)
156+
if err != nil {
157+
return false, err
158+
}
159+
160+
return m.setIfNotPresent(gvk, name, extractValue), nil
161+
}

clientutils/fieldindexer_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2021 OnMetal authors
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 clientutils
16+
17+
import (
18+
"context"
19+
20+
"github.com/golang/mock/gomock"
21+
mockclient "github.com/onmetal/controller-utils/mock/controller-runtime/client"
22+
. "github.com/onsi/ginkgo"
23+
. "github.com/onsi/gomega"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
"k8s.io/client-go/kubernetes/scheme"
28+
"sigs.k8s.io/controller-runtime/pkg/client"
29+
)
30+
31+
var _ = Describe("FieldIndexer", func() {
32+
var (
33+
ctx context.Context
34+
ctrl *gomock.Controller
35+
)
36+
BeforeEach(func() {
37+
ctx = context.Background()
38+
ctrl = gomock.NewController(GinkgoT())
39+
})
40+
AfterEach(func() {
41+
ctrl.Finish()
42+
})
43+
44+
Context("SharedFieldIndexer", func() {
45+
var (
46+
fieldIndexer *mockclient.MockFieldIndexer
47+
)
48+
BeforeEach(func() {
49+
fieldIndexer = mockclient.NewMockFieldIndexer(ctrl)
50+
})
51+
52+
It("should register an indexer func and call it", func() {
53+
f := mockclient.NewMockIndexerFunc(ctrl)
54+
gomock.InOrder(
55+
fieldIndexer.EXPECT().IndexField(ctx, &corev1.Pod{}, ".spec", gomock.Any()).Do(
56+
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
57+
f(obj)
58+
return nil
59+
}),
60+
f.EXPECT().Call(&corev1.Pod{}).Times(1),
61+
)
62+
63+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
64+
65+
Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())
66+
67+
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
68+
})
69+
70+
It("should error if a field is indexed twice", func() {
71+
f := mockclient.NewMockIndexerFunc(ctrl)
72+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
73+
74+
Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())
75+
Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(MatchError("indexer for type *v1.Pod field .spec already registered"))
76+
})
77+
78+
It("should call the index function only once", func() {
79+
f := mockclient.NewMockIndexerFunc(ctrl)
80+
gomock.InOrder(
81+
fieldIndexer.EXPECT().IndexField(ctx, &corev1.Pod{}, ".spec", gomock.Any()).Do(
82+
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
83+
f(obj)
84+
return nil
85+
}),
86+
f.EXPECT().Call(&corev1.Pod{}).Times(1),
87+
)
88+
89+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
90+
91+
Expect(idx.Register(&corev1.Pod{}, ".spec", f.Call)).To(Succeed())
92+
93+
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
94+
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
95+
Expect(idx.IndexField(ctx, &corev1.Pod{}, ".spec")).To(Succeed())
96+
})
97+
98+
It("should work with unstructured objects", func() {
99+
pod := &unstructured.Unstructured{
100+
Object: map[string]interface{}{
101+
"apiVersion": "v1",
102+
"kind": "Pod",
103+
},
104+
}
105+
f := mockclient.NewMockIndexerFunc(ctrl)
106+
gomock.InOrder(
107+
fieldIndexer.EXPECT().IndexField(ctx, pod, ".spec", gomock.Any()).Do(
108+
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
109+
f(obj)
110+
return nil
111+
}),
112+
f.EXPECT().Call(pod).Times(1),
113+
)
114+
115+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
116+
117+
Expect(idx.Register(pod, ".spec", f.Call)).To(Succeed())
118+
119+
Expect(idx.IndexField(ctx, pod, ".spec")).To(Succeed())
120+
})
121+
122+
It("should work with partial object metadata", func() {
123+
pod := &metav1.PartialObjectMetadata{
124+
TypeMeta: metav1.TypeMeta{
125+
APIVersion: "v1",
126+
Kind: "Pod",
127+
},
128+
}
129+
f := mockclient.NewMockIndexerFunc(ctrl)
130+
gomock.InOrder(
131+
fieldIndexer.EXPECT().IndexField(ctx, pod, ".spec", gomock.Any()).Do(
132+
func(ctx context.Context, obj client.Object, field string, f client.IndexerFunc) error {
133+
f(obj)
134+
return nil
135+
}),
136+
f.EXPECT().Call(pod).Times(1),
137+
)
138+
139+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
140+
141+
Expect(idx.Register(pod, ".spec", f.Call)).To(Succeed())
142+
143+
Expect(idx.IndexField(ctx, pod, ".spec")).To(Succeed())
144+
})
145+
146+
It("should error if the gvk could not be obtained", func() {
147+
f := mockclient.NewMockIndexerFunc(ctrl)
148+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
149+
150+
type CustomObject struct{ corev1.Pod }
151+
152+
Expect(idx.Register(&CustomObject{}, ".spec", f.Call)).To(HaveOccurred())
153+
})
154+
155+
It("should error if the indexed function is unknown", func() {
156+
idx := NewSharedFieldIndexer(fieldIndexer, scheme.Scheme)
157+
Expect(idx.IndexField(ctx, &corev1.Pod{}, "unknown")).To(HaveOccurred())
158+
})
159+
})
160+
})

mock/controller-runtime/client/doc.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,12 @@
1313
// limitations under the License.
1414

1515
// Package client contains mocks for controller-runtime's client package.
16-
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination mocks.go sigs.k8s.io/controller-runtime/pkg/client Client
16+
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination mocks.go sigs.k8s.io/controller-runtime/pkg/client Client,FieldIndexer
17+
//go:generate go run github.com/golang/mock/mockgen -copyright_file ../../../hack/boilerplate.go.txt -package client -destination funcs.go github.com/onmetal/controller-utils/mock/controller-runtime/client IndexerFunc
1718
package client
19+
20+
import "sigs.k8s.io/controller-runtime/pkg/client"
21+
22+
type IndexerFunc interface {
23+
Call(object client.Object) []string
24+
}

0 commit comments

Comments
 (0)