Skip to content

Commit f9ef112

Browse files
add EnqueueRequestForAnnotation
1 parent 4e1e8ed commit f9ef112

File tree

4 files changed

+319
-0
lines changed

4 files changed

+319
-0
lines changed

examples/builtins/main.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package main
1818

1919
import (
20+
"fmt"
2021
"os"
2122

2223
appsv1 "k8s.io/api/apps/v1"
@@ -70,6 +71,14 @@ func main() {
7071
os.Exit(1)
7172
}
7273

74+
// Watch Pods and enqueue by resource annotation key
75+
var pod *corev1.Pod
76+
exampleType := fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
77+
if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForAnnotation{exampleType}); err != nil {
78+
entryLog.Error(err, "unable to watch Pods")
79+
os.Exit(1)
80+
}
81+
7382
// Setup webhooks
7483
entryLog.Info("setting up webhook server")
7584
hookServer := mgr.GetWebhookServer()

pkg/handler/enqueue.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ var _ EventHandler = &EnqueueRequestForObject{}
3333
// Controllers that have associated Resources (e.g. CRDs) to reconcile the associated Resource.
3434
type EnqueueRequestForObject struct{}
3535

36+
const (
37+
// NamespacedNameAnnotation - annotation that will be used to get the primary resource namespaced name.
38+
NamespacedNameAnnotation = "sigs.k8s.io/primary-resource"
39+
40+
// TypeAnnotation - annotation that will be used to verify that the primary resource is the primary resource to use.
41+
TypeAnnotation = "sigs.k8s.io/primary-resource-type"
42+
)
43+
3644
// Create implements EventHandler
3745
func (e *EnqueueRequestForObject) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
3846
if evt.Meta == nil {

pkg/handler/enqueue_annotation.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
Copyright 2020 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 handler
18+
19+
import (
20+
"strings"
21+
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/types"
24+
"k8s.io/client-go/util/workqueue"
25+
"sigs.k8s.io/controller-runtime/pkg/event"
26+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
27+
)
28+
29+
var _ EventHandler = &EnqueueRequestForAnnotation{}
30+
31+
// EnqueueRequestForAnnotation enqueues Requests based on the presence of an annotation that contains the
32+
// namespaced name of the primary resource. The purpose of this handler is to support cross-scope ownership
33+
// relationships that are not supported by native owner references and then, it should only be used in very
34+
// specific circumstances.
35+
//
36+
// Note that an annotation-based watch handler does not have the same owner reference restrictions.
37+
// However, do not provide the same feature set either. Beware that owner references are used for garbage collection
38+
// and then, if a resource's owner is deleted, the resource will also be deleted. However, this scenario cannot be satisfied
39+
// with an annotation-based watch handler and to achieve the same would be required implement finalizers to allow
40+
// the operator manually garbage collect the objects.
41+
//
42+
// The primary use case for this, is to have a controller enqueue requests for the following scenarios
43+
// 1. namespaced primary object and dependent cluster scoped resource
44+
// 2. cluster scoped primary object.
45+
// 3. namespaced primary object and dependent namespaced scoped but in a different namespace object.
46+
type EnqueueRequestForAnnotation struct {
47+
Type string
48+
}
49+
50+
// Create implements EventHandler
51+
func (e *EnqueueRequestForAnnotation) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
52+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
53+
q.Add(req)
54+
}
55+
}
56+
57+
// Update implements EventHandler
58+
func (e *EnqueueRequestForAnnotation) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
59+
if ok, req := e.getAnnotationRequests(evt.MetaOld); ok {
60+
q.Add(req)
61+
}
62+
if ok, req := e.getAnnotationRequests(evt.MetaNew); ok {
63+
q.Add(req)
64+
}
65+
}
66+
67+
// Delete implements EventHandler
68+
func (e *EnqueueRequestForAnnotation) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
69+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
70+
q.Add(req)
71+
}
72+
}
73+
74+
// Generic implements EventHandler
75+
func (e *EnqueueRequestForAnnotation) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
76+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
77+
q.Add(req)
78+
}
79+
}
80+
81+
func (e *EnqueueRequestForAnnotation) getAnnotationRequests(object metav1.Object) (bool, reconcile.Request) {
82+
if typeString, ok := object.GetAnnotations()[TypeAnnotation]; ok && typeString == e.Type {
83+
namespacedNameString, ok := object.GetAnnotations()[NamespacedNameAnnotation]
84+
if !ok {
85+
log.Info("Unable to find namespaced name annotation for resource", "resource", object)
86+
}
87+
if namespacedNameString == "" {
88+
return false, reconcile.Request{}
89+
}
90+
return true, reconcile.Request{NamespacedName: parseNamespacedName(namespacedNameString)}
91+
}
92+
return false, reconcile.Request{}
93+
}
94+
95+
func parseNamespacedName(namespacedName string) types.NamespacedName {
96+
values := strings.Split(namespacedName, "/")
97+
if len(values) == 1 {
98+
return types.NamespacedName{
99+
Name: values[0],
100+
Namespace: "",
101+
}
102+
}
103+
if len(values) >= 2 {
104+
return types.NamespacedName{
105+
Name: values[1],
106+
Namespace: values[0],
107+
}
108+
}
109+
return types.NamespacedName{}
110+
}

pkg/handler/eventhandler_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package handler_test
1818

1919
import (
20+
"fmt"
2021
. "github.com/onsi/ginkgo"
2122
. "github.com/onsi/gomega"
2223
appsv1 "k8s.io/api/apps/v1"
@@ -32,6 +33,7 @@ import (
3233
"sigs.k8s.io/controller-runtime/pkg/event"
3334
"sigs.k8s.io/controller-runtime/pkg/handler"
3435
"sigs.k8s.io/controller-runtime/pkg/reconcile"
36+
"strings"
3537
)
3638

3739
var _ = Describe("Eventhandler", func() {
@@ -45,6 +47,17 @@ var _ = Describe("Eventhandler", func() {
4547
pod = &corev1.Pod{
4648
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "baz"},
4749
}
50+
51+
a := pod.GetAnnotations()
52+
if a == nil {
53+
a = map[string]string{}
54+
}
55+
56+
a[handler.NamespacedNameAnnotation] = strings.Join([]string{pod.Namespace, pod.Name}, "/")
57+
a[handler.TypeAnnotation] = fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
58+
59+
pod.SetAnnotations(a)
60+
4861
Expect(cfg).NotTo(BeNil())
4962

5063
var err error
@@ -950,4 +963,183 @@ var _ = Describe("Eventhandler", func() {
950963
close(done)
951964
})
952965
})
966+
967+
Describe("EnqueueRequestForAnnotation", func() {
968+
It("should enqueue a Request with the Annotation of the object in the CreateEvent.", func() {
969+
typeString := fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
970+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
971+
972+
evt := event.CreateEvent{
973+
Object: pod,
974+
Meta: pod.GetObjectMeta(),
975+
}
976+
instance.Create(evt, q)
977+
Expect(q.Len()).To(Equal(1))
978+
})
979+
980+
It("should enqueue a Request with the annotation of the object in the DeleteEvent.", func() {
981+
typeString := fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
982+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
983+
984+
evt := event.DeleteEvent{
985+
Object: pod,
986+
Meta: pod.GetObjectMeta(),
987+
}
988+
instance.Delete(evt, q)
989+
Expect(q.Len()).To(Equal(1))
990+
991+
i, _ := q.Get()
992+
Expect(i).To(Equal(reconcile.Request{
993+
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "baz"}}))
994+
})
995+
996+
It("should enqueue a Request with the Annotations of the object in the UpdateEvent.", func() {
997+
newPod := pod.DeepCopy()
998+
newPod.Name = pod.Name + "2"
999+
newPod.Namespace = pod.Namespace + "2"
1000+
1001+
typeString := fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
1002+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1003+
1004+
evt := event.UpdateEvent{
1005+
ObjectOld: pod,
1006+
MetaOld: pod.GetObjectMeta(),
1007+
ObjectNew: newPod,
1008+
MetaNew: newPod.GetObjectMeta(),
1009+
}
1010+
instance.Update(evt, q)
1011+
Expect(q.Len()).To(Equal(1))
1012+
1013+
i, _ := q.Get()
1014+
Expect(i).To(Equal(reconcile.Request{
1015+
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "baz"}}))
1016+
})
1017+
1018+
It("should enqueue a Request with the Annotation of the object in the GenericEvent.", func() {
1019+
typeString := fmt.Sprintf("%v.%v", pod.Kind, pod.APIVersion)
1020+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1021+
1022+
evt := event.GenericEvent{
1023+
Object: pod,
1024+
Meta: pod.GetObjectMeta(),
1025+
}
1026+
instance.Generic(evt, q)
1027+
Expect(q.Len()).To(Equal(1))
1028+
1029+
i, _ := q.Get()
1030+
Expect(i).To(Equal(reconcile.Request{
1031+
NamespacedName: types.NamespacedName{Namespace: pod.GetNamespace(), Name: "baz"}}))
1032+
})
1033+
1034+
It("should not enqueue a Request if there are no annotations matching with the object.", func() {
1035+
var repl *appsv1.ReplicaSet
1036+
1037+
repl = &appsv1.ReplicaSet{
1038+
ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "faz"},
1039+
}
1040+
1041+
typeString := fmt.Sprintf("%v.%v", repl.Kind,repl.APIVersion)
1042+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1043+
1044+
evt := event.CreateEvent{
1045+
Object: repl,
1046+
Meta: repl.GetObjectMeta(),
1047+
}
1048+
1049+
instance.Create(evt, q)
1050+
Expect(q.Len()).To(Equal(0))
1051+
1052+
})
1053+
1054+
It("should not enqueue a Request if there are no NamespacedNameAnnotation matching Namespace and Name.", func() {
1055+
var repl *appsv1.ReplicaSet
1056+
1057+
repl = &appsv1.ReplicaSet{
1058+
ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "faz"},
1059+
}
1060+
1061+
typeString := fmt.Sprintf("%v.%v", repl.Kind,repl.APIVersion)
1062+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1063+
1064+
a := repl.GetAnnotations()
1065+
if a == nil {
1066+
a = map[string]string{}
1067+
}
1068+
1069+
a[handler.TypeAnnotation] = fmt.Sprintf("%v.%v", repl.Kind, repl.APIVersion)
1070+
1071+
repl.SetAnnotations(a)
1072+
1073+
evt := event.CreateEvent{
1074+
Object: repl,
1075+
Meta: repl.GetObjectMeta(),
1076+
}
1077+
1078+
instance.Create(evt, q)
1079+
Expect(q.Len()).To(Equal(0))
1080+
1081+
})
1082+
1083+
It("should not enqueue a Request if there are no TypeAnnotation matching Group and Kind.", func() {
1084+
var repl *appsv1.ReplicaSet
1085+
1086+
repl = &appsv1.ReplicaSet{
1087+
ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "faz"},
1088+
}
1089+
1090+
typeString := fmt.Sprintf("%v.%v", repl.Kind,repl.APIVersion)
1091+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1092+
1093+
a := repl.GetAnnotations()
1094+
if a == nil {
1095+
a = map[string]string{}
1096+
}
1097+
1098+
a[handler.NamespacedNameAnnotation] = strings.Join([]string{repl.Namespace, repl.Name}, "/")
1099+
1100+
repl.SetAnnotations(a)
1101+
1102+
evt := event.CreateEvent{
1103+
Object: repl,
1104+
Meta: repl.GetObjectMeta(),
1105+
}
1106+
1107+
instance.Create(evt, q)
1108+
Expect(q.Len()).To(Equal(0))
1109+
1110+
})
1111+
1112+
It("should not enqueue a Request for a object that is cluster scoped and has the annotations", func() {
1113+
1114+
var nd *corev1.Node
1115+
1116+
nd = &corev1.Node{
1117+
ObjectMeta: metav1.ObjectMeta{Name: "node-1"},
1118+
}
1119+
1120+
typeString := fmt.Sprintf("%v.%v", nd.Kind, nd.APIVersion)
1121+
instance := handler.EnqueueRequestForAnnotation{Type: typeString}
1122+
1123+
a := nd.GetAnnotations()
1124+
if a == nil {
1125+
a = map[string]string{}
1126+
}
1127+
1128+
a[handler.NamespacedNameAnnotation] = strings.Join([]string{nd.Namespace, nd.Name}, "/")
1129+
a[handler.TypeAnnotation] = fmt.Sprintf("%v.%v", nd.Kind, nd.APIVersion)
1130+
1131+
evt := event.CreateEvent{
1132+
Object: nd,
1133+
Meta: nd.GetObjectMeta(),
1134+
}
1135+
instance.Create(evt, q)
1136+
Expect(q.Len()).To(Equal(0))
1137+
1138+
i, _ := q.Get()
1139+
Expect(i).To(Equal(reconcile.Request{
1140+
NamespacedName: types.NamespacedName{Namespace: "", Name: "node-1"}}))
1141+
1142+
})
1143+
})
1144+
9531145
})

0 commit comments

Comments
 (0)