Skip to content

Commit 714fbf6

Browse files
JoelSpeedschrej
authored andcommitted
Add Komega matcher and interfaces
This adds a utility that is intended to be used with envtest to make it easier for users to write tests. The Matcher wraps common operations that you might do with gomega when interacting with Kubernetes to allow simpler test assertions.
1 parent e52a8b1 commit 714fbf6

File tree

2 files changed

+294
-0
lines changed

2 files changed

+294
-0
lines changed

pkg/envtest/komega/interfaces.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
Copyright 2021 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 komega
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
"github.com/onsi/gomega"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
)
27+
28+
// Komega is the root interface that the Matcher implements.
29+
type Komega interface {
30+
KomegaAsync
31+
KomegaSync
32+
WithContext(context.Context) Komega
33+
}
34+
35+
// KomegaSync is the interface for any sync assertions that
36+
// the matcher implements.
37+
type KomegaSync interface {
38+
Create(client.Object, ...client.CreateOption) gomega.GomegaAssertion
39+
Delete(client.Object, ...client.DeleteOption) gomega.GomegaAssertion
40+
WithExtras(...interface{}) KomegaSync
41+
}
42+
43+
// KomegaAsync is the interface for any async assertions that
44+
// the matcher implements.
45+
type KomegaAsync interface {
46+
Consistently(runtime.Object, ...client.ListOption) gomega.AsyncAssertion
47+
Eventually(runtime.Object, ...client.ListOption) gomega.AsyncAssertion
48+
Get(client.Object) gomega.AsyncAssertion
49+
List(client.ObjectList, ...client.ListOption) gomega.AsyncAssertion
50+
Update(client.Object, UpdateFunc, ...client.UpdateOption) gomega.AsyncAssertion
51+
UpdateStatus(client.Object, UpdateFunc, ...client.UpdateOption) gomega.AsyncAssertion
52+
WithTimeout(time.Duration) KomegaAsync
53+
WithPollInterval(time.Duration) KomegaAsync
54+
}
55+
56+
// UpdateFunc modifies the object fetched from the API server before sending
57+
// the update
58+
type UpdateFunc func(client.Object) client.Object

pkg/envtest/komega/matcher.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
Copyright 2021 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 komega
18+
19+
import (
20+
"context"
21+
"time"
22+
23+
"github.com/onsi/gomega"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/types"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
)
28+
29+
// Matcher has Gomega Matchers that use the controller-runtime client.
30+
type Matcher struct {
31+
Client client.Client
32+
ctx context.Context
33+
extras []interface{}
34+
timeout time.Duration
35+
pollInterval time.Duration
36+
}
37+
38+
// WithContext sets the context to be used for the underlying client
39+
// during assertions.
40+
func (m *Matcher) WithContext(ctx context.Context) Komega {
41+
m.ctx = ctx
42+
return m
43+
}
44+
45+
// context returns the matcher context if one has been set.
46+
// Else it returns the context.TODO().
47+
func (m *Matcher) context() context.Context {
48+
if m.ctx == nil {
49+
return context.TODO()
50+
}
51+
return m.ctx
52+
}
53+
54+
// WithExtras sets extra arguments for sync assertions.
55+
// Any extras passed will be expected to be nil during assertion.
56+
func (m *Matcher) WithExtras(extras ...interface{}) KomegaSync {
57+
m.extras = extras
58+
return m
59+
}
60+
61+
// WithTimeout sets the timeout for any async assertions.
62+
func (m *Matcher) WithTimeout(timeout time.Duration) KomegaAsync {
63+
m.timeout = timeout
64+
return m
65+
}
66+
67+
// WithPollInterval sets the poll interval for any async assertions.
68+
// Note: This will only work if an explicit timeout has been set with WithTimeout.
69+
func (m *Matcher) WithPollInterval(pollInterval time.Duration) KomegaAsync {
70+
m.pollInterval = pollInterval
71+
return m
72+
}
73+
74+
// intervals constructs the intervals for async assertions.
75+
// If no timeout is set, the list will be empty.
76+
func (m *Matcher) intervals() []interface{} {
77+
if m.timeout == 0 {
78+
return []interface{}{}
79+
}
80+
out := []interface{}{m.timeout}
81+
if m.pollInterval != 0 {
82+
out = append(out, m.pollInterval)
83+
}
84+
return out
85+
}
86+
87+
// Create creates the object on the API server.
88+
func (m *Matcher) Create(obj client.Object, opts ...client.CreateOption) gomega.GomegaAssertion {
89+
err := m.Client.Create(m.context(), obj, opts...)
90+
return gomega.Expect(err, m.extras...)
91+
}
92+
93+
// Delete deletes the object from the API server.
94+
func (m *Matcher) Delete(obj client.Object, opts ...client.DeleteOption) gomega.GomegaAssertion {
95+
err := m.Client.Delete(m.context(), obj, opts...)
96+
return gomega.Expect(err, m.extras...)
97+
}
98+
99+
// Update udpates the object on the API server by fetching the object
100+
// and applying a mutating UpdateFunc before sending the update.
101+
func (m *Matcher) Update(obj client.Object, fn UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion {
102+
key := types.NamespacedName{
103+
Name: obj.GetName(),
104+
Namespace: obj.GetNamespace(),
105+
}
106+
update := func() error {
107+
err := m.Client.Get(m.context(), key, obj)
108+
if err != nil {
109+
return err
110+
}
111+
return m.Client.Update(m.context(), fn(obj), opts...)
112+
}
113+
return gomega.Eventually(update, m.intervals()...)
114+
}
115+
116+
// UpdateStatus udpates the object's status subresource on the API server by
117+
// fetching the object and applying a mutating UpdateFunc before sending the
118+
// update.
119+
func (m *Matcher) UpdateStatus(obj client.Object, fn UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion {
120+
key := types.NamespacedName{
121+
Name: obj.GetName(),
122+
Namespace: obj.GetNamespace(),
123+
}
124+
update := func() error {
125+
err := m.Client.Get(m.context(), key, obj)
126+
if err != nil {
127+
return err
128+
}
129+
return m.Client.Status().Update(m.context(), fn(obj), opts...)
130+
}
131+
return gomega.Eventually(update, m.intervals()...)
132+
}
133+
134+
// Get gets the object from the API server.
135+
func (m *Matcher) Get(obj client.Object) gomega.GomegaAsyncAssertion {
136+
key := types.NamespacedName{
137+
Name: obj.GetName(),
138+
Namespace: obj.GetNamespace(),
139+
}
140+
get := func() error {
141+
return m.Client.Get(m.context(), key, obj)
142+
}
143+
return gomega.Eventually(get, m.intervals()...)
144+
}
145+
146+
// List gets the list object from the API server.
147+
func (m *Matcher) List(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion {
148+
list := func() error {
149+
return m.Client.List(m.context(), obj, opts...)
150+
}
151+
return gomega.Eventually(list, m.intervals()...)
152+
}
153+
154+
// Consistently continually gets the object from the API for comparison.
155+
// It can be used to check for either List types or regular Objects.
156+
func (m *Matcher) Consistently(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion {
157+
// If the object is a list, return a list
158+
if o, ok := obj.(client.ObjectList); ok {
159+
return m.consistentlyList(o, opts...)
160+
}
161+
if o, ok := obj.(client.Object); ok {
162+
return m.consistentlyObject(o)
163+
}
164+
//Should not get here
165+
panic("Unknown object.")
166+
}
167+
168+
// consistentlyclient.Object gets an individual object from the API server.
169+
func (m *Matcher) consistentlyObject(obj client.Object) gomega.GomegaAsyncAssertion {
170+
key := types.NamespacedName{
171+
Name: obj.GetName(),
172+
Namespace: obj.GetNamespace(),
173+
}
174+
get := func() client.Object {
175+
err := m.Client.Get(m.context(), key, obj)
176+
if err != nil {
177+
panic(err)
178+
}
179+
return obj
180+
}
181+
return gomega.Consistently(get, m.intervals()...)
182+
}
183+
184+
// consistentlyList gets an list of objects from the API server.
185+
func (m *Matcher) consistentlyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion {
186+
list := func() client.ObjectList {
187+
err := m.Client.List(m.context(), obj, opts...)
188+
if err != nil {
189+
panic(err)
190+
}
191+
return obj
192+
}
193+
return gomega.Consistently(list, m.intervals()...)
194+
}
195+
196+
// Eventually continually gets the object from the API for comparison.
197+
// It can be used to check for either List types or regular Objects.
198+
func (m *Matcher) Eventually(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion {
199+
// If the object is a list, return a list
200+
if o, ok := obj.(client.ObjectList); ok {
201+
return m.eventuallyList(o, opts...)
202+
}
203+
if o, ok := obj.(client.Object); ok {
204+
return m.eventuallyObject(o)
205+
}
206+
//Should not get here
207+
panic("Unknown object.")
208+
}
209+
210+
// eventuallyObject gets an individual object from the API server.
211+
func (m *Matcher) eventuallyObject(obj client.Object) gomega.GomegaAsyncAssertion {
212+
key := types.NamespacedName{
213+
Name: obj.GetName(),
214+
Namespace: obj.GetNamespace(),
215+
}
216+
get := func() client.Object {
217+
err := m.Client.Get(m.context(), key, obj)
218+
if err != nil {
219+
panic(err)
220+
}
221+
return obj
222+
}
223+
return gomega.Eventually(get, m.intervals()...)
224+
}
225+
226+
// eventuallyList gets a list type from the API server.
227+
func (m *Matcher) eventuallyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion {
228+
list := func() client.ObjectList {
229+
err := m.Client.List(m.context(), obj, opts...)
230+
if err != nil {
231+
panic(err)
232+
}
233+
return obj
234+
}
235+
return gomega.Eventually(list, m.intervals()...)
236+
}

0 commit comments

Comments
 (0)