Skip to content

Commit b5f1ed8

Browse files
committed
Test Conditional Controllers
1 parent f2cf263 commit b5f1ed8

File tree

5 files changed

+417
-0
lines changed

5 files changed

+417
-0
lines changed

pkg/controller/controller_integration_test.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,27 @@ package controller_test
1818

1919
import (
2020
"context"
21+
goerrors "errors"
22+
"path/filepath"
23+
"time"
2124

2225
appsv1 "k8s.io/api/apps/v1"
2326
corev1 "k8s.io/api/core/v1"
27+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
28+
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
29+
"k8s.io/apimachinery/pkg/api/errors"
30+
"k8s.io/apimachinery/pkg/api/meta"
2431
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
2533
"k8s.io/apimachinery/pkg/runtime/schema"
2634
"k8s.io/apimachinery/pkg/types"
35+
"k8s.io/apimachinery/pkg/util/wait"
2736
"sigs.k8s.io/controller-runtime/pkg/cache"
37+
"sigs.k8s.io/controller-runtime/pkg/client"
2838
"sigs.k8s.io/controller-runtime/pkg/controller"
2939
"sigs.k8s.io/controller-runtime/pkg/controller/controllertest"
40+
foo "sigs.k8s.io/controller-runtime/pkg/controller/testdata/foo/v1"
41+
"sigs.k8s.io/controller-runtime/pkg/envtest"
3042
"sigs.k8s.io/controller-runtime/pkg/handler"
3143
"sigs.k8s.io/controller-runtime/pkg/reconcile"
3244
"sigs.k8s.io/controller-runtime/pkg/source"
@@ -173,6 +185,198 @@ var _ = Describe("controller", func() {
173185
close(done)
174186
}, 5)
175187
})
188+
189+
It("should reconcile when the CRD is installed, uninstalled, reinstalled", func(done Done) {
190+
By("Initializing the scheme and crd")
191+
s := runtime.NewScheme()
192+
err := v1beta1.AddToScheme(s)
193+
Expect(err).NotTo(HaveOccurred())
194+
err = apiextensionsv1.AddToScheme(s)
195+
Expect(err).NotTo(HaveOccurred())
196+
err = foo.AddToScheme(s)
197+
Expect(err).NotTo(HaveOccurred())
198+
options := manager.Options{Scheme: s}
199+
200+
By("Creating the Manager")
201+
cm, err := manager.New(cfg, options)
202+
Expect(err).NotTo(HaveOccurred())
203+
204+
By("Creating the Controller")
205+
instance, err := controller.New("foo-controller", cm, controller.Options{
206+
Reconciler: reconcile.Func(
207+
func(_ context.Context, request reconcile.Request) (reconcile.Result, error) {
208+
reconciled <- request
209+
return reconcile.Result{}, nil
210+
}),
211+
})
212+
Expect(err).NotTo(HaveOccurred())
213+
214+
By("Watching foo CRD as conditional kinds")
215+
f := &foo.Foo{}
216+
gvk := schema.GroupVersionKind{
217+
Group: "bar.example.com",
218+
Version: "v1",
219+
Kind: "Foo",
220+
}
221+
Expect(err).NotTo(HaveOccurred())
222+
existsInDiscovery := func() bool {
223+
resources, err := clientset.Discovery().ServerResourcesForGroupVersion(gvk.GroupVersion().String())
224+
if err != nil {
225+
return false
226+
}
227+
for _, res := range resources.APIResources {
228+
if res.Kind == gvk.Kind {
229+
return true
230+
}
231+
}
232+
return false
233+
}
234+
err = instance.Watch(&source.ConditionalKind{Kind: source.Kind{Type: f}, DiscoveryCheck: existsInDiscovery}, &handler.EnqueueRequestForObject{})
235+
Expect(err).NotTo(HaveOccurred())
236+
237+
By("Starting the Manager")
238+
ctx, cancel := context.WithCancel(context.Background())
239+
defer cancel()
240+
go func() {
241+
defer GinkgoRecover()
242+
Expect(cm.Start(ctx)).NotTo(HaveOccurred())
243+
}()
244+
245+
testFoo := &foo.Foo{
246+
TypeMeta: metav1.TypeMeta{Kind: gvk.Kind, APIVersion: gvk.GroupVersion().String()},
247+
ObjectMeta: metav1.ObjectMeta{Name: "test-foo", Namespace: "default"},
248+
}
249+
250+
expectedReconcileRequest := reconcile.Request{NamespacedName: types.NamespacedName{
251+
Name: "test-foo",
252+
Namespace: "default",
253+
}}
254+
_ = expectedReconcileRequest
255+
256+
By("Failing to create a foo object if the crd isn't installed")
257+
kindMatchErr := &meta.NoKindMatchError{}
258+
err = cm.GetClient().Create(ctx, testFoo)
259+
Expect(goerrors.As(err, &kindMatchErr)).To(BeTrue())
260+
261+
By("Installing the CRD")
262+
crdPath := filepath.Join(".", "testdata", "foo", "foocrd.yaml")
263+
crdOpts := envtest.CRDInstallOptions{
264+
Paths: []string{crdPath},
265+
MaxTime: 50 * time.Millisecond,
266+
PollInterval: 15 * time.Millisecond,
267+
}
268+
crds, err := envtest.InstallCRDs(cfg, crdOpts)
269+
Expect(err).NotTo(HaveOccurred())
270+
Expect(len(crds)).To(Equal(1))
271+
272+
By("Expecting to find the CRD")
273+
crdv1 := &apiextensionsv1.CustomResourceDefinition{}
274+
err = cm.GetClient().Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crdv1)
275+
Expect(err).NotTo(HaveOccurred())
276+
Expect(crdv1.Spec.Names.Kind).To(Equal("Foo"))
277+
278+
err = envtest.WaitForCRDs(cfg, []client.Object{
279+
&v1beta1.CustomResourceDefinition{
280+
Spec: v1beta1.CustomResourceDefinitionSpec{
281+
Group: "bar.example.com",
282+
Names: v1beta1.CustomResourceDefinitionNames{
283+
Kind: "Foo",
284+
Plural: "foos",
285+
},
286+
Versions: []v1beta1.CustomResourceDefinitionVersion{
287+
{
288+
Name: "v1",
289+
Storage: true,
290+
Served: true,
291+
},
292+
}},
293+
},
294+
},
295+
crdOpts,
296+
)
297+
Expect(err).NotTo(HaveOccurred())
298+
299+
By("Invoking Reconcile for foo Create")
300+
err = cm.GetClient().Create(ctx, testFoo)
301+
Expect(err).NotTo(HaveOccurred())
302+
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
303+
304+
By("Uninstalling the CRD")
305+
err = envtest.UninstallCRDs(cfg, crdOpts)
306+
Expect(err).NotTo(HaveOccurred())
307+
// wait for discovery to not recognize the resource after uninstall
308+
wait.PollImmediate(15*time.Millisecond, 50*time.Millisecond, func() (bool, error) {
309+
if _, err := clientset.Discovery().ServerResourcesForGroupVersion(gvk.Group + "/" + gvk.Version); err != nil {
310+
if err.Error() == "the server could not find the requested resource" {
311+
return true, nil
312+
}
313+
}
314+
return false, nil
315+
})
316+
317+
By("Failing create foo object if the crd isn't installed")
318+
errNotFound := errors.NewGenericServerResponse(404, "POST", schema.GroupResource{Group: "bar.example.com", Resource: "foos"}, "", "404 page not found", 0, true)
319+
err = cm.GetClient().Create(ctx, testFoo)
320+
Expect(err).To(Equal(errNotFound))
321+
322+
By("Reinstalling the CRD")
323+
crds, err = envtest.InstallCRDs(cfg, crdOpts)
324+
Expect(err).NotTo(HaveOccurred())
325+
Expect(len(crds)).To(Equal(1))
326+
327+
By("Expecting to find the CRD")
328+
crdv1 = &apiextensionsv1.CustomResourceDefinition{}
329+
err = cm.GetClient().Get(context.TODO(), types.NamespacedName{Name: "foos.bar.example.com"}, crdv1)
330+
Expect(err).NotTo(HaveOccurred())
331+
Expect(crdv1.Spec.Names.Kind).To(Equal("Foo"))
332+
333+
err = envtest.WaitForCRDs(cfg, []client.Object{
334+
&v1beta1.CustomResourceDefinition{
335+
Spec: v1beta1.CustomResourceDefinitionSpec{
336+
Group: "bar.example.com",
337+
Names: v1beta1.CustomResourceDefinitionNames{
338+
Kind: "Foo",
339+
Plural: "foos",
340+
},
341+
Versions: []v1beta1.CustomResourceDefinitionVersion{
342+
{
343+
Name: "v1",
344+
Storage: true,
345+
Served: true,
346+
},
347+
}},
348+
},
349+
},
350+
crdOpts,
351+
)
352+
Expect(err).NotTo(HaveOccurred())
353+
354+
By("Invoking Reconcile for foo Create")
355+
testFoo.ResourceVersion = ""
356+
err = cm.GetClient().Create(ctx, testFoo)
357+
Expect(err).NotTo(HaveOccurred())
358+
Expect(<-reconciled).To(Equal(expectedReconcileRequest))
359+
360+
By("Uninstalling the CRD")
361+
err = envtest.UninstallCRDs(cfg, crdOpts)
362+
Expect(err).NotTo(HaveOccurred())
363+
// wait for discovery to not recognize the resource after uninstall
364+
wait.PollImmediate(15*time.Millisecond, 50*time.Millisecond, func() (bool, error) {
365+
if _, err := clientset.Discovery().ServerResourcesForGroupVersion(gvk.Group + "/" + gvk.Version); err != nil {
366+
if err.Error() == "the server could not find the requested resource" {
367+
return true, nil
368+
}
369+
}
370+
return false, nil
371+
})
372+
373+
By("Failing create foo object if the crd isn't installed")
374+
err = cm.GetClient().Create(ctx, testFoo)
375+
Expect(err).To(Equal(errNotFound))
376+
377+
close(done)
378+
}, 10)
379+
176380
})
177381

178382
func truePtr() *bool {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: apiextensions.k8s.io/v1
2+
kind: CustomResourceDefinition
3+
metadata:
4+
name: foos.bar.example.com
5+
spec:
6+
group: bar.example.com
7+
names:
8+
kind: Foo
9+
plural: foos
10+
scope: Namespaced
11+
versions:
12+
- name: "v1"
13+
storage: true
14+
served: true
15+
schema:
16+
openAPIV3Schema:
17+
type: object
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
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+
16+
package v1
17+
18+
import (
19+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
20+
)
21+
22+
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
23+
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
24+
25+
// FooSpec defines the desired state of Foo
26+
type FooSpec struct {
27+
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
28+
// Important: Run "make" to regenerate code after modifying this file
29+
RunAt string `json:"runAt"`
30+
}
31+
32+
// FooStatus defines the observed state of Foo
33+
type FooStatus struct {
34+
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
35+
// Important: Run "make" to regenerate code after modifying this file
36+
}
37+
38+
// +kubebuilder:object:root=true
39+
40+
// Foo is the Schema for the externaljobs API
41+
type Foo struct {
42+
metav1.TypeMeta `json:",inline"`
43+
metav1.ObjectMeta `json:"metadata,omitempty"`
44+
45+
Spec FooSpec `json:"spec,omitempty"`
46+
Status FooStatus `json:"status,omitempty"`
47+
}
48+
49+
// +kubebuilder:object:root=true
50+
51+
// FooList contains a list of Foo
52+
type FooList struct {
53+
metav1.TypeMeta `json:",inline"`
54+
metav1.ListMeta `json:"metadata,omitempty"`
55+
Items []Foo `json:"items"`
56+
}
57+
58+
func init() {
59+
SchemeBuilder.Register(&Foo{}, &FooList{})
60+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
Copyright 2018 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+
// +kubebuilder:object:generate=true
18+
// +groupName=chaosapps.metamagical.io
19+
package v1
20+
21+
import (
22+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
logf "sigs.k8s.io/controller-runtime/pkg/log"
24+
"sigs.k8s.io/controller-runtime/pkg/scheme"
25+
)
26+
27+
var (
28+
log = logf.Log.WithName("foo-resource")
29+
30+
// SchemeGroupVersion is group version used to register these objects
31+
SchemeGroupVersion = schema.GroupVersion{Group: "bar.example.com", Version: "v1"}
32+
33+
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
34+
SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}
35+
36+
// AddToScheme is required by pkg/client/...
37+
AddToScheme = SchemeBuilder.AddToScheme
38+
)

0 commit comments

Comments
 (0)