Skip to content

Commit 2cc696d

Browse files
Add EnqueueRequestForAnnotation enqueues Requests based on the presence of an annotation to watch resources
1 parent ea32729 commit 2cc696d

File tree

4 files changed

+666
-1
lines changed

4 files changed

+666
-1
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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 main
18+
19+
import (
20+
"context"
21+
22+
"github.com/go-logr/logr"
23+
24+
appsv1 "k8s.io/api/apps/v1"
25+
"k8s.io/apimachinery/pkg/api/errors"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
28+
)
29+
30+
// reconcileReplicaSet reconciles ReplicaSets
31+
type reconcileReplicaSet struct {
32+
// client can be used to retrieve objects from the APIServer.
33+
client client.Client
34+
log logr.Logger
35+
}
36+
37+
// Implement reconcile.Reconciler so the controller can reconcile objects
38+
var _ reconcile.Reconciler = &reconcileReplicaSet{}
39+
40+
func (r *reconcileReplicaSet) Reconcile(request reconcile.Request) (reconcile.Result, error) {
41+
// set up a convenient log object so we don't have to type request over and over again
42+
log := r.log.WithValues("request", request)
43+
44+
// Fetch the ReplicaSet from the cache
45+
rs := &appsv1.ReplicaSet{}
46+
err := r.client.Get(context.TODO(), request.NamespacedName, rs)
47+
if errors.IsNotFound(err) {
48+
log.Error(nil, "Could not find ReplicaSet")
49+
return reconcile.Result{}, nil
50+
}
51+
52+
if err != nil {
53+
log.Error(err, "Could not fetch ReplicaSet")
54+
return reconcile.Result{}, err
55+
}
56+
57+
// Print the ReplicaSet
58+
log.Info("Reconciling ReplicaSet", "container name", rs.Spec.Template.Spec.Containers[0].Name)
59+
60+
// Set the label if it is missing
61+
if rs.Labels == nil {
62+
rs.Labels = map[string]string{}
63+
}
64+
if rs.Labels["hello"] == "world" {
65+
return reconcile.Result{}, nil
66+
}
67+
68+
// Update the ReplicaSet
69+
rs.Labels["hello"] = "world"
70+
err = r.client.Update(context.TODO(), rs)
71+
if err != nil {
72+
log.Error(err, "Could not write ReplicaSet")
73+
return reconcile.Result{}, err
74+
}
75+
76+
return reconcile.Result{}, nil
77+
}

examples/annotationbasedwatch/main.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 main
18+
19+
import (
20+
"k8s.io/apimachinery/pkg/runtime/schema"
21+
"os"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
rbacv1 "k8s.io/api/rbac/v1"
25+
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
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+
logf "sigs.k8s.io/controller-runtime/pkg/log"
30+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
31+
"sigs.k8s.io/controller-runtime/pkg/manager"
32+
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
33+
"sigs.k8s.io/controller-runtime/pkg/source"
34+
)
35+
36+
var log = logf.Log.WithName("example-annotationbasedwatch")
37+
38+
func main() {
39+
logf.SetLogger(zap.Logger(false))
40+
entryLog := log.WithName("entrypoint")
41+
42+
// Setup a Manager
43+
entryLog.Info("setting up manager")
44+
mgr, err := manager.New(config.GetConfigOrDie(), manager.Options{})
45+
if err != nil {
46+
entryLog.Error(err, "unable to set up overall controller manager")
47+
os.Exit(1)
48+
}
49+
50+
// Setup a new controller to reconcile ReplicaSets
51+
entryLog.Info("Setting up controller")
52+
c, err := controller.New("foo-controller", mgr, controller.Options{
53+
Reconciler: &reconcileReplicaSet{client: mgr.GetClient(), log: log.WithName("reconciler")},
54+
})
55+
if err != nil {
56+
entryLog.Error(err, "unable to set up individual controller")
57+
os.Exit(1)
58+
}
59+
60+
// Watch Pods that has the following annotations:
61+
// ...
62+
// annotations:
63+
// watch.kubebuilder.io/owner-namespaced-name:my-namespace/my-pod
64+
// watch.kubebuilder.io/owner-type:core.Pods
65+
// ...
66+
// It will enqueue a Request to the primary-resource-namespace when some change occurs in a Pod resource with these
67+
// annotations
68+
podTypeAnnotations := schema.GroupKind{Group: "Pods", Kind: "core"}
69+
if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForAnnotation{podTypeAnnotations}); err != nil {
70+
entryLog.Error(err, "unable to watch Pods")
71+
os.Exit(1)
72+
}
73+
74+
// Watch ClusterRoles that has the following annotations:
75+
// ...
76+
// annotations:
77+
// watch.kubebuilder.io/owner-namespaced-name:my-namespace/my-replicaset
78+
// watch.kubebuilder.io/owner-type:apps.ReplicaSet
79+
// ...
80+
// It will enqueue a Request to the primary-resource-namespace when some change occurs in a ClusterRole resource with these
81+
// annotations
82+
if err := c.Watch(&source.Kind{
83+
// Watch cluster roles
84+
Type: &rbacv1.ClusterRole{}},
85+
86+
// Enqueue ReplicaSet reconcile requests using the
87+
// namespacedName annotation value in the request.
88+
&handler.EnqueueRequestForAnnotation{schema.GroupKind{Group:"ReplicaSet", Kind:"apps"}}); err != nil {
89+
entryLog.Error(err, "unable to watch Cluster Role")
90+
os.Exit(1)
91+
}
92+
93+
entryLog.Info("starting manager")
94+
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
95+
entryLog.Error(err, "unable to run manager")
96+
os.Exit(1)
97+
}
98+
}

pkg/handler/enqueue_annotation.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
"k8s.io/apimachinery/pkg/runtime/schema"
23+
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/types"
26+
"k8s.io/client-go/util/workqueue"
27+
"sigs.k8s.io/controller-runtime/pkg/event"
28+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
29+
)
30+
31+
var _ EventHandler = &EnqueueRequestForAnnotation{}
32+
33+
const (
34+
// NamespacedNameAnnotation defines the annotation that will be used to get the primary resource namespaced name.
35+
// The handler will use this value to build the types.NamespacedName object used to enqueue a Request when an event
36+
// to update, to create or to delete the observed object is raised. Note that if only one value be informed without
37+
// the "/" then it will be set as the name of the primary resource.
38+
NamespacedNameAnnotation = "watch.kubebuilder.io/owner-namespaced-name"
39+
40+
// TypeAnnotation define the annotation that will be used to verify that the primary resource is the primary resource
41+
// to use. It should be the type schema.GroupKind. E.g watch.kubebuilder.io/owner-type:core.Pods
42+
TypeAnnotation = "watch.kubebuilder.io/owner-type"
43+
)
44+
45+
// EnqueueRequestForAnnotation enqueues Requests based on the presence of annotations that contain the type and
46+
// namespaced name of the primary resource. The purpose of this handler is to support cross-scope ownership
47+
// relationships that are not supported by native owner references.
48+
//
49+
// This handler should ALWAYS be paired with a finalizer on the primary resource. While the
50+
// annotation-based watch handler does not have the same scope restrictions that owner references
51+
// do, they also do not have the garbage collection guarantees that owner references do. Therefore,
52+
// if the reconciler of a primary resource creates a child resource across scopes not supported by
53+
// owner references, it is up to the reconciler to clean up that child resource.
54+
//
55+
// **NOTE** You might prefer to use the EnqueueRequestsFromMapFunc with predicates instead of it.
56+
//
57+
// **Examples:**
58+
//
59+
// The following code will enqueue a Request to the `primary-resource-namespace` when some change occurs in a Pod
60+
// resource:
61+
//
62+
// ...
63+
// podTypeAnnotations := schema.GroupKind{Group: "Pods", Kind: "core"}
64+
// if err := c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForAnnotation{podTypeAnnotations}); err != nil {
65+
// entryLog.Error(err, "unable to watch Pods")
66+
// os.Exit(1)
67+
// }
68+
// ...
69+
//
70+
// With the annotations:
71+
//
72+
// ...
73+
// annotations:
74+
// watch.kubebuilder.io/owner-namespaced-name:my-namespace/my-pod
75+
// watch.kubebuilder.io/owner-type:core.Pods
76+
// ...
77+
//
78+
// The following code will enqueue a Request to the `primary-resource-namespace`, ReplicaSet reconcile,
79+
// when some change occurs in a ClusterRole resource
80+
//
81+
// ...
82+
// if err := c.Watch(&source.Kind{
83+
// // Watch cluster roles
84+
// Type: &rbacv1.ClusterRole{}},
85+
//
86+
// // Enqueue ReplicaSet reconcile requests using the
87+
// // namespacedName annotation value in the request.
88+
// &handler.EnqueueRequestForAnnotation{schema.GroupKind{Group:"ReplicaSet", Kind:"apps"}}); err != nil {
89+
// entryLog.Error(err, "unable to watch ClusterRole")
90+
// os.Exit(1)
91+
// }
92+
// }
93+
// ...
94+
// With the annotations:
95+
//
96+
// ...
97+
// annotations:
98+
// watch.kubebuilder.io/owner-namespaced-name:my-namespace/my-replicaset
99+
// watch.kubebuilder.io/owner-type:apps.ReplicaSet
100+
// ...
101+
//
102+
// **NOTE** Cluster-scoped resources will have the NamespacedNameAnnotation such as:
103+
// `watch.kubebuilder.io/owner-namespaced-name:my-replicaset
104+
type EnqueueRequestForAnnotation struct {
105+
// It is used to verify that the primary resource is the primary resource to use.
106+
// E.g watch.kubebuilder.io/owner-type:core.Pods
107+
Type schema.GroupKind
108+
}
109+
110+
// Create implements EventHandler
111+
func (e *EnqueueRequestForAnnotation) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) {
112+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
113+
q.Add(req)
114+
}
115+
}
116+
117+
// Update implements EventHandler
118+
func (e *EnqueueRequestForAnnotation) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) {
119+
if ok, req := e.getAnnotationRequests(evt.MetaOld); ok {
120+
q.Add(req)
121+
} else if ok, req := e.getAnnotationRequests(evt.MetaNew); ok {
122+
q.Add(req)
123+
}
124+
}
125+
126+
// Delete implements EventHandler
127+
func (e *EnqueueRequestForAnnotation) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) {
128+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
129+
q.Add(req)
130+
}
131+
}
132+
133+
// Generic implements EventHandler
134+
func (e *EnqueueRequestForAnnotation) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) {
135+
if ok, req := e.getAnnotationRequests(evt.Meta); ok {
136+
q.Add(req)
137+
}
138+
}
139+
140+
// getAnnotationRequests will check if the object has the annotations for the watch handler and requeue
141+
func (e *EnqueueRequestForAnnotation) getAnnotationRequests(object metav1.Object) (bool, reconcile.Request) {
142+
if typeString, ok := object.GetAnnotations()[TypeAnnotation]; ok && typeString == e.Type.String() {
143+
namespacedNameString, ok := object.GetAnnotations()[NamespacedNameAnnotation]
144+
if !ok {
145+
log.Info("Unable to find the annotation for handle watch annotation",
146+
"resource", object, "annotation", NamespacedNameAnnotation)
147+
}
148+
if len(namespacedNameString) < 1 {
149+
return false, reconcile.Request{}
150+
}
151+
return true, reconcile.Request{NamespacedName: parseNamespacedName(namespacedNameString)}
152+
}
153+
return false, reconcile.Request{}
154+
}
155+
156+
// parseNamespacedName will parse the value informed in the NamespacedNameAnnotation and return types.NamespacedName
157+
// with. Note that if just one value is informed, then it will be set as the name.
158+
func parseNamespacedName(namespacedNameString string) types.NamespacedName {
159+
values := strings.SplitN(namespacedNameString, "/", 2)
160+
if len(values) == 1 {
161+
return types.NamespacedName{
162+
Name: values[0],
163+
Namespace: "",
164+
}
165+
}
166+
if len(values) >= 2 {
167+
return types.NamespacedName{
168+
Name: values[1],
169+
Namespace: values[0],
170+
}
171+
}
172+
return types.NamespacedName{}
173+
}

0 commit comments

Comments
 (0)