From 1db5cc09a7cda6c3ea93a16c9b9ebeded5a1f9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Jun 2025 13:07:05 +0200 Subject: [PATCH 01/15] improve: remove owner refernce check (#2838) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this was added to fabric8 client meanwhile Signed-off-by: Attila Mészáros --- .../operator/ReconcilerUtilsTest.java | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) 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 6bb0c427cd..5d2e37d718 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 @@ -130,29 +130,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 = @@ -191,6 +168,44 @@ void handleKubernetesExceptionShouldThrowMissingCRDExceptionWhenAppropriate() { HasMetadata.getFullResourceName(Tomcat.class))); } + @Test + void checksIfOwnerReferenceCanBeAdded() { + assertThrows( + OperatorException.class, + () -> + ReconcilerUtils.checkIfCanAddOwnerReference( + namespacedResource(), namespacedResourceFromOtherNamespace())); + + assertThrows( + OperatorException.class, + () -> + ReconcilerUtils.checkIfCanAddOwnerReference( + namespacedResource(), clusterScopedResource())); + + assertDoesNotThrow( + () -> { + ReconcilerUtils.checkIfCanAddOwnerReference( + clusterScopedResource(), clusterScopedResource()); + ReconcilerUtils.checkIfCanAddOwnerReference(namespacedResource(), namespacedResource()); + }); + } + + private ClusterRole clusterScopedResource() { + return new ClusterRoleBuilder().withMetadata(new ObjectMetaBuilder().build()).build(); + } + + private ConfigMap namespacedResource() { + return new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder().withNamespace("testns1").build()) + .build(); + } + + private ConfigMap namespacedResourceFromOtherNamespace() { + return new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder().withNamespace("testns2").build()) + .build(); + } + @Group("tomcatoperator.io") @Version("v1") @ShortNames("tc") From da90dad58fd83da8ec5137fd4e528056f9f3cfe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 25 Sep 2025 13:57:45 +0200 Subject: [PATCH 02/15] [WIP] feat: external id provider for external dependent resources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../AbstractExternalDependentResource.java | 24 +++++++++++++++++++ .../ExternalDependentIDProvider.java | 6 +++++ .../operator/sample/schema/Schema.java | 9 ++++++- 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java 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 9db3da6829..8e94450659 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 @@ -15,6 +15,10 @@ */ 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; @@ -121,4 +125,24 @@ 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).id().equals(desiredWithId.id())) + .toList(); + } else { + 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)); + } } 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..dca785cf29 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java @@ -0,0 +1,6 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +public interface ExternalDependentIDProvider { + + T 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 5cf521eac9..ab046c676e 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 @@ -18,7 +18,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; @@ -53,4 +55,9 @@ public int hashCode() { public String toString() { return "Schema{" + "name='" + name + '\'' + ", characterSet='" + characterSet + '\'' + '}'; } + + @Override + public String id() { + return name; + } } From 4a1cae75bd406d9fd5ff223d635b91bf7e7acdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 25 Sep 2025 14:23:35 +0200 Subject: [PATCH 03/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../java/io/javaoperatorsdk/operator/sample/schema/Schema.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ab046c676e..f9f6a638a5 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 @@ -43,7 +43,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 From b8da9bbf9cb1eedce6764fd4712b00a4e46205db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Fri, 26 Sep 2025 09:55:45 +0200 Subject: [PATCH 04/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/AbstractExternalDependentResource.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 8e94450659..fdbfd00cee 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 @@ -137,7 +137,9 @@ protected Optional selectTargetSecondaryResource( .filter(r -> ((ExternalDependentIDProvider) r).id().equals(desiredWithId.id())) .toList(); } else { - targetResources = secondaryResources.stream().filter(r -> r.equals(desired)).toList(); + throw new IllegalStateException( + "Either implement ExternalDependentIDProvider or override" + + " selectTargetSecondaryResource."); } if (targetResources.size() > 1) { throw new IllegalStateException( From 75c0eee60108d9191a0984869991291ae825d5aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 30 Sep 2025 10:20:19 +0200 Subject: [PATCH 05/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/AbstractDependentResource.java | 12 ++---------- .../BulkDependentResourceReconciler.java | 8 ++++++++ .../dependent/AbstractDependentResourceTest.java | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) 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 7258dbca83..9e827e16de 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 @@ -149,16 +149,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/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java index f8c215856a..c808f75ea4 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 @@ -19,6 +19,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; @@ -120,6 +121,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/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java index d150751a70..bb9d6cf71e 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 @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.processing.dependent; import java.util.Optional; +import java.util.Set; import org.junit.jupiter.api.Test; @@ -99,6 +100,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) {} From f9fa4d9bf412235672d009ae4999e1612d40ca50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 30 Sep 2025 10:47:55 +0200 Subject: [PATCH 06/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/dependent/ExternalDependentIDProvider.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 index dca785cf29..27591b5a8e 100644 --- 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 @@ -1,5 +1,12 @@ package io.javaoperatorsdk.operator.processing.dependent; +/** + * Provides the identifier for a bean that represents and 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 id(); From 427c1ca8bfaa956a67dc49026cbc29b7ced51283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 30 Sep 2025 12:30:56 +0200 Subject: [PATCH 07/15] external resource ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/AbstractExternalDependentResource.java | 6 +++++- .../processing/dependent/ExternalDependentIDProvider.java | 2 +- .../MultipleManagedExternalDependentSameTypeIT.java | 8 ++++---- .../operator/support/ExternalResource.java | 8 +++++++- .../io/javaoperatorsdk/operator/sample/schema/Schema.java | 2 +- 5 files changed, 18 insertions(+), 8 deletions(-) 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 fdbfd00cee..9b447ddc6c 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 @@ -134,7 +134,11 @@ protected Optional selectTargetSecondaryResource( if (desired instanceof ExternalDependentIDProvider desiredWithId) { targetResources = secondaryResources.stream() - .filter(r -> ((ExternalDependentIDProvider) r).id().equals(desiredWithId.id())) + .filter( + r -> + ((ExternalDependentIDProvider) r) + .externalResourceId() + .equals(desiredWithId.externalResourceId())) .toList(); } else { throw new IllegalStateException( 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 index 27591b5a8e..1df1b4bf08 100644 --- 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 @@ -9,5 +9,5 @@ */ public interface ExternalDependentIDProvider { - T id(); + T externalResourceId(); } 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 f91a6ec9a7..38def100f4 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 @@ -29,7 +29,7 @@ class MultipleManagedExternalDependentSameTypeIT { @RegisterExtension - LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension extension = LocallyRunOperatorExtension.builder() .withReconciler(new MultipleManagedExternalDependentResourceReconciler()) .build(); @@ -42,15 +42,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 53dc793a6b..f281fe95db 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 @@ -18,9 +18,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 = "#"; @@ -80,4 +81,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 f9f6a638a5..baaa829534 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 @@ -57,7 +57,7 @@ public String toString() { } @Override - public String id() { + public String externalResourceId() { return name; } } From b60dd662a416352fec8df25748a5dfc559a51b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 30 Sep 2025 12:57:13 +0200 Subject: [PATCH 08/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent-resources.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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..27c11cb55d 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 it +[subclasses](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external). + +While with kubernetes resources we do some nice assumptions about the resources, 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 From 03b27f98cf90d6a89b70a7fe3534561a0902f37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 30 Sep 2025 13:03:16 +0200 Subject: [PATCH 09/15] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent-resource-and-workflows/dependent-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 27c11cb55d..09a9ecff8f 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 @@ -361,7 +361,7 @@ Dependent resources are designed to manage also non-Kubernetes or external resou To implement such dependent you can extend `AbstractExternalDependentResource` or one of it [subclasses](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external). -While with kubernetes resources we do some nice assumptions about the resources, like +While with kubernetes resources we do some 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. From 07156fef79ff38788c74b68fcd51f07c08aaea4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 1 Oct 2025 09:15:37 +0200 Subject: [PATCH 10/15] Update docs/content/en/docs/documentation/dependent-resource-and-workflows/dependent-resources.md Co-authored-by: Martin Stefanko --- .../dependent-resource-and-workflows/dependent-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 09a9ecff8f..17d4e4bd25 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 @@ -358,7 +358,7 @@ To see how bulk dependent resources interact with workflow conditions, please re ## 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 it +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). While with kubernetes resources we do some nice assumptions, like From 665a61fad864def029a478038afd56908e5b43c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 1 Oct 2025 09:23:21 +0200 Subject: [PATCH 11/15] Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalDependentIDProvider.java Co-authored-by: Martin Stefanko --- .../processing/dependent/ExternalDependentIDProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1df1b4bf08..e8aaf5d522 100644 --- 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 @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing.dependent; /** - * Provides the identifier for a bean that represents and external resource. This ID is used to + * 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)}`. * From d45b8c8b34cf45e4423a8484e015236857daccfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 1 Oct 2025 09:26:22 +0200 Subject: [PATCH 12/15] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent-resource-and-workflows/dependent-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 17d4e4bd25..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 @@ -361,7 +361,7 @@ Dependent resources are designed to manage also non-Kubernetes or external resou 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). -While with kubernetes resources we do some nice assumptions, like +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. From 437e28dd70687793482e2d8d41611ac1e88f7691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 2 Oct 2025 10:41:48 +0200 Subject: [PATCH 13/15] message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/AbstractExternalDependentResource.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9b447ddc6c..2c5be82288 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 @@ -142,8 +142,8 @@ protected Optional selectTargetSecondaryResource( .toList(); } else { throw new IllegalStateException( - "Either implement ExternalDependentIDProvider or override" - + " selectTargetSecondaryResource."); + "Either implement ExternalDependentIDProvider or override this " + + " (selectTargetSecondaryResource) method."); } if (targetResources.size() > 1) { throw new IllegalStateException( From ba8ba20fc8d1adfbcfc2bc2da39832eb978a6f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 8 Oct 2025 10:32:02 +0200 Subject: [PATCH 14/15] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/ReconcilerUtilsTest.java | 39 ------------------- 1 file changed, 39 deletions(-) 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 5d2e37d718..3bbe2a894b 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 @@ -23,7 +23,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; @@ -168,44 +167,6 @@ void handleKubernetesExceptionShouldThrowMissingCRDExceptionWhenAppropriate() { HasMetadata.getFullResourceName(Tomcat.class))); } - @Test - void checksIfOwnerReferenceCanBeAdded() { - assertThrows( - OperatorException.class, - () -> - ReconcilerUtils.checkIfCanAddOwnerReference( - namespacedResource(), namespacedResourceFromOtherNamespace())); - - assertThrows( - OperatorException.class, - () -> - ReconcilerUtils.checkIfCanAddOwnerReference( - namespacedResource(), clusterScopedResource())); - - assertDoesNotThrow( - () -> { - ReconcilerUtils.checkIfCanAddOwnerReference( - clusterScopedResource(), clusterScopedResource()); - ReconcilerUtils.checkIfCanAddOwnerReference(namespacedResource(), namespacedResource()); - }); - } - - private ClusterRole clusterScopedResource() { - return new ClusterRoleBuilder().withMetadata(new ObjectMetaBuilder().build()).build(); - } - - private ConfigMap namespacedResource() { - return new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("testns1").build()) - .build(); - } - - private ConfigMap namespacedResourceFromOtherNamespace() { - return new ConfigMapBuilder() - .withMetadata(new ObjectMetaBuilder().withNamespace("testns2").build()) - .build(); - } - @Group("tomcatoperator.io") @Version("v1") @ShortNames("tc") From 83321965c0017e2d79fa59d4854c38ac640d2c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 9 Oct 2025 15:12:11 +0200 Subject: [PATCH 15/15] rebase on next MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/ExternalDependentIDProvider.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 index e8aaf5d522..b6c755178d 100644 --- 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 @@ -1,3 +1,18 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.javaoperatorsdk.operator.processing.dependent; /**