From a4ff3991ff699660d58a9785e31520ffcabccf36 Mon Sep 17 00:00:00 2001 From: Camila Macedo Date: Sun, 15 Sep 2024 11:37:08 +0100 Subject: [PATCH] Revamp "Watching Resources" documentation for accuracy and clarity - Fully revamped the "Watching Resources" section for improved clarity and accuracy. - Updated subsections: - [Owned Resources](./reference/watching-resources/secondary-owned-resources) - [Resources Owned by Other Controllers or Projects](./reference/watching-resources/secondary-resources-owned-by-other-controllers.md) - [Using Predicates in Watches](./reference/watching-resources/predicates-with-watch) - Ensured terminology aligns with Kubernetes and controller-runtime standards. - Provided detailed examples for watching owned resources, non-owned resources, and applying predicates to refine watches. --- docs/book/src/SUMMARY.md | 5 +- docs/book/src/reference/reference.md | 6 +- docs/book/src/reference/watching-resources.md | 98 +++++++- .../watching-resources/externally-managed.md | 31 --- .../watching-resources/operator-managed.md | 25 --- .../predicates-with-watch.md | 113 ++++++++++ .../secondary-owned-resources.md | 181 +++++++++++++++ ...ry-resources-owned-by-other-controllers.md | 78 +++++++ .../testdata/external-indexed-field/api.go | 79 ------- .../external-indexed-field/controller.go | 209 ------------------ .../testdata/owned-resource/api.go | 77 ------- .../testdata/owned-resource/controller.go | 136 ------------ 12 files changed, 465 insertions(+), 573 deletions(-) delete mode 100644 docs/book/src/reference/watching-resources/externally-managed.md delete mode 100644 docs/book/src/reference/watching-resources/operator-managed.md create mode 100644 docs/book/src/reference/watching-resources/predicates-with-watch.md create mode 100644 docs/book/src/reference/watching-resources/secondary-owned-resources.md create mode 100644 docs/book/src/reference/watching-resources/secondary-resources-owned-by-other-controllers.md delete mode 100644 docs/book/src/reference/watching-resources/testdata/external-indexed-field/api.go delete mode 100644 docs/book/src/reference/watching-resources/testdata/external-indexed-field/controller.go delete mode 100644 docs/book/src/reference/watching-resources/testdata/owned-resource/api.go delete mode 100644 docs/book/src/reference/watching-resources/testdata/owned-resource/controller.go diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index d1dda4374ff..94f7b6ecc74 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -76,8 +76,9 @@ - [Good Practices](./reference/good-practices.md) - [Raising Events](./reference/raising-events.md) - [Watching Resources](./reference/watching-resources.md) - - [Resources Managed by the Operator](./reference/watching-resources/operator-managed.md) - - [Externally Managed Resources](./reference/watching-resources/externally-managed.md) + - [Owned Resources](./reference/watching-resources/secondary-owned-resources) + - [Owned by Other Controllers or Projects](./reference/watching-resources/secondary-resources-owned-by-other-controllers.md) + - [Using Predicates](./reference/watching-resources/predicates-with-watch) - [Kind for Dev & CI](reference/kind.md) - [What's a webhook?](reference/webhook-overview.md) - [Admission webhook](reference/admission-webhook.md) diff --git a/docs/book/src/reference/reference.md b/docs/book/src/reference/reference.md index 1d9026777d8..516ee9b5d0b 100644 --- a/docs/book/src/reference/reference.md +++ b/docs/book/src/reference/reference.md @@ -7,9 +7,9 @@ Kubernetes cluster. - [Watching Resources](watching-resources.md) Watch resources in the Kubernetes cluster to be informed and take actions on changes. - - [Resources Managed by the Operator](watching-resources/operator-managed.md) - - [Externally Managed Resources](watching-resources/externally-managed.md) - Controller Runtime provides the ability to watch additional resources relevant to the controlled ones. + - [Watching Secondary Owned Resources](watching-resources/secondary-owned-resources) + - [Watching Secondary Resources Owned by Other Controllers or Projects](watching-resources/secondary-resources-owned-by-other-controllers.md) + - [Using Predicates to Refine Watches](watching-resources/predicates-with-watch) - [Kind cluster](kind.md) - [What's a webhook?](webhook-overview.md) Webhooks are HTTP callbacks, there are 3 diff --git a/docs/book/src/reference/watching-resources.md b/docs/book/src/reference/watching-resources.md index 85c73959368..cb4c9ab7191 100644 --- a/docs/book/src/reference/watching-resources.md +++ b/docs/book/src/reference/watching-resources.md @@ -1,16 +1,92 @@ # Watching Resources -Inside a `Reconcile()` control loop, you are looking to do a collection of operations until it has the desired state on the cluster. -Therefore, it can be necessary to know when a resource that you care about is changed. -In the case that there is an action (create, update, edit, delete, etc.) on a watched resource, `Reconcile()` should be called for the resources watching it. +When extending the Kubernetes API, we aim to ensure that our solutions behave consistently with Kubernetes itself. +For example, consider a `Deployment` resource, which is managed by a controller. This controller is responsible +for responding to changes in the cluster—such as when a `Deployment` is created, updated, or deleted—by triggering +reconciliation to ensure the resource’s state matches the desired state. -[Controller Runtime libraries](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/builder) provide many ways for resources to be managed and watched. -This ranges from the easy and obvious use cases, such as watching the resources which were created and managed by the controller, to more unique and advanced use cases. +Similarly, when developing our controllers, we want to watch for relevant changes in resources that are crucial +to our solution. These changes—whether creations, updates, or deletions—should trigger the reconciliation +loop to take appropriate actions and maintain consistency across the cluster. -See each subsection for explanations and examples of the different ways in which your controller can _Watch_ the resources it cares about. +The [controller-runtime][controller-runtime] library provides several ways to watch and manage resources. +The simplest case is watching **Primary Resources**. -- [Watching Operator Managed Resources](watching-resources/operator-managed.md) - - These resources are created and managed by the same operator as the resource watching them. - This section covers both if they are managed by the same controller or separate controllers. -- [Watching Externally Managed Resources](watching-resources/externally-managed.md) - - These resources could be manually created, or managed by other operators/controllers or the Kubernetes control plane. \ No newline at end of file +## Primary Resources + +The **Primary Resource** is the resource that your controller is responsible +for managing. For example, if you create a custom resource definition (CRD) for `MyApp`, +the corresponding controller is responsible for managing instances of `MyApp`. + +In this case, `MyApp` is the **Primary Resource** for that controller, and your controller’s +reconciliation loop focuses on ensuring the desired state of these primary resources is maintained. + +When you create a new API using Kubebuilder, the following default code is scaffolded, +ensuring that the controller watches all relevant events—such as creations, updates, and +deletions—for the new API. + +This setup guarantees that the reconciliation loop is triggered whenever an instance +of the API is created, updated, or deleted: + +```go +// Watches the primary resource (e.g., MyApp) for create, update, delete events +if err := ctrl.NewControllerManagedBy(mgr). + For(&{}). <-- See there that the Controller is For this API + Complete(r); err != nil { + return err +} +``` + +## Secondary Resources + +Your controller may also need to manage **Secondary Resources**, +which are resources created and owned by the controller to support +the **Primary Resource**. + +These secondary resources, such as `Services`, `ConfigMaps`, or `Deployments`, +are tied to the Primary Resource via [OwnerReferences][owner-ref-k8s-docs]. +Changes to these secondary resources can directly impact the **Primary Resource**, +so the controller must watch and reconcile these resources accordingly. + +Additionally, if the **Primary Resource** is deleted, Kubernetes' garbage collection +mechanism ensures that all associated **Secondary Resources** are automatically deleted in a cascading manner. + + + +## General Concept of Watching Resources + +Whether a resource is defined within your project or comes from an external project, the concept of **Primary** and **Secondary Resources** remains the same: +- The **Primary Resource** is the resource the controller is primarily responsible for managing. +- **Secondary Resources** are those that are required to ensure the primary resource works as desired. + +Therefore, regardless of whether the resource was defined by your project or by another project, +your controller can watch, reconcile, and manage changes to these resources as needed. + +## Usage of Predicates + +For more complex use cases, [Predicates][cr-predicates] can be used to fine-tune +when your controller should trigger reconciliation. Predicates allow you to filter +events based on specific conditions, such as changes to particular fields, labels, or annotations, +ensuring that your controller only responds to relevant events and operates efficiently. + +[controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime +[owner-ref-k8s-docs]: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ +[cr-predicates]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/predicate +[secondary-resources-doc]: watching-resources/secondary-owned-resources +[predicates-with-external-type-doc]: watching-resources/predicates-with-watch \ No newline at end of file diff --git a/docs/book/src/reference/watching-resources/externally-managed.md b/docs/book/src/reference/watching-resources/externally-managed.md deleted file mode 100644 index 3f64b5aed90..00000000000 --- a/docs/book/src/reference/watching-resources/externally-managed.md +++ /dev/null @@ -1,31 +0,0 @@ -# Watching Externally Managed Resources - -By default, Kubebuilder and the Controller Runtime libraries allow for controllers -to easily watch the resources that they manage as well as dependent resources that are `Owned` by the controller. -However, those are not always the only resources that need to be watched in the cluster. - -## User Specified Resources - -There are many examples of Resource Specs that allow users to reference external resources. -- Ingresses have references to Service objects -- Pods have references to ConfigMaps, Secrets and Volumes -- Deployments and Services have references to Pods - -This same functionality can be added to CRDs and custom controllers. -This will allow for resources to be reconciled when another resource it references is changed. - -As an example, we are going to create a `ConfigDeployment` resource. -The `ConfigDeployment`'s purpose is to manage a `Deployment` whose pods are always using the latest version of a `ConfigMap`. -While ConfigMaps are auto-updated within Pods, applications may not always be able to auto-refresh config from the file system. -Some applications require restarts to apply configuration updates. -- The `ConfigDeployment` CRD will hold a reference to a ConfigMap inside its Spec. -- The `ConfigDeployment` controller will be in charge of creating a deployment with Pods that use the ConfigMap. -These pods should be updated anytime that the referenced ConfigMap changes, therefore the ConfigDeployments will need to be reconciled on changes to the referenced ConfigMap. - -### Allow for linking of resources in the `Spec` - -{{#literatego ./testdata/external-indexed-field/api.go}} - -### Watch linked resources - -{{#literatego ./testdata/external-indexed-field/controller.go}} diff --git a/docs/book/src/reference/watching-resources/operator-managed.md b/docs/book/src/reference/watching-resources/operator-managed.md deleted file mode 100644 index 6853ade2dba..00000000000 --- a/docs/book/src/reference/watching-resources/operator-managed.md +++ /dev/null @@ -1,25 +0,0 @@ -# Watching Operator Managed Resources - -Kubebuilder and the Controller Runtime libraries allow for controllers -to implement the logic of their CRD through easy management of Kubernetes resources. - -## Controlled & Owned Resources - -Managing dependency resources is fundamental to a controller, and it's not possible to manage them without watching for changes to their state. -- Deployments must know when the ReplicaSets that they manage are changed -- ReplicaSets must know when their Pods are deleted, or change from healthy to unhealthy. - -Through the `Owns()` functionality, Controller Runtime provides an easy way to watch dependency resources for changes. -A resource can be defined as dependent on another resource through the 'ownerReferences' field. - -As an example, we are going to create a `SimpleDeployment` resource. -The `SimpleDeployment`'s purpose is to manage a `Deployment` that users can change certain aspects of, through the `SimpleDeployment` Spec. -The `SimpleDeployment` controller's purpose is to make sure that it's owned `Deployment` (has an ownerReference which points to `SimpleDeployment` resource) always uses the settings provided by the user. - -### Provide basic templating in the `Spec` - -{{#literatego ./testdata/owned-resource/api.go}} - -### Manage the Owned Resource - -{{#literatego ./testdata/owned-resource/controller.go}} \ No newline at end of file diff --git a/docs/book/src/reference/watching-resources/predicates-with-watch.md b/docs/book/src/reference/watching-resources/predicates-with-watch.md new file mode 100644 index 00000000000..129e2069d1d --- /dev/null +++ b/docs/book/src/reference/watching-resources/predicates-with-watch.md @@ -0,0 +1,113 @@ +# Using Predicates to Refine Watches + +When working with controllers, it's often beneficial to use **Predicates** to +filter events and control when the reconciliation loop should be triggered. + +[Predicates][predicates-doc] allow you to define conditions based on events (such as create, update, or delete) +and resource fields (such as labels, annotations, or status fields). By using **[Predicates][predicates-doc]**, +you can refine your controller’s behavior to respond only to specific changes in the resources +it watches. + +This can be especially useful when you want to refine which +changes in resources should trigger a reconciliation. By using predicates, +you avoid unnecessary reconciliations and can ensure that the +controller only reacts to relevant changes. + +## When to Use Predicates + +**Predicates are useful when:** + +- You want to ignore certain changes, such as updates that don't impact the fields your controller is concerned with. +- You want to trigger reconciliation only for resources with specific labels or annotations. +- You want to watch external resources and react only to specific changes. + +## Example: Using Predicates to Filter Update Events + +Let’s say that we only want our **`BackupBusybox`** controller to reconcile +when certain fields of the **`Busybox`** resource change, for example, when +the `spec.size` field changes, but we want to ignore all other changes (such as status updates). + +### Defining a Predicate + +In the following example, we define a predicate that only +allows reconciliation when there’s a meaningful update +to the **`Busybox`** resource: + +```go +import ( + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +// Predicate to trigger reconciliation only on size changes in the Busybox spec +updatePred := predicate.Funcs{ + // Only allow updates when the spec.size of the Busybox resource changes + UpdateFunc: func(e event.UpdateEvent) bool { + oldObj := e.ObjectOld.(*examplecomv1alpha1.Busybox) + newObj := e.ObjectNew.(*examplecomv1alpha1.Busybox) + + // Trigger reconciliation only if the spec.size field has changed + return oldObj.Spec.Size != newObj.Spec.Size + }, + + // Allow create events + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + + // Allow delete events + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + + // Allow generic events (e.g., external triggers) + GenericFunc: func(e event.GenericEvent) bool { + return true + }, +} +``` + +### Explanation + +In this example: +- The **`UpdateFunc`** returns `true` only if the **`spec.size`** field has changed between the old and new objects, meaning that all other changes in the `spec`, like annotations or other fields, will be ignored. +- **`CreateFunc`**, **`DeleteFunc`**, and **`GenericFunc`** return `true`, meaning that create, delete, and generic events are still processed, allowing reconciliation to happen for these event types. + +This ensures that the controller reconciles only when the specific field **`spec.size`** is modified, while ignoring any other modifications in the `spec` that are irrelevant to your logic. + +### Example: Using Predicates in `Watches` + +Now, we apply this predicate in the **`Watches()`** method of +the **`BackupBusyboxReconciler`** to trigger reconciliation only for relevant events: + +```go +// SetupWithManager sets up the controller with the Manager. +// The controller will watch both the BackupBusybox primary resource and the Busybox resource, using predicates. +func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) + Watches( + &source.Kind{Type: &examplecomv1alpha1.Busybox{}}, // Watch the Busybox CR + handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "backupbusybox", // Reconcile the associated BackupBusybox resource + Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox + }, + }, + } + }), + builder.WithPredicates(updatePred), // Apply the predicate + ). // Trigger reconciliation when the Busybox resource changes (if it meets predicate conditions) + Complete(r) +} +``` + +### Explanation + +- **[`builder.WithPredicates(updatePred)`][predicates-doc]**: This method applies the predicate, ensuring that reconciliation only occurs +when the **`spec.size`** field in **`Busybox`** changes. +- **Other Events**: The controller will still trigger reconciliation on `Create`, `Delete`, and `Generic` events. + +[predicates-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/source#WithPredicates \ No newline at end of file diff --git a/docs/book/src/reference/watching-resources/secondary-owned-resources.md b/docs/book/src/reference/watching-resources/secondary-owned-resources.md new file mode 100644 index 00000000000..b8e4e296b3c --- /dev/null +++ b/docs/book/src/reference/watching-resources/secondary-owned-resources.md @@ -0,0 +1,181 @@ +# Watching Secondary Owned Resources + +In Kubernetes controllers, it’s common to manage both **Primary Resources** and **Secondary Resources**. A **Primary Resource** is the main resource that the controller is responsible for, while **Secondary Resources** are created and managed by the controller to support the **Primary Resource**. + +In this section, we will explain how to manage **Secondary Resources** which are `Owned` by the controller. +This example shows how to: + +- Set the [Owner Reference][cr-owner-ref-doc] between the primary resource (`Busybox`) and the secondary resource (`Deployment`) to ensure proper lifecycle management. +- Configure the controller to `Watch` the secondary resource using `Owns()` in `SetupWithManager()`. See that `Deployment` is owned by the `Busybox` controller because +it will be created and managed by it. + +## Setting the Owner Reference + +To link the lifecycle of the secondary resource (`Deployment`) to the primary resource (`Busybox`), we need to set an [Owner Reference][cr-owner-ref-doc] on the secondary resource. This ensures that Kubernetes automatically handles cascading deletions: if the primary resource is deleted, the secondary resource will also be deleted. + +Controller-runtime provides the [controllerutil.SetControllerReference][cr-owner-ref-doc] function, which you can use to set this relationship between the resources. + +### Setting the Owner Reference + +Below, we create the `Deployment` and set the Owner reference between the `Busybox` custom resource and the `Deployment` using `controllerutil.SetControllerReference()`. + +```go +// deploymentForBusybox returns a Deployment object for Busybox +func (r *BusyboxReconciler) deploymentForBusybox(busybox *examplecomv1alpha1.Busybox) *appsv1.Deployment { + replicas := busybox.Spec.Size + + dep := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: busybox.Name, + Namespace: busybox.Namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": busybox.Name}, + }, + Template: metav1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": busybox.Name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "busybox", + Image: "busybox:latest", + }, + }, + }, + }, + }, + } + + // Set the ownerRef for the Deployment, ensuring that the Deployment + // will be deleted when the Busybox CR is deleted. + controllerutil.SetControllerReference(busybox, dep, r.Scheme) + return dep +} +``` + +### Explanation + +By setting the `OwnerReference`, if the `Busybox` resource is deleted, Kubernetes will automatically delete +the `Deployment` as well. This also allows the controller to watch for changes in the `Deployment` +and ensure that the desired state (such as the number of replicas) is maintained. + +For example, if someone modifies the `Deployment` to change the replica count to 3, +while the `Busybox` CR defines the desired state as 1 replica, +the controller will reconcile this and ensure the `Deployment` +is scaled back to 1 replica. + +**Reconcile Function Example** + +```go +// Reconcile handles the main reconciliation loop for Busybox and the Deployment +func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + // Fetch the Busybox instance + busybox := &examplecomv1alpha1.Busybox{} + if err := r.Get(ctx, req.NamespacedName, busybox); err != nil { + if apierrors.IsNotFound(err) { + log.Info("Busybox resource not found. Ignoring since it must be deleted") + return ctrl.Result{}, nil + } + log.Error(err, "Failed to get Busybox") + return ctrl.Result{}, err + } + + // Check if the Deployment already exists, if not create a new one + found := &appsv1.Deployment{} + err := r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found) + if err != nil && apierrors.IsNotFound(err) { + // Define a new Deployment + dep := r.deploymentForBusybox(busybox) + log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + if err := r.Create(ctx, dep); err != nil { + log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) + return ctrl.Result{}, err + } + // Requeue the request to ensure the Deployment is created + return ctrl.Result{RequeueAfter: time.Minute}, nil + } else if err != nil { + log.Error(err, "Failed to get Deployment") + return ctrl.Result{}, err + } + + // Ensure the Deployment size matches the desired state + size := busybox.Spec.Size + if *found.Spec.Replicas != size { + found.Spec.Replicas = &size + if err := r.Update(ctx, found); err != nil { + log.Error(err, "Failed to update Deployment size", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) + return ctrl.Result{}, err + } + // Requeue the request to ensure the correct state is achieved + return ctrl.Result{Requeue: true}, nil + } + + // Update Busybox status to reflect that the Deployment is available + busybox.Status.AvailableReplicas = found.Status.AvailableReplicas + if err := r.Status().Update(ctx, busybox); err != nil { + log.Error(err, "Failed to update Busybox status") + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} +``` + +## Watching Secondary Resources + +To ensure that changes to the secondary resource (such as the `Deployment`) trigger +a reconciliation of the primary resource (`Busybox`), we configure the controller +to watch both resources. + +The `Owns()` method allows you to specify secondary resources +that the controller should monitor. This way, the controller will +automatically reconcile the primary resource whenever the secondary +resource changes (e.g., is updated or deleted). + +### Example: Configuring `SetupWithManager` to Watch Secondary Resources + +```go +// SetupWithManager sets up the controller with the Manager. +// The controller will watch both the Busybox primary resource and the Deployment secondary resource. +func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.Busybox{}). // Watch the primary resource + Owns(&appsv1.Deployment{}). // Watch the secondary resource (Deployment) + Complete(r) +} +``` + +## Ensuring the Right Permissions + +Kubebuilder uses [markers][markers] to define RBAC permissions +required by the controller. In order for the controller to +properly watch and manage both the primary (`Busybox`) and secondary (`Deployment`) +resources, it must have the appropriate permissions granted; +i.e. to `watch`, `get`, `list`, `create`, `update`, and `delete` permissions for those resources. + +### Example: RBAC Markers + +Before the `Reconcile` method, we need to define the appropriate RBAC markers. +These markers will be used by [controller-gen][controller-gen] to generate the necessary +roles and permissions when you run `make manifests`. + +```go +// +kubebuilder:rbac:groups=example.com,resources=busyboxes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +``` + +- The first marker gives the controller permission to manage the `Busybox` custom resource (the primary resource). +- The second marker grants the controller permission to manage `Deployment` resources (the secondary resource). + +Note that we are granting permissions to `watch` the resources. + +[owner-ref-k8s-docs]: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ +[cr-owner-ref-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetOwnerReference +[controller-gen]: ./../controller-gen.md +[markers]:./../markers/rbac.md diff --git a/docs/book/src/reference/watching-resources/secondary-resources-owned-by-other-controllers.md b/docs/book/src/reference/watching-resources/secondary-resources-owned-by-other-controllers.md new file mode 100644 index 00000000000..f35b9ab077c --- /dev/null +++ b/docs/book/src/reference/watching-resources/secondary-resources-owned-by-other-controllers.md @@ -0,0 +1,78 @@ +# Watching Secondary Resources Owned by Other Controllers or Projects + +In some cases, a controller may need to watch and react to changes in resources +that are not `Owned` by it, but are created and managed by another controller. + +The examples below demonstrate how a controller can watch and reconcile resources that it does not directly own. +This applies to both Kubernetes core API resources (like `ConfigMaps`, `Services`, or `Deployments`) and +custom resources defined by other projects (such as CRDs from other operators). + +For example, if you have two custom resources —`Busybox` and `BackupBusybox`— +and changes in `Busybox` should trigger a reconciliation for the `BackupBusybox` controller, +you can configure the `BackupBusybox` controller to watch `Busybox` resources. + +### Example: Watching Non-Owned Busybox Resource to Reconcile BackupBusybox + +Here's how you can configure the `BackupBusyboxReconciler` to watch changes in the +`Busybox` resource and trigger reconciliation for `BackupBusybox`: + +```go +// SetupWithManager sets up the controller with the Manager. +// The controller will watch both the BackupBusybox primary resource and the Busybox resource. +func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) + Watches( + &source.Kind{Type: &examplecomv1alpha1.Busybox{}}, // Watch the Busybox CR + handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request { + // Trigger reconciliation for the BackupBusybox in the same namespace + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "backupbusybox", // Reconcile the associated BackupBusybox resource + Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox + }, + }, + } + }), + ). // Trigger reconciliation when the Busybox resource changes + Complete(r) +} +``` + +Now, let's imagine that not all `Busybox` resources across the cluster are +relevant for our `BackupBusybox` controller. We only want to trigger reconciliation of +`BackupBusybox` controller when changes are made for specific `Busybox` instances +that are marked with a particular label. + +For example, for our `BackupBusybox` controller logic only matters those `Busybox` resources that have +the label `backup-needed: "true"`. + +```go +// SetupWithManager sets up the controller with the Manager. +// The controller will watch both the BackupBusybox primary resource and the Busybox resource, filtering by a label. +func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) + Watches( + &source.Kind{Type: &examplecomv1alpha1.Busybox{}}, // Watch the Busybox CR + handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request { + // Check if the Busybox resource has the label 'backup-needed: "true"' + if val, ok := obj.GetLabels()["backup-needed"]; ok && val == "true" { + // If the label is present and set to "true", trigger reconciliation for BackupBusybox + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: "backupbusybox", // Reconcile the associated BackupBusybox resource + Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox + }, + }, + } + } + // If the label is not present or doesn't match, don't trigger reconciliation + return []reconcile.Request{} + }), + ). // Trigger reconciliation when the labeled Busybox resource changes + Complete(r) +} +``` diff --git a/docs/book/src/reference/watching-resources/testdata/external-indexed-field/api.go b/docs/book/src/reference/watching-resources/testdata/external-indexed-field/api.go deleted file mode 100644 index 0f82d43bd7d..00000000000 --- a/docs/book/src/reference/watching-resources/testdata/external-indexed-field/api.go +++ /dev/null @@ -1,79 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -// +kubebuilder:docs-gen:collapse=Apache License -/* */ -package external_indexed_field - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +kubebuilder:docs-gen:collapse=Imports - -/* -In our type's Spec, we want to allow the user to pass in a reference to a configMap in the same namespace. -It's also possible for this to be a namespaced reference, but in this example we will assume that the referenced object -lives in the same namespace. - -This field does not need to be optional. -If the field is required, the indexing code in the controller will need to be modified. -*/ - -// ConfigDeploymentSpec defines the desired state of ConfigDeployment -type ConfigDeploymentSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Name of an existing ConfigMap in the same namespace, to add to the deployment - // +optional - ConfigMap string `json:"configMap,omitempty"` -} - -/* -The rest of the API configuration is covered in the CronJob tutorial. -*/ -/* */ -// ConfigDeploymentStatus defines the observed state of ConfigDeployment -type ConfigDeploymentStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// ConfigDeployment is the Schema for the configdeployments API -type ConfigDeployment struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ConfigDeploymentSpec `json:"spec,omitempty"` - Status ConfigDeploymentStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// ConfigDeploymentList contains a list of ConfigDeployment -type ConfigDeploymentList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []ConfigDeployment `json:"items"` -} - -func init() { - SchemeBuilder.Register(&ConfigDeployment{}, &ConfigDeploymentList{}) -} - -// +kubebuilder:docs-gen:collapse=Remaining API Code diff --git a/docs/book/src/reference/watching-resources/testdata/external-indexed-field/controller.go b/docs/book/src/reference/watching-resources/testdata/external-indexed-field/controller.go deleted file mode 100644 index a3ee1096d89..00000000000 --- a/docs/book/src/reference/watching-resources/testdata/external-indexed-field/controller.go +++ /dev/null @@ -1,209 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -// +kubebuilder:docs-gen:collapse=Apache License - -/* -Along with the standard imports, we need additional controller-runtime and apimachinery libraries. -All additional libraries, necessary for Watching, have the comment `Required For Watching` appended. -*/ -package external_indexed_field - -import ( - "context" - - "github.com/go-logr/logr" - kapps "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" // Required for Watching - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" // Required for Watching - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" // Required for Watching - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" // Required for Watching - "sigs.k8s.io/controller-runtime/pkg/predicate" // Required for Watching - "sigs.k8s.io/controller-runtime/pkg/reconcile" // Required for Watching - "sigs.k8s.io/controller-runtime/pkg/source" // Required for Watching - - appsv1 "tutorial.kubebuilder.io/project/api/v1" -) - -/* -Determine the path of the field in the ConfigDeployment CRD that we wish to use as the "object reference". -This will be used in both the indexing and watching. -*/ -const ( - configMapField = ".spec.configMap" -) - -/* - */ - -// ConfigDeploymentReconciler reconciles a ConfigDeployment object -type ConfigDeploymentReconciler struct { - client.Client - Log logr.Logger - Scheme *runtime.Scheme -} - -// +kubebuilder:docs-gen:collapse=Reconciler Declaration - -/* -There are two additional resources that the controller needs to have access to, other than ConfigDeployments. -- It needs to be able to fully manage Deployments, as well as check their status. -- It also needs to be able to get, list and watch ConfigMaps. -All 3 of these are important, and you will see usages of each below. -*/ - -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=configdeployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=configdeployments/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=configdeployments/finalizers,verbs=update -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get -// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch - -/* -`Reconcile` will be in charge of reconciling the state of ConfigDeployments. -ConfigDeployments are used to manage Deployments whose pods are updated whenever the configMap that they use is updated. - -For that reason we need to add an annotation to the PodTemplate within the Deployment we create. -This annotation will keep track of the latest version of the data within the referenced ConfigMap. -Therefore when the version of the configMap is changed, the PodTemplate in the Deployment will change. -This will cause a rolling upgrade of all Pods managed by the Deployment. - -Skip down to the `SetupWithManager` function to see how we ensure that `Reconcile` is called when the referenced `ConfigMaps` are updated. -*/ -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -func (r *ConfigDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - /* */ - log := r.Log.WithValues("configDeployment", req.NamespacedName) - - var configDeployment appsv1.ConfigDeployment - if err := r.Get(ctx, req.NamespacedName, &configDeployment); err != nil { - log.Error(err, "unable to fetch ConfigDeployment") - // we'll ignore not-found errors, since they can't be fixed by an immediate - // requeue (we'll need to wait for a new notification), and we can get them - // on deleted requests. - return ctrl.Result{}, client.IgnoreNotFound(err) - } - // +kubebuilder:docs-gen:collapse=Begin the Reconcile - - // your logic here - - var configMapVersion string - if configDeployment.Spec.ConfigMap != "" { - configMapName := configDeployment.Spec.ConfigMap - foundConfigMap := &corev1.ConfigMap{} - err := r.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: configDeployment.Namespace}, foundConfigMap) - if err != nil { - // If a configMap name is provided, then it must exist - // You will likely want to create an Event for the user to understand why their reconcile is failing. - return ctrl.Result{}, err - } - - // Hash the data in some way, or just use the version of the Object - configMapVersion = foundConfigMap.ResourceVersion - } - - // Logic here to add the configMapVersion as an annotation on your Deployment Pods. - - return ctrl.Result{}, nil -} - -/* -Finally, we add this reconciler to the manager, so that it gets started -when the manager is started. - -Since we create dependency Deployments during the reconcile, we can specify that the controller `Owns` Deployments. - -However the ConfigMaps that we want to watch are not owned by the ConfigDeployment object. -Therefore we must specify a custom way of watching those objects. -This watch logic is complex, so we have split it into a separate method. -*/ - -// SetupWithManager sets up the controller with the Manager. -func (r *ConfigDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { - /* - The `configMap` field must be indexed by the manager, so that we will be able to lookup `ConfigDeployments` by a referenced `ConfigMap` name. - This will allow for quickly answer the question: - - If ConfigMap _x_ is updated, which ConfigDeployments are affected? - */ - - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &appsv1.ConfigDeployment{}, configMapField, func(rawObj client.Object) []string { - // Extract the ConfigMap name from the ConfigDeployment Spec, if one is provided - configDeployment := rawObj.(*appsv1.ConfigDeployment) - if configDeployment.Spec.ConfigMap == "" { - return nil - } - return []string{configDeployment.Spec.ConfigMap} - }); err != nil { - return err - } - - /* - As explained in the CronJob tutorial, the controller will first register the Type that it manages, as well as the types of subresources that it controls. - Since we also want to watch ConfigMaps that are not controlled or managed by the controller, we will need to use the `Watches()` functionality as well. - - The `Watches()` function is a controller-runtime API that takes: - - A Kind (i.e. `ConfigMap`) - - A mapping function that converts a `ConfigMap` object to a list of reconcile requests for `ConfigDeployments`. - We have separated this out into a separate function. - - A list of options for watching the `ConfigMaps` - - In our case, we only want the watch to be triggered when the ResourceVersion of the ConfigMap is changed. - */ - - return ctrl.NewControllerManagedBy(mgr). - For(&appsv1.ConfigDeployment{}). - Owns(&kapps.Deployment{}). - Watches( - &corev1.ConfigMap{}, - handler.EnqueueRequestsFromMapFunc(r.findObjectsForConfigMap), - builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), - ). - Complete(r) -} - -/* -Because we have already created an index on the `configMap` reference field, this mapping function is quite straight forward. -We first need to list out all `ConfigDeployments` that use `ConfigMap` given in the mapping function. -This is done by merely submitting a List request using our indexed field as the field selector. - -When the list of `ConfigDeployments` that reference the `ConfigMap` is found, -we just need to loop through the list and create a reconcile request for each one. -If an error occurs fetching the list, or no `ConfigDeployments` are found, then no reconcile requests will be returned. -*/ -func (r *ConfigDeploymentReconciler) findObjectsForConfigMap(ctx context.Context, configMap client.Object) []reconcile.Request { - attachedConfigDeployments := &appsv1.ConfigDeploymentList{} - listOps := &client.ListOptions{ - FieldSelector: fields.OneTermEqualSelector(configMapField, configMap.GetName()), - Namespace: configMap.GetNamespace(), - } - err := r.List(ctx, attachedConfigDeployments, listOps) - if err != nil { - return []reconcile.Request{} - } - - requests := make([]reconcile.Request, len(attachedConfigDeployments.Items)) - for i, item := range attachedConfigDeployments.Items { - requests[i] = reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: item.GetName(), - Namespace: item.GetNamespace(), - }, - } - } - return requests -} diff --git a/docs/book/src/reference/watching-resources/testdata/owned-resource/api.go b/docs/book/src/reference/watching-resources/testdata/owned-resource/api.go deleted file mode 100644 index 61334fd3a2f..00000000000 --- a/docs/book/src/reference/watching-resources/testdata/owned-resource/api.go +++ /dev/null @@ -1,77 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -// +kubebuilder:docs-gen:collapse=Apache License -/* */ -package owned_resource - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// +kubebuilder:docs-gen:collapse=Imports - -/* -In this example the controller is doing basic management of a Deployment object. - -The Spec here allows the user to customize the deployment created in various ways. -For example, the number of replicas it runs with. -*/ - -// SimpleDeploymentSpec defines the desired state of SimpleDeployment -type SimpleDeploymentSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // The number of replicas that the deployment should have - // +optional - Replicas *int `json:"replicas,omitempty"` -} - -/* -The rest of the API configuration is covered in the CronJob tutorial. -*/ -/* */ -// SimpleDeploymentStatus defines the observed state of SimpleDeployment -type SimpleDeploymentStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status - -// SimpleDeployment is the Schema for the simpledeployments API -type SimpleDeployment struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec SimpleDeploymentSpec `json:"spec,omitempty"` - Status SimpleDeploymentStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// SimpleDeploymentList contains a list of SimpleDeployment -type SimpleDeploymentList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []SimpleDeployment `json:"items"` -} - -func init() { - SchemeBuilder.Register(&SimpleDeployment{}, &SimpleDeploymentList{}) -} - -// +kubebuilder:docs-gen:collapse=Remaining API Code diff --git a/docs/book/src/reference/watching-resources/testdata/owned-resource/controller.go b/docs/book/src/reference/watching-resources/testdata/owned-resource/controller.go deleted file mode 100644 index 7d3c0006655..00000000000 --- a/docs/book/src/reference/watching-resources/testdata/owned-resource/controller.go +++ /dev/null @@ -1,136 +0,0 @@ -/* - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -// +kubebuilder:docs-gen:collapse=Apache License - -/* -Along with the standard imports, we need additional controller-runtime and apimachinery libraries. -The extra imports are necessary for managing the objects that are "Owned" by the controller. -*/ -package owned_resource - -import ( - "context" - - "github.com/go-logr/logr" - kapps "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - appsv1 "tutorial.kubebuilder.io/project/api/v1" -) - -/* - */ - -// SimpleDeploymentReconciler reconciles a SimpleDeployment object -type SimpleDeploymentReconciler struct { - client.Client - Log logr.Logger - Scheme *runtime.Scheme -} - -// +kubebuilder:docs-gen:collapse=Reconciler Declaration - -/* -In addition to the `SimpleDeployment` permissions, we will also need permissions to manage `Deployments`. -In order to fully manage the workflow of deployments, our app will need to be able to use all verbs on a deployment as well as "get" it's status. -*/ - -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=simpledeployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=simpledeployments/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=apps.tutorial.kubebuilder.io,resources=simpledeployments/finalizers,verbs=update -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get - -/* -`Reconcile` will be in charge of reconciling the state of `SimpleDeployments`. - -In this basic example, `SimpleDeployments` are used to create and manage simple `Deployments` that can be configured through the `SimpleDeployment` Spec. -*/ -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -func (r *SimpleDeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - /* */ - log := r.Log.WithValues("simpleDeployment", req.NamespacedName) - - var simpleDeployment appsv1.SimpleDeployment - if err := r.Get(ctx, req.NamespacedName, &simpleDeployment); err != nil { - log.Error(err, "unable to fetch SimpleDeployment") - // we'll ignore not-found errors, since they can't be fixed by an immediate - // requeue (we'll need to wait for a new notification), and we can get them - // on deleted requests. - return ctrl.Result{}, client.IgnoreNotFound(err) - } - // +kubebuilder:docs-gen:collapse=Begin the Reconcile - - /* - Build the deployment that we want to see exist within the cluster - */ - - deployment := &kapps.Deployment{} - - // Set the information you care about - deployment.Spec.Replicas = simpleDeployment.Spec.Replicas - - /* - Set the controller reference, specifying that this `Deployment` is controlled by the `SimpleDeployment` being reconciled. - - This will allow for the `SimpleDeployment` to be reconciled when changes to the `Deployment` are noticed. - */ - if err := controllerutil.SetControllerReference(simpleDeployment, deployment, r.scheme); err != nil { - return ctrl.Result{}, err - } - - /* - Manage your `Deployment`. - - - Create it if it doesn't exist. - - Update it if it is configured incorrectly. - */ - foundDeployment := &kapps.Deployment{} - err := r.Get(ctx, types.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, foundDeployment) - if err != nil && errors.IsNotFound(err) { - log.V(1).Info("Creating Deployment", "deployment", deployment.Name) - err = r.Create(ctx, deployment) - } else if err == nil { - if foundDeployment.Spec.Replicas != deployment.Spec.Replicas { - foundDeployment.Spec.Replicas = deployment.Spec.Replicas - log.V(1).Info("Updating Deployment", "deployment", deployment.Name) - err = r.Update(ctx, foundDeployment) - } - } - - return ctrl.Result{}, err -} - -/* -Finally, we add this reconciler to the manager, so that it gets started -when the manager is started. - -Since we create dependency `Deployments` during the reconcile, we can specify that the controller `Owns` `Deployments`. -This will tell the manager that if a `Deployment`, or its status, is updated, then the `SimpleDeployment` in its ownerRef field should be reconciled. -*/ - -// SetupWithManager sets up the controller with the Manager. -func (r *SimpleDeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appsv1.SimpleDeployment{}). - Owns(&kapps.Deployment{}). - Complete(r) -}