Skip to content

Commit a2c1366

Browse files
feat(core): add by-name secondary resource lookup on Context (#3373)
1 parent 492744e commit a2c1366

3 files changed

Lines changed: 377 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
@@ -114,6 +114,89 @@ default <R> Stream<R> getSecondaryResourcesAsStream(Class<R> expectedType) {
114114

115115
<R> Optional<R> getSecondaryResource(Class<R> expectedType, String eventSourceName);
116116

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

0 commit comments

Comments
 (0)