Skip to content
Open
39 changes: 32 additions & 7 deletions docs/content/en/docs/documentation/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Operator operator = new Operator(client, o -> o.withMetrics(metrics));
### Micrometer implementation

The micrometer implementation is typically created using one of the provided factory methods which, depending on which
is used, will return either a ready to use instance or a builder allowing users to customized how the implementation
is used, will return either a ready to use instance or a builder allowing users to customize how the implementation
behaves, in particular when it comes to the granularity of collected metrics. It is, for example, possible to collect
metrics on a per-resource basis via tags that are associated with meters. This is the default, historical behavior but
this will change in a future version of JOSDK because this dramatically increases the cardinality of metrics, which
Expand All @@ -62,14 +62,13 @@ instance via:

```java
MeterRegistry registry; // initialize your registry implementation
Metrics metrics = new MicrometerMetrics(registry);
Metrics metrics = MicrometerMetrics.newMicrometerMetricsBuilder(registry).build();
```

Note, however, that this constructor is deprecated and we encourage you to use the factory methods instead, which either
return a fully pre-configured instance or a builder object that will allow you to configure more easily how the instance
will behave. You can, for example, configure whether or not the implementation should collect metrics on a per-resource
basis, whether or not associated meters should be removed when a resource is deleted and how the clean-up is performed.
See the relevant classes documentation for more details.
The class provides factory methods which either return a fully pre-configured instance or a builder object that will
allow you to configure more easily how the instance will behave. You can, for example, configure whether the
implementation should collect metrics on a per-resource basis, whether associated meters should be removed when a
resource is deleted and how the clean-up is performed. See the relevant classes documentation for more details.

For example, the following will create a `MicrometerMetrics` instance configured to collect metrics on a per-resource
basis, deleting the associated meters after 5 seconds when a resource is deleted, using up to 2 threads to do so.
Expand Down Expand Up @@ -109,4 +108,30 @@ brackets (`[]`) won't be present when per-resource collection is disabled and ta
omitted if the associated value is empty. Of note, when in the context of controllers' execution metrics, these tag
names are prefixed with `resource.`. This prefix might be removed in a future version for greater consistency.

### Aggregated Metrics

The `AggregatedMetrics` class provides a way to combine multiple metrics providers into a single metrics instance using
the composite pattern. This is particularly useful when you want to simultaneously collect metrics data from different
monitoring systems or providers.

You can create an `AggregatedMetrics` instance by providing a list of existing metrics implementations:

```java
// create individual metrics instances
Metrics micrometerMetrics = MicrometerMetrics.withoutPerResourceMetrics(registry);
Metrics customMetrics = new MyCustomMetrics();
Metrics loggingMetrics = new LoggingMetrics();

// combine them into a single aggregated instance
Metrics aggregatedMetrics = new AggregatedMetrics(List.of(
micrometerMetrics,
customMetrics,
loggingMetrics
));

// use the aggregated metrics with your operator
Operator operator = new Operator(client, o -> o.withMetrics(aggregatedMetrics));
```

This approach allows you to easily combine different metrics collection strategies, such as sending metrics to both
Prometheus (via Micrometer) and a custom logging system simultaneously.
49 changes: 49 additions & 0 deletions docs/content/en/docs/documentation/reconciler.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,52 @@ called, either by calling any of the `PrimeUpdateAndCacheUtils` methods again or
updated via `PrimaryUpdateAndCacheUtils`.

See related integration test [here](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/statuscache).

### Trigger reconciliation for all events

TLDR; We provide an execution mode where `reconcile` method is called on every event from event source.

The framework optimizes execution for generic use cases, which, in almost all cases, fall into two categories:

1. The controller does not use finalizers; thus when the primary resource is deleted, all the managed secondary
resources are cleaned up using the Kubernetes garbage collection mechanism, a.k.a., using owner references. This
mechanism, however, only works when all secondary resources are Kubernetes resources in the same namespace as the
primary resource.
2. The controller uses finalizers (the controller implements the `Cleaner` interface), when explicit cleanup logic is
required, typically for external resources and when secondary resources are in different namespace than the primary
resources (owner references cannot be used in this case).

Note that neither of those cases trigger the `reconcile` method of the controller on the `Delete` event of the primary
resource. When a finalizer is used, the SDK calls the `cleanup` method of the `Cleaner` implementation when the resource
is marked for deletion and the finalizer specified by the controller is present on the primary resource. When there is
no finalizer, there is no need to call the `reconcile` method on a `Delete` event since all the cleanup will be done by
the garbage collector. This avoids reconciliation cycles.

However, there are cases when controllers do not strictly follow those patterns, typically when:

- Only some of the primary resources use finalizers, e.g., for some of the primary resources you need
to create an external resource for others not.
- You maintain some additional in memory caches (so not all the caches are encapsulated by an `EventSource`)
and you don't want to use finalizers. For those cases, you typically want to clean up your caches when the primary
resource is deleted.

For such use cases you can set [`triggerReconcilerOnAllEvent`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ControllerConfiguration.java#L81)
to `true`, as a result, the `reconcile` method will be triggered on ALL events (so also `Delete` events), making it
possible to support the above use cases.

In this mode:

- even if the primary resource is already deleted from the Informer's cache, we will still pass the last known state
as the parameter for the reconciler. You can check if the resource is deleted using
`Context.isPrimaryResourceDeleted()`.
- The retry, rate limiting, re-schedule, filters mechanisms work normally. The internal caches related to the resource
are cleaned up only when there is a successful reconciliation after a `Delete` event was received for the primary
resource
and reconciliation is not re-scheduled.
- you cannot use the `Cleaner` interface. The framework assumes you will explicitly manage the finalizers. To
add finalizer you can use [
`PrimeUpdateAndCacheUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/PrimaryUpdateAndCacheUtils.java#L308).
- you cannot use managed dependent resources since those manage the finalizers and other logic related to the normal
execution mode.

See also [sample](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/triggerallevent/finalizerhandling) for selectively adding finalizers for resources;
5 changes: 5 additions & 0 deletions operator-framework-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>kube-api-test-client-inject</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,11 @@
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.regex.Pattern;

import io.fabric8.kubernetes.api.builder.Builder;
import io.fabric8.kubernetes.api.model.GenericKubernetesResource;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.client.KubernetesClientException;
import io.fabric8.kubernetes.client.utils.Serialization;
Expand Down Expand Up @@ -73,36 +70,6 @@ public static String getNameFor(Class<? extends Reconciler> reconcilerClass) {
return getDefaultNameFor(reconcilerClass);
}

public static void checkIfCanAddOwnerReference(HasMetadata owner, HasMetadata resource) {
if (owner instanceof GenericKubernetesResource
|| resource instanceof GenericKubernetesResource) {
return;
}
if (owner instanceof Namespaced) {
if (!(resource instanceof Namespaced)) {
throw new OperatorException(
"Cannot add owner reference from a cluster scoped to a namespace scoped resource."
+ resourcesIdentifierDescription(owner, resource));
} else if (!Objects.equals(
owner.getMetadata().getNamespace(), resource.getMetadata().getNamespace())) {
throw new OperatorException(
"Cannot add owner reference between two resource in different namespaces."
+ resourcesIdentifierDescription(owner, resource));
}
}
}

private static String resourcesIdentifierDescription(HasMetadata owner, HasMetadata resource) {
return " Owner name: "
+ owner.getMetadata().getName()
+ " Kind: "
+ owner.getKind()
+ ", Resource name: "
+ resource.getMetadata().getName()
+ " Kind: "
+ resource.getKind();
}

public static String getNameFor(Reconciler reconciler) {
return getNameFor(reconciler.getClass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,15 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
final var dependentFieldManager =
fieldManager.equals(CONTROLLER_NAME_AS_FIELD_MANAGER) ? name : fieldManager;

var triggerReconcilerOnAllEvent =
annotation != null && annotation.triggerReconcilerOnAllEvent();

InformerConfiguration<P> informerConfig =
InformerConfiguration.builder(resourceClass)
.initFromAnnotation(annotation != null ? annotation.informer() : null, context)
.buildForController();

return new ResolvedControllerConfiguration<P>(
return new ResolvedControllerConfiguration<>(
name,
generationAware,
associatedReconcilerClass,
Expand All @@ -323,7 +326,8 @@ private <P extends HasMetadata> ResolvedControllerConfiguration<P> controllerCon
null,
dependentFieldManager,
this,
informerConfig);
informerConfig,
triggerReconcilerOnAllEvent);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ default boolean isGenerationAware() {
String getAssociatedReconcilerClassName();

default Retry getRetry() {
return GenericRetry.DEFAULT;
return GenericRetry.defaultLimitedExponentialRetry();
}

@SuppressWarnings("rawtypes")
Expand Down Expand Up @@ -92,4 +92,8 @@ default String fieldManager() {
}

<C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec);

default boolean triggerReconcilerOnAllEvent() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
private Duration reconciliationMaxInterval;
private Map<DependentResourceSpec, Object> configurations;
private final InformerConfiguration<R>.Builder config;
private boolean triggerReconcilerOnAllEvent;

private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.finalizer = original.getFinalizerName();
Expand All @@ -42,6 +43,7 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
this.rateLimiter = original.getRateLimiter();
this.name = original.getName();
this.fieldManager = original.fieldManager();
this.triggerReconcilerOnAllEvent = original.triggerReconcilerOnAllEvent();
}

public ControllerConfigurationOverrider<R> withFinalizer(String finalizer) {
Expand Down Expand Up @@ -154,6 +156,12 @@ public ControllerConfigurationOverrider<R> withFieldManager(String dependentFiel
return this;
}

public ControllerConfigurationOverrider<R> withTriggerReconcilerOnAllEvent(
boolean triggerReconcilerOnAllEvent) {
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
return this;
}

/**
* Sets a max page size limit when starting the informer. This will result in pagination while
* populating the cache. This means that longer lists will take multiple requests to fetch. See
Expand Down Expand Up @@ -198,6 +206,7 @@ public ControllerConfiguration<R> build() {
fieldManager,
original.getConfigurationService(),
config.buildForController(),
triggerReconcilerOnAllEvent,
original.getWorkflowSpec().orElse(null));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class ResolvedControllerConfiguration<P extends HasMetadata>
private final Map<DependentResourceSpec, Object> configurations;
private final ConfigurationService configurationService;
private final String fieldManager;
private final boolean triggerReconcilerOnAllEvent;
private WorkflowSpec workflowSpec;

public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
Expand All @@ -44,6 +45,7 @@ public ResolvedControllerConfiguration(ControllerConfiguration<P> other) {
other.fieldManager(),
other.getConfigurationService(),
other.getInformerConfig(),
other.triggerReconcilerOnAllEvent(),
other.getWorkflowSpec().orElse(null));
}

Expand All @@ -59,6 +61,7 @@ public ResolvedControllerConfiguration(
String fieldManager,
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvent,
WorkflowSpec workflowSpec) {
this(
name,
Expand All @@ -71,7 +74,8 @@ public ResolvedControllerConfiguration(
configurations,
fieldManager,
configurationService,
informerConfig);
informerConfig,
triggerReconcilerOnAllEvent);
setWorkflowSpec(workflowSpec);
}

Expand All @@ -86,7 +90,8 @@ protected ResolvedControllerConfiguration(
Map<DependentResourceSpec, Object> configurations,
String fieldManager,
ConfigurationService configurationService,
InformerConfiguration<P> informerConfig) {
InformerConfiguration<P> informerConfig,
boolean triggerReconcilerOnAllEvent) {
this.informerConfig = informerConfig;
this.configurationService = configurationService;
this.name = ControllerConfiguration.ensureValidName(name, associatedReconcilerClassName);
Expand All @@ -99,6 +104,7 @@ protected ResolvedControllerConfiguration(
this.finalizer =
ControllerConfiguration.ensureValidFinalizerName(finalizer, getResourceTypeName());
this.fieldManager = fieldManager;
this.triggerReconcilerOnAllEvent = triggerReconcilerOnAllEvent;
}

protected ResolvedControllerConfiguration(
Expand All @@ -117,7 +123,8 @@ protected ResolvedControllerConfiguration(
null,
null,
configurationService,
InformerConfiguration.builder(resourceClass).buildForController());
InformerConfiguration.builder(resourceClass).buildForController(),
false);
}

@Override
Expand Down Expand Up @@ -207,4 +214,9 @@ public <C> C getConfigurationFor(DependentResourceSpec<?, P, C> spec) {
public String fieldManager() {
return fieldManager;
}

@Override
public boolean triggerReconcilerOnAllEvent() {
return triggerReconcilerOnAllEvent;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.javaoperatorsdk.operator.api.config.informer;

public @interface Field {

String path();

String value();

boolean negated() default false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.Arrays;
import java.util.List;

public class FieldSelector {
private final List<Field> fields;

public FieldSelector(List<Field> fields) {
this.fields = fields;
}

public FieldSelector(Field... fields) {
this.fields = Arrays.asList(fields);
}

public List<Field> getFields() {
return fields;
}

public record Field(String path, String value, boolean negated) {
public Field(String path, String value) {
this(path, value, false);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.ArrayList;
import java.util.List;

public class FieldSelectorBuilder {

private final List<FieldSelector.Field> fields = new ArrayList<>();

public FieldSelectorBuilder withField(String path, String value) {
fields.add(new FieldSelector.Field(path, value));
return this;
}

public FieldSelectorBuilder withoutField(String path, String value) {
fields.add(new FieldSelector.Field(path, value, true));
return this;
}

public FieldSelector build() {
return new FieldSelector(fields);
}
}
Loading
Loading