Skip to content

Commit 6a05ebc

Browse files
authored
Dependent resources standalone mode (#914)
1 parent d3ad7e5 commit 6a05ebc

File tree

26 files changed

+821
-460
lines changed

26 files changed

+821
-460
lines changed

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/ReconcilerUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.javaoperatorsdk.operator;
22

3+
import java.lang.reflect.InvocationTargetException;
4+
import java.lang.reflect.Method;
35
import java.util.Locale;
46

57
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -98,4 +100,19 @@ public static String getDefaultReconcilerName(String reconcilerClassName) {
98100
}
99101
return reconcilerClassName.toLowerCase(Locale.ROOT);
100102
}
103+
104+
public static boolean specsEqual(HasMetadata r1, HasMetadata r2) {
105+
return getSpec(r1).equals(getSpec(r2));
106+
}
107+
108+
// will be replaced with: https://github.com/fabric8io/kubernetes-client/issues/3816
109+
public static Object getSpec(HasMetadata resource) {
110+
try {
111+
Method getSpecMethod = resource.getClass().getMethod("getSpec");
112+
return getSpecMethod.invoke(resource);
113+
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
114+
throw new IllegalStateException(e);
115+
}
116+
}
117+
101118
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 abstract class AbstractDependentResource<R, P extends HasMetadata>
7+
implements DependentResource<R, P> {
8+
9+
@Override
10+
public void reconcile(P primary, Context context) {
11+
var actual = getResource(primary);
12+
var desired = desired(primary, context);
13+
if (actual.isEmpty()) {
14+
create(desired, primary, context);
15+
} else {
16+
if (!match(actual.get(), desired, context)) {
17+
update(actual.get(), desired, primary, context);
18+
}
19+
}
20+
}
21+
22+
protected abstract R desired(P primary, Context context);
23+
24+
protected abstract boolean match(R actual, R target, Context context);
25+
26+
protected abstract R create(R target, P primary, Context context);
27+
28+
// the actual needed to copy/preserve new labels or annotations
29+
protected abstract R update(R actual, R target, P primary, Context context);
30+
31+
}

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

Lines changed: 7 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,54 +9,18 @@
99
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
1010

1111
public interface DependentResource<R, P extends HasMetadata> {
12-
default EventSource initEventSource(EventSourceContext<P> context) {
13-
throw new IllegalStateException("Must be implemented if not automatically provided by the SDK");
14-
}
12+
13+
Optional<EventSource> eventSource(EventSourceContext<P> context);
1514

1615
@SuppressWarnings("unchecked")
1716
default Class<R> resourceType() {
1817
return (Class<R>) Utils.getFirstTypeArgumentFromInterface(getClass());
1918
}
2019

21-
default void delete(R fetched, P primary, Context context) {}
22-
23-
/**
24-
* Computes the desired state of the dependent based on the state provided by the specified
25-
* primary resource.
26-
*
27-
* The default implementation returns {@code empty} which corresponds to the case where the
28-
* associated dependent should never be created by the associated reconciler or that the global
29-
* state of the cluster doesn't allow for the resource to be created at this point.
30-
*
31-
* @param primary the primary resource associated with the reconciliation process
32-
* @param context the {@link Context} associated with the reconciliation process
33-
* @return an instance of the dependent resource matching the desired state specified by the
34-
* primary resource or {@code empty} if the dependent shouldn't be created at this point
35-
* (or ever)
36-
*/
37-
default Optional<R> desired(P primary, Context context) {
38-
return Optional.empty();
39-
}
20+
void reconcile(P primary, Context context);
21+
22+
void delete(P primary, Context context);
23+
24+
Optional<R> getResource(P primaryResource);
4025

41-
/**
42-
* Checks whether the actual resource as fetched from the cluster matches the desired state
43-
* expressed by the specified primary resource.
44-
*
45-
* The default implementation always return {@code true}, which corresponds to the behavior where
46-
* the dependent never needs to be updated after it's been created.
47-
*
48-
* Note that failure to properly implement this method will lead to infinite loops. In particular,
49-
* for typical Kubernetes resource implementations, simply calling
50-
* {@code desired(primary, context).equals(actual)} is not enough because metadata will usually be
51-
* different.
52-
*
53-
* @param actual the current state of the resource as fetched from the cluster
54-
* @param primary the primary resource associated with the reconciliation request
55-
* @param context the {@link Context} associated with the reconciliation request
56-
* @return {@code true} if the actual state of the resource matches the desired state expressed by
57-
* the specified primary resource, {@code false} otherwise
58-
*/
59-
default boolean match(R actual, P primary, Context context) {
60-
return true;
61-
}
6226
}
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.api.reconciler.Context;
4+
5+
@FunctionalInterface
6+
public interface DesiredSupplier<R, P> {
7+
8+
R getDesired(P primary, Context context);
9+
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import java.util.Optional;
4+
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
import io.fabric8.kubernetes.api.model.HasMetadata;
9+
import io.fabric8.kubernetes.client.KubernetesClient;
10+
import io.javaoperatorsdk.operator.ReconcilerUtils;
11+
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
12+
import io.javaoperatorsdk.operator.api.reconciler.Context;
13+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
14+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
15+
import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier;
16+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
17+
import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever;
18+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
19+
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
20+
21+
public abstract class KubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
22+
extends AbstractDependentResource<R, P> {
23+
24+
private static final Logger log = LoggerFactory.getLogger(KubernetesDependentResource.class);
25+
26+
protected KubernetesClient client;
27+
private boolean explicitDelete = false;
28+
private boolean owned = true;
29+
private InformerEventSource<R, P> informerEventSource;
30+
31+
public KubernetesDependentResource() {
32+
this(null);
33+
}
34+
35+
public KubernetesDependentResource(KubernetesClient client) {
36+
this.client = client;
37+
}
38+
39+
protected void beforeCreateOrUpdate(R desired, P primary) {
40+
if (owned) {
41+
desired.addOwnerReference(primary);
42+
}
43+
}
44+
45+
@Override
46+
protected boolean match(R actual, R target, Context context) {
47+
return ReconcilerUtils.specsEqual(actual, target);
48+
}
49+
50+
@SuppressWarnings("unchecked")
51+
@Override
52+
protected R create(R target, P primary, Context context) {
53+
log.debug("Creating target resource with type: " +
54+
"{}, with id: {}", target.getClass(), ResourceID.fromResource(target));
55+
beforeCreateOrUpdate(target, primary);
56+
Class<R> targetClass = (Class<R>) target.getClass();
57+
return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace())
58+
.create(target);
59+
}
60+
61+
@SuppressWarnings("unchecked")
62+
@Override
63+
protected R update(R actual, R target, P primary, Context context) {
64+
log.debug("Updating target resource with type: {}, with id: {}", target.getClass(),
65+
ResourceID.fromResource(target));
66+
beforeCreateOrUpdate(target, primary);
67+
Class<R> targetClass = (Class<R>) target.getClass();
68+
return client.resources(targetClass).inNamespace(target.getMetadata().getNamespace())
69+
.replace(target);
70+
}
71+
72+
@SuppressWarnings({"unchecked", "rawtypes"})
73+
@Override
74+
public Optional<EventSource> eventSource(EventSourceContext<P> context) {
75+
if (informerEventSource != null) {
76+
return Optional.of(informerEventSource);
77+
}
78+
var informerConfig = initInformerConfiguration(context);
79+
informerEventSource = new InformerEventSource(informerConfig, context);
80+
return Optional.of(informerEventSource);
81+
}
82+
83+
@SuppressWarnings("unchecked")
84+
private InformerConfiguration<R, P> initInformerConfiguration(EventSourceContext<P> context) {
85+
PrimaryResourcesRetriever<R> associatedPrimaries =
86+
(this instanceof PrimaryResourcesRetriever) ? (PrimaryResourcesRetriever<R>) this
87+
: getDefaultPrimaryResourcesRetriever();
88+
89+
AssociatedSecondaryResourceIdentifier<P> associatedSecondary =
90+
(this instanceof AssociatedSecondaryResourceIdentifier)
91+
? (AssociatedSecondaryResourceIdentifier<P>) this
92+
: getDefaultAssociatedSecondaryResourceIdentifier();
93+
94+
return InformerConfiguration.from(context, resourceType())
95+
.withPrimaryResourcesRetriever(associatedPrimaries)
96+
.withAssociatedSecondaryResourceIdentifier(associatedSecondary)
97+
.build();
98+
}
99+
100+
protected AssociatedSecondaryResourceIdentifier<P> getDefaultAssociatedSecondaryResourceIdentifier() {
101+
return ResourceID::fromResource;
102+
}
103+
104+
protected PrimaryResourcesRetriever<R> getDefaultPrimaryResourcesRetriever() {
105+
return Mappers.fromOwnerReference();
106+
}
107+
108+
public KubernetesDependentResource<R, P> setInformerEventSource(
109+
InformerEventSource<R, P> informerEventSource) {
110+
this.informerEventSource = informerEventSource;
111+
return this;
112+
}
113+
114+
@Override
115+
public void delete(P primary, Context context) {
116+
if (explicitDelete) {
117+
var resource = getResource(primary);
118+
resource.ifPresent(r -> client.resource(r).delete());
119+
}
120+
}
121+
122+
@Override
123+
public Optional<R> getResource(P primaryResource) {
124+
return informerEventSource.getAssociated(primaryResource);
125+
}
126+
127+
public KubernetesDependentResource<R, P> setClient(KubernetesClient client) {
128+
this.client = client;
129+
return this;
130+
}
131+
132+
133+
public KubernetesDependentResource<R, P> setExplicitDelete(boolean explicitDelete) {
134+
this.explicitDelete = explicitDelete;
135+
return this;
136+
}
137+
138+
public boolean isExplicitDelete() {
139+
return explicitDelete;
140+
}
141+
142+
public boolean isOwned() {
143+
return owned;
144+
}
145+
146+
public KubernetesDependentResource<R, P> setOwned(boolean owned) {
147+
this.owned = owned;
148+
return this;
149+
}
150+
}

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

Lines changed: 0 additions & 11 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.fabric8.kubernetes.client.KubernetesClient;
5+
import io.javaoperatorsdk.operator.api.reconciler.Context;
6+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
7+
import io.javaoperatorsdk.operator.processing.event.source.AssociatedSecondaryResourceIdentifier;
8+
import io.javaoperatorsdk.operator.processing.event.source.PrimaryResourcesRetriever;
9+
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
10+
11+
// todo shorter name
12+
public class StandaloneKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
13+
extends KubernetesDependentResource<R, P> {
14+
15+
private final DesiredSupplier<R, P> desiredSupplier;
16+
private final Class<R> resourceType;
17+
private AssociatedSecondaryResourceIdentifier<P> associatedSecondaryResourceIdentifier =
18+
ResourceID::fromResource;
19+
private PrimaryResourcesRetriever<R> primaryResourcesRetriever = Mappers.fromOwnerReference();
20+
21+
public StandaloneKubernetesDependentResource(
22+
Class<R> resourceType, DesiredSupplier<R, P> desiredSupplier) {
23+
this(null, resourceType, desiredSupplier);
24+
}
25+
26+
public StandaloneKubernetesDependentResource(
27+
KubernetesClient client, Class<R> resourceType, DesiredSupplier<R, P> desiredSupplier) {
28+
super(client);
29+
this.desiredSupplier = desiredSupplier;
30+
this.resourceType = resourceType;
31+
}
32+
33+
@Override
34+
protected R desired(P primary, Context context) {
35+
return desiredSupplier.getDesired(primary, context);
36+
}
37+
38+
public Class<R> resourceType() {
39+
return resourceType;
40+
}
41+
42+
public StandaloneKubernetesDependentResource<R, P> setAssociatedSecondaryResourceIdentifier(
43+
AssociatedSecondaryResourceIdentifier<P> associatedSecondaryResourceIdentifier) {
44+
this.associatedSecondaryResourceIdentifier = associatedSecondaryResourceIdentifier;
45+
return this;
46+
}
47+
48+
public StandaloneKubernetesDependentResource<R, P> setPrimaryResourcesRetriever(
49+
PrimaryResourcesRetriever<R> primaryResourcesRetriever) {
50+
this.primaryResourcesRetriever = primaryResourcesRetriever;
51+
return this;
52+
}
53+
54+
@Override
55+
protected AssociatedSecondaryResourceIdentifier<P> getDefaultAssociatedSecondaryResourceIdentifier() {
56+
return this.associatedSecondaryResourceIdentifier;
57+
}
58+
59+
@Override
60+
protected PrimaryResourcesRetriever<R> getDefaultPrimaryResourcesRetriever() {
61+
return this.primaryResourcesRetriever;
62+
}
63+
}

0 commit comments

Comments
 (0)