Skip to content

Commit 9ee63fc

Browse files
authored
Merge pull request kubernetes-sigs#1767 from schrej/feature/komega
✨ Add Kubernetes Gomega extension with to make testing controllers easier
2 parents eb292e5 + c19a6ed commit 9ee63fc

File tree

8 files changed

+563
-5
lines changed

8 files changed

+563
-5
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ require (
88
github.com/go-logr/logr v1.2.0
99
github.com/go-logr/zapr v1.2.0
1010
github.com/onsi/ginkgo v1.16.5
11-
github.com/onsi/gomega v1.17.0
11+
github.com/onsi/gomega v1.18.1
1212
github.com/prometheus/client_golang v1.11.1
1313
github.com/prometheus/client_model v0.2.0
1414
go.uber.org/goleak v1.1.12
1515
go.uber.org/zap v1.19.1
16-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8
16+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
1717
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
1818
gomodules.xyz/jsonpatch/v2 v2.2.0
1919
k8s.io/api v0.23.0

go.sum

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
233233
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
234234
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
235235
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
236+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
236237
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
237238
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
238239
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -353,11 +354,14 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
353354
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
354355
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
355356
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
357+
github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ=
358+
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
356359
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
357360
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
358361
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
359-
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
360362
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
363+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
364+
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
361365
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
362366
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
363367
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -676,8 +680,8 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
676680
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
677681
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
678682
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
679-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk=
680-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
683+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
684+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
681685
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
682686
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
683687
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

pkg/envtest/komega/OWNERS

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
approvers:
2+
- controller-runtime-admins
3+
- controller-runtime-maintainers
4+
- controller-runtime-approvers
5+
- schrej
6+
- JoelSpeed
7+
- sbueringer
8+
reviewers:
9+
- controller-runtime-admins
10+
- controller-runtime-reviewers
11+
- controller-runtime-approvers
12+
- schrej
13+
- JoelSpeed
14+
- sbueringer

pkg/envtest/komega/default.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package komega
2+
3+
import (
4+
"context"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
)
8+
9+
// defaultK is the Komega used by the package global functions.
10+
var defaultK = &komega{ctx: context.Background()}
11+
12+
// SetClient sets the client used by the package global functions.
13+
func SetClient(c client.Client) {
14+
defaultK.client = c
15+
}
16+
17+
// SetContext sets the context used by the package global functions.
18+
func SetContext(c context.Context) {
19+
defaultK.ctx = c
20+
}
21+
22+
func checkDefaultClient() {
23+
if defaultK.client == nil {
24+
panic("Default Komega's client is not set. Use SetClient to set it.")
25+
}
26+
}
27+
28+
// Get returns a function that fetches a resource and returns the occurring error.
29+
// It can be used with gomega.Eventually() like this
30+
// deployment := appsv1.Deployment{ ... }
31+
// gomega.Eventually(komega.Get(&deployment)).To(gomega.Succeed())
32+
// By calling the returned function directly it can also be used with gomega.Expect(komega.Get(...)()).To(...)
33+
func Get(obj client.Object) func() error {
34+
checkDefaultClient()
35+
return defaultK.Get(obj)
36+
}
37+
38+
// List returns a function that lists resources and returns the occurring error.
39+
// It can be used with gomega.Eventually() like this
40+
// deployments := v1.DeploymentList{ ... }
41+
// gomega.Eventually(k.List(&deployments)).To(gomega.Succeed())
42+
// By calling the returned function directly it can also be used as gomega.Expect(k.List(...)()).To(...)
43+
func List(list client.ObjectList, opts ...client.ListOption) func() error {
44+
checkDefaultClient()
45+
return defaultK.List(list, opts...)
46+
}
47+
48+
// Update returns a function that fetches a resource, applies the provided update function and then updates the resource.
49+
// It can be used with gomega.Eventually() like this:
50+
// deployment := appsv1.Deployment{ ... }
51+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
52+
// deployment.Spec.Replicas = 3
53+
// return &deployment
54+
// })).To(gomega.Scucceed())
55+
// By calling the returned function directly it can also be used as gomega.Expect(k.Update(...)()).To(...)
56+
func Update(obj client.Object, f func(), opts ...client.UpdateOption) func() error {
57+
checkDefaultClient()
58+
return defaultK.Update(obj, f, opts...)
59+
}
60+
61+
// UpdateStatus returns a function that fetches a resource, applies the provided update function and then updates the resource's status.
62+
// It can be used with gomega.Eventually() like this:
63+
// deployment := appsv1.Deployment{ ... }
64+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
65+
// deployment.Status.AvailableReplicas = 1
66+
// return &deployment
67+
// })).To(gomega.Scucceed())
68+
// By calling the returned function directly it can also be used as gomega.Expect(k.UpdateStatus(...)()).To(...)
69+
func UpdateStatus(obj client.Object, f func(), opts ...client.UpdateOption) func() error {
70+
checkDefaultClient()
71+
return defaultK.UpdateStatus(obj, f, opts...)
72+
}
73+
74+
// Object returns a function that fetches a resource and returns the object.
75+
// It can be used with gomega.Eventually() like this:
76+
// deployment := appsv1.Deployment{ ... }
77+
// gomega.Eventually(k.Object(&deployment)).To(HaveField("Spec.Replicas", gomega.Equal(pointer.Int32(3))))
78+
// By calling the returned function directly it can also be used as gomega.Expect(k.Object(...)()).To(...)
79+
func Object(obj client.Object) func() (client.Object, error) {
80+
checkDefaultClient()
81+
return defaultK.Object(obj)
82+
}
83+
84+
// ObjectList returns a function that fetches a resource and returns the object.
85+
// It can be used with gomega.Eventually() like this:
86+
// deployments := appsv1.DeploymentList{ ... }
87+
// gomega.Eventually(k.ObjectList(&deployments)).To(HaveField("Items", HaveLen(1)))
88+
// By calling the returned function directly it can also be used as gomega.Expect(k.ObjectList(...)()).To(...)
89+
func ObjectList(list client.ObjectList, opts ...client.ListOption) func() (client.ObjectList, error) {
90+
checkDefaultClient()
91+
return defaultK.ObjectList(list, opts...)
92+
}

pkg/envtest/komega/default_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package komega
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
appsv1 "k8s.io/api/apps/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/utils/pointer"
10+
)
11+
12+
func TestDefaultGet(t *testing.T) {
13+
g := NewWithT(t)
14+
15+
fc := createFakeClient()
16+
SetClient(fc)
17+
18+
fetched := appsv1.Deployment{
19+
ObjectMeta: metav1.ObjectMeta{
20+
Namespace: "default",
21+
Name: "test",
22+
},
23+
}
24+
g.Eventually(Get(&fetched)).Should(Succeed())
25+
26+
g.Expect(*fetched.Spec.Replicas).To(BeEquivalentTo(5))
27+
}
28+
29+
func TestDefaultList(t *testing.T) {
30+
g := NewWithT(t)
31+
32+
fc := createFakeClient()
33+
SetClient(fc)
34+
35+
list := appsv1.DeploymentList{}
36+
g.Eventually(List(&list)).Should(Succeed())
37+
38+
g.Expect(list.Items).To(HaveLen(1))
39+
depl := exampleDeployment()
40+
g.Expect(list.Items[0]).To(And(
41+
HaveField("ObjectMeta.Name", Equal(depl.ObjectMeta.Name)),
42+
HaveField("ObjectMeta.Namespace", Equal(depl.ObjectMeta.Namespace)),
43+
))
44+
}
45+
46+
func TestDefaultUpdate(t *testing.T) {
47+
g := NewWithT(t)
48+
49+
fc := createFakeClient()
50+
SetClient(fc)
51+
52+
updateDeployment := appsv1.Deployment{
53+
ObjectMeta: exampleDeployment().ObjectMeta,
54+
}
55+
g.Eventually(Update(&updateDeployment, func() {
56+
updateDeployment.Annotations = map[string]string{"updated": "true"}
57+
})).Should(Succeed())
58+
59+
fetched := appsv1.Deployment{
60+
ObjectMeta: exampleDeployment().ObjectMeta,
61+
}
62+
g.Expect(Object(&fetched)()).To(HaveField("ObjectMeta.Annotations", HaveKeyWithValue("updated", "true")))
63+
}
64+
65+
func TestDefaultUpdateStatus(t *testing.T) {
66+
g := NewWithT(t)
67+
68+
fc := createFakeClient()
69+
SetClient(fc)
70+
71+
updateDeployment := appsv1.Deployment{
72+
ObjectMeta: exampleDeployment().ObjectMeta,
73+
}
74+
g.Eventually(UpdateStatus(&updateDeployment, func() {
75+
updateDeployment.Status.AvailableReplicas = 1
76+
})).Should(Succeed())
77+
78+
fetched := appsv1.Deployment{
79+
ObjectMeta: exampleDeployment().ObjectMeta,
80+
}
81+
g.Expect(Object(&fetched)()).To(HaveField("Status.AvailableReplicas", BeEquivalentTo(1)))
82+
}
83+
84+
func TestDefaultObject(t *testing.T) {
85+
g := NewWithT(t)
86+
87+
fc := createFakeClient()
88+
SetClient(fc)
89+
90+
fetched := appsv1.Deployment{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Namespace: "default",
93+
Name: "test",
94+
},
95+
}
96+
g.Eventually(Object(&fetched)).Should(And(
97+
Not(BeNil()),
98+
HaveField("Spec.Replicas", Equal(pointer.Int32(5))),
99+
))
100+
}
101+
102+
func TestDefaultObjectList(t *testing.T) {
103+
g := NewWithT(t)
104+
105+
fc := createFakeClient()
106+
SetClient(fc)
107+
108+
list := appsv1.DeploymentList{}
109+
g.Eventually(ObjectList(&list)).Should(And(
110+
Not(BeNil()),
111+
HaveField("Items", And(
112+
HaveLen(1),
113+
ContainElement(HaveField("Spec.Replicas", Equal(pointer.Int32(5)))),
114+
)),
115+
))
116+
}

pkg/envtest/komega/interfaces.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
)
24+
25+
// Komega is a collection of utilites for writing tests involving a mocked
26+
// Kubernetes API.
27+
type Komega interface {
28+
// Get returns a function that fetches a resource and returns the occurring error.
29+
// It can be used with gomega.Eventually() like this
30+
// deployment := appsv1.Deployment{ ... }
31+
// gomega.Eventually(k.Get(&deployment)).To(gomega.Succeed())
32+
// By calling the returned function directly it can also be used with gomega.Expect(k.Get(...)()).To(...)
33+
Get(client.Object) func() error
34+
35+
// List returns a function that lists resources and returns the occurring error.
36+
// It can be used with gomega.Eventually() like this
37+
// deployments := v1.DeploymentList{ ... }
38+
// gomega.Eventually(k.List(&deployments)).To(gomega.Succeed())
39+
// By calling the returned function directly it can also be used as gomega.Expect(k.List(...)()).To(...)
40+
List(client.ObjectList, ...client.ListOption) func() error
41+
42+
// Update returns a function that fetches a resource, applies the provided update function and then updates the resource.
43+
// It can be used with gomega.Eventually() like this:
44+
// deployment := appsv1.Deployment{ ... }
45+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
46+
// deployment.Spec.Replicas = 3
47+
// return &deployment
48+
// })).To(gomega.Scucceed())
49+
// By calling the returned function directly it can also be used as gomega.Expect(k.Update(...)()).To(...)
50+
Update(client.Object, func(), ...client.UpdateOption) func() error
51+
52+
// UpdateStatus returns a function that fetches a resource, applies the provided update function and then updates the resource's status.
53+
// It can be used with gomega.Eventually() like this:
54+
// deployment := appsv1.Deployment{ ... }
55+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
56+
// deployment.Status.AvailableReplicas = 1
57+
// return &deployment
58+
// })).To(gomega.Scucceed())
59+
// By calling the returned function directly it can also be used as gomega.Expect(k.UpdateStatus(...)()).To(...)
60+
UpdateStatus(client.Object, func(), ...client.UpdateOption) func() error
61+
62+
// Object returns a function that fetches a resource and returns the object.
63+
// It can be used with gomega.Eventually() like this:
64+
// deployment := appsv1.Deployment{ ... }
65+
// gomega.Eventually(k.Object(&deployment)).To(HaveField("Spec.Replicas", gomega.Equal(pointer.Int32(3))))
66+
// By calling the returned function directly it can also be used as gomega.Expect(k.Object(...)()).To(...)
67+
Object(client.Object) func() (client.Object, error)
68+
69+
// ObjectList returns a function that fetches a resource and returns the object.
70+
// It can be used with gomega.Eventually() like this:
71+
// deployments := appsv1.DeploymentList{ ... }
72+
// gomega.Eventually(k.ObjectList(&deployments)).To(HaveField("Items", HaveLen(1)))
73+
// By calling the returned function directly it can also be used as gomega.Expect(k.ObjectList(...)()).To(...)
74+
ObjectList(client.ObjectList, ...client.ListOption) func() (client.ObjectList, error)
75+
76+
// WithContext returns a copy that uses the given context.
77+
WithContext(context.Context) Komega
78+
}

0 commit comments

Comments
 (0)