Skip to content

Commit 1012314

Browse files
feat(core): add by-name secondary resource lookup on Context
Signed-off-by: Antonio Fernandez Alhambra <antonio.alhambra@hivemq.com>
1 parent afa83b9 commit 1012314

3 files changed

Lines changed: 385 additions & 11 deletions

File tree

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
2626
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
2727
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
28+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
2829
import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache;
2930

3031
public interface Context<P extends HasMetadata> {
@@ -114,6 +115,88 @@ default <R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType) {
114115

115116
<R> Optional<R> getSecondaryResource(Class<R> expectedType, String eventSourceName);
116117

118+
/**
119+
* Retrieves a specific secondary resource by its {@link ResourceID} from the event source
120+
* identified by the given name.
121+
*
122+
* <p>This is a typed convenience over manually retrieving the {@link
123+
* io.javaoperatorsdk.operator.processing.event.source.EventSource} and calling its cache. When
124+
* the underlying event source implements {@link
125+
* io.javaoperatorsdk.operator.processing.event.source.Cache}, the lookup is a direct cache lookup
126+
* and read-cache-after-write consistent.
127+
*
128+
* <p>{@code eventSourceName} may be {@code null}. When {@code null} and {@code expectedType} is
129+
* part of a managed workflow whose activation condition may not have registered the event source,
130+
* an empty {@link Optional} is returned instead of throwing {@link
131+
* io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException}.
132+
*
133+
* @param expectedType the class representing the type of secondary resource to retrieve
134+
* @param eventSourceName the name of the event source to look in (may be {@code null})
135+
* @param resourceID the {@link ResourceID} identifying the secondary resource
136+
* @param <R> the type of secondary resource to retrieve
137+
* @return an {@link Optional} containing the matching secondary resource, or {@link
138+
* Optional#empty()} if none matches
139+
* @throws io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException if no event
140+
* source is registered for the given type and name (and no workflow activation condition
141+
* accounts for it)
142+
* @since 5.3.5
143+
*/
144+
<R> Optional<R> getSecondaryResource(
145+
Class<R> expectedType, String eventSourceName, ResourceID resourceID);
146+
147+
/**
148+
* Convenience overload of {@link #getSecondaryResource(Class, String, ResourceID)} that
149+
* constructs the {@link ResourceID} using the given name and the primary resource's namespace.
150+
*
151+
* <p>If the primary resource is cluster-scoped (no namespace), the lookup is performed against
152+
* the cluster scope. To target a specific namespace from a cluster-scoped primary, use {@link
153+
* #getSecondaryResource(Class, String, ResourceID)} directly.
154+
*
155+
* <p>{@code eventSourceName} may be {@code null} with the same semantics as in {@link
156+
* #getSecondaryResource(Class, String, ResourceID)}.
157+
*
158+
* @param expectedType the class representing the type of secondary resource to retrieve
159+
* @param eventSourceName the name of the event source to look in (may be {@code null})
160+
* @param name the name of the secondary resource (namespace inferred from the primary)
161+
* @param <R> the type of secondary resource to retrieve
162+
* @return an {@link Optional} containing the matching secondary resource, or {@link
163+
* Optional#empty()} if none matches
164+
* @since 5.3.5
165+
*/
166+
default <R> Optional<R> getSecondaryResource(
167+
Class<R> expectedType, String eventSourceName, String name) {
168+
return getSecondaryResource(
169+
expectedType,
170+
eventSourceName,
171+
new ResourceID(name, getPrimaryResource().getMetadata().getNamespace()));
172+
}
173+
174+
/**
175+
* Retrieves a {@link Stream} of the secondary resources of the specified type from the event
176+
* source identified by the given name. Useful when several event sources are registered for the
177+
* same type and you need to scope retrieval to one of them, or when you want to apply a custom
178+
* filter at the call site.
179+
*
180+
* <p>When the underlying event source implements {@link ResourceCache}, the stream is
181+
* read-cache-after-write consistent.
182+
*
183+
* <p>{@code eventSourceName} may be {@code null} with the same semantics as in {@link
184+
* #getSecondaryResource(Class, String, ResourceID)}: when {@code null} and {@code expectedType}
185+
* is part of a managed workflow whose activation condition may not have registered the event
186+
* source, an empty {@link Stream} is returned instead of throwing {@link
187+
* io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException}.
188+
*
189+
* @param expectedType the class representing the type of secondary resources to retrieve
190+
* @param eventSourceName the name of the event source to look in (may be {@code null})
191+
* @param <R> the type of secondary resources to retrieve
192+
* @return a {@link Stream} of secondary resources of the specified type
193+
* @throws io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException if no event
194+
* source is registered for the given type and name (and no workflow activation condition
195+
* accounts for it)
196+
* @since 5.3.5
197+
*/
198+
<R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType, String eventSourceName);
199+
117200
ControllerConfiguration<P> getControllerConfiguration();
118201

119202
/**

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
3737
import io.javaoperatorsdk.operator.processing.event.NoEventSourceForClassException;
3838
import io.javaoperatorsdk.operator.processing.event.ResourceID;
39+
import io.javaoperatorsdk.operator.processing.event.source.Cache;
3940

4041
public class DefaultContext<P extends HasMetadata> implements Context<P> {
4142
private RetryInfo retryInfo;
@@ -95,6 +96,20 @@ public <R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType, boolea
9596
}
9697
}
9798

99+
/**
100+
* Whether a missing event source for the given type is the expected case, in which case callers
101+
* should return an empty result instead of propagating the {@link
102+
* NoEventSourceForClassException}.
103+
*
104+
* <p>If a workflow has an activation condition there can be event sources which are only
105+
* registered if the activation condition holds, but to provide a consistent API we return an
106+
* empty result instead of throwing an exception. Note that not only the resource which has an
107+
* activation condition might not be registered but dependents which depend on it.
108+
*/
109+
private boolean isMissingEventSourceExpected(String eventSourceName, Class<?> expectedType) {
110+
return eventSourceName == null && controller.workflowContainsDependentForType(expectedType);
111+
}
112+
98113
private <R> Map<ResourceID, R> deduplicatedMap(Stream<R> stream) {
99114
return stream.collect(
100115
Collectors.toUnmodifiableMap(
@@ -120,19 +135,53 @@ public <T> Optional<T> getSecondaryResource(Class<T> expectedType, String eventS
120135
.getEventSourceFor(expectedType, eventSourceName)
121136
.getSecondaryResource(primaryResource);
122137
} catch (NoEventSourceForClassException e) {
123-
/*
124-
* If a workflow has an activation condition there can be event sources which are only
125-
* registered if the activation condition holds, but to provide a consistent API we return an
126-
* Optional instead of throwing an exception.
127-
*
128-
* Note that not only the resource which has an activation condition might not be registered
129-
* but dependents which depend on it.
130-
*/
131-
if (eventSourceName == null && controller.workflowContainsDependentForType(expectedType)) {
138+
if (isMissingEventSourceExpected(eventSourceName, expectedType)) {
132139
return Optional.empty();
133-
} else {
134-
throw e;
135140
}
141+
throw e;
142+
}
143+
}
144+
145+
@Override
146+
public <R> Optional<R> getSecondaryResource(
147+
Class<R> expectedType, String eventSourceName, ResourceID resourceID) {
148+
try {
149+
final var eventSource =
150+
controller.getEventSourceManager().getEventSourceFor(expectedType, eventSourceName);
151+
if (eventSource instanceof Cache<?> cache) {
152+
return cache.get(resourceID).map(expectedType::cast);
153+
}
154+
return eventSource.getSecondaryResources(primaryResource).stream()
155+
.filter(HasMetadata.class::isInstance)
156+
.map(HasMetadata.class::cast)
157+
.filter(r -> ResourceID.fromResource(r).equals(resourceID))
158+
.findFirst()
159+
.map(expectedType::cast);
160+
} catch (NoEventSourceForClassException e) {
161+
if (isMissingEventSourceExpected(eventSourceName, expectedType)) {
162+
return Optional.empty();
163+
}
164+
throw e;
165+
}
166+
}
167+
168+
@Override
169+
public <R> Stream<R> getSecondaryResourcesAsStream(
170+
Class<R> expectedType, String eventSourceName) {
171+
try {
172+
final var eventSource =
173+
controller.getEventSourceManager().getEventSourceFor(expectedType, eventSourceName);
174+
if (eventSource instanceof ResourceCache<?> resourceCache) {
175+
final var ns = primaryResource.getMetadata().getNamespace();
176+
final Stream<?> stream = ns == null ? resourceCache.list() : resourceCache.list(ns);
177+
return stream.map(expectedType::cast);
178+
}
179+
return eventSource.getSecondaryResources(primaryResource).stream().map(expectedType::cast);
180+
} catch (NoEventSourceForClassException e) {
181+
if (isMissingEventSourceExpected(eventSourceName, expectedType)) {
182+
return Stream.empty();
183+
}
184+
throw e;
136185
}
137186
}
138187

0 commit comments

Comments
 (0)