diff --git a/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md index 7416949869..8a575f716f 100644 --- a/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md +++ b/docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md @@ -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 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 diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index 9471d52cc4..c878279dc4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -134,16 +134,8 @@ public Optional getSecondaryResource(P primary, Context

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 selectTargetSecondaryResource( - Set secondaryResources, P primary, Context

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 selectTargetSecondaryResource( + Set secondaryResources, P primary, Context

context); private void throwIfNull(R desired, P primary, String descriptor) { if (desired == null) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java index 4c828b7eb9..ee24ff2ed0 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java @@ -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; @@ -106,4 +110,30 @@ public void handleDeleteTargetResource(P primary, R resource, String key, Contex protected InformerEventSource getExternalStateEventSource() { return externalStateEventSource; } + + @Override + protected Optional selectTargetSecondaryResource( + Set secondaryResources, P primary, Context

context) { + R desired = desired(primary, context); + List 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)); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java index ebb47fd355..37c1148f5e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java @@ -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; @@ -105,6 +106,13 @@ public Result match(R resource, P primary, Context

context) { return bulkDependentResource.match(resource, desired, primary, context); } + @Override + protected Optional selectTargetSecondaryResource( + Set secondaryResources, P primary, Context

context) { + throw new IllegalStateException( + "BulkDependentResource should not call selectTargetSecondaryResource."); + } + @Override protected void onCreated(P primary, R created, Context

context) { asAbstractDependentResource().onCreated(primary, created, context); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java new file mode 100644 index 0000000000..e8aaf5d522 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java @@ -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 + */ +public interface ExternalDependentIDProvider { + + T externalResourceId(); +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java index ada543c6f1..6445373c78 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/ReconcilerUtilsTest.java @@ -8,7 +8,6 @@ import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder; import io.fabric8.kubernetes.api.model.apps.DeploymentSpec; -import io.fabric8.kubernetes.api.model.apps.DeploymentStatus; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClientException; import io.fabric8.kubernetes.client.http.HttpRequest; @@ -115,29 +114,6 @@ void setsSpecCustomResourceWithReflection() { assertThat(tomcat.getSpec().getReplicas()).isEqualTo(1); } - @Test - void setsStatusWithReflection() { - Deployment deployment = new Deployment(); - DeploymentStatus status = new DeploymentStatus(); - status.setReplicas(2); - - ReconcilerUtils.setStatus(deployment, status); - - assertThat(deployment.getStatus().getReplicas()).isEqualTo(2); - } - - @Test - void getsStatusWithReflection() { - Deployment deployment = new Deployment(); - DeploymentStatus status = new DeploymentStatus(); - status.setReplicas(2); - deployment.setStatus(status); - - var res = ReconcilerUtils.getStatus(deployment); - - assertThat(((DeploymentStatus) res).getReplicas()).isEqualTo(2); - } - @Test void loadYamlAsBuilder() { DeploymentBuilder builder = diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java index 939d046e5d..e2f78abb27 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java @@ -1,6 +1,7 @@ package io.javaoperatorsdk.operator.processing.dependent; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -84,6 +85,20 @@ public Optional getSecondaryResource( return Optional.ofNullable(secondary); } + @Override + protected Optional selectTargetSecondaryResource( + Set secondaryResources, + TestCustomResource primary, + Context 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 context) {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java index a8c1f889d0..82b0df3df4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/dependent/multiplemanagedexternaldependenttype/MultipleManagedExternalDependentSameTypeIT.java @@ -14,7 +14,7 @@ class MultipleManagedExternalDependentSameTypeIT { @RegisterExtension - LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withReconciler(new MultipleManagedExternalDependentResourceReconciler()) .build(); @@ -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(); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java index 048b1642c8..ab8a940252 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/support/ExternalResource.java @@ -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 { public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#"; @@ -65,4 +66,9 @@ public static String toExternalResourceId(HasMetadata primary) { + EXTERNAL_RESOURCE_NAME_DELIMITER + primary.getMetadata().getNamespace(); } + + @Override + public String externalResourceId() { + return id; + } } diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java index 3ec6d8f008..d718f321b9 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/schema/Schema.java @@ -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 { private final String name; private final String characterSet; @@ -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 @@ -38,4 +40,9 @@ public int hashCode() { public String toString() { return "Schema{" + "name='" + name + '\'' + ", characterSet='" + characterSet + '\'' + '}'; } + + @Override + public String externalResourceId() { + return name; + } }