Skip to content

Commit 45fb7e0

Browse files
authored
feat: Dependent Resources for External Resources (#991)
1 parent 9d269a6 commit 45fb7e0

File tree

40 files changed

+720
-334
lines changed

40 files changed

+720
-334
lines changed

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

Lines changed: 80 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import io.fabric8.kubernetes.api.model.HasMetadata;
77
import io.javaoperatorsdk.operator.api.reconciler.Context;
8+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
89

910
public abstract class AbstractDependentResource<R, P extends HasMetadata>
1011
implements DependentResource<R, P> {
@@ -13,21 +14,25 @@ public abstract class AbstractDependentResource<R, P extends HasMetadata>
1314
private final boolean creatable = this instanceof Creator;
1415
private final boolean updatable = this instanceof Updater;
1516
private final boolean deletable = this instanceof Deleter;
17+
private final boolean filteringEventSource;
18+
private final boolean cachingEventSource;
1619
protected Creator<R, P> creator;
1720
protected Updater<R, P> updater;
1821
protected Deleter<P> deleter;
1922

2023
@SuppressWarnings("unchecked")
2124
public AbstractDependentResource() {
22-
init(Creator.NOOP, Updater.NOOP, Deleter.NOOP);
23-
}
24-
25-
@SuppressWarnings({"unchecked"})
26-
protected void init(Creator<R, P> defaultCreator, Updater<R, P> defaultUpdater,
27-
Deleter<P> defaultDeleter) {
28-
creator = creatable ? (Creator<R, P>) this : defaultCreator;
29-
updater = updatable ? (Updater<R, P>) this : defaultUpdater;
30-
deleter = deletable ? (Deleter<P>) this : defaultDeleter;
25+
if (this instanceof EventSourceProvider) {
26+
final var eventSource = ((EventSourceProvider<P>) this).getEventSource();
27+
filteringEventSource = eventSource instanceof RecentOperationEventFilter;
28+
cachingEventSource = eventSource instanceof RecentOperationCacheFiller;
29+
} else {
30+
filteringEventSource = false;
31+
cachingEventSource = false;
32+
}
33+
creator = creatable ? (Creator<R, P>) this : null;
34+
updater = updatable ? (Updater<R, P>) this : null;
35+
deleter = deletable ? (Deleter<P>) this : null;
3136
}
3237

3338
@Override
@@ -40,7 +45,7 @@ public void reconcile(P primary, Context context) {
4045
if (creatable) {
4146
var desired = desired(primary, context);
4247
log.debug("Creating dependent {} for primary {}", desired, primary);
43-
creator.create(desired, primary, context);
48+
handleCreate(desired, primary, context);
4449
}
4550
} else {
4651
final var actual = maybeActual.get();
@@ -49,7 +54,7 @@ public void reconcile(P primary, Context context) {
4954
if (!match.matched()) {
5055
final var desired = match.computedDesired().orElse(desired(primary, context));
5156
log.debug("Updating dependent {} for primary {}", desired, primary);
52-
updater.update(actual, desired, primary, context);
57+
handleUpdate(actual, desired, primary, context);
5358
}
5459
} else {
5560
log.debug("Update skipped for dependent {} as it matched the existing one", actual);
@@ -62,8 +67,71 @@ public void reconcile(P primary, Context context) {
6267
}
6368
}
6469

70+
protected void handleCreate(R desired, P primary, Context context) {
71+
ResourceID resourceID = ResourceID.fromResource(primary);
72+
R created = null;
73+
try {
74+
prepareEventFiltering(desired, resourceID);
75+
created = creator.create(desired, primary, context);
76+
cacheAfterCreate(resourceID, created);
77+
} catch (RuntimeException e) {
78+
cleanupAfterEventFiltering(desired, resourceID, created);
79+
throw e;
80+
}
81+
}
82+
83+
private void cleanupAfterEventFiltering(R desired, ResourceID resourceID, R created) {
84+
if (filteringEventSource) {
85+
eventSourceAsRecentOperationEventFilter()
86+
.cleanupOnCreateOrUpdateEventFiltering(resourceID, created);
87+
}
88+
}
89+
90+
private void cacheAfterCreate(ResourceID resourceID, R created) {
91+
if (cachingEventSource) {
92+
eventSourceAsRecentOperationCacheFiller().handleRecentResourceCreate(resourceID, created);
93+
}
94+
}
95+
96+
private void cacheAfterUpdate(R actual, ResourceID resourceID, R updated) {
97+
if (cachingEventSource) {
98+
eventSourceAsRecentOperationCacheFiller().handleRecentResourceUpdate(resourceID, updated,
99+
actual);
100+
}
101+
}
102+
103+
private void prepareEventFiltering(R desired, ResourceID resourceID) {
104+
if (filteringEventSource) {
105+
eventSourceAsRecentOperationEventFilter().prepareForCreateOrUpdateEventFiltering(resourceID,
106+
desired);
107+
}
108+
}
109+
110+
protected void handleUpdate(R actual, R desired, P primary, Context context) {
111+
ResourceID resourceID = ResourceID.fromResource(primary);
112+
R updated = null;
113+
try {
114+
prepareEventFiltering(desired, resourceID);
115+
updated = updater.update(actual, desired, primary, context);
116+
cacheAfterUpdate(actual, resourceID, updated);
117+
} catch (RuntimeException e) {
118+
cleanupAfterEventFiltering(desired, resourceID, updated);
119+
throw e;
120+
}
121+
}
122+
123+
@SuppressWarnings("unchecked")
124+
private RecentOperationEventFilter<R> eventSourceAsRecentOperationEventFilter() {
125+
return (RecentOperationEventFilter<R>) ((EventSourceProvider<P>) this).getEventSource();
126+
}
127+
128+
@SuppressWarnings("unchecked")
129+
private RecentOperationCacheFiller<R> eventSourceAsRecentOperationCacheFiller() {
130+
return (RecentOperationCacheFiller<R>) ((EventSourceProvider<P>) this).getEventSource();
131+
}
132+
65133
@Override
66-
public void delete(P primary, Context context) {
134+
public void cleanup(P primary, Context context) {
67135
if (isDeletable(primary, context)) {
68136
deleter.delete(primary, context);
69137
}

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
import io.fabric8.kubernetes.api.model.HasMetadata;
44
import io.javaoperatorsdk.operator.api.reconciler.Context;
55

6-
@SuppressWarnings("rawtypes")
6+
@FunctionalInterface
77
public interface Creator<R, P extends HasMetadata> {
8-
Creator NOOP = (desired, primary, context) -> {
9-
};
10-
11-
void create(R desired, P primary, Context context);
8+
R create(R desired, P primary, Context context);
129
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33
import io.fabric8.kubernetes.api.model.HasMetadata;
44
import io.javaoperatorsdk.operator.api.reconciler.Context;
55

6-
@SuppressWarnings("rawtypes")
6+
@FunctionalInterface
77
public interface Deleter<P extends HasMetadata> {
8-
Deleter NOOP = (primary, context) -> {
9-
};
10-
118
void delete(P primary, Context context);
129
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
public interface DependentResource<R, P extends HasMetadata> {
99
void reconcile(P primary, Context context);
1010

11-
default void delete(P primary, Context context) {}
11+
default void cleanup(P primary, Context context) {}
1212

1313
Optional<R> getResource(P primaryResource);
1414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
6+
public class DesiredEqualsMatcher<R, P extends HasMetadata> implements Matcher<R, P> {
7+
8+
private final AbstractDependentResource<R, P> abstractDependentResource;
9+
10+
public DesiredEqualsMatcher(AbstractDependentResource<R, P> abstractDependentResource) {
11+
this.abstractDependentResource = abstractDependentResource;
12+
}
13+
14+
@Override
15+
public Result<R> match(R actualResource, P primary, Context context) {
16+
var desired = abstractDependentResource.desired(primary, context);
17+
return Result.computed(actualResource.equals(desired), desired);
18+
}
19+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
66

77
public interface EventSourceProvider<P extends HasMetadata> {
8+
/**
9+
* @param context - event source context where the event source is initialized
10+
* @return the initiated event source.
11+
*/
12+
EventSource initEventSource(EventSourceContext<P> context);
813

9-
EventSource eventSource(EventSourceContext<P> context);
14+
EventSource getEventSource();
1015
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
4+
5+
public interface RecentOperationCacheFiller<R> {
6+
7+
void handleRecentResourceCreate(ResourceID resourceID, R resource);
8+
9+
void handleRecentResourceUpdate(ResourceID resourceID, R resource, R previousResourceVersion);
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
4+
5+
public interface RecentOperationEventFilter<R> extends RecentOperationCacheFiller<R> {
6+
7+
void prepareForCreateOrUpdateEventFiltering(ResourceID resourceID, R resource);
8+
9+
void cleanupOnCreateOrUpdateEventFiltering(ResourceID resourceID, R resource);
10+
11+
}

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

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,8 @@
44
import io.javaoperatorsdk.operator.api.reconciler.Context;
55
import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher.Result;
66

7-
@SuppressWarnings("rawtypes")
87
public interface Updater<R, P extends HasMetadata> {
9-
Updater NOOP = new Updater() {
10-
@Override
11-
public void update(Object actual, Object desired, HasMetadata primary, Context context) {}
12-
13-
@Override
14-
public Result match(Object actualResource, HasMetadata primary, Context context) {
15-
return Result.nonComputed(true);
16-
}
17-
};
18-
19-
void update(R actual, R desired, P primary, Context context);
8+
R update(R actual, R desired, P primary, Context context);
209

2110
Result<R> match(R actualResource, P primary, Context context);
2211
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceManager.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public List<EventSource> prepareEventSources(EventSourceContext<P> context) {
5454
final var dependentResource = createAndConfigureFrom(drc, context.getClient());
5555
if (dependentResource instanceof EventSourceProvider) {
5656
EventSourceProvider provider = (EventSourceProvider) dependentResource;
57-
sources.add(provider.eventSource(context));
57+
sources.add(provider.initEventSource(context));
5858
}
5959
return dependentResource;
6060
})
@@ -72,7 +72,7 @@ public UpdateControl<P> reconcile(P resource, Context context) {
7272
@Override
7373
public DeleteControl cleanup(P resource, Context context) {
7474
initContextIfNeeded(resource, context);
75-
dependents.forEach(dependent -> dependent.delete(resource, context));
75+
dependents.forEach(dependent -> dependent.cleanup(resource, context));
7676
return Reconciler.super.cleanup(resource, context);
7777
}
7878

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.external;
2+
3+
import java.util.Optional;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.api.config.Utils;
7+
import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource;
8+
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
9+
import io.javaoperatorsdk.operator.processing.event.ExternalResourceCachingEventSource;
10+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
11+
12+
public abstract class AbstractCachingDependentResource<R, P extends HasMetadata>
13+
extends AbstractDependentResource<R, P> implements EventSourceProvider<P> {
14+
15+
protected ExternalResourceCachingEventSource<R, P> eventSource;
16+
17+
public Optional<R> fetchResource(P primaryResource) {
18+
return eventSource.getAssociated(primaryResource);
19+
}
20+
21+
@Override
22+
public EventSource getEventSource() {
23+
return eventSource;
24+
}
25+
26+
protected Class<R> resourceType() {
27+
return (Class<R>) Utils.getFirstTypeArgumentFromExtendedClass(getClass());
28+
}
29+
30+
@Override
31+
public Optional<R> getResource(P primaryResource) {
32+
return eventSource.getAssociated(primaryResource);
33+
}
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.external;
2+
3+
import java.util.Optional;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.api.reconciler.Context;
7+
import io.javaoperatorsdk.operator.api.reconciler.dependent.AbstractDependentResource;
8+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DesiredEqualsMatcher;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher;
10+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
11+
import io.javaoperatorsdk.operator.processing.event.source.ConcurrentHashMapCache;
12+
import io.javaoperatorsdk.operator.processing.event.source.UpdatableCache;
13+
14+
/** A base class for external dependent resources that don't have an event source. */
15+
public abstract class AbstractSimpleDependentResource<R, P extends HasMetadata>
16+
extends AbstractDependentResource<R, P> {
17+
18+
// cache serves only to keep the resource readable again until next reconciliation when the
19+
// new resource is read again.
20+
protected final UpdatableCache<R> cache;
21+
protected Matcher<R, P> matcher;
22+
23+
public AbstractSimpleDependentResource() {
24+
this(new ConcurrentHashMapCache<>());
25+
}
26+
27+
public AbstractSimpleDependentResource(UpdatableCache<R> cache) {
28+
this.cache = cache;
29+
initMatcher();
30+
}
31+
32+
@Override
33+
public Optional<R> getResource(HasMetadata primaryResource) {
34+
return cache.get(ResourceID.fromResource(primaryResource));
35+
}
36+
37+
/** Actually read the resource from the target API */
38+
public abstract Optional<R> fetchResource(HasMetadata primaryResource);
39+
40+
@Override
41+
public void reconcile(P primary, Context context) {
42+
var resourceId = ResourceID.fromResource(primary);
43+
Optional<R> resource = fetchResource(primary);
44+
resource.ifPresentOrElse(r -> cache.put(resourceId, r), () -> cache.remove(resourceId));
45+
super.reconcile(primary, context);
46+
}
47+
48+
public void cleanup(P primary, Context context) {
49+
super.cleanup(primary, context);
50+
cache.remove(ResourceID.fromResource(primary));
51+
}
52+
53+
@Override
54+
protected void handleCreate(R desired, P primary, Context context) {
55+
var res = this.creator.create(desired, primary, context);
56+
cache.put(ResourceID.fromResource(primary), res);
57+
}
58+
59+
@Override
60+
protected void handleUpdate(R actual, R desired, P primary, Context context) {
61+
var res = updater.update(actual, desired, primary, context);
62+
cache.put(ResourceID.fromResource(primary), res);
63+
}
64+
65+
public Matcher.Result<R> match(R actualResource, P primary, Context context) {
66+
return matcher.match(actualResource, primary, context);
67+
}
68+
69+
protected void initMatcher() {
70+
matcher = new DesiredEqualsMatcher<>(this);
71+
}
72+
73+
}

0 commit comments

Comments
 (0)