Skip to content

feat: read-only bulk dependent #2372

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 4 commits into from
May 14, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public ReconcileResult<R> reconcile(P primary, Context<P> context) {
}

protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> context) {
if (creatable || updatable) {
if (creatable() || updatable()) {
if (actualResource == null) {
if (creatable) {
var desired = desired(primary, context);
Expand All @@ -70,7 +70,7 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
return ReconcileResult.resourceCreated(createdResource);
}
} else {
if (updatable) {
if (updatable()) {
final Matcher.Result<R> match = match(actualResource, primary, context);
if (!match.matched()) {
final var desired = match.computedDesired().orElseGet(() -> desired(primary, context));
Expand Down Expand Up @@ -216,4 +216,12 @@ public String name() {
public void setName(String name) {
this.name = name;
}

protected boolean creatable() {
return creatable;
}

protected boolean updatable() {
return updatable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
* {@link Creator} and {@link Deleter} interfaces out of the box. A concrete dependent resource can
* implement additionally also {@link Updater}.
*/
public interface BulkDependentResource<R, P extends HasMetadata>
extends Creator<R, P>, Deleter<P> {
public interface BulkDependentResource<R, P extends HasMetadata> {

/**
* Retrieves a map of desired secondary resources associated with the specified primary resource,
Expand All @@ -26,7 +25,10 @@ public interface BulkDependentResource<R, P extends HasMetadata>
* @return a Map associating desired secondary resources with the specified primary via arbitrary
* identifiers
*/
Map<String, R> desiredResources(P primary, Context<P> context);
default Map<String, R> desiredResources(P primary, Context<P> context) {
throw new IllegalStateException(
"Implement desiredResources in case a non read-only bulk dependent resource");
}

/**
* Retrieves the actual secondary resources currently existing on the server and associated with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,26 @@ class BulkDependentResourceReconciler<R, P extends HasMetadata>

@Override
public ReconcileResult<R> reconcile(P primary, Context<P> context) {
final var desiredResources = bulkDependentResource.desiredResources(primary, context);

Map<String, R> actualResources = bulkDependentResource.getSecondaryResources(primary, context);
if (!(bulkDependentResource instanceof Creator<?, ?>)
&& !(bulkDependentResource instanceof Deleter<?>)
&& !(bulkDependentResource instanceof Updater<?, ?>)) {
return ReconcileResult
.aggregatedResult(actualResources.values().stream().map(ReconcileResult::noOperation)
.toList());
}

// remove existing resources that are not needed anymore according to the primary state
deleteExtraResources(desiredResources.keySet(), actualResources, primary, context);
final var desiredResources = bulkDependentResource.desiredResources(primary, context);

if (bulkDependentResource instanceof Deleter<?>) {
// remove existing resources that are not needed anymore according to the primary state
deleteExtraResources(desiredResources.keySet(), actualResources, primary, context);
}

final List<ReconcileResult<R>> results = new ArrayList<>(desiredResources.size());
final var updatable = bulkDependentResource instanceof Updater;
desiredResources.forEach((key, value) -> {
final var instance =
updatable ? new UpdatableBulkDependentResourceInstance<>(bulkDependentResource, value)
: new BulkDependentResourceInstance<>(bulkDependentResource, value);
final var instance = new BulkDependentResourceInstance<>(bulkDependentResource, value);
results.add(instance.reconcile(primary, actualResources.get(key), context));
});

Expand Down Expand Up @@ -67,7 +75,7 @@ private void deleteExtraResources(Set<String> expectedKeys,
@Ignore
private static class BulkDependentResourceInstance<R, P extends HasMetadata>
extends AbstractDependentResource<R, P>
implements Creator<R, P>, Deleter<P> {
implements Creator<R, P>, Deleter<P>, Updater<R, P> {
private final BulkDependentResource<R, P> bulkDependentResource;
private final R desired;

Expand Down Expand Up @@ -112,26 +120,24 @@ public Class<R> resourceType() {
return asAbstractDependentResource().resourceType();
}

@Override
@SuppressWarnings("unchecked")
public R create(R desired, P primary, Context<P> context) {
return bulkDependentResource.create(desired, primary, context);
return ((Creator<R, P>) bulkDependentResource).create(desired, primary, context);
}
}

/**
* Makes sure that the instance implements Updater if its precursor does as well.
*
* @param <R>
* @param <P>
*/
@Ignore
private static class UpdatableBulkDependentResourceInstance<R, P extends HasMetadata>
extends BulkDependentResourceInstance<R, P> implements Updater<R, P> {
@Override
protected boolean isCreatable() {
return bulkDependentResource instanceof Creator<?, ?>;
}

private UpdatableBulkDependentResourceInstance(
BulkDependentResource<R, P> bulkDependentResource,
R desired) {
super(bulkDependentResource, desired);
@Override
protected boolean isUpdatable() {
return bulkDependentResource instanceof Updater<?, ?>;
}

@Override
public boolean isDeletable() {
return bulkDependentResource instanceof Deleter<?>;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.javaoperatorsdk.operator.processing.dependent;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;

public interface CRUDBulkDependentResource<R, P extends HasMetadata>
extends BulkDependentResource<R, P>,
Creator<R, P>,
BulkUpdater<R, P>,
Deleter<P> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package io.javaoperatorsdk.operator;

import java.time.Duration;

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.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestSpec;
import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkDependentResource;
import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkReconciler;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

public class ReadOnlyBulkDependentIT {

public static final int EXPECTED_NUMBER_OF_RESOURCES = 2;
public static final String TEST = "test";

@RegisterExtension
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
.withReconciler(new ReadOnlyBulkReconciler())
.build();

@Test
void readOnlyBulkDependent() {
var primary = extension.create(testCustomResource());

await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST);

assertThat(actualPrimary.getStatus()).isNotNull();
assertThat(actualPrimary.getStatus().getReady()).isFalse();
});

var configMap1 = createConfigMap(1, primary);
extension.create(configMap1);
var configMap2 = createConfigMap(2, primary);
extension.create(configMap2);

await().untilAsserted(() -> {
var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST);
assertThat(actualPrimary.getStatus().getReady()).isTrue();
});
}

private ConfigMap createConfigMap(int i, BulkDependentTestCustomResource primary) {
ConfigMap configMap = new ConfigMap();
configMap.setMetadata(new ObjectMetaBuilder()
.withName(TEST + ReadOnlyBulkDependentResource.INDEX_DELIMITER + i)
.withNamespace(primary.getMetadata().getNamespace())
.build());
configMap.addOwnerReference(primary);
return configMap;
}

BulkDependentTestCustomResource testCustomResource() {
BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource();
customResource.setMetadata(new ObjectMetaBuilder()
.withName(TEST)
.build());
customResource.setSpec(new BulkDependentTestSpec());
customResource.getSpec().setNumberOfResources(EXPECTED_NUMBER_OF_RESOURCES);

return customResource;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.Updater;
import io.javaoperatorsdk.operator.processing.dependent.*;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;

/**
Expand All @@ -20,10 +17,7 @@
public class ConfigMapDeleterBulkDependentResource
extends
KubernetesDependentResource<ConfigMap, BulkDependentTestCustomResource>
implements Creator<ConfigMap, BulkDependentTestCustomResource>,
Updater<ConfigMap, BulkDependentTestCustomResource>,
Deleter<BulkDependentTestCustomResource>,
BulkDependentResource<ConfigMap, BulkDependentTestCustomResource> {
implements CRUDBulkDependentResource<ConfigMap, BulkDependentTestCustomResource> {

public static final String LABEL_KEY = "bulk";
public static final String LABEL_VALUE = "true";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@
import java.util.stream.Collectors;

import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.BulkUpdater;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;

public class ExternalBulkDependentResource
extends PollingDependentResource<ExternalResource, BulkDependentTestCustomResource>
implements BulkDependentResource<ExternalResource, BulkDependentTestCustomResource>,
Creator<ExternalResource, BulkDependentTestCustomResource>,
Deleter<BulkDependentTestCustomResource>,
BulkUpdater<ExternalResource, BulkDependentTestCustomResource> {

public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;

import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;


public class ReadOnlyBulkDependentResource
extends
KubernetesDependentResource<ConfigMap, BulkDependentTestCustomResource>
implements BulkDependentResource<ConfigMap, BulkDependentTestCustomResource>,
SecondaryToPrimaryMapper<ConfigMap> {

public static final String INDEX_DELIMITER = "-";

public ReadOnlyBulkDependentResource() {
super(ConfigMap.class);
}

@Override
protected Class<BulkDependentTestCustomResource> getPrimaryResourceType() {
return BulkDependentTestCustomResource.class;
}

@Override
public Map<String, ConfigMap> getSecondaryResources(BulkDependentTestCustomResource primary,
Context<BulkDependentTestCustomResource> context) {
return context.getSecondaryResourcesAsStream(ConfigMap.class)
.filter(cm -> getName(cm).startsWith(primary.getMetadata().getName()))
.collect(Collectors.toMap(
cm -> getName(cm).substring(getName(cm).lastIndexOf(INDEX_DELIMITER) + 1),
Function.identity()));
}

private static String getName(ConfigMap cm) {
return cm.getMetadata().getName();
}

@Override
public Set<ResourceID> toPrimaryResourceIDs(ConfigMap resource) {
return Mappers.fromOwnerReferences(false).toPrimaryResourceIDs(resource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;

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.BulkDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;

public class ReadOnlyBulkReadyPostCondition
implements Condition<ConfigMap, BulkDependentTestCustomResource> {
@Override
public boolean isMet(
DependentResource<ConfigMap, BulkDependentTestCustomResource> dependentResource,
BulkDependentTestCustomResource primary, Context<BulkDependentTestCustomResource> context) {
var minResourceNumber = primary.getSpec().getNumberOfResources();
@SuppressWarnings("unchecked")
var secondaryResources =
((BulkDependentResource<ConfigMap, BulkDependentTestCustomResource>) dependentResource)
.getSecondaryResources(primary, context);
return minResourceNumber <= secondaryResources.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.api.reconciler.*;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestStatus;

@Workflow(dependents = @Dependent(type = ReadOnlyBulkDependentResource.class,
readyPostcondition = ReadOnlyBulkReadyPostCondition.class))
@ControllerConfiguration
public class ReadOnlyBulkReconciler implements Reconciler<BulkDependentTestCustomResource> {
@Override
public UpdateControl<BulkDependentTestCustomResource> reconcile(
BulkDependentTestCustomResource resource, Context<BulkDependentTestCustomResource> context) {

var nonReadyDependents =
context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult()
.getNotReadyDependents();


BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource();
customResource.setMetadata(new ObjectMetaBuilder()
.withName(resource.getMetadata().getName())
.withNamespace(resource.getMetadata().getNamespace())
.build());
var status = new BulkDependentTestStatus();
status.setReady(nonReadyDependents.isEmpty());
customResource.setStatus(status);

return UpdateControl.patchStatus(customResource);
}
}
Loading
Loading