Skip to content
Open
Show file tree
Hide file tree
Changes from 19 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
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,42 @@ as [integration tests](https://github.com/operator-framework/java-operator-sdk/t
To see how bulk dependent resources interact with workflow conditions, please refer to this
[integration test](https://github.com/operator-framework/java-operator-sdk/tree/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/bulkdependent/conidition).

## Dependent Resources with External Resource

Dependent resources are designed to manage also non-Kubernetes or external resources.
To implement such dependent you can extend `AbstractExternalDependentResource` or one of its
[subclasses](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external).

For Kubernetes resources we can have nice assumptions, like
if there are multiple resources of the same type, we can select the target resource
that dependent resource manages based on the name and namespace of the desired resource;
or we can use a matcher based SSA in most of the cases if the resource is managed using SSA.

### Selecting the target resource

Unfortunately this is not true for external resources. So to make sure we are selecting
the target resources from an event source, we provide a [mechanism](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java#L114-L138) that helps with that logic.
Your POJO representing an external resource can implement [`ExternalResourceIDProvider`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java) :

```java

public interface ExternalDependentIDProvider<T> {

T externalResourceId();
}
```

That will provide an ID, what is used to check for equality for desired state and resources from event source caches.
Not that if some reason this mechanism does not suit for you, you can simply
override [`selectTargetSecondaryResource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java)
method.

### Matching external resources

By default, external resources are matched using [equality](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java#L88-L92).
So you can override equals of you POJO representing an external resource.
As an alternative you can always override the whole `match` method to completely customize matching.

## External State Tracking Dependent Resources

It is sometimes necessary for a controller to track external (i.e. non-Kubernetes) state to
Expand Down
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.
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 @@ -57,7 +57,7 @@ default boolean isGenerationAware() {
String getAssociatedReconcilerClassName();

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

@SuppressWarnings("rawtypes")
Expand Down
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,7 @@
* the informer cache.
*/
long informerListLimit() default NO_LONG_VALUE_SET;

/** Kubernetes field selector for additional resource filtering */
Field[] fieldSelector() default {};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.javaoperatorsdk.operator.api.config.informer;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
Expand Down Expand Up @@ -36,6 +37,7 @@ public class InformerConfiguration<R extends HasMetadata> {
private GenericFilter<? super R> genericFilter;
private ItemStore<R> itemStore;
private Long informerListLimit;
private FieldSelector fieldSelector;

protected InformerConfiguration(
Class<R> resourceClass,
Expand All @@ -48,7 +50,8 @@ protected InformerConfiguration(
OnDeleteFilter<? super R> onDeleteFilter,
GenericFilter<? super R> genericFilter,
ItemStore<R> itemStore,
Long informerListLimit) {
Long informerListLimit,
FieldSelector fieldSelector) {
this(resourceClass);
this.name = name;
this.namespaces = namespaces;
Expand All @@ -60,6 +63,7 @@ protected InformerConfiguration(
this.genericFilter = genericFilter;
this.itemStore = itemStore;
this.informerListLimit = informerListLimit;
this.fieldSelector = fieldSelector;
}

private InformerConfiguration(Class<R> resourceClass) {
Expand Down Expand Up @@ -93,7 +97,8 @@ public static <R extends HasMetadata> InformerConfiguration<R>.Builder builder(
original.onDeleteFilter,
original.genericFilter,
original.itemStore,
original.informerListLimit)
original.informerListLimit,
original.fieldSelector)
.builder;
}

Expand Down Expand Up @@ -264,6 +269,10 @@ public Long getInformerListLimit() {
return informerListLimit;
}

public FieldSelector getFieldSelector() {
return fieldSelector;
}

@SuppressWarnings("UnusedReturnValue")
public class Builder {

Expand Down Expand Up @@ -329,6 +338,12 @@ public InformerConfiguration<R>.Builder initFromAnnotation(
final var informerListLimit =
informerListLimitValue == Constants.NO_LONG_VALUE_SET ? null : informerListLimitValue;
withInformerListLimit(informerListLimit);

withFieldSelector(
new FieldSelector(
Arrays.stream(informerConfig.fieldSelector())
.map(f -> new FieldSelector.Field(f.path(), f.value(), f.negated()))
.toList()));
}
return this;
}
Expand Down Expand Up @@ -424,5 +439,10 @@ public Builder withInformerListLimit(Long informerListLimit) {
InformerConfiguration.this.informerListLimit = informerListLimit;
return this;
}

public Builder withFieldSelector(FieldSelector fieldSelector) {
InformerConfiguration.this.fieldSelector = fieldSelector;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ public Builder<R> withInformerListLimit(Long informerListLimit) {
return this;
}

public Builder<R> withFieldSelector(FieldSelector fieldSelector) {
config.withFieldSelector(fieldSelector);
return this;
}

public void updateFrom(InformerConfiguration<R> informerConfig) {
if (informerConfig != null) {
final var informerConfigName = informerConfig.getName();
Expand All @@ -281,7 +286,8 @@ public void updateFrom(InformerConfiguration<R> informerConfig) {
.withOnUpdateFilter(informerConfig.getOnUpdateFilter())
.withOnDeleteFilter(informerConfig.getOnDeleteFilter())
.withGenericFilter(informerConfig.getGenericFilter())
.withInformerListLimit(informerConfig.getInformerListLimit());
.withInformerListLimit(informerConfig.getInformerListLimit())
.withFieldSelector(informerConfig.getFieldSelector());
}
}

Expand Down
Loading
Loading