Skip to content

Commit caffb6d

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

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;
@@ -64,18 +66,29 @@ private void configureWith(String labelSelector, Set<String> namespaces,
6466
namespaces = context.getControllerConfiguration().getNamespaces();
6567
}
6668

67-
final SecondaryToPrimaryMapper<R> primaryResourcesRetriever =
68-
(this instanceof SecondaryToPrimaryMapper) ? (SecondaryToPrimaryMapper<R>) this
69-
: Mappers.fromOwnerReference();
7069
var ic = InformerConfiguration.from(resourceType())
7170
.withLabelSelector(labelSelector)
72-
.withSecondaryToPrimaryMapper(primaryResourcesRetriever)
71+
.withSecondaryToPrimaryMapper(getSecondaryToPrimaryMapper())
7372
.withNamespaces(namespaces, inheritNamespacesOnChange)
7473
.build();
7574

7675
configureWith(new InformerEventSource<>(ic, client));
7776
}
7877

78+
@SuppressWarnings("unchecked")
79+
private SecondaryToPrimaryMapper<R> getSecondaryToPrimaryMapper() {
80+
if (this instanceof SecondaryToPrimaryMapper) {
81+
return (SecondaryToPrimaryMapper<R>) this;
82+
} else if (garbageCollected) {
83+
return Mappers.fromOwnerReference();
84+
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
85+
return Mappers.fromDefaultAnnotations();
86+
} else {
87+
throw new OperatorException("Provide a SecondaryToPrimaryMapper to associate " +
88+
"this resource with the primary resource. DependentResource: " + getClass().getName());
89+
}
90+
}
91+
7992
/**
8093
* Use to share informers between event more resources.
8194
*
@@ -136,6 +149,8 @@ protected NonNamespaceOperation<R, KubernetesResourceList<R>, Resource<R>> prepa
136149
ResourceID.fromResource(desired));
137150
if (addOwnerReference()) {
138151
desired.addOwnerReference(primary);
152+
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
153+
addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
139154
}
140155
Class<R> targetClass = (Class<R>) desired.getClass();
141156
return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace());
@@ -157,6 +172,24 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
157172
return eventSource();
158173
}
159174

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

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)