Skip to content

Commit 29abc88

Browse files
Add EnqueueRequestForAnnotation enqueues Requests based on the presence of an annotation to watch resources
1 parent 4e1e8ed commit 29abc88

File tree

3 files changed

+345
-1
lines changed

3 files changed

+345
-1
lines changed

examples/builtins/main.go

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

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

2223
appsv1 "k8s.io/api/apps/v1"
2324
corev1 "k8s.io/api/core/v1"
25+
rbacv1 "k8s.io/api/rbac/v1"
2426
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
2527
"sigs.k8s.io/controller-runtime/pkg/client/config"
2628
"sigs.k8s.io/controller-runtime/pkg/controller"
@@ -70,6 +72,38 @@ func main() {
7072
os.Exit(1)
7173
}
7274

75+
// Watch Pods that has the following annotations:
76+
// ...
77+
// annotations:
78+
// sigs.k8s.io/primary-resource-namespace: "my-namespace"
79+
// sigs.k8s.io/primary-resource-name: "my-pod"
80+
// sigs.k8s.io/primary-resource-type: "Pods.core"
81+
// ...
82+
podAnnotationType := fmt.Sprintf("%v.%v", "Pods", "core")
83+
if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForAnnotation{podAnnotationType}); err != nil {
84+
entryLog.Error(err, "unable to watch Pods")
85+
os.Exit(1)
86+
}
87+
88+
// Watch ClusterRoles that has the following annotations:
89+
// ...
90+
// annotations:
91+
// sigs.k8s.io/primary-resource-namespace: "my-namespace"
92+
// sigs.k8s.io/primary-resource-name: "my-replicaset"
93+
// sigs.k8s.io/primary-resource-type: "ReplicaSet.apps"
94+
// ...
95+
if err := c.Watch(&source.Kind{
96+
// Watch cluster roles
97+
Type: &rbacv1.ClusterRole{}},
98+
99+
// Enqueue ReplicaSet reconcile requests using the
100+
// namespacedName annotation value in the request.
101+
&handler.EnqueueRequestForAnnotation{"ReplicaSet.apps"}); err != nil {
102+
entryLog.Error(err, "unable to watch Nodes")
103+
os.Exit(1)
104+
}
105+
106+
73107
// Setup webhooks
74108
entryLog.Info("setting up webhook server")
75109
hookServer := mgr.GetWebhookServer()

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+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/apimachinery/pkg/types"
22+
"k8s.io/client-go/util/workqueue"
23+
"sigs.k8s.io/controller-runtime/pkg/event"
24+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
25+
)
26+
27+
var _ EventHandler = &EnqueueRequestForAnnotation{}
28+
29+
const (
30+
// NamespaceAnnotation - annotation that will be used to get the primary resource namespace. (E.g my-namespace)
31+
NamespaceAnnotation = "sigs.k8s.io/primary-resource-namespace"
32+
33+
// NameAnnotation - annotation that will be used to get the primary resource name. (E.g my-resource-name)
34+
NameAnnotation = "sigs.k8s.io/primary-resource-name"
35+
36+
// TypeAnnotation - annotation that will be used to verify that the primary resource is the primary resource to use. (E.g Pods.core)
37+
TypeAnnotation = "sigs.k8s.io/primary-resource-type"
38+
)
39+
40+
// EnqueueRequestForAnnotation enqueues Requests based on the presence of annotations that contain the type and
41+
// namespaced name of the primary resource. The purpose of this handler is to support cross-scope ownership
42+
// relationships that are not supported by native owner references.
43+
//
44+
// This handler should ALWAYS be paired with a finalizer on the primary resource. While the
45+
// annotation-based watch handler does not have the same scope restrictions that owner references
46+
// do, they also do not have the garbage collection guarantees that owner references do. Therefore,
47+
// if the reconciler of a primary resource creates a child resource across scopes not supported by
48+
// owner references, it is up to the reconciler to clean up that child resource.
49+
//
50+
// The primary use case for this, is to have a controller enqueue requests for the following scenarios
51+
// 1. namespaced primary object and dependent cluster scoped resource
52+
// 2. cluster scoped primary object.
53+
// 3. namespaced primary object and dependent namespaced scoped but in a different namespace object.
54+
type EnqueueRequestForAnnotation struct {
55+
Type string
56+
}
57+
58+
// Create implements EventHandler
59+
func (e *EnqueueRequestForAnnotation) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
60+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
61+
q.Add(req)
62+
}
63+
}
64+
65+
// Update implements EventHandler
66+
func (e *EnqueueRequestForAnnotation) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
67+
if ok, req := e.getAnnotationRequests(evt.MetaOld); ok {
68+
q.Add(req)
69+
} else if ok, req := e.getAnnotationRequests(evt.MetaNew); ok {
70+
q.Add(req)
71+
}
72+
}
73+
74+
// Delete implements EventHandler
75+
func (e *EnqueueRequestForAnnotation) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
76+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
77+
q.Add(req)
78+
}
79+
}
80+
81+
// Generic implements EventHandler
82+
func (e *EnqueueRequestForAnnotation) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
83+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
84+
q.Add(req)
85+
}
86+
}
87+
88+
// getAnnotationRequests will check if the object has the annotations for the watch handler and requeue
89+
func (e *EnqueueRequestForAnnotation) getAnnotationRequests(object metav1.Object) (bool, reconcile.Request) {
90+
if typeString, ok := object.GetAnnotations()[TypeAnnotation]; ok && typeString == e.Type {
91+
name, ok := object.GetAnnotations()[NameAnnotation]
92+
if !ok {
93+
log.Info("Unable to find the annotation for handle watch annotation",
94+
"resource", object, "annotation", NameAnnotation)
95+
}
96+
if len(name) < 1 {
97+
return false, reconcile.Request{}
98+
}
99+
ns, ok := object.GetAnnotations()[NamespaceAnnotation]
100+
if !ok {
101+
log.Info("Unable to find the annotation for handle watch annotation",
102+
"resource", object, "annotation", NamespaceAnnotation)
103+
}
104+
return true, reconcile.Request{NamespacedName: types.NamespacedName{
105+
Name: name,
106+
Namespace: ns,
107+
}}
108+
}
109+
return false, reconcile.Request{}
110+
}

pkg/handler/eventhandler_test.go

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package handler_test
1818

1919
import (
20+
"fmt"
21+
2022
. "github.com/onsi/ginkgo"
2123
. "github.com/onsi/gomega"
2224
appsv1 "k8s.io/api/apps/v1"
@@ -39,12 +41,22 @@ var _ = Describe("Eventhandler", func() {
3941
var instance handler.EnqueueRequestForObject
4042
var pod *corev1.Pod
4143
var mapper meta.RESTMapper
44+
podTypeAnnotations := fmt.Sprintf("%v.%v", "Pods", "core")
4245
t := true
4346
BeforeEach(func() {
4447
q = controllertest.Queue{Interface: workqueue.New()}
4548
pod = &corev1.Pod{
46-
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "baz"},
49+
ObjectMeta: metav1.ObjectMeta{
50+
Namespace: "biz",
51+
Name: "baz",
52+
Annotations: map[string]string{
53+
handler.NamespaceAnnotation: "biz",
54+
handler.NameAnnotation: "baz",
55+
handler.TypeAnnotation: podTypeAnnotations,
56+
},
57+
},
4758
}
59+
4860
Expect(cfg).NotTo(BeNil())
4961

5062
var err error
@@ -950,4 +962,192 @@ var _ = Describe("Eventhandler", func() {
950962
close(done)
951963
})
952964
})
965+
966+
Describe("EnqueueRequestForAnnotation", func() {
967+
It("should enqueue a Request with the Annotation of the object in the CreateEvent.", func() {
968+
instance := handler.EnqueueRequestForAnnotation{Type: podTypeAnnotations}
969+
970+
evt := event.CreateEvent{
971+
Object: pod,
972+
Meta: pod.GetObjectMeta(),
973+
}
974+
instance.Create(evt, q)
975+
Expect(q.Len()).To(Equal(1))
976+
977+
i, _ := q.Get()
978+
Expect(i).To(Equal(reconcile.Request{
979+
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
980+
})
981+
982+
It("should enqueue a Request with the annotation of the object in the DeleteEvent.", func() {
983+
instance := handler.EnqueueRequestForAnnotation{Type: podTypeAnnotations}
984+
985+
evt := event.DeleteEvent{
986+
Object: pod,
987+
Meta: pod.GetObjectMeta(),
988+
}
989+
instance.Delete(evt, q)
990+
Expect(q.Len()).To(Equal(1))
991+
992+
i, _ := q.Get()
993+
Expect(i).To(Equal(reconcile.Request{
994+
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
995+
})
996+
997+
It("should enqueue a Request with the Annotations of the object in the UpdateEvent.", func() {
998+
newPod := pod.DeepCopy()
999+
newPod.Name = pod.Name + "2"
1000+
newPod.Namespace = pod.Namespace + "2"
1001+
1002+
instance := handler.EnqueueRequestForAnnotation{Type: podTypeAnnotations}
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: "biz", Name: "baz"}}))
1016+
})
1017+
1018+
It("should enqueue a Request with the Annotation of the object in the GenericEvent.", func() {
1019+
instance := handler.EnqueueRequestForAnnotation{Type: podTypeAnnotations}
1020+
1021+
evt := event.GenericEvent{
1022+
Object: pod,
1023+
Meta: pod.GetObjectMeta(),
1024+
}
1025+
instance.Generic(evt, q)
1026+
Expect(q.Len()).To(Equal(1))
1027+
1028+
i, _ := q.Get()
1029+
Expect(i).To(Equal(reconcile.Request{
1030+
NamespacedName: types.NamespacedName{Namespace: "biz", Name: "baz"}}))
1031+
})
1032+
1033+
It("should not enqueue a Request if there are no annotations matching with the object.", func() {
1034+
var repl *appsv1.ReplicaSet
1035+
1036+
repl = &appsv1.ReplicaSet{
1037+
ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "faz"},
1038+
}
1039+
1040+
instance := handler.EnqueueRequestForAnnotation{Type: "ReplicaSet.apps"}
1041+
1042+
evt := event.CreateEvent{
1043+
Object: repl,
1044+
Meta: repl.GetObjectMeta(),
1045+
}
1046+
1047+
instance.Create(evt, q)
1048+
Expect(q.Len()).To(Equal(0))
1049+
1050+
})
1051+
1052+
It("should not enqueue a Request if there are no NamespacedNameAnnotation matching Namespace and Name.", func() {
1053+
var repl *appsv1.ReplicaSet
1054+
1055+
repl = &appsv1.ReplicaSet{
1056+
ObjectMeta: metav1.ObjectMeta{
1057+
Namespace: "foo",
1058+
Name: "faz",
1059+
Annotations: map[string]string{
1060+
handler.TypeAnnotation: "ReplicaSet.apps",
1061+
},
1062+
},
1063+
}
1064+
1065+
instance := handler.EnqueueRequestForAnnotation{Type: "ReplicaSet.apps"}
1066+
evt := event.CreateEvent{
1067+
Object: repl,
1068+
Meta: repl.GetObjectMeta(),
1069+
}
1070+
1071+
instance.Create(evt, q)
1072+
Expect(q.Len()).To(Equal(0))
1073+
1074+
})
1075+
1076+
It("should not enqueue a Request if there are no TypeAnnotation matching Group and Kind.", func() {
1077+
var repl *appsv1.ReplicaSet
1078+
1079+
repl = &appsv1.ReplicaSet{
1080+
ObjectMeta: metav1.ObjectMeta{
1081+
Namespace: "foo",
1082+
Name: "faz",
1083+
1084+
Annotations: map[string]string{
1085+
handler.NamespaceAnnotation: "foo",
1086+
handler.NameAnnotation: "faz",
1087+
},
1088+
},
1089+
}
1090+
1091+
instance := handler.EnqueueRequestForAnnotation{Type: "ReplicaSet.apps"}
1092+
evt := event.CreateEvent{
1093+
Object: repl,
1094+
Meta: repl.GetObjectMeta(),
1095+
}
1096+
1097+
instance.Create(evt, q)
1098+
Expect(q.Len()).To(Equal(0))
1099+
1100+
})
1101+
1102+
It("should enqueue a Request for a object that is cluster scoped which has the annotations", func() {
1103+
1104+
var nd *corev1.Node
1105+
1106+
nd = &corev1.Node{
1107+
ObjectMeta: metav1.ObjectMeta{
1108+
Name: "node-1",
1109+
Annotations: map[string]string{
1110+
handler.NameAnnotation: "node-1",
1111+
handler.TypeAnnotation: "Node.core",
1112+
},
1113+
},
1114+
}
1115+
1116+
instance := handler.EnqueueRequestForAnnotation{Type: "Node.core"}
1117+
evt := event.CreateEvent{
1118+
Object: nd,
1119+
Meta: nd.GetObjectMeta(),
1120+
}
1121+
instance.Create(evt, q)
1122+
Expect(q.Len()).To(Equal(1))
1123+
1124+
i, _ := q.Get()
1125+
Expect(i).To(Equal(reconcile.Request{
1126+
NamespacedName: types.NamespacedName{Namespace: "", Name: "node-1"}}))
1127+
1128+
})
1129+
1130+
It("should not enqueue a Request for a object that is cluster scoped which has not the annotations", func() {
1131+
1132+
var nd *corev1.Node
1133+
1134+
nd = &corev1.Node{
1135+
ObjectMeta: metav1.ObjectMeta{Name: "node-1"},
1136+
}
1137+
1138+
instance := handler.EnqueueRequestForAnnotation{Type: "Node.core"}
1139+
evt := event.CreateEvent{
1140+
Object: nd,
1141+
Meta: nd.GetObjectMeta(),
1142+
}
1143+
instance.Create(evt, q)
1144+
Expect(q.Len()).To(Equal(0))
1145+
1146+
})
1147+
1148+
// TODO:Do we need to test the cases where:
1149+
// the old pod has the annotations and the new pod does not
1150+
// the new pod has the annotations and the old pod does not
1151+
})
1152+
9531153
})

0 commit comments

Comments
 (0)