Skip to content
Open
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
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
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,8 @@ public Optional<R> getSecondaryResource(P primary, Context<P> context) {
* @throws IllegalStateException if more than one candidate is found, in which case some other
* mechanism might be necessary to distinguish between candidate secondary resources
*/
protected Optional<R> selectTargetSecondaryResource(
Set<R> secondaryResources, P primary, Context<P> context) {
R desired = desired(primary, context);
var targetResources = secondaryResources.stream().filter(r -> r.equals(desired)).toList();
if (targetResources.size() > 1) {
throw new IllegalStateException(
"More than one secondary resource related to primary: " + targetResources);
}
return targetResources.isEmpty() ? Optional.empty() : Optional.of(targetResources.get(0));
}
protected abstract Optional<R> selectTargetSecondaryResource(
Set<R> secondaryResources, P primary, Context<P> context);

private void throwIfNull(R desired, P primary, String descriptor) {
if (desired == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.javaoperatorsdk.operator.processing.dependent;

import java.util.List;
import java.util.Optional;
import java.util.Set;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
Expand Down Expand Up @@ -106,4 +110,30 @@ public void handleDeleteTargetResource(P primary, R resource, String key, Contex
protected InformerEventSource getExternalStateEventSource() {
return externalStateEventSource;
}

@Override
protected Optional<R> selectTargetSecondaryResource(
Set<R> secondaryResources, P primary, Context<P> context) {
R desired = desired(primary, context);
List<R> targetResources;
if (desired instanceof ExternalDependentIDProvider<?> desiredWithId) {
targetResources =
secondaryResources.stream()
.filter(
r ->
((ExternalDependentIDProvider<?>) r)
.externalResourceId()
.equals(desiredWithId.externalResourceId()))
.toList();
} else {
throw new IllegalStateException(
"Either implement ExternalDependentIDProvider or override this "
+ " (selectTargetSecondaryResource) method.");
}
if (targetResources.size() > 1) {
throw new IllegalStateException(
"More than one secondary resource related to primary: " + targetResources);
}
return targetResources.isEmpty() ? Optional.empty() : Optional.of(targetResources.get(0));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import io.fabric8.kubernetes.api.model.HasMetadata;
Expand Down Expand Up @@ -105,6 +106,13 @@ public Result<R> match(R resource, P primary, Context<P> context) {
return bulkDependentResource.match(resource, desired, primary, context);
}

@Override
protected Optional<R> selectTargetSecondaryResource(
Set<R> secondaryResources, P primary, Context<P> context) {
throw new IllegalStateException(
"BulkDependentResource should not call selectTargetSecondaryResource.");
}

@Override
protected void onCreated(P primary, R created, Context<P> context) {
asAbstractDependentResource().onCreated(primary, created, context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.javaoperatorsdk.operator.processing.dependent;

/**
* Provides the identifier for an object that represents an external resource. This ID is used to
* select target resource for a dependent resource from the resources returned by `{@link
* io.javaoperatorsdk.operator.api.reconciler.Context#getSecondaryResources(Class)}`.
*
* @param <T>
*/
public interface ExternalDependentIDProvider<T> {

T externalResourceId();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.javaoperatorsdk.operator.processing.dependent;

import java.util.Optional;
import java.util.Set;

import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -84,6 +85,20 @@ public Optional<ConfigMap> getSecondaryResource(
return Optional.ofNullable(secondary);
}

@Override
protected Optional<ConfigMap> selectTargetSecondaryResource(
Set<ConfigMap> secondaryResources,
TestCustomResource primary,
Context<TestCustomResource> context) {
if (secondaryResources.size() == 1) {
return Optional.of(secondaryResources.iterator().next());
} else if (secondaryResources.isEmpty()) {
return Optional.empty();
} else {
throw new IllegalStateException();
}
}

@Override
protected void onCreated(
TestCustomResource primary, ConfigMap created, Context<TestCustomResource> context) {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
class MultipleManagedExternalDependentSameTypeIT {

@RegisterExtension
LocallyRunOperatorExtension operator =
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
.withReconciler(new MultipleManagedExternalDependentResourceReconciler())
.build();
Expand All @@ -27,15 +27,15 @@ class MultipleManagedExternalDependentSameTypeIT {

@Test
void handlesExternalCrudOperations() {
operator.create(testResource());
extension.create(testResource());
assertResourceCreatedWithData(DEFAULT_SPEC_VALUE);

var updatedResource = testResource();
updatedResource.getSpec().setValue(UPDATED_SPEC_VALUE);
operator.replace(updatedResource);
extension.replace(updatedResource);
assertResourceCreatedWithData(UPDATED_SPEC_VALUE);

operator.delete(testResource());
extension.delete(testResource());
assertExternalResourceDeleted();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import java.util.Objects;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.processing.dependent.ExternalDependentIDProvider;
import io.javaoperatorsdk.operator.processing.event.ResourceID;

public class ExternalResource {
public class ExternalResource implements ExternalDependentIDProvider<String> {

public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#";

Expand Down Expand Up @@ -65,4 +66,9 @@ public static String toExternalResourceId(HasMetadata primary) {
+ EXTERNAL_RESOURCE_NAME_DELIMITER
+ primary.getMetadata().getNamespace();
}

@Override
public String externalResourceId() {
return id;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import java.io.Serializable;
import java.util.Objects;

public class Schema implements Serializable {
import io.javaoperatorsdk.operator.processing.dependent.ExternalDependentIDProvider;

public class Schema implements Serializable, ExternalDependentIDProvider<String> {

private final String name;
private final String characterSet;
Expand All @@ -26,7 +28,7 @@ public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Schema schema = (Schema) o;
return Objects.equals(name, schema.name);
return Objects.equals(name, schema.name) && Objects.equals(characterSet, schema.characterSet);
}

@Override
Expand All @@ -38,4 +40,9 @@ public int hashCode() {
public String toString() {
return "Schema{" + "name='" + name + '\'' + ", characterSet='" + characterSet + '\'' + '}';
}

@Override
public String externalResourceId() {
return name;
}
}