Skip to content

Commit

Permalink
docs: dependent resource ssa (#1960)
Browse files Browse the repository at this point in the history
* docs: dependent resource ssa

* docs: improve wording

---------

Co-authored-by: Chris Laprun <[email protected]>
  • Loading branch information
csviri and metacosm committed Jun 21, 2023
1 parent 869f7c6 commit 1042fc5
Showing 1 changed file with 136 additions and 90 deletions.
226 changes: 136 additions & 90 deletions docs/documentation/dependent-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebPage>, ErrorStatusHandler<WebPage> {
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage> {

// omitted code
// omitted code

@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context) {
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> 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);
}

}
```
Expand All @@ -244,68 +244,68 @@ conditionally creating an `Ingress`:

@ControllerConfiguration
public class WebPageStandaloneDependentsReconciler
implements Reconciler<WebPage>, ErrorStatusHandler<WebPage>,
EventSourceInitializer<WebPage> {

private KubernetesDependentResource<ConfigMap, WebPage> configMapDR;
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
private KubernetesDependentResource<Service, WebPage> serviceDR;
private KubernetesDependentResource<Service, WebPage> ingressDR;

public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) {
// 1.
createDependentResources(kubernetesClient);
}

@Override
public List<EventSource> prepareEventSources(EventSourceContext<WebPage> context) {
// 2.
return List.of(
configMapDR.initEventSource(context),
deploymentDR.initEventSource(context),
serviceDR.initEventSource(context));
}

@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> 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<WebPage>, ErrorStatusHandler<WebPage>,
EventSourceInitializer<WebPage> {

private KubernetesDependentResource<ConfigMap, WebPage> configMapDR;
private KubernetesDependentResource<Deployment, WebPage> deploymentDR;
private KubernetesDependentResource<Service, WebPage> serviceDR;
private KubernetesDependentResource<Service, WebPage> ingressDR;

public WebPageStandaloneDependentsReconciler(KubernetesClient kubernetesClient) {
// 1.
createDependentResources(kubernetesClient);
}

@Override
public List<EventSource> prepareEventSources(EventSourceContext<WebPage> context) {
// 2.
return List.of(
configMapDR.initEventSource(context),
deploymentDR.initEventSource(context),
serviceDR.initEventSource(context));
}

@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> 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
}
```

Expand All @@ -331,6 +331,50 @@ sample [here](https://github.com/operator-framework/java-operator-sdk/blob/main/
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)
Expand Down Expand Up @@ -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

0 comments on commit 1042fc5

Please sign in to comment.