Skip to content

Commit e8ea4c9

Browse files
committed
Using Annotations to Identify primary for a secondary object if no owner reference can be added (#1197)
1 parent a84a1f1 commit e8ea4c9

File tree

7 files changed

+193
-5
lines changed

7 files changed

+193
-5
lines changed

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

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

3+
import java.util.HashMap;
34
import java.util.Optional;
45
import java.util.Set;
56

@@ -11,6 +12,7 @@
1112
import io.fabric8.kubernetes.client.KubernetesClient;
1213
import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
1314
import io.fabric8.kubernetes.client.dsl.Resource;
15+
import io.javaoperatorsdk.operator.OperatorException;
1416
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
1517
import io.javaoperatorsdk.operator.api.reconciler.Constants;
1618
import io.javaoperatorsdk.operator.api.reconciler.Context;
@@ -66,18 +68,29 @@ private void configureWith(String labelSelector, Set<String> namespaces,
6668
namespaces = context.getControllerConfiguration().getNamespaces();
6769
}
6870

69-
final SecondaryToPrimaryMapper<R> primaryResourcesRetriever =
70-
(this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper<R>) this
71-
: Mappers.fromOwnerReference();
7271
var ic = InformerConfiguration.from(resourceType())
7372
.withLabelSelector(labelSelector)
74-
.withSecondaryToPrimaryMapper(primaryResourcesRetriever)
73+
.withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper())
7574
.withNamespaces(namespaces, inheritNamespacesOnChange)
7675
.build();
7776

7877
configureWith(new InformerEventSource<>(ic, client));
7978
}
8079

80+
@SuppressWarnings("unchecked")
81+
private SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
82+
if (this instanceof SecondaryToPrimaryMapper) {
83+
return (SecondaryToPrimaryMapper<R>) this;
84+
} else if (garbageCollected) {
85+
return Mappers.fromOwnerReference();
86+
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
87+
return Mappers.fromDefaultAnnotations();
88+
} else {
89+
throw new OperatorException("Provide a SecondaryToPrimaryMapper to associate " +
90+
"this resource with the primary resource. DependentResource: " + getClass().getName());
91+
}
92+
}
93+
8194
/**
8295
* Use to share informers between event more resources.
8396
*
@@ -138,6 +151,8 @@ protected NonNamespaceOperation<R, KubernetesResourceList<R>, Resource<R>> prepa
138151
ResourceID.fromResource(desired));
139152
if (addOwnerReference()) {
140153
desired.addOwnerReference(primary);
154+
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
155+
addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
141156
}
142157
Class<R> targetClass = (Class<R>) desired.getClass();
143158
return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace());
@@ -159,6 +174,24 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
159174
return eventSource();
160175
}
161176

177+
private boolean useDefaultAnnotationsToIdentifyPrimary() {
178+
return !(this instanceof SecondaryToPrimaryMapper) && !garbageCollected && creatable;
179+
}
180+
181+
private void addDefaultSecondaryToPrimaryMapperAnnotations(R desired, P primary) {
182+
var annotations = desired.getMetadata().getAnnotations();
183+
if (annotations == null) {
184+
annotations = new HashMap<>();
185+
desired.getMetadata().setAnnotations(annotations);
186+
}
187+
annotations.put(Mappers.DEFAULT_ANNOTATION_FOR_NAME, primary.getMetadata().getName());
188+
var primaryNamespaces = primary.getMetadata().getNamespace();
189+
if (primaryNamespaces != null) {
190+
annotations.put(
191+
Mappers.DEFAULT_ANNOTATION_FOR_NAMESPACE, primary.getMetadata().getNamespace());
192+
}
193+
}
194+
162195
protected boolean addOwnerReference() {
163196
return garbageCollected;
164197
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public KubernetesDependentResourceConfig(Set<String> namespaces, String labelSel
1919
boolean configuredNS) {
2020
this.namespaces = namespaces;
2121
this.labelSelector = labelSelector;
22-
namespacesWereConfigured = configuredNS;
22+
this.namespacesWereConfigured = configuredNS;
2323
}
2424

2525
public KubernetesDependentResourceConfig(Set<String> namespaces, String labelSelector) {
@@ -48,4 +48,5 @@ public String labelSelector() {
4848
public boolean wereNamespacesConfigured() {
4949
return namespacesWereConfigured;
5050
}
51+
5152
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/Mappers.java

+8
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
public class Mappers {
1111

12+
public static final String DEFAULT_ANNOTATION_FOR_NAME = "io.javaoperatorsdk/primary-name";
13+
public static final String DEFAULT_ANNOTATION_FOR_NAMESPACE =
14+
"io.javaoperatorsdk/primary-namespace";
15+
1216
private Mappers() {}
1317

1418
public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromAnnotation(
@@ -26,6 +30,10 @@ public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromLabel(
2630
return fromMetadata(nameKey, null, true);
2731
}
2832

33+
public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromDefaultAnnotations() {
34+
return fromMetadata(DEFAULT_ANNOTATION_FOR_NAME, DEFAULT_ANNOTATION_FOR_NAMESPACE, false);
35+
}
36+
2937
public static <T extends HasMetadata> SecondaryToPrimaryMapper<T> fromLabel(
3038
String nameKey, String namespaceKey) {
3139
return fromMetadata(nameKey, namespaceKey, true);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.fabric8.kubernetes.api.model.ConfigMap;
9+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
10+
import io.javaoperatorsdk.operator.junit.LocalOperatorExtension;
11+
import io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper.DependentAnnotationSecondaryMapperReconciler;
12+
import io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper.DependentAnnotationSecondaryMapperResource;
13+
14+
import static io.javaoperatorsdk.operator.processing.event.source.informer.Mappers.DEFAULT_ANNOTATION_FOR_NAME;
15+
import static io.javaoperatorsdk.operator.processing.event.source.informer.Mappers.DEFAULT_ANNOTATION_FOR_NAMESPACE;
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.awaitility.Awaitility.await;
18+
19+
class DependentAnnotationSecondaryMapperIT {
20+
21+
public static final String TEST_RESOURCE_NAME = "test1";
22+
23+
@RegisterExtension
24+
LocalOperatorExtension operator =
25+
LocalOperatorExtension.builder()
26+
.withReconciler(DependentAnnotationSecondaryMapperReconciler.class)
27+
.build();
28+
29+
@Test
30+
void mapsSecondaryByAnnotation() {
31+
operator.create(DependentAnnotationSecondaryMapperResource.class, testResource());
32+
33+
var reconciler =
34+
operator.getReconcilerOfType(DependentAnnotationSecondaryMapperReconciler.class);
35+
36+
await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
37+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1);
38+
});
39+
var configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
40+
41+
var annotations = configMap.getMetadata().getAnnotations();
42+
43+
assertThat(annotations)
44+
.containsEntry(DEFAULT_ANNOTATION_FOR_NAME, TEST_RESOURCE_NAME)
45+
.containsEntry(DEFAULT_ANNOTATION_FOR_NAMESPACE, operator.getNamespace());
46+
47+
assertThat(configMap.getMetadata().getOwnerReferences()).isEmpty();
48+
49+
configMap.getData().put("additional_data", "data");
50+
operator.replace(ConfigMap.class, configMap);
51+
52+
await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
53+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2);
54+
});
55+
}
56+
57+
58+
DependentAnnotationSecondaryMapperResource testResource() {
59+
var res = new DependentAnnotationSecondaryMapperResource();
60+
res.setMetadata(new ObjectMetaBuilder()
61+
.withName(TEST_RESOURCE_NAME)
62+
.build());
63+
return res;
64+
}
65+
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
import io.fabric8.kubernetes.api.model.ConfigMap;
7+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
8+
import io.javaoperatorsdk.operator.api.reconciler.*;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
10+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
11+
import io.javaoperatorsdk.operator.processing.dependent.Creator;
12+
import io.javaoperatorsdk.operator.processing.dependent.Updater;
13+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
14+
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
15+
16+
@ControllerConfiguration(dependents = {@Dependent(
17+
type = DependentAnnotationSecondaryMapperReconciler.ConfigMapDependentResource.class)})
18+
public class DependentAnnotationSecondaryMapperReconciler
19+
implements Reconciler<DependentAnnotationSecondaryMapperResource>, TestExecutionInfoProvider {
20+
21+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
22+
23+
@Override
24+
public UpdateControl<DependentAnnotationSecondaryMapperResource> reconcile(
25+
DependentAnnotationSecondaryMapperResource resource,
26+
Context<DependentAnnotationSecondaryMapperResource> context) {
27+
numberOfExecutions.addAndGet(1);
28+
return UpdateControl.noUpdate();
29+
}
30+
31+
public int getNumberOfExecutions() {
32+
return numberOfExecutions.get();
33+
}
34+
35+
public static class ConfigMapDependentResource extends
36+
KubernetesDependentResource<ConfigMap, DependentAnnotationSecondaryMapperResource>
37+
implements Creator<ConfigMap, DependentAnnotationSecondaryMapperResource>,
38+
Updater<ConfigMap, DependentAnnotationSecondaryMapperResource>,
39+
Deleter<DependentAnnotationSecondaryMapperResource> {
40+
41+
public ConfigMapDependentResource() {
42+
super(ConfigMap.class);
43+
}
44+
45+
@Override
46+
protected ConfigMap desired(DependentAnnotationSecondaryMapperResource primary,
47+
Context<DependentAnnotationSecondaryMapperResource> context) {
48+
ConfigMap configMap = new ConfigMap();
49+
configMap.setMetadata(new ObjectMetaBuilder()
50+
.withName(primary.getMetadata().getName())
51+
.withNamespace(primary.getMetadata().getNamespace())
52+
.build());
53+
configMap.setData(Map.of("data", primary.getMetadata().getName()));
54+
return configMap;
55+
}
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;
2+
3+
import io.fabric8.kubernetes.api.model.Namespaced;
4+
import io.fabric8.kubernetes.client.CustomResource;
5+
import io.fabric8.kubernetes.model.annotation.Group;
6+
import io.fabric8.kubernetes.model.annotation.Kind;
7+
import io.fabric8.kubernetes.model.annotation.ShortNames;
8+
import io.fabric8.kubernetes.model.annotation.Version;
9+
10+
@Group("sample.javaoperatorsdk")
11+
@Version("v1")
12+
@Kind("MaxIntervalTestCustomResource")
13+
@ShortNames("mit")
14+
public class DependentAnnotationSecondaryMapperResource
15+
extends CustomResource<Void, DependentAnnotationSecondaryMapperResourceStatus>
16+
implements Namespaced {
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.javaoperatorsdk.operator.sample.dependentannotationsecondarymapper;
2+
3+
public class DependentAnnotationSecondaryMapperResourceStatus {
4+
5+
}

0 commit comments

Comments
 (0)