Skip to content
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 @@ -15,15 +15,19 @@
*/
package io.javaoperatorsdk.operator.api.reconciler;

import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
import io.javaoperatorsdk.operator.processing.Controller;
Expand All @@ -41,6 +45,7 @@ public class DefaultContext<P extends HasMetadata> implements Context<P> {
defaultManagedDependentResourceContext;
private final boolean primaryResourceDeleted;
private final boolean primaryResourceFinalStateUnknown;
private final Map<DependentResource<?, P>, Object> desiredStates = new ConcurrentHashMap<>();

public DefaultContext(
RetryInfo retryInfo,
Expand Down Expand Up @@ -157,4 +162,12 @@ public DefaultContext<P> setRetryInfo(RetryInfo retryInfo) {
this.retryInfo = retryInfo;
return this;
}

@SuppressWarnings("unchecked")
public <R> R getOrComputeDesiredStateFor(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this should be addded under defaultManagedDependentResourceContext , logically relates to that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used for non-managed dependent resources as well: it's used everywhere desired is used and this isn't part of the public API anyway. It's only public because it's used in different packages but otherwise it would have been a package-only method.

DependentResource<R, P> dependentResource, Function<P, R> desiredStateComputer) {
return (R)
desiredStates.computeIfAbsent(
dependentResource, ignored -> desiredStateComputer.apply(getPrimaryResource()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
Expand Down Expand Up @@ -85,7 +86,7 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
if (creatable() || updatable()) {
if (actualResource == null) {
if (creatable) {
var desired = desired(primary, context);
var desired = getOrComputeDesired(context);
throwIfNull(desired, primary, "Desired");
logForOperation("Creating", primary, desired);
var createdResource = handleCreate(desired, primary, context);
Expand All @@ -95,7 +96,8 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
if (updatable()) {
final Matcher.Result<R> match = match(actualResource, primary, context);
if (!match.matched()) {
final var desired = match.computedDesired().orElseGet(() -> desired(primary, context));
final var desired =
match.computedDesired().orElseGet(() -> getOrComputeDesired(context));
throwIfNull(desired, primary, "Desired");
logForOperation("Updating", primary, desired);
var updatedResource = handleUpdate(actualResource, desired, primary, context);
Expand Down Expand Up @@ -127,7 +129,6 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c

@Override
public Optional<R> getSecondaryResource(P primary, Context<P> context) {

var secondaryResources = context.getSecondaryResources(resourceType());
if (secondaryResources.isEmpty()) {
return Optional.empty();
Expand Down Expand Up @@ -212,6 +213,27 @@ protected R desired(P primary, Context<P> context) {
+ " updated");
}

/**
* Retrieves the desired state from the {@link Context} if it has already been computed or calls
* {@link #desired(HasMetadata, Context)} and stores its result in the context for further use.
* This ensures that {@code desired} is only called once per reconciliation to avoid unneeded
* processing and supports scenarios where idempotent computation of the desired state is not
* feasible.
*
* <p>Note that this method should normally only be called by the SDK itself and exclusively (i.e.
* {@link #desired(HasMetadata, Context)} should not be called directly by the SDK) whenever the
* desired state is needed to ensure it is properly cached for the current reconciliation.
*
* @param context the {@link Context} in scope for the current reconciliation
* @return the desired state associated with this dependent resource based on the currently
* in-scope primary resource as found in the context
*/
protected R getOrComputeDesired(Context<P> context) {
assert context instanceof DefaultContext<P>;
DefaultContext<P> defaultContext = (DefaultContext<P>) context;
return defaultContext.getOrComputeDesiredStateFor(this, p -> desired(p, defaultContext));
}

public void delete(P primary, Context<P> context) {
dependentResourceReconciler.delete(primary, context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ protected void handleExplicitStateCreation(P primary, R created, Context<P> cont

@Override
public Matcher.Result<R> match(R resource, P primary, Context<P> context) {
var desired = desired(primary, context);
var desired = getOrComputeDesired(context);
return Matcher.Result.computed(resource.equals(desired), desired);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result;

class BulkDependentResourceReconciler<R, P extends HasMetadata, ID>
implements DependentResourceReconciler<R, P> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ public static <R extends HasMetadata, P extends HasMetadata> Matcher.Result<R> m
Context<P> context,
boolean labelsAndAnnotationsEquality,
String... ignorePaths) {
final var desired = dependentResource.desired(primary, context);
final var desired = dependentResource.getOrComputeDesired(context);
return match(desired, actualResource, labelsAndAnnotationsEquality, context, ignorePaths);
}

Expand All @@ -150,7 +150,7 @@ public static <R extends HasMetadata, P extends HasMetadata> Matcher.Result<R> m
boolean specEquality,
boolean labelsAndAnnotationsEquality,
String... ignorePaths) {
final var desired = dependentResource.desired(primary, context);
final var desired = dependentResource.getOrComputeDesired(context);
return match(
desired, actualResource, labelsAndAnnotationsEquality, specEquality, context, ignorePaths);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ public R update(R actual, R desired, P primary, Context<P> context) {

@Override
public Result<R> match(R actualResource, P primary, Context<P> context) {
final var desired = desired(primary, context);
final var desired = getOrComputeDesired(context);
return match(actualResource, desired, primary, context);
}

Expand Down Expand Up @@ -297,16 +297,16 @@ protected Optional<R> selectTargetSecondaryResource(
* @return id of the target managed resource
*/
protected ResourceID targetSecondaryResourceID(P primary, Context<P> context) {
return ResourceID.fromResource(desired(primary, context));
return ResourceID.fromResource(getOrComputeDesired(context));
}

protected boolean addOwnerReference() {
return garbageCollected;
}

@Override
protected R desired(P primary, Context<P> context) {
return super.desired(primary, context);
protected R getOrComputeDesired(Context<P> context) {
return super.getOrComputeDesired(context);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;

import static org.junit.jupiter.api.Assertions.*;
Expand All @@ -31,15 +33,21 @@

class AbstractDependentResourceTest {

private static final TestCustomResource PRIMARY = new TestCustomResource();
private static final DefaultContext<TestCustomResource> CONTEXT = createContext(PRIMARY);

private static DefaultContext<TestCustomResource> createContext(TestCustomResource primary) {
return new DefaultContext<>(mock(), mock(), primary, false, false);
}

@Test
void throwsExceptionIfDesiredIsNullOnCreate() {
TestDependentResource testDependentResource = new TestDependentResource();
testDependentResource.setSecondary(null);
testDependentResource.setDesired(null);

assertThrows(
DependentResourceException.class,
() -> testDependentResource.reconcile(new TestCustomResource(), null));
DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT));
}

@Test
Expand All @@ -49,8 +57,7 @@ void throwsExceptionIfDesiredIsNullOnUpdate() {
testDependentResource.setDesired(null);

assertThrows(
DependentResourceException.class,
() -> testDependentResource.reconcile(new TestCustomResource(), null));
DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT));
}

@Test
Expand All @@ -60,8 +67,7 @@ void throwsExceptionIfCreateReturnsNull() {
testDependentResource.setDesired(configMap());

assertThrows(
DependentResourceException.class,
() -> testDependentResource.reconcile(new TestCustomResource(), null));
DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT));
}

@Test
Expand All @@ -71,8 +77,28 @@ void throwsExceptionIfUpdateReturnsNull() {
testDependentResource.setDesired(configMap());

assertThrows(
DependentResourceException.class,
() -> testDependentResource.reconcile(new TestCustomResource(), null));
DependentResourceException.class, () -> testDependentResource.reconcile(PRIMARY, CONTEXT));
}

@Test
void checkThatDesiredIsOnlyCalledOnce() {
final var testDependentResource = new DesiredCallCountCheckingDR();
final var primary = new TestCustomResource();
final var spec = primary.getSpec();
spec.setConfigMapName("foo");
spec.setKey("key");
spec.setValue("value");
final var context = createContext(primary);
testDependentResource.reconcile(primary, context);

spec.setValue("value2");
testDependentResource.reconcile(primary, context);

assertEquals(1, testDependentResource.desiredCallCount);

context.getOrComputeDesiredStateFor(
testDependentResource, p -> testDependentResource.desired(p, context));
assertEquals(1, testDependentResource.desiredCallCount);
}

private ConfigMap configMap() {
Expand Down Expand Up @@ -130,22 +156,12 @@ protected ConfigMap desired(TestCustomResource primary, Context<TestCustomResour
return desired;
}

public ConfigMap getSecondary() {
return secondary;
}

public TestDependentResource setSecondary(ConfigMap secondary) {
public void setSecondary(ConfigMap secondary) {
this.secondary = secondary;
return this;
}

public ConfigMap getDesired() {
return desired;
}

public TestDependentResource setDesired(ConfigMap desired) {
public void setDesired(ConfigMap desired) {
this.desired = desired;
return this;
}

@Override
Expand All @@ -172,4 +188,35 @@ public Matcher.Result<ConfigMap> match(
return result;
}
}

private static class DesiredCallCountCheckingDR extends TestDependentResource {
private short desiredCallCount;

@Override
public ConfigMap update(
ConfigMap actual,
ConfigMap desired,
TestCustomResource primary,
Context<TestCustomResource> context) {
return desired;
}

@Override
public ConfigMap create(
ConfigMap desired, TestCustomResource primary, Context<TestCustomResource> context) {
return desired;
}

@Override
protected ConfigMap desired(TestCustomResource primary, Context<TestCustomResource> context) {
final var spec = primary.getSpec();
desiredCallCount++;
return new ConfigMapBuilder()
.editOrNewMetadata()
.withName(spec.getConfigMapName())
.endMetadata()
.addToData(spec.getKey(), spec.getValue())
.build();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,37 +18,48 @@
import java.util.Map;
import java.util.Optional;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import io.fabric8.kubernetes.api.model.*;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentStatusBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.MockKubernetesClient;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.DefaultContext;

import static io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher.match;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@SuppressWarnings({"unchecked"})
class GenericKubernetesResourceMatcherTest {

private static final Context context = mock(Context.class);
private static final Context context = new TestContext();

private static class TestContext extends DefaultContext<HasMetadata> {
private final KubernetesClient client = MockKubernetesClient.client(HasMetadata.class);

public TestContext() {
this(null);
}

public TestContext(HasMetadata primary) {
super(mock(), mock(), primary, false, false);
}

@Override
public KubernetesClient getClient() {
return client;
}
}

Deployment actual = createDeployment();
Deployment desired = createDeployment();
TestDependentResource dependentResource = new TestDependentResource(desired);

@BeforeAll
static void setUp() {
final var client = MockKubernetesClient.client(HasMetadata.class);
when(context.getClient()).thenReturn(client);
}

@Test
void matchesTrivialCases() {
assertThat(GenericKubernetesResourceMatcher.match(desired, actual, context).matched()).isTrue();
Expand Down Expand Up @@ -77,9 +88,10 @@ void matchesWithStrongSpecEquality() {
@Test
void doesNotMatchRemovedValues() {
actual = createDeployment();
final var localContext = new TestContext(createPrimary("removed"));
assertThat(
GenericKubernetesResourceMatcher.match(
dependentResource.desired(createPrimary("removed"), null), actual, context)
dependentResource.getOrComputeDesired(localContext), actual, localContext)
.matched())
.withFailMessage("Removing values in metadata should lead to a mismatch")
.isFalse();
Expand Down