Skip to content

Commit f23d2fd

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

File tree

4 files changed

+664
-1
lines changed

4 files changed

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

0 commit comments

Comments
 (0)