Skip to content

Commit a484ef7

Browse files
committed
Add simple operator package
1 parent d5af5c5 commit a484ef7

File tree

6 files changed

+534
-0
lines changed

6 files changed

+534
-0
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
package application
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"k8s.io/apimachinery/pkg/runtime"
24+
"k8s.io/client-go/rest"
25+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
26+
"sigs.k8s.io/controller-runtime/pkg/client/config"
27+
"sigs.k8s.io/controller-runtime/pkg/controller"
28+
"sigs.k8s.io/controller-runtime/pkg/handler"
29+
"sigs.k8s.io/controller-runtime/pkg/manager"
30+
"sigs.k8s.io/controller-runtime/pkg/predicate"
31+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
32+
"sigs.k8s.io/controller-runtime/pkg/source"
33+
)
34+
35+
// application is a simple Controller for a single API type. It will create a Manager for itself
36+
// if one is not provided.
37+
type application struct {
38+
mgr manager.Manager
39+
ctrl controller.Controller
40+
}
41+
42+
// Supporting mocking out functions for testing
43+
var getConfig = config.GetConfig
44+
var newController = controller.New
45+
var newManager = manager.New
46+
var getGvk = apiutil.GVKForObject
47+
48+
// Builder builds an Application Controller (e.g. Operator) and returns a manager.Manager to start it.
49+
type Builder struct {
50+
apiType runtime.Object
51+
mgr manager.Manager
52+
predicates []predicate.Predicate
53+
managedObjects []runtime.Object
54+
config *rest.Config
55+
reconcile reconcile.Reconciler
56+
}
57+
58+
// BuilderFor returns a new Builder for and BuilderFor
59+
func BuilderFor(apiType runtime.Object) *Builder {
60+
return &Builder{apiType: apiType}
61+
}
62+
63+
// Owns configures the Application Controller to respond to create / delete / update events for objects it managedObjects
64+
// - e.g. creates. apiType is an empty instance of an object matching the managed object type.
65+
func (b *Builder) Owns(apiType runtime.Object) *Builder {
66+
b.managedObjects = append(b.managedObjects, apiType)
67+
return b
68+
}
69+
70+
// WithConfig sets the Config to use for configuring clients. Defaults to the in-cluster config or to ~/.kube/config.
71+
func (b *Builder) WithConfig(config *rest.Config) *Builder {
72+
b.config = config
73+
return b
74+
}
75+
76+
// WithManager sets the Manager to use for registering the Controller. Defaults to a new manager.Manager.
77+
func (b *Builder) WithManager(m manager.Manager) *Builder {
78+
b.mgr = m
79+
return b
80+
}
81+
82+
// WithEventFilter sets the event filters, to filter which create/update/delete/generic events eventually
83+
// trigger reconciliations. For example, filtering on whether the resource version has changed.
84+
// Defaults to the empty list.
85+
func (b *Builder) WithEventFilter(p predicate.Predicate) *Builder {
86+
b.predicates = append(b.predicates, p)
87+
return b
88+
}
89+
90+
// WithReconciler sets the Reconcile called in response to create / update / delete events for the
91+
// BuilderFor type or Created object types.
92+
func (b *Builder) WithReconciler(r reconcile.Reconciler) *Builder {
93+
b.reconcile = r
94+
return b
95+
}
96+
97+
// Build builds the Application Controller and returns the Manager used to start it.
98+
func (b *Builder) Build() (manager.Manager, error) {
99+
a := &application{}
100+
101+
if b.reconcile == nil {
102+
return nil, fmt.Errorf("must call WithReconciler to set Reconciler")
103+
}
104+
105+
var err error
106+
if b.config == nil {
107+
b.config, err = getConfig()
108+
if err != nil {
109+
return nil, err
110+
}
111+
}
112+
113+
if b.mgr == nil {
114+
b.mgr, err = newManager(b.config, manager.Options{})
115+
if err != nil {
116+
return nil, err
117+
}
118+
}
119+
120+
a.mgr = b.mgr
121+
122+
gvk, err := getGvk(b.apiType, a.mgr.GetScheme())
123+
if err != nil {
124+
return nil, err
125+
}
126+
127+
// Create the controller
128+
name := fmt.Sprintf("%s-application", strings.ToLower(gvk.Kind))
129+
a.ctrl, err = newController(name, a.mgr, controller.Options{Reconciler: b.reconcile})
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
// Reconcile type
135+
err = a.ctrl.Watch(&source.Kind{Type: b.apiType}, &handler.EnqueueRequestForObject{}, b.predicates...)
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
// Watch the managed types
141+
for _, t := range b.managedObjects {
142+
err = a.ctrl.Watch(&source.Kind{Type: t}, &handler.EnqueueRequestForOwner{
143+
OwnerType: b.apiType, IsController: true}, b.predicates...)
144+
if err != nil {
145+
return nil, err
146+
}
147+
}
148+
149+
return a.mgr, nil
150+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package application
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
"k8s.io/client-go/rest"
9+
"sigs.k8s.io/controller-runtime/pkg/envtest"
10+
logf "sigs.k8s.io/controller-runtime/pkg/runtime/log"
11+
)
12+
13+
func TestSource(t *testing.T) {
14+
RegisterFailHandler(Fail)
15+
RunSpecsWithDefaultAndCustomReporters(t, "application Suite", []Reporter{envtest.NewlineReporter{}})
16+
}
17+
18+
var testenv *envtest.Environment
19+
var cfg *rest.Config
20+
21+
var _ = BeforeSuite(func(done Done) {
22+
logf.SetLogger(logf.ZapLogger(false))
23+
24+
testenv = &envtest.Environment{}
25+
26+
var err error
27+
cfg, err = testenv.Start()
28+
Expect(err).NotTo(HaveOccurred())
29+
30+
close(done)
31+
}, 60)
32+
33+
var _ = AfterSuite(func() {
34+
testenv.Stop()
35+
})
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package application
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
. "github.com/onsi/ginkgo"
8+
. "github.com/onsi/gomega"
9+
appsv1 "k8s.io/api/apps/v1"
10+
corev1 "k8s.io/api/core/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/apimachinery/pkg/runtime/schema"
14+
"k8s.io/apimachinery/pkg/types"
15+
"k8s.io/client-go/rest"
16+
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
17+
"sigs.k8s.io/controller-runtime/pkg/controller"
18+
"sigs.k8s.io/controller-runtime/pkg/manager"
19+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
20+
)
21+
22+
var _ = Describe("application", func() {
23+
var stop chan struct{}
24+
25+
BeforeEach(func() {
26+
stop = make(chan struct{})
27+
getConfig = func() (*rest.Config, error) { return cfg, nil }
28+
newController = controller.New
29+
newManager = manager.New
30+
getGvk = apiutil.GVKForObject
31+
})
32+
33+
AfterEach(func() {
34+
close(stop)
35+
})
36+
37+
noop := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) { return reconcile.Result{}, nil })
38+
39+
Describe("New", func() {
40+
It("should return success if given valid objects", func() {
41+
instance, err := BuilderFor(&appsv1.ReplicaSet{}).Owns(&appsv1.ReplicaSet{}).WithReconciler(noop).Build()
42+
Expect(err).NotTo(HaveOccurred())
43+
Expect(instance).NotTo(BeNil())
44+
})
45+
46+
It("should return an error if the Config is invalid", func() {
47+
getConfig = func() (*rest.Config, error) { return cfg, fmt.Errorf("expected error") }
48+
instance, err := BuilderFor(&appsv1.ReplicaSet{}).Owns(&appsv1.ReplicaSet{}).WithReconciler(noop).Build()
49+
Expect(err).To(HaveOccurred())
50+
Expect(err.Error()).To(ContainSubstring("expected error"))
51+
Expect(instance).To(BeNil())
52+
})
53+
54+
It("should return an error if there is no GVK for an object", func() {
55+
instance, err := BuilderFor(fakeType("")).Owns(&appsv1.ReplicaSet{}).WithReconciler(noop).Build()
56+
Expect(err).To(HaveOccurred())
57+
Expect(err.Error()).To(ContainSubstring("expected pointer, but got application.fakeType"))
58+
Expect(instance).To(BeNil())
59+
60+
instance, err = BuilderFor(&appsv1.ReplicaSet{}).Owns(fakeType("")).WithReconciler(noop).Build()
61+
Expect(err).To(HaveOccurred())
62+
Expect(err.Error()).To(ContainSubstring("expected pointer, but got application.fakeType"))
63+
Expect(instance).To(BeNil())
64+
})
65+
66+
It("should return an error if it cannot create the manager", func() {
67+
newManager = func(config *rest.Config, options manager.Options) (manager.Manager, error) {
68+
return nil, fmt.Errorf("expected error")
69+
}
70+
instance, err := BuilderFor(&appsv1.ReplicaSet{}).Owns(&appsv1.ReplicaSet{}).WithReconciler(noop).Build()
71+
Expect(err).To(HaveOccurred())
72+
Expect(err.Error()).To(ContainSubstring("expected error"))
73+
Expect(instance).To(BeNil())
74+
})
75+
76+
It("should return an error if it cannot create the controller", func() {
77+
newController = func(name string, mgr manager.Manager, options controller.Options) (
78+
controller.Controller, error) {
79+
return nil, fmt.Errorf("expected error")
80+
}
81+
instance, err := BuilderFor(&appsv1.ReplicaSet{}).Owns(&appsv1.ReplicaSet{}).WithReconciler(noop).Build()
82+
Expect(err).To(HaveOccurred())
83+
Expect(err.Error()).To(ContainSubstring("expected error"))
84+
Expect(instance).To(BeNil())
85+
})
86+
87+
It("should return an error if there is no Reconciler", func() {
88+
// Prevent getGVK from failing so watch fails
89+
getGvk = func(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
90+
return schema.GroupVersionKind{Kind: "foo"}, nil
91+
}
92+
instance, err := BuilderFor(&appsv1.ReplicaSet{}).Owns(&appsv1.ReplicaSet{}).Build()
93+
Expect(err).To(HaveOccurred())
94+
Expect(err.Error()).To(ContainSubstring("must call WithReconciler to set Reconciler"))
95+
Expect(instance).To(BeNil())
96+
})
97+
98+
It("should use the provide config", func() {
99+
100+
})
101+
102+
It("should use the provide manager", func() {
103+
104+
})
105+
106+
It("should use the provide filters", func() {
107+
108+
})
109+
})
110+
111+
Describe("Start", func() {
112+
It("should Reconcile objects", func(done Done) {
113+
By("Creating the application")
114+
ch := make(chan reconcile.Request)
115+
fn := reconcile.Func(func(req reconcile.Request) (reconcile.Result, error) {
116+
defer GinkgoRecover()
117+
ch <- req
118+
return reconcile.Result{}, nil
119+
})
120+
121+
instance, err := BuilderFor(&appsv1.Deployment{}).
122+
WithConfig(cfg).
123+
Owns(&appsv1.ReplicaSet{}).
124+
WithReconciler(fn).
125+
Build()
126+
Expect(err).NotTo(HaveOccurred())
127+
128+
By("Starting the application")
129+
go func() {
130+
defer GinkgoRecover()
131+
Expect(instance.Start(stop)).NotTo(HaveOccurred())
132+
By("Stopping the application")
133+
}()
134+
135+
By("Creating a Deployment")
136+
// Expect a Reconcile when the Deployment is managedObjects.
137+
dep := &appsv1.Deployment{
138+
ObjectMeta: metav1.ObjectMeta{
139+
Namespace: "default",
140+
Name: "deploy-name",
141+
},
142+
Spec: appsv1.DeploymentSpec{
143+
Selector: &metav1.LabelSelector{
144+
MatchLabels: map[string]string{"foo": "bar"},
145+
},
146+
Template: corev1.PodTemplateSpec{
147+
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"foo": "bar"}},
148+
Spec: corev1.PodSpec{
149+
Containers: []corev1.Container{
150+
{
151+
Name: "nginx",
152+
Image: "nginx",
153+
},
154+
},
155+
},
156+
},
157+
},
158+
}
159+
err = instance.GetClient().Create(context.TODO(), dep)
160+
Expect(err).NotTo(HaveOccurred())
161+
162+
By("Waiting for the Deployment Reconcile")
163+
Expect(<-ch).To(Equal(reconcile.Request{
164+
NamespacedName: types.NamespacedName{Namespace: "default", Name: "deploy-name"}}))
165+
166+
By("Creating a ReplicaSet")
167+
// Expect a Reconcile when an Owned object is managedObjects.
168+
t := true
169+
rs := &appsv1.ReplicaSet{
170+
ObjectMeta: metav1.ObjectMeta{
171+
Namespace: "default",
172+
Name: "rs-name",
173+
Labels: dep.Spec.Selector.MatchLabels,
174+
OwnerReferences: []metav1.OwnerReference{
175+
{
176+
Name: "deploy-name",
177+
Kind: "Deployment",
178+
APIVersion: "apps/v1",
179+
Controller: &t,
180+
UID: dep.UID,
181+
},
182+
},
183+
},
184+
Spec: appsv1.ReplicaSetSpec{
185+
Selector: dep.Spec.Selector,
186+
Template: dep.Spec.Template,
187+
},
188+
}
189+
err = instance.GetClient().Create(context.TODO(), rs)
190+
Expect(err).NotTo(HaveOccurred())
191+
192+
By("Waiting for the ReplicaSet Reconcile")
193+
Expect(<-ch).To(Equal(reconcile.Request{
194+
NamespacedName: types.NamespacedName{Namespace: "default", Name: "deploy-name"}}))
195+
196+
close(done)
197+
}, 10)
198+
})
199+
})
200+
201+
var _ runtime.Object = fakeType("")
202+
203+
type fakeType string
204+
205+
func (fakeType) GetObjectKind() schema.ObjectKind { return nil }
206+
func (fakeType) DeepCopyObject() runtime.Object { return nil }

0 commit comments

Comments
 (0)