Skip to content

Commit ebd6fd5

Browse files
Add EnqueueRequestForAnnotation enqueues Requests based on the presence of an annotation to watch resources
1 parent 485fbfa commit ebd6fd5

File tree

3 files changed

+350
-1
lines changed

3 files changed

+350
-1
lines changed

examples/builtins/main.go

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

pkg/handler/enqueue_annotation.go

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

pkg/handler/eventhandler_test.go

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

1919
import (
20+
"fmt"
21+
"strings"
22+
2023
. "github.com/onsi/ginkgo"
2124
. "github.com/onsi/gomega"
2225
appsv1 "k8s.io/api/apps/v1"
@@ -39,12 +42,22 @@ var _ = Describe("Eventhandler", func() {
3942
var instance handler.EnqueueRequestForObject
4043
var pod *corev1.Pod
4144
var mapper meta.RESTMapper
45+
podTypeAnnotations := fmt.Sprintf("%v.%v", "Pods", "core")
46+
podNamespacedNameAnnotation := strings.Join([]string{"biz", "baz"}, "/")
4247
t := true
4348
BeforeEach(func() {
4449
q = controllertest.Queue{Interface: workqueue.New()}
4550
pod = &corev1.Pod{
46-
ObjectMeta: metav1.ObjectMeta{Namespace: "biz", Name: "baz"},
51+
ObjectMeta: metav1.ObjectMeta{
52+
Namespace: "biz",
53+
Name: "baz",
54+
Annotations: map[string]string{
55+
handler.NamespacedNameAnnotation: podNamespacedNameAnnotation,
56+
handler.TypeAnnotation: podTypeAnnotations,
57+
},
58+
},
4759
}
60+
4861
Expect(cfg).NotTo(BeNil())
4962

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

0 commit comments

Comments
 (0)