Skip to content

Primary to seconday dr informer sample #1744

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/documentation/v4-3-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ permalink: /docs/v4-3-migration

## Condition API Change

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

New API:

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package io.javaoperatorsdk.operator;

import java.time.Duration;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentCustomResource;
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler;
import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentSpec;

import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.ConfigMapReconcilePrecondition.DO_NOT_RECONCILE;
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

class PrimaryToSecondaryDependentIT {

public static final String TEST_CONFIG_MAP_NAME = "testconfigmap";
public static final String TEST_CR_NAME = "test1";
public static final String TEST_DATA = "testData";
public

@RegisterExtension LocallyRunOperatorExtension operator =
LocallyRunOperatorExtension.builder()
.withReconciler(new PrimaryToSecondaryDependentReconciler())
.build();

@Test
void testPrimaryToSecondaryInDependentResources() {
var reconciler = operator.getReconcilerOfType(PrimaryToSecondaryDependentReconciler.class);
var cm = operator.create(configMap(DO_NOT_RECONCILE));
operator.create(testCustomResource());

await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> {
assertThat(reconciler.getNumberOfExecutions()).isPositive();
assertThat(operator.get(Secret.class, TEST_CR_NAME)).isNull();
});

cm.setData(Map.of(DATA_KEY, TEST_DATA));
var executions = reconciler.getNumberOfExecutions();
operator.replace(cm);

await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> {
assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(executions);
var secret = operator.get(Secret.class, TEST_CR_NAME);
assertThat(secret).isNotNull();
assertThat(secret.getData().get(DATA_KEY)).isEqualTo(TEST_DATA);
});
}

PrimaryToSecondaryDependentCustomResource testCustomResource() {
var res = new PrimaryToSecondaryDependentCustomResource();
res.setMetadata(new ObjectMetaBuilder()
.withName(TEST_CR_NAME)
.build());
res.setSpec(new PrimaryToSecondaryDependentSpec());
res.getSpec().setConfigMapName(TEST_CONFIG_MAP_NAME);
return res;
}

ConfigMap configMap(String data) {
var cm = new ConfigMap();
cm.setMetadata(new ObjectMetaBuilder()
.withName(TEST_CONFIG_MAP_NAME)
.build());
cm.setData(Map.of(DATA_KEY, data));
return cm;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;

public class ConfigMapDependent extends
KubernetesDependentResource<ConfigMap, PrimaryToSecondaryDependentCustomResource> {

public ConfigMapDependent() {
super(ConfigMap.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;

import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;

public class ConfigMapReconcilePrecondition
implements Condition<ConfigMap, PrimaryToSecondaryDependentCustomResource> {

public static final String DO_NOT_RECONCILE = "doNotReconcile";

@Override
public boolean isMet(
DependentResource<ConfigMap, PrimaryToSecondaryDependentCustomResource> dependentResource,
PrimaryToSecondaryDependentCustomResource primary,
Context<PrimaryToSecondaryDependentCustomResource> context) {
return dependentResource.getSecondaryResource(primary, context).map(cm -> {
var data = cm.getData().get(DATA_KEY);
return data != null && !data.equals(DO_NOT_RECONCILE);
})
.orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("ptsd")
public class PrimaryToSecondaryDependentCustomResource
extends CustomResource<PrimaryToSecondaryDependentSpec, Void>
implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;

import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP;
import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP_EVENT_SOURCE;

/**
* Sample showcases how it is possible to do a primary to secondary mapper for a dependent resource.
* Note that this is usually just used with read only resources. So it has limited usage, one reason
* to use it is to have nice condition on that resource within a workflow.
*/
@ControllerConfiguration(dependents = {@Dependent(type = ConfigMapDependent.class,
name = CONFIG_MAP,
reconcilePrecondition = ConfigMapReconcilePrecondition.class,
useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE),
@Dependent(type = SecretDependent.class, dependsOn = CONFIG_MAP)})
public class PrimaryToSecondaryDependentReconciler
implements Reconciler<PrimaryToSecondaryDependentCustomResource>, TestExecutionInfoProvider,
EventSourceInitializer<PrimaryToSecondaryDependentCustomResource> {

public static final String DATA_KEY = "data";
public static final String CONFIG_MAP = "ConfigMap";
public static final String CONFIG_MAP_INDEX = "ConfigMapIndex";
public static final String CONFIG_MAP_EVENT_SOURCE = "ConfigMapEventSource";

private final AtomicInteger numberOfExecutions = new AtomicInteger(0);

@Override
public UpdateControl<PrimaryToSecondaryDependentCustomResource> reconcile(
PrimaryToSecondaryDependentCustomResource resource,
Context<PrimaryToSecondaryDependentCustomResource> context) {
numberOfExecutions.addAndGet(1);
return UpdateControl.noUpdate();
}

public int getNumberOfExecutions() {
return numberOfExecutions.get();
}

/**
* Creating an Event Source and setting it for the Dependent Resource. Since it is not possible to
* do this setup elegantly within the bounds of the KubernetesDependentResource API. However, this
* is quite a corner case; might be covered more out of the box in the future if there will be
* demand for it.
**/
@Override
public Map<String, EventSource> prepareEventSources(
EventSourceContext<PrimaryToSecondaryDependentCustomResource> context) {
// there is no owner reference in the config map, but we still want to trigger reconciliation if
// the config map changes. So first we add an index which custom resource references the config
// map.
context.getPrimaryCache().addIndexer(CONFIG_MAP_INDEX, (primary -> List
.of(indexKey(primary.getSpec().getConfigMapName(), primary.getMetadata().getNamespace()))));

var cmES = new InformerEventSource<>(InformerConfiguration
.from(ConfigMap.class, context)
// if there is a many-to-many relationship (thus no direct owner reference)
// PrimaryToSecondaryMapper needs to be added
.withPrimaryToSecondaryMapper(
(PrimaryToSecondaryMapper<PrimaryToSecondaryDependentCustomResource>) p -> Set
.of(new ResourceID(p.getSpec().getConfigMapName(), p.getMetadata().getNamespace())))
// the index is used to trigger reconciliation of related custom resources if config map
// changes
.withSecondaryToPrimaryMapper(cm -> context.getPrimaryCache()
.byIndex(CONFIG_MAP_INDEX, indexKey(cm.getMetadata().getName(),
cm.getMetadata().getNamespace()))
.stream().map(ResourceID::fromResource).collect(Collectors.toSet()))
.build(),
context);

return Map.of(CONFIG_MAP_EVENT_SOURCE, cmES);
}

private String indexKey(String configMapName, String namespace) {
return configMapName + "#" + namespace;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

public class PrimaryToSecondaryDependentSpec {

private String configMapName;

public String getConfigMapName() {
return configMapName;
}

public PrimaryToSecondaryDependentSpec setConfigMapName(String configMapName) {
this.configMapName = configMapName;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.javaoperatorsdk.operator.sample.primarytosecondaydependent;

import java.util.Map;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.Secret;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;

import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY;

public class SecretDependent
extends CRUDKubernetesDependentResource<Secret, PrimaryToSecondaryDependentCustomResource> {

public SecretDependent() {
super(Secret.class);
}

@Override
protected Secret desired(PrimaryToSecondaryDependentCustomResource primary,
Context<PrimaryToSecondaryDependentCustomResource> context) {
Secret secret = new Secret();
secret.setMetadata(new ObjectMetaBuilder()
.withName(primary.getMetadata().getName())
.withNamespace(primary.getMetadata().getNamespace())
.build());
secret.setData(Map.of(DATA_KEY, context.getSecondaryResource(ConfigMap.class)
.orElseThrow().getData().get(DATA_KEY)));
return secret;
}
}