Skip to content

Commit

Permalink
Add documentation for watchers
Browse files Browse the repository at this point in the history
* Add documentation for watcher/handler mechanism
  • Loading branch information
psav committed Jan 15, 2025
1 parent bc49607 commit d3b0684
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 9 deletions.
11 changes: 6 additions & 5 deletions controllers/cloud.redhat.com/clowdapp_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,11 +200,12 @@ func (r *ClowdAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
builder.WithPredicates(environmentPredicate(r.Log, "app")),
)

watchers := []Watcher{}
watchers = append(watchers, Watcher{obj: &apps.Deployment{}, filter: deploymentFilter})
watchers = append(watchers, Watcher{obj: &core.Service{}, filter: generationOnlyFilter})
watchers = append(watchers, Watcher{obj: &core.ConfigMap{}, filter: generationOnlyFilter})
watchers = append(watchers, Watcher{obj: &core.Secret{}, filter: alwaysFilter})
watchers := []Watcher{
{obj: &apps.Deployment{}, filter: deploymentFilter},
{obj: &core.Service{}, filter: generationOnlyFilter},
{obj: &core.ConfigMap{}, filter: generationOnlyFilter},
{obj: &core.Secret{}, filter: alwaysFilter},
}

for _, watcher := range watchers {
err := r.setupWatch(ctrlr, mgr, watcher.obj, watcher.filter)
Expand Down
9 changes: 5 additions & 4 deletions controllers/cloud.redhat.com/clowdenvironment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,11 @@ func (r *ClowdEnvironmentReconciler) SetupWithManager(mgr ctrl.Manager) error {

ctrlr := ctrl.NewControllerManagedBy(mgr).For(&crd.ClowdEnvironment{})

watchers := []Watcher{}
watchers = append(watchers, Watcher{obj: &apps.Deployment{}, filter: deploymentFilter})
watchers = append(watchers, Watcher{obj: &core.Service{}, filter: alwaysFilter})
watchers = append(watchers, Watcher{obj: &core.Secret{}, filter: alwaysFilter})
watchers := []Watcher{
{obj: &apps.Deployment{}, filter: deploymentFilter},
{obj: &core.Service{}, filter: alwaysFilter},
{obj: &core.Secret{}, filter: alwaysFilter},
}

if clowderconfig.LoadedConfig.Features.WatchStrimziResources {
watchers = append(watchers, Watcher{obj: &strimzi.Kafka{}, filter: kafkaFilter})
Expand Down
104 changes: 104 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,110 @@ resource is one which is expected to hold multiple resources of the same type, b
different names. If these resources are required to be updated, then an `Update()` call is
necessary on each one as can be seen above.

### Handlers and Watching
This file contains the entrypoints into the watch functions that are used in Clowder. Watches are
used to get Clowder to reconcile when another resource changes. Clowder creates a number of
resources, and example of which is a `Deployment`. When a Deployment that Clowder creates changes,
Clowder needs to know about it so that it can reconcile again and replace the changes if necessary.
Below is an example of multiple watches being set up in the controller.

```golang
watchers := []Watcher{
{obj: &apps.Deployment{}, filter: deploymentFilter},
{obj: &core.Service{}, filter: generationOnlyFilter},
{obj: &core.ConfigMap{}, filter: generationOnlyFilter},
{obj: &core.Secret{}, filter: alwaysFilter},
}

for _, watcher := range watchers {
err := r.setupWatch(ctrlr, mgr, watcher.obj, watcher.filter)
if err != nil {
return err
}
}
```

This example sets up four watcher objects with various different filters. There are multiple levels
of filtering that happens to ensure Clowder only reconciles when necessary is to employ the use of
filters. An example of over reconciling would be when a deployment with multiple pods is started, or
redeploys. Every time a pod becomes available, it will change the Deployment resources. This would
ordinarily trigger a reconciliation of ClowdApp, or ClowdEnvironment that owns it. With the correct
filtering in place that doesn't happen.

The `generationOnlyFilter` looks like this:

```golang
func deploymentFilter(logr logr.Logger, ctrlName string) HandlerFuncs {
return genFilterFunc(deploymentUpdateFunc, logr, ctrlName)
}
```

This creates a new `HandlerFuncs` object that has been configured with the `deploymentUpdateFunc`
object.

```golang
func deploymentUpdateFunc(e event.UpdateEvent) bool {
objOld := e.ObjectOld.(*apps.Deployment)
objNew := e.ObjectNew.(*apps.Deployment)
if objNew.GetGeneration() != objOld.GetGeneration() {
return true
}
if (objOld.Status.AvailableReplicas != objNew.Status.AvailableReplicas) && (objNew.Status.AvailableReplicas == objNew.Status.ReadyReplicas) {
return true
}
if (objOld.Status.AvailableReplicas == objOld.Status.ReadyReplicas) && (objNew.Status.AvailableReplicas != objNew.Status.ReadyReplicas) {
return true
}
return false
}
```

The `genFilterFunc` will take this `deploymentUpdateFunc` and apply it to the `Update` function of
the `HandlerFuncs` object. In this example there are several checks made against the spec and in
certain circumstances, a `true` will be returned. The `true` is an instruction to Clowder to
reconcile objects that are owned by one of Clowder's resources.

There are 4 events that can be triggered when resources change:
* Create
* Update
* Delete
* Generic

The `genFilterFunc` returns an object that contains one of each of these functions.

The functions are then tied to the watcher for a particular type and bound to the
`enqueueRequestForObjectCustom` object in the handlers file. This handler is used for every request
that comes into Clowder. When an Event arrives, the following code will be executed in the example
of a `Create` event.

```golang
func (e *enqueueRequestForObjectCustom) Create(ctx context.Context, evt event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
shouldUpdate, err := e.updateHashCacheForConfigMapAndSecret(evt.Object)
if err != nil {
e.logMessage(evt.Object, err.Error(), "", getNamespacedName(evt.Object))
}

if shouldUpdate {
_ = e.doUpdateToHash(evt.Object, q)
e.reconcileAllAppsUsingObject(ctx, evt.Object, q)
}

if own, toKind := e.getOwner(evt.Object); own != nil {
if doRequest, msg := e.HandlerFuncs.CreateFunc(evt); doRequest {
e.logMessage(evt.Object, msg, toKind, own)
q.Add(reconcile.Request{NamespacedName: *own})
}
}
}
```

This runs through some special routines that create a hashCache for for serets/configmaps, but
ultimately ends up checking the ownership of the resource to ensure it's owned by the controller
type `ClowdApp` for example. If it is owned by a ClowdApp, the `Create` function that was associated
with the `HandlerFunc` object. If `doRequest` comes back as `true` then the owning Clowder resource
is triggered for reconciliation.


## Commits
We are currently testing [Conventional Commits](https://www.conventionalcommits.org) as a mandatory
step in the pipeline. This requires that each commit to the repo be formatted in the following way:
Expand Down

0 comments on commit d3b0684

Please sign in to comment.