Skip to content

Commit ae4ad56

Browse files
authored
feat: dependents use traits to specify which features they support (#963)
Also fixes an issue with dependent spec creation.
1 parent 776ea21 commit ae4ad56

File tree

27 files changed

+430
-235
lines changed

27 files changed

+430
-235
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.Optional;
44

5+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
6+
57
public interface Context extends AttributeHolder {
68

79
Optional<RetryInfo> getRetryInfo();
@@ -18,4 +20,6 @@ default <T> T getMandatory(Object key, Class<T> expectedType) {
1820
"Mandatory attribute (key: " + key + ", type: " + expectedType.getName()
1921
+ ") is missing or not of the expected type"));
2022
}
23+
24+
ConfigurationService getConfigurationService();
2125
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33
import java.util.Optional;
44

55
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
67
import io.javaoperatorsdk.operator.processing.Controller;
78

89
public class DefaultContext<P extends HasMetadata> extends MapAttributeHolder implements Context {
910

1011
private final RetryInfo retryInfo;
1112
private final Controller<P> controller;
1213
private final P primaryResource;
14+
private final ConfigurationService configurationService;
1315

1416
public DefaultContext(RetryInfo retryInfo, Controller<P> controller, P primaryResource) {
1517
this.retryInfo = retryInfo;
1618
this.controller = controller;
1719
this.primaryResource = primaryResource;
20+
this.configurationService = controller.getConfiguration().getConfigurationService();
1821
}
1922

2023
@Override
@@ -28,4 +31,9 @@ public <T> Optional<T> getSecondaryResource(Class<T> expectedType, String eventS
2831
.getResourceEventSourceFor(expectedType, eventSourceName)
2932
.flatMap(es -> es.getAssociated(primaryResource));
3033
}
34+
35+
@Override
36+
public ConfigurationService getConfigurationService() {
37+
return configurationService;
38+
}
3139
}
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,87 @@
11
package io.javaoperatorsdk.operator.api.reconciler.dependent;
22

3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
36
import io.fabric8.kubernetes.api.model.HasMetadata;
47
import io.javaoperatorsdk.operator.api.reconciler.Context;
58

6-
public abstract class AbstractDependentResource<R, P extends HasMetadata, C>
9+
public abstract class AbstractDependentResource<R, P extends HasMetadata>
710
implements DependentResource<R, P> {
11+
private static final Logger log = LoggerFactory.getLogger(AbstractDependentResource.class);
12+
13+
private final boolean creatable = this instanceof Creator;
14+
private final boolean updatable = this instanceof Updater;
15+
private final boolean deletable = this instanceof Deleter;
16+
protected Creator<R, P> creator;
17+
protected Updater<R, P> updater;
18+
protected Deleter<P> deleter;
19+
20+
@SuppressWarnings("unchecked")
21+
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;
31+
}
832

933
@Override
1034
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);
35+
final var creatable = isCreatable(primary, context);
36+
final var updatable = isUpdatable(primary, context);
37+
if (creatable || updatable) {
38+
var maybeActual = getResource(primary);
39+
var desired = desired(primary, context);
40+
if (maybeActual.isEmpty()) {
41+
if (creatable) {
42+
log.debug("Creating dependent {} for primary {}", desired, primary);
43+
creator.create(desired, primary, context);
44+
}
45+
} else {
46+
final var actual = maybeActual.get();
47+
if (updatable && !updater.match(actual, desired, context)) {
48+
log.debug("Updating dependent {} for primary {}", desired, primary);
49+
updater.update(actual, desired, primary, context);
50+
} else {
51+
log.debug("Update skipped for dependent {} as it matched the existing one", desired);
52+
}
1853
}
54+
} else {
55+
log.debug(
56+
"Dependent {} is read-only, implement Creator and/or Updater interfaces to modify it",
57+
getClass().getSimpleName());
1958
}
2059
}
2160

22-
protected abstract R desired(P primary, Context context);
61+
@Override
62+
public void delete(P primary, Context context) {
63+
if (isDeletable(primary, context)) {
64+
deleter.delete(primary, context);
65+
}
66+
}
2367

24-
protected abstract boolean match(R actual, R target, Context context);
68+
protected R desired(P primary, Context context) {
69+
throw new IllegalStateException(
70+
"desired method must be implemented if this DependentResource can be created and/or updated");
71+
}
2572

26-
protected abstract R create(R target, P primary, Context context);
73+
@SuppressWarnings("unused")
74+
protected boolean isCreatable(P primary, Context context) {
75+
return creatable;
76+
}
2777

28-
// the actual needed to copy/preserve new labels or annotations
29-
protected abstract R update(R actual, R target, P primary, Context context);
78+
@SuppressWarnings("unused")
79+
protected boolean isUpdatable(P primary, Context context) {
80+
return updatable;
81+
}
3082

83+
@SuppressWarnings("unused")
84+
protected boolean isDeletable(P primary, Context context) {
85+
return deletable;
86+
}
3187
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
@SuppressWarnings("rawtypes")
7+
public interface Creator<R, P extends HasMetadata> {
8+
Creator NOOP = (desired, primary, context) -> {
9+
};
10+
11+
void create(R desired, P primary, Context context);
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
@SuppressWarnings("rawtypes")
7+
public interface Deleter<P extends HasMetadata> {
8+
Deleter NOOP = (primary, context) -> {
9+
};
10+
11+
void delete(P primary, Context context);
12+
}

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-
void delete(P primary, Context context);
11+
default void delete(P primary, Context context) {}
1212

1313
Optional<R> getResource(P primaryResource);
1414
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.Context;
4+
5+
public interface Matcher<R> {
6+
boolean match(R actualResource, R desiredResource, Context context);
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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 interface ResourceUpdatePreProcessor<R extends HasMetadata> {
7+
8+
R replaceSpecOnActual(R actual, R desired, Context context);
9+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import java.util.Objects;
4+
5+
import io.fabric8.kubernetes.api.model.HasMetadata;
6+
import io.javaoperatorsdk.operator.api.reconciler.Context;
7+
8+
@SuppressWarnings("rawtypes")
9+
public interface Updater<R, P extends HasMetadata> {
10+
Updater NOOP = (actual, desired, primary, context) -> {
11+
};
12+
13+
void update(R actual, R desired, P primary, Context context);
14+
15+
default boolean match(R actualResource, R desiredResource, Context context) {
16+
return Objects.equals(actualResource, desiredResource);
17+
}
18+
}
Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,28 @@
66
import io.fabric8.zjsonpatch.JsonDiff;
77
import io.javaoperatorsdk.operator.ReconcilerUtils;
88
import io.javaoperatorsdk.operator.api.reconciler.Context;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Matcher;
910

10-
import com.fasterxml.jackson.databind.ObjectMapper;
11+
public class GenericKubernetesResourceMatcher<R extends HasMetadata> implements Matcher<R> {
1112

12-
public class DesiredValueMatcher implements ResourceMatcher {
13+
private GenericKubernetesResourceMatcher() {}
1314

14-
private final ObjectMapper objectMapper;
15-
16-
public DesiredValueMatcher(ObjectMapper objectMapper) {
17-
this.objectMapper = objectMapper;
15+
@SuppressWarnings({"rawtypes", "unchecked"})
16+
public static <R extends HasMetadata> Matcher<R> matcherFor(Class<R> resourceType) {
17+
if (Secret.class.isAssignableFrom(resourceType)) {
18+
return (actual, desired, context) -> ResourceComparators.compareSecretData((Secret) desired,
19+
(Secret) actual);
20+
} else if (ConfigMap.class.isAssignableFrom(resourceType)) {
21+
return (actual, desired, context) -> ResourceComparators
22+
.compareConfigMapData((ConfigMap) desired, (ConfigMap) actual);
23+
} else {
24+
return new GenericKubernetesResourceMatcher();
25+
}
1826
}
1927

2028
@Override
21-
public boolean match(HasMetadata actualResource, HasMetadata desiredResource, Context context) {
22-
if (actualResource instanceof Secret) {
23-
return ResourceComparators.compareSecretData((Secret) desiredResource,
24-
(Secret) actualResource);
25-
}
26-
if (actualResource instanceof ConfigMap) {
27-
return ResourceComparators.compareConfigMapData((ConfigMap) desiredResource,
28-
(ConfigMap) actualResource);
29-
}
29+
public boolean match(R actualResource, R desiredResource, Context context) {
30+
final var objectMapper = context.getConfigurationService().getObjectMapper();
3031
// reflection will be replaced by this:
3132
// https://github.com/fabric8io/kubernetes-client/issues/3816
3233
var desiredSpecNode = objectMapper.valueToTree(ReconcilerUtils.getSpec(desiredResource));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
2+
3+
import io.fabric8.kubernetes.api.model.ConfigMap;
4+
import io.fabric8.kubernetes.api.model.HasMetadata;
5+
import io.fabric8.kubernetes.api.model.Secret;
6+
import io.javaoperatorsdk.operator.ReconcilerUtils;
7+
import io.javaoperatorsdk.operator.api.reconciler.Context;
8+
import io.javaoperatorsdk.operator.api.reconciler.dependent.ResourceUpdatePreProcessor;
9+
10+
public abstract class GenericResourceUpdatePreProcessor<R extends HasMetadata> implements
11+
ResourceUpdatePreProcessor<R> {
12+
13+
private GenericResourceUpdatePreProcessor() {}
14+
15+
@SuppressWarnings("unchecked")
16+
public static <R extends HasMetadata> ResourceUpdatePreProcessor<R> processorFor(
17+
Class<R> resourceType) {
18+
if (Secret.class.isAssignableFrom(resourceType)) {
19+
return (ResourceUpdatePreProcessor<R>) new GenericResourceUpdatePreProcessor<Secret>() {
20+
@Override
21+
protected void updateClonedActual(Secret actual, Secret desired) {
22+
actual.setData(desired.getData());
23+
actual.setStringData(desired.getStringData());
24+
}
25+
};
26+
} else if (ConfigMap.class.isAssignableFrom(resourceType)) {
27+
return (ResourceUpdatePreProcessor<R>) new GenericResourceUpdatePreProcessor<ConfigMap>() {
28+
29+
@Override
30+
protected void updateClonedActual(ConfigMap actual, ConfigMap desired) {
31+
actual.setData(desired.getData());
32+
actual.setBinaryData((desired.getBinaryData()));
33+
}
34+
};
35+
} else {
36+
return new GenericResourceUpdatePreProcessor<>() {
37+
@Override
38+
protected void updateClonedActual(R actual, R desired) {
39+
var desiredSpec = ReconcilerUtils.getSpec(desired);
40+
ReconcilerUtils.setSpec(actual, desiredSpec);
41+
}
42+
};
43+
}
44+
}
45+
46+
public R replaceSpecOnActual(R actual, R desired, Context context) {
47+
var clonedActual = context.getConfigurationService().getResourceCloner().clone(actual);
48+
updateClonedActual(clonedActual, desired);
49+
return clonedActual;
50+
}
51+
52+
protected abstract void updateClonedActual(R actual, R desired);
53+
}

0 commit comments

Comments
 (0)