Skip to content

feat: make it possible to also check annotations/labels when matching #1393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 109 additions & 80 deletions docs/documentation/dependent-resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,35 @@ possible to not implement any of these traits and therefore create read-only dep
that will trigger your reconciler whenever a user interacts with them but that are never
modified by your reconciler itself.

[`AbstractSimpleDependentResource`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractSimpleDependentResource.java)
and [`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)
sub-classes can also implement
the [`Matcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/Matcher.java)
interface to customize how the SDK decides whether or not the actual state of the dependent
matches the desired state. This makes it convenient to use these abstract base classes for your
implementation, only customizing the matching logic. Note that in many cases, there is no need
to customize that logic as the SDK already provides convenient default implementations in the
form
of [`DesiredEqualsMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DesiredEqualsMatcher.java)
and
[`GenericKubernetesResourceMatcher`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/GenericKubernetesResourceMatcher.java)
implementations, respectively. If you want to provide custom logic, you only need your
`DependentResource` implementation to implement the `Matcher` interface as below, which shows
how to customize the default matching logic for Kubernetes resource to also consider annotations
and labels, which are ignored by default:

```java
public class MyDependentResource extends KubernetesDependentResource<MyDependent, MyPrimary>
implements Matcher<MyDependent, MyPrimary> {
// your implementation

public Result<MyDependent> match(MyDependent actualResource, MyPrimary primary,
Context<MyPrimary> context) {
return GenericKubernetesResourceMatcher.match(this, actualResource, primary, context, true);
}
}
```

### Batteries included: convenient DependentResource implementations!

JOSDK also offers several other convenient implementations building on top of
Expand Down Expand Up @@ -116,7 +145,7 @@ Deleted (or set to be garbage collected). The following example shows how to cre
@KubernetesDependent(labelSelector = WebPageManagedDependentsReconciler.SELECTOR)
class DeploymentDependentResource extends CRUDKubernetesDependentResource<Deployment, WebPage> {

public DeploymentDependentResource() {
public DeploymentDependentResource() {
super(Deployment.class);
}

Expand Down Expand Up @@ -169,26 +198,26 @@ instances are managed by JOSDK, an example of which can be seen below:
```java

@ControllerConfiguration(
labelSelector = SELECTOR,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get this, we are both using the same formatter from maven, how was this not formatted? It should fail also on build if not properly formatted

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know but I've seen weird things going on with format… it doesn't appear as deterministic as it should be (or rather, the determinism is not obvious to me) 😓

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

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 @@ -215,69 +244,69 @@ 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)
throws Exception {

// 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)
throws Exception {

// 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,68 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;

/**
* Implement this interface to provide custom matching logic when determining whether secondary
* resources match their desired state. This is used by some default implementations of the
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} interface, notably
* {@link io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource}.
*
* @param <R> the type associated with the secondary resources we want to match
* @param <P> the type associated with the primary resources with which the related
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource}
* implementation is associated
*/
public interface Matcher<R, P extends HasMetadata> {

/**
* Abstracts the matching result letting implementations also return the desired state if it has
* been computed as part of their logic. This allows the SDK to avoid re-computing it if not
* needed.
*
* @param <R> the type associated with the secondary resources we want to match
*/
interface Result<R> {

/**
* Whether or not the actual resource matched the desired state
*
* @return {@code true} if the observed resource matched the desired state, {@code false}
* otherwise
*/
boolean matched();

/**
* Retrieves the associated desired state if it has been computed during the matching process or
* empty if not.
*
* @return an {@link Optional} holding the desired state if it has been computed during the
* matching process or {@link Optional#empty()} if not
*/
default Optional<R> computedDesired() {
return Optional.empty();
}

/**
* Creates a result stating only whether the resource matched the desired state without having
* computed the desired state.
*
* @param matched whether the actual resource matched the desired state
* @return a {@link Result} with an empty computed desired state
* @param <T> the type of resources being matched
*/
static <T> Result<T> nonComputed(boolean matched) {
return () -> matched;
}

/**
* Creates a result stating whether the resource matched and the associated computed desired
* state so that the SDK can use it downstream without having to recompute it.
*
* @param matched whether the actual resource matched the desired state
* @param computedDesired the associated desired state as computed during the matching process
* @return a {@link Result} with the associated desired state
* @param <T> the type of resources being matched
*/
static <T> Result<T> computed(boolean matched, T computedDesired) {
return new Result<>() {
@Override
Expand All @@ -32,5 +82,17 @@ public Optional<T> computedDesired() {
}
}

/**
* Determines whether the specified secondary resource matches the desired state as defined from
* the specified primary resource, given the specified {@link Context}.
*
* @param actualResource the resource we want to determine whether it's matching the desired state
* @param primary the primary resource from which the desired state is inferred
* @param context the context in which the resource is being matched
* @return a {@link Result} encapsulating whether the resource matched its desired state and this
* associated state if it was computed as part of the matching process. Use the static
* convenience methods ({@link Result#nonComputed(boolean)} and
* {@link Result#computed(boolean, Object)})
*/
Result<R> match(R actualResource, P primary, Context<P> context);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;

import java.util.Objects;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
Expand Down Expand Up @@ -41,8 +43,44 @@ static <R extends HasMetadata, P extends HasMetadata> Matcher<R, P> matcherFor(

@Override
public Result<R> match(R actualResource, P primary, Context<P> context) {
final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();
return match(dependentResource, actualResource, primary, context, false);
}

/**
* Determines whether the specified actual resource matches the desired state defined by the
* specified {@link KubernetesDependentResource} based on the observed state of the associated
* specified primary resource.
*
* @param dependentResource the {@link KubernetesDependentResource} implementation used to
* computed the desired state associated with the specified primary resource
* @param actualResource the observed dependent resource for which we want to determine whether it
* matches the desired state or not
* @param primary the primary resource from which we want to compute the desired state
* @param context the {@link Context} instance within which this method is called
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
* determining if it matches the desired state, {@code false} if matching should occur only
* considering the spec of the resources
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
* @param <R> the type of resource we want to determine whether they match or not
* @param <P> the type of primary resources associated with the secondary resources we want to
* match
*/
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
Context<P> context, boolean considerMetadata) {
final var desired = dependentResource.desired(primary, context);
if (considerMetadata) {
final var desiredMetadata = desired.getMetadata();
final var actualMetadata = actualResource.getMetadata();
final var matched =
Objects.equals(desiredMetadata.getAnnotations(), actualMetadata.getAnnotations()) &&
Objects.equals(desiredMetadata.getLabels(), actualMetadata.getLabels());
if (!matched) {
return Result.computed(false, desired);
}
}

final var objectMapper = ConfigurationServiceProvider.instance().getObjectMapper();

// reflection will be replaced by this:
// https://github.com/fabric8io/kubernetes-client/issues/3816
Expand Down
Loading