Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1bc89e1
improve: ExternalResourceIDProvider extended to be used in CacheKeyMa…
csviri Oct 18, 2025
4b0fbbf
wip
csviri Oct 19, 2025
4de9b85
improve: ResourceIDMapper and ResourceIDProvider for external resources
csviri Oct 23, 2025
26fb256
wip
csviri Oct 23, 2025
7626c48
wip
csviri Oct 24, 2025
1d335f8
feat: ResourceIDMapper for external event sources, external dependent…
csviri Oct 24, 2025
d923e55
wip
csviri Oct 24, 2025
ddef4b0
wip
csviri Oct 24, 2025
f9afd93
migration guide skeleton
csviri Oct 24, 2025
bb02c98
changed sample
csviri Oct 24, 2025
7e9d43e
wip
csviri Oct 24, 2025
991be80
wip
csviri Oct 24, 2025
3549883
wip
csviri Oct 24, 2025
30f3a36
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 27, 2025
8158925
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 27, 2025
1712f8e
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 27, 2025
e952b08
Update operator-framework-core/src/main/java/io/javaoperatorsdk/opera…
csviri Oct 27, 2025
680c355
format
csviri Oct 27, 2025
26a9850
javadoc
csviri Oct 29, 2025
f957efb
migration guide
csviri Oct 29, 2025
a71dd7f
migration guide grammar
csviri Oct 29, 2025
2b41b84
docs update
csviri Oct 29, 2025
c34e7ab
fix: use namespaced constant to avoid potential conflicts
metacosm Oct 30, 2025
3c7da23
fix: typo
metacosm Oct 30, 2025
6885b50
revert change of the id,
csviri Oct 30, 2025
100a09d
improve javadoc for ResourceIDMapper.singleResourceResourceIDMapper
csviri Nov 1, 2025
f69ec1f
comment
csviri Nov 10, 2025
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 @@ -369,19 +369,22 @@ or we can use a matcher based SSA in most of the cases if the resource is manage
### 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) :
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#L133-L147) that helps with that logic.
[`ResourceIDMapper`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceIDMapper.java)
maps the resource to and ID and the ID of desired and actual resource is checked for equality.

Your POJO representing an external resource can implement [`ResourceIDProvider`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceIDProvider.java).
The default `ResourceIDMapper` implementation works on top of resource which implements the `ResourceIDProvider`:

```java

public interface ExternalDependentIDProvider<T> {
public interface ResourceIDProvider<T> {

T externalResourceId();
T resourceId();
}
```

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
Note that if those approaches does not work for your use case, 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.

Expand Down
44 changes: 44 additions & 0 deletions docs/content/en/docs/migration/v5-2-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: Migrating from v5.1 to v5.2
description: Migrating from v5.1 to v5.2
---

Version 5.2 brings some breaking changes to certain components. This document provides
a migration guide for these changes. For all the new features, see the release notes.

## Custom ID types across multiple components using ResourceIDMapper and ResourceIDProvider

Working with the id of a resource is needed across various components in the framework.
Until this version, the components provided by the framework assumed that you could easily
convert the id of a resource into a String representation. For example,
[BulkDependentResources](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java#L46)
worked with a `Map<String,R>` of resources, where the id was always of type String.

Mainly because of the need to manage external dependent resources more elegantly,
we introduced a cross-cutting concept: [`ResourceIDMapper`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceIDMapper.java),
which gets the ID of a resource. This is used across various components, see:

- [`ExternalResourceCachingEventSource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/ExternalResourceCachingEventSource.java#L66)
- [`ExternalBulkDependentResource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/ExternalBulkDependentResource.java)
- [`AbstractExternalDependentResource`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java#L39)
and its subclasses.

We also added [`ResourceIDProvider`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceIDProvider.java),
which you can implement in your Pojo representing a resource.

The easiest way to migrate to this new approach is to implement this interface for your (external) resource
and set the ID type generics for the components above. The default implementation of the `ResourceIDMapper`
works with `ResourceIDProvider` (see [related implementation](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/ResourceIDMapper.java#L52)).

If you cannot implement `ResourceIDProvider` because, for example, the class that represents the external resource is generated and final,
you can always set a custom `ResourceIDMapper` on the components above.

See also:
- related issue: [link](https://github.com/operator-framework/java-operator-sdk/issues/2972)
- related pull requests:
- [2970](https://github.com/operator-framework/java-operator-sdk/pull/2970)
- [3020](https://github.com/operator-framework/java-operator-sdk/pull/3020)




Empty file removed docs/content/fileList.txt
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource;

/**
* Provides id for the target resource. This mapper is used across multiple components of the
* framework, like:
*
* <ul>
* <li>{@link io.javaoperatorsdk.operator.processing.dependent.AbstractExternalDependentResource}
* <li>{@link ExternalResourceCachingEventSource}
* <li>{@link io.javaoperatorsdk.operator.processing.dependent.KubernetesBulkDependentResource}
* </ul>
*
* @see ResourceIDProvider<ID>
*/
public interface ResourceIDMapper<R, ID> {

/**
* @return id for the target resource.
*/
ID idFor(R resource);

/**
* Can be used if a polling event source handles only single secondary resource and the id is
* String. See also docs for: {@link ExternalResourceCachingEventSource}; or in {@link
* io.javaoperatorsdk.operator.processing.dependent.AbstractExternalDependentResource} when there
* is always only one secondary resource for that dependent resource. By definition cannot be used
* for a BulkDependent resources.
*
* @return static id mapper, all resources are mapped for same id.
* @param <R> secondary resource type
*/
static <R> ResourceIDMapper<R, String> singleResourceResourceIDMapper() {
// the result could be any string, by definition would work with any value
return r -> "id";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value should be irrelevant and we should rather use a value that has not risk of conflicting with an identifier a user would create.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have to upgrade upgrade the javadocs, there is no situation where there can be a collision. Assuming that this is by definition not usable for BulkDependentResources - will add javadoc.

If you see some situation where it actually can lead to a collision pls describe it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the harm of choosing a value that we're sure won't cause a collision, regardless of whether one such collision can happen? I mean if the value doesn't matter, then why not choose one that doesn't look like it is meaningful? For that matter, it could be a totally random one.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I belive in this cases we should focus:

  1. the readability and simplicity is the key;
  2. And of course we should not solve not solve issues which are not present - so if there is no situation where it can cause collision, then we should not solve the issue of collision; since that would cause confusion.

So to fulfill those two as you said, any string that is simple enough would do. I choose simply id, but could be for example void, anyId, any string. But imho (and maybe this is a personal pref in some cases), should not be any string that if someone reads the code will have to start thinking why is that a "special value".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But added also a comment that it could be any values, feel free to choose any simple and expressive string literal, it is fine by me.

}

@SuppressWarnings({"rawtypes", "unchecked"})
static <R, ID> ResourceIDMapper<R, ID> resourceIdProviderMapper() {
return r -> {
if (r instanceof ResourceIDProvider resourceIDProvider) {
return (ID) resourceIDProvider.resourceId();
} else {
throw new IllegalStateException(
"Resource does not implement ExternalDependentIDProvider: " + r.getClass());
}
};
}

static <R extends HasMetadata> ResourceIDMapper<R, ResourceID> kubernetesResourceIdMapper() {
return ResourceID::fromResource;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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;

/**
* Provides the identifier for an object that represents a resource. This ID is used:
*
* <ul>
* <li>to select the target external resource for a dependent resource from the resources returned
* by {@link io.javaoperatorsdk.operator.api.reconciler.Context#getSecondaryResources(Class)},
* <li>used in {@link ResourceIDMapper} for event sources in external resources. But also for bulk
* dependent resource see {@link
* io.javaoperatorsdk.operator.processing.dependent.ExternalBulkDependentResource},
* <li>and external event sources, see {@link
* io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource}
* </ul>
*
* @see ResourceIDMapper
* @param <ID> type of the id
*/
public interface ResourceIDProvider<ID> {

/** ID for the resource POJO that implement this interface. */
ID resourceId();
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ protected AbstractDependentResource(String name) {

dependentResourceReconciler =
this instanceof BulkDependentResource
? new BulkDependentResourceReconciler<>((BulkDependentResource<R, P>) this)
? new BulkDependentResourceReconciler<>((BulkDependentResource<R, P, ?>) this)
: new SingleDependentResourceReconciler<>(this);
this.name = name == null ? DependentResource.defaultNameFor(this.getClass()) : name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,22 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller;
import io.javaoperatorsdk.operator.processing.ResourceIDMapper;
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;

public abstract class AbstractExternalDependentResource<
R, P extends HasMetadata, T extends EventSource<R, P>>
R, P extends HasMetadata, T extends EventSource<R, P>, ID>
extends AbstractEventSourceHolderDependentResource<R, P, T> {

private final boolean isDependentResourceWithExplicitState =
this instanceof DependentResourceWithExplicitState;
private final boolean isBulkDependentResource = this instanceof BulkDependentResource;

protected ResourceIDMapper<R, ID> resourceIDMapper = ResourceIDMapper.resourceIdProviderMapper();

@SuppressWarnings("rawtypes")
private DependentResourceWithExplicitState dependentResourceWithExplicitState;

Expand Down Expand Up @@ -131,24 +134,23 @@ 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.");
}
var desiredID = resourceIDMapper.idFor(desired);
targetResources =
secondaryResources.stream()
.filter(r -> resourceIDMapper.idFor(r).equals(desiredID))
.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));
}

public ResourceIDMapper<R, ID> resourceIDMapper() {
return resourceIDMapper;
}

public void setResourceIDMapper(ResourceIDMapper<R, ID> resourceIDMapper) {
this.resourceIDMapper = resourceIDMapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
* implementing this interface will typically also implement one or more additional interfaces such
* as {@link Creator}, {@link Updater}, {@link Deleter}.
*
* @param <ID> type of the id to distinguish resource
* @param <R> the dependent resource type
* @param <P> the primary resource type
*/
public interface BulkDependentResource<R, P extends HasMetadata> {
public interface BulkDependentResource<R, P extends HasMetadata, ID> {

/**
* Retrieves a map of desired secondary resources associated with the specified primary resource,
Expand All @@ -42,7 +43,7 @@ public interface BulkDependentResource<R, P extends HasMetadata> {
* @return a Map associating desired secondary resources with the specified primary via arbitrary
* identifiers
*/
default Map<String, R> desiredResources(P primary, Context<P> context) {
default Map<ID, R> desiredResources(P primary, Context<P> context) {
throw new IllegalStateException(
"Implement desiredResources in case a non read-only bulk dependent resource");
}
Expand All @@ -57,7 +58,7 @@ default Map<String, R> desiredResources(P primary, Context<P> context) {
* @return a Map associating actual secondary resources with the specified primary via arbitrary
* identifiers
*/
Map<String, R> getSecondaryResources(P primary, Context<P> context);
Map<ID, R> getSecondaryResources(P primary, Context<P> context);

/**
* Deletes the actual resource identified by the specified key if it's not in the set of desired
Expand All @@ -69,7 +70,7 @@ default Map<String, R> desiredResources(P primary, Context<P> context) {
* @param key key of the resource
* @param context actual context
*/
void deleteTargetResource(P primary, R resource, String key, Context<P> context);
void deleteTargetResource(P primary, R resource, ID key, Context<P> context);

/**
* Determines whether the specified secondary resource matches the desired state with target index
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,19 @@
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result;

class BulkDependentResourceReconciler<R, P extends HasMetadata>
class BulkDependentResourceReconciler<R, P extends HasMetadata, ID>
implements DependentResourceReconciler<R, P> {

private final BulkDependentResource<R, P> bulkDependentResource;
private final BulkDependentResource<R, P, ID> bulkDependentResource;

BulkDependentResourceReconciler(BulkDependentResource<R, P> bulkDependentResource) {
BulkDependentResourceReconciler(BulkDependentResource<R, P, ID> bulkDependentResource) {
this.bulkDependentResource = bulkDependentResource;
}

@Override
public ReconcileResult<R> reconcile(P primary, Context<P> context) {

Map<String, R> actualResources = bulkDependentResource.getSecondaryResources(primary, context);
Map<ID, R> actualResources = bulkDependentResource.getSecondaryResources(primary, context);
if (!(bulkDependentResource instanceof Creator<?, ?>)
&& !(bulkDependentResource instanceof Deleter<?>)
&& !(bulkDependentResource instanceof Updater<?, ?>)) {
Expand Down Expand Up @@ -73,7 +73,7 @@ public void delete(P primary, Context<P> context) {
}

private void deleteExtraResources(
Set<String> expectedKeys, Map<String, R> actualResources, P primary, Context<P> context) {
Set<ID> expectedKeys, Map<ID, R> actualResources, P primary, Context<P> context) {
actualResources.forEach(
(key, value) -> {
if (!expectedKeys.contains(key)) {
Expand All @@ -90,13 +90,13 @@ private void deleteExtraResources(
* @param <P>
*/
@Ignore
private static class BulkDependentResourceInstance<R, P extends HasMetadata>
private static class BulkDependentResourceInstance<R, P extends HasMetadata, ID>
extends AbstractDependentResource<R, P> implements Creator<R, P>, Deleter<P>, Updater<R, P> {
private final BulkDependentResource<R, P> bulkDependentResource;
private final BulkDependentResource<R, P, ID> bulkDependentResource;
private final R desired;

private BulkDependentResourceInstance(
BulkDependentResource<R, P> bulkDependentResource, R desired) {
BulkDependentResource<R, P, ID> bulkDependentResource, R desired) {
this.bulkDependentResource = bulkDependentResource;
this.desired = desired;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;

public interface CRUDBulkDependentResource<R, P extends HasMetadata>
extends BulkDependentResource<R, P>, Creator<R, P>, BulkUpdater<R, P>, Deleter<P> {}
public interface CRUDBulkDependentResource<R, P extends HasMetadata, ID>
extends BulkDependentResource<R, P, ID>, Creator<R, P>, BulkUpdater<R, P>, Deleter<P> {}
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.javaoperatorsdk.operator.processing.event.source;
package io.javaoperatorsdk.operator.processing.dependent;

public interface CacheKeyMapper<R> {
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.processing.ResourceIDProvider;

String keyFor(R resource);

/**
* Used if a polling event source handles only single secondary resource. See also docs for:
* {@link ExternalResourceCachingEventSource}
*
* @return static id mapper, all resources are mapped for same id.
* @param <T> secondary resource type
*/
static <T> CacheKeyMapper<T> singleResourceCacheKeyMapper() {
return r -> "id";
}
}
public interface CRUDExternalBulkDependentResource<
R extends ResourceIDProvider<ID>, P extends HasMetadata, ID>
extends ExternalBulkDependentResource<R, P, ID>, Creator<R, P>, BulkUpdater<R, P>, Deleter<P> {}
Loading