Skip to content

Commit 55f5d0b

Browse files
committed
Primary to seconday dr informer sample (#1744)
1 parent 77f5571 commit 55f5d0b

8 files changed

+270
-2
lines changed

docs/documentation/v4-3-migration.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ permalink: /docs/v4-3-migration
99

1010
## Condition API Change
1111

12-
In Workflows the target of the condition was the managed resource itself, not a dependent resource. This changed, from
13-
not the API contains the dependent resource.
12+
In Workflows the target of the condition was the managed resource itself, not the target dependent resource.
13+
This changed, now the API contains the dependent resource.
1414

1515
New API:
1616

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
import java.util.Map;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.fabric8.kubernetes.api.model.ConfigMap;
10+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
11+
import io.fabric8.kubernetes.api.model.Secret;
12+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
13+
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentCustomResource;
14+
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler;
15+
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentSpec;
16+
17+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.ConfigMapReconcilePrecondition.DO_NOT_RECONCILE;
18+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.awaitility.Awaitility.await;
21+
22+
class PrimaryToSecondaryDependentIT {
23+
24+
public static final String TEST_CONFIG_MAP_NAME = "testconfigmap";
25+
public static final String TEST_CR_NAME = "test1";
26+
public static final String TEST_DATA = "testData";
27+
public
28+
29+
@RegisterExtension LocallyRunOperatorExtension operator =
30+
LocallyRunOperatorExtension.builder()
31+
.withReconciler(new PrimaryToSecondaryDependentReconciler())
32+
.build();
33+
34+
@Test
35+
void testPrimaryToSecondaryInDependentResources() {
36+
var reconciler = operator.getReconcilerOfType(PrimaryToSecondaryDependentReconciler.class);
37+
var cm = operator.create(configMap(DO_NOT_RECONCILE));
38+
operator.create(testCustomResource());
39+
40+
await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> {
41+
assertThat(reconciler.getNumberOfExecutions()).isPositive();
42+
assertThat(operator.get(Secret.class, TEST_CR_NAME)).isNull();
43+
});
44+
45+
cm.setData(Map.of(DATA_KEY, TEST_DATA));
46+
var executions = reconciler.getNumberOfExecutions();
47+
operator.replace(cm);
48+
49+
await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> {
50+
assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(executions);
51+
var secret = operator.get(Secret.class, TEST_CR_NAME);
52+
assertThat(secret).isNotNull();
53+
assertThat(secret.getData().get(DATA_KEY)).isEqualTo(TEST_DATA);
54+
});
55+
}
56+
57+
PrimaryToSecondaryDependentCustomResource testCustomResource() {
58+
var res = new PrimaryToSecondaryDependentCustomResource();
59+
res.setMetadata(new ObjectMetaBuilder()
60+
.withName(TEST_CR_NAME)
61+
.build());
62+
res.setSpec(new PrimaryToSecondaryDependentSpec());
63+
res.getSpec().setConfigMapName(TEST_CONFIG_MAP_NAME);
64+
return res;
65+
}
66+
67+
ConfigMap configMap(String data) {
68+
var cm = new ConfigMap();
69+
cm.setMetadata(new ObjectMetaBuilder()
70+
.withName(TEST_CONFIG_MAP_NAME)
71+
.build());
72+
cm.setData(Map.of(DATA_KEY, data));
73+
return cm;
74+
}
75+
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
2+
3+
import io.fabric8.kubernetes.api.model.ConfigMap;
4+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
5+
6+
public class ConfigMapDependent extends
7+
KubernetesDependentResource<ConfigMap, PrimaryToSecondaryDependentCustomResource> {
8+
9+
public ConfigMapDependent() {
10+
super(ConfigMap.class);
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
2+
3+
import io.fabric8.kubernetes.api.model.ConfigMap;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
6+
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
7+
8+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;
9+
10+
public class ConfigMapReconcilePrecondition
11+
implements Condition<ConfigMap, PrimaryToSecondaryDependentCustomResource> {
12+
13+
public static final String DO_NOT_RECONCILE = "doNotReconcile";
14+
15+
@Override
16+
public boolean isMet(
17+
DependentResource<ConfigMap, PrimaryToSecondaryDependentCustomResource> dependentResource,
18+
PrimaryToSecondaryDependentCustomResource primary,
19+
Context<PrimaryToSecondaryDependentCustomResource> context) {
20+
return dependentResource.getSecondaryResource(primary, context).map(cm -> {
21+
var data = cm.getData().get(DATA_KEY);
22+
return data != null && !data.equals(DO_NOT_RECONCILE);
23+
})
24+
.orElse(false);
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
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.ShortNames;
7+
import io.fabric8.kubernetes.model.annotation.Version;
8+
9+
@Group("sample.javaoperatorsdk")
10+
@Version("v1")
11+
@ShortNames("ptsd")
12+
public class PrimaryToSecondaryDependentCustomResource
13+
extends CustomResource<PrimaryToSecondaryDependentSpec, Void>
14+
implements Namespaced {
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.Set;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
import java.util.stream.Collectors;
8+
9+
import io.fabric8.kubernetes.api.model.ConfigMap;
10+
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
11+
import io.javaoperatorsdk.operator.api.reconciler.*;
12+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
13+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
14+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
15+
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
16+
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
17+
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
18+
19+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP;
20+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP_EVENT_SOURCE;
21+
22+
/**
23+
* Sample showcases how it is possible to do a primary to secondary mapper for a dependent resource.
24+
* Note that this is usually just used with read only resources. So it has limited usage, one reason
25+
* to use it is to have nice condition on that resource within a workflow.
26+
*/
27+
@ControllerConfiguration(dependents = {@Dependent(type = ConfigMapDependent.class,
28+
name = CONFIG_MAP,
29+
reconcilePrecondition = ConfigMapReconcilePrecondition.class,
30+
useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
31+
@Dependent(type = SecretDependent.class, dependsOn = CONFIG_MAP)})
32+
public class PrimaryToSecondaryDependentReconciler
33+
implements Reconciler<PrimaryToSecondaryDependentCustomResource>, TestExecutionInfoProvider,
34+
EventSourceInitializer<PrimaryToSecondaryDependentCustomResource> {
35+
36+
public static final String DATA_KEY = "data";
37+
public static final String CONFIG_MAP = "ConfigMap";
38+
public static final String CONFIG_MAP_INDEX = "ConfigMapIndex";
39+
public static final String CONFIG_MAP_EVENT_SOURCE = "ConfigMapEventSource";
40+
41+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
42+
43+
@Override
44+
public UpdateControl<PrimaryToSecondaryDependentCustomResource> reconcile(
45+
PrimaryToSecondaryDependentCustomResource resource,
46+
Context<PrimaryToSecondaryDependentCustomResource> context) {
47+
numberOfExecutions.addAndGet(1);
48+
return UpdateControl.noUpdate();
49+
}
50+
51+
public int getNumberOfExecutions() {
52+
return numberOfExecutions.get();
53+
}
54+
55+
/**
56+
* Creating an Event Source and setting it for the Dependent Resource. Since it is not possible to
57+
* do this setup elegantly within the bounds of the KubernetesDependentResource API. However, this
58+
* is quite a corner case; might be covered more out of the box in the future if there will be
59+
* demand for it.
60+
**/
61+
@Override
62+
public Map<String, EventSource> prepareEventSources(
63+
EventSourceContext<PrimaryToSecondaryDependentCustomResource> context) {
64+
// there is no owner reference in the config map, but we still want to trigger reconciliation if
65+
// the config map changes. So first we add an index which custom resource references the config
66+
// map.
67+
context.getPrimaryCache().addIndexer(CONFIG_MAP_INDEX, (primary -> List
68+
.of(indexKey(primary.getSpec().getConfigMapName(), primary.getMetadata().getNamespace()))));
69+
70+
var cmES = new InformerEventSource<>(InformerConfiguration
71+
.from(ConfigMap.class, context)
72+
// if there is a many-to-many relationship (thus no direct owner reference)
73+
// PrimaryToSecondaryMapper needs to be added
74+
.withPrimaryToSecondaryMapper(
75+
(PrimaryToSecondaryMapper<PrimaryToSecondaryDependentCustomResource>) p -> Set
76+
.of(new ResourceID(p.getSpec().getConfigMapName(), p.getMetadata().getNamespace())))
77+
// the index is used to trigger reconciliation of related custom resources if config map
78+
// changes
79+
.withSecondaryToPrimaryMapper(cm -> context.getPrimaryCache()
80+
.byIndex(CONFIG_MAP_INDEX, indexKey(cm.getMetadata().getName(),
81+
cm.getMetadata().getNamespace()))
82+
.stream().map(ResourceID::fromResource).collect(Collectors.toSet()))
83+
.build(),
84+
context);
85+
86+
return Map.of(CONFIG_MAP_EVENT_SOURCE, cmES);
87+
}
88+
89+
private String indexKey(String configMapName, String namespace) {
90+
return configMapName + "#" + namespace;
91+
}
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
2+
3+
public class PrimaryToSecondaryDependentSpec {
4+
5+
private String configMapName;
6+
7+
public String getConfigMapName() {
8+
return configMapName;
9+
}
10+
11+
public PrimaryToSecondaryDependentSpec setConfigMapName(String configMapName) {
12+
this.configMapName = configMapName;
13+
return this;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;
2+
3+
import java.util.Map;
4+
5+
import io.fabric8.kubernetes.api.model.ConfigMap;
6+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
7+
import io.fabric8.kubernetes.api.model.Secret;
8+
import io.javaoperatorsdk.operator.api.reconciler.Context;
9+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
10+
11+
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;
12+
13+
public class SecretDependent
14+
extends CRUDKubernetesDependentResource<Secret, PrimaryToSecondaryDependentCustomResource> {
15+
16+
public SecretDependent() {
17+
super(Secret.class);
18+
}
19+
20+
@Override
21+
protected Secret desired(PrimaryToSecondaryDependentCustomResource primary,
22+
Context<PrimaryToSecondaryDependentCustomResource> context) {
23+
Secret secret = new Secret();
24+
secret.setMetadata(new ObjectMetaBuilder()
25+
.withName(primary.getMetadata().getName())
26+
.withNamespace(primary.getMetadata().getNamespace())
27+
.build());
28+
secret.setData(Map.of(DATA_KEY, context.getSecondaryResource(ConfigMap.class)
29+
.orElseThrow().getData().get(DATA_KEY)));
30+
return secret;
31+
}
32+
}

0 commit comments

Comments
 (0)