From c84c3e39a52151cb9c7a612a2d7e4e2bc78122b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 21 Jun 2023 15:10:14 +0200 Subject: [PATCH] docs: dependent resource ssa (#1960) * docs: dependent resource ssa * docs: improve wording --------- Co-authored-by: Chris Laprun --- docs/documentation/dependent-resources.md | 226 +++++++++++++--------- 1 file changed, 136 insertions(+), 90 deletions(-) diff --git a/docs/documentation/dependent-resources.md b/docs/documentation/dependent-resources.md index dc206983f6..324acca763 100644 --- a/docs/documentation/dependent-resources.md +++ b/docs/documentation/dependent-resources.md @@ -199,25 +199,25 @@ instances are managed by JOSDK, an example of which can be seen below: ```java @ControllerConfiguration( - labelSelector = SELECTOR, - dependents = { - @Dependent(type = ConfigMapDependentResource.class), - @Dependent(type = DeploymentDependentResource.class), - @Dependent(type = ServiceDependentResource.class) - }) + labelSelector = SELECTOR, + dependents = { + @Dependent(type = ConfigMapDependentResource.class), + @Dependent(type = DeploymentDependentResource.class), + @Dependent(type = ServiceDependentResource.class) + }) public class WebPageManagedDependentsReconciler - implements Reconciler, ErrorStatusHandler { + implements Reconciler, ErrorStatusHandler { - // omitted code + // omitted code - @Override - public UpdateControl reconcile(WebPage webPage, Context context) { + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { - final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() - .getMetadata().getName(); - webPage.setStatus(createStatus(name)); - return UpdateControl.patchStatus(webPage); - } + final var name = context.getSecondaryResource(ConfigMap.class).orElseThrow() + .getMetadata().getName(); + webPage.setStatus(createStatus(name)); + return UpdateControl.patchStatus(webPage); + } } ``` @@ -244,68 +244,68 @@ conditionally creating an `Ingress`: @ControllerConfiguration public class WebPageStandaloneDependentsReconciler - implements Reconciler, ErrorStatusHandler, - EventSourceInitializer { - - private KubernetesDependentResource configMapDR; - private KubernetesDependentResource deploymentDR; - private KubernetesDependentResource serviceDR; - private KubernetesDependentResource ingressDR; - - public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { - // 1. - createDependentResources(kubernetesClient); - } - - @Override - public List prepareEventSources(EventSourceContext context) { - // 2. - return List.of( - configMapDR.initEventSource(context), - deploymentDR.initEventSource(context), - serviceDR.initEventSource(context)); - } - - @Override - public UpdateControl reconcile(WebPage webPage, Context context) { - - // 3. - if (!isValidHtml(webPage.getHtml())) { - return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage)); - } - - // 4. - configMapDR.reconcile(webPage, context); - deploymentDR.reconcile(webPage, context); - serviceDR.reconcile(webPage, context); - - // 5. - if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) { - ingressDR.reconcile(webPage, context); - } else { - ingressDR.delete(webPage, context); - } - - // 6. - webPage.setStatus( - createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); - return UpdateControl.patchStatus(webPage); - } - - private void createDependentResources(KubernetesClient client) { - this.configMapDR = new ConfigMapDependentResource(); - this.deploymentDR = new DeploymentDependentResource(); - this.serviceDR = new ServiceDependentResource(); - this.ingressDR = new IngressDependentResource(); - - Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { - dr.setKubernetesClient(client); - dr.configureWith(new KubernetesDependentResourceConfig() - .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); - }); - } - - // omitted code + implements Reconciler, ErrorStatusHandler, + EventSourceInitializer { + + private KubernetesDependentResource configMapDR; + private KubernetesDependentResource deploymentDR; + private KubernetesDependentResource serviceDR; + private KubernetesDependentResource ingressDR; + + public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) { + // 1. + createDependentResources(kubernetesClient); + } + + @Override + public List prepareEventSources(EventSourceContext context) { + // 2. + return List.of( + configMapDR.initEventSource(context), + deploymentDR.initEventSource(context), + serviceDR.initEventSource(context)); + } + + @Override + public UpdateControl reconcile(WebPage webPage, Context context) { + + // 3. + if (!isValidHtml(webPage.getHtml())) { + return UpdateControl.patchStatus(setInvalidHtmlErrorMessage(webPage)); + } + + // 4. + configMapDR.reconcile(webPage, context); + deploymentDR.reconcile(webPage, context); + serviceDR.reconcile(webPage, context); + + // 5. + if (Boolean.TRUE.equals(webPage.getSpec().getExposed())) { + ingressDR.reconcile(webPage, context); + } else { + ingressDR.delete(webPage, context); + } + + // 6. + webPage.setStatus( + createStatus(configMapDR.getResource(webPage).orElseThrow().getMetadata().getName())); + return UpdateControl.patchStatus(webPage); + } + + private void createDependentResources(KubernetesClient client) { + this.configMapDR = new ConfigMapDependentResource(); + this.deploymentDR = new DeploymentDependentResource(); + this.serviceDR = new ServiceDependentResource(); + this.ingressDR = new IngressDependentResource(); + + Arrays.asList(configMapDR, deploymentDR, serviceDR, ingressDR).forEach(dr -> { + dr.setKubernetesClient(client); + dr.configureWith(new KubernetesDependentResourceConfig() + .setLabelSelector(DEPENDENT_RESOURCE_LABEL_SELECTOR)); + }); + } + + // omitted code } ``` @@ -331,6 +331,50 @@ sample [here](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/s Note also the Workflows feature makes it possible to also support this conditional creation use case in managed dependent resources. +## Creating/Updating Kubernetes Resources + +From version 4.4 of the framework the resources are created and updated +using [Server Side Apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) +, thus the desired state is simply sent using this approach to update the actual resource. + +## Comparing desired and actual state (matching) + +During the reconciliation of a dependent resource, the desired state is matched with the actual +state from the caches. The dependent resource only gets updated on the server if the actual, +observed state differs from the desired one. Comparing these two states is a complex problem +when dealing with Kubernetes resources because a strict equality check is usually not what is +wanted due to the fact that multiple fields might be automatically updated or added by +the platform ( +by [dynamic admission controllers](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/) +or validation webhooks, for example). Solving this problem in a generic way is therefore a tricky +proposition. + +JOSDK provides such a generic matching implementation which is used by default: +[SSABasedGenericKubernetesResourceMatcher](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/SSABasedGenericKubernetesResourceMatcher.java) +This implementation relies on the managed fields used by the Server Side Apply feature to +compare only the values of the fields that the controller manages. This ensures that only +semantically relevant fields are compared. See javadoc for further details. + +JOSDK versions prior to 4.4 were using a different matching algorithm as implemented in +[GenericKubernetesResourceMatcher](https://github.com/java-operator-sdk/java-operator-sdk/blob/e16559fd41bbb8bef6ce9d1f47bffa212a941b09/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java). + +Since SSA is a complex feature, JOSDK implements a feature flag allowing users to switch between +these implementations. See +in [ConfigurationService](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationService.java#L332-L358). + +It is, however, important to note that these implementations are default, generic +implementations that the framework can provide expected behavior out of the box. In many +situations, these will work just fine but it is also possible to provide matching algorithms +optimized for specific use cases. This is easily done by simply overriding +the `match(...)` [method](https://github.com/java-operator-sdk/java-operator-sdk/blob/e16559fd41bbb8bef6ce9d1f47bffa212a941b09/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java#L156-L156). + +It is also possible to bypass the matching logic altogether to simply rely on the server-side +apply mechanism if always sending potentially unchanged resources to the cluster is not an issue. +JOSDK's matching mechanism allows to spare some potentially useless calls to the Kubernetes API +server. To bypass the matching feature completely, simply override the `match` method to always +return `false`, thus telling JOSDK that the actual state never matches the desired one, making +it always update the resources using SSA. + ## Telling JOSDK how to find which secondary resources are associated with a given primary resource [`KubernetesDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java) @@ -467,29 +511,31 @@ as a sample. there should be a shared event source between them, or a label selector on the event sources to select only the relevant events, see in [related integration test](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/ConfigMapDependentResource1.java) - . + . ## "Read-only" Dependent Resources vs. Event Source -See Integration test for a read-only dependent [here](https://github.com/java-operator-sdk/java-operator-sdk/blob/249b41f3c68c4d0e9c77c41eca647a69a24347b0/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryDependentIT.java). +See Integration test for a read-only +dependent [here](https://github.com/java-operator-sdk/java-operator-sdk/blob/249b41f3c68c4d0e9c77c41eca647a69a24347b0/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryDependentIT.java). Some secondary resources only exist as input for the reconciliation process and are never updated *by a controller* (they might, and actually usually do, get updated by users interacting -with the resources directly, however). This might be the case, for example, of a `ConfigMap`that is +with the resources directly, however). This might be the case, for example, of a `ConfigMap`that is used to configure common characteristics of multiple resources in one convenient place. -In such situations, one might wonder whether it makes sense to create a dependent resource in -this case or simply use an `EventSource` so that the primary resource gets reconciled whenever a -user changes the resource. Typical dependent resources provide a desired state that the -reconciliation process attempts to match. In the case of so-called read-only dependents, though, -there is no such desired state because the operator / controller will never update the resource -itself, just react to external changes to it. An `EventSource` would achieve the same result. +In such situations, one might wonder whether it makes sense to create a dependent resource in +this case or simply use an `EventSource` so that the primary resource gets reconciled whenever a +user changes the resource. Typical dependent resources provide a desired state that the +reconciliation process attempts to match. In the case of so-called read-only dependents, though, +there is no such desired state because the operator / controller will never update the resource +itself, just react to external changes to it. An `EventSource` would achieve the same result. -Using a dependent resource for that purpose instead of a simple `EventSource`, however, provides +Using a dependent resource for that purpose instead of a simple `EventSource`, however, provides several benefits: + - dependents can be created declaratively, while an event source would need to be manually created -- if dependents are already used in a controller, it makes sense to unify the handling of all +- if dependents are already used in a controller, it makes sense to unify the handling of all secondary resources as dependents from a code organization perspective -- dependent resources can also interact with the workflow feature, thus allowing the read-only - resource to participate in conditions, in particular to decide whether or not the primary +- dependent resources can also interact with the workflow feature, thus allowing the read-only + resource to participate in conditions, in particular to decide whether or not the primary resource needs/can be reconciled using reconcile pre-conditions \ No newline at end of file