Skip to content

improve: blocklist of problematic resources for previous version annotation #2774

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 9 commits into from
May 16, 2025
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 @@ -13,6 +13,8 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.ConfigBuilder;
import io.fabric8.kubernetes.client.CustomResource;
Expand Down Expand Up @@ -442,12 +444,40 @@ default Set<Class<? extends HasMetadata>> defaultNonSSAResource() {
*
* @return if special annotation should be used for dependent resource to filter events
* @since 4.5.0
* @return if special annotation should be used for dependent resource to filter events
*/
default boolean previousAnnotationForDependentResourcesEventFiltering() {
return true;
}

/**
* For dependent resources, the framework can add an annotation to filter out events resulting
* directly from the framework's operation. There are, however, some resources that do not follow
* the Kubernetes API conventions that changes in metadata should not increase the generation of
* the resource (as recorded in the {@code generation} field of the resource's {@code metadata}).
* For these resources, this convention is not respected and results in a new event for the
* framework to process. If that particular case is not handled correctly in the resource matcher,
* the framework will consider that the resource doesn't match the desired state and therefore
* triggers an update, which in turn, will re-add the annotation, thus starting the loop again,
* infinitely.
*
* <p>As a workaround, we automatically skip adding previous annotation for those well-known
* resources. Note that if you are sure that the matcher works for your use case, and it should in
* most instances, you can remove the resource type from the blocklist.
*
* <p>The consequence of adding a resource type to the set is that the framework will not use
* event filtering to prevent events, initiated by changes made by the framework itself as a
* result of its processing of dependent resources, to trigger the associated reconciler again.
*
* <p>Note that this method only takes effect if annotating dependent resources to prevent
* dependent resources events from triggering the associated reconciler again is activated as
* controlled by {@link #previousAnnotationForDependentResourcesEventFiltering()}
*
* @return a Set of resource classes where the previous version annotation won't be used.
*/
default Set<Class<? extends HasMetadata>> withPreviousAnnotationForDependentResourcesBlocklist() {
return Set.of(Deployment.class, StatefulSet.class);
}

/**
* If the event logic should parse the resourceVersion to determine the ordering of dependent
* resource events. This is typically not needed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class ConfigurationServiceOverrider {
private Boolean parseResourceVersions;
private Boolean useSSAToPatchPrimaryResource;
private Boolean cloneSecondaryResourcesWhenGettingFromCache;
private Set<Class<? extends HasMetadata>> previousAnnotationUsageBlocklist;

@SuppressWarnings("rawtypes")
private DependentResourceFactory dependentResourceFactory;
Expand Down Expand Up @@ -188,6 +189,12 @@ public ConfigurationServiceOverrider withCloneSecondaryResourcesWhenGettingFromC
return this;
}

public ConfigurationServiceOverrider withPreviousAnnotationForDependentResourcesBlocklist(
Set<Class<? extends HasMetadata>> blocklist) {
this.previousAnnotationUsageBlocklist = blocklist;
return this;
}

public ConfigurationService build() {
return new BaseConfigurationService(original.getVersion(), cloner, client) {
@Override
Expand Down Expand Up @@ -328,6 +335,14 @@ public boolean cloneSecondaryResourcesWhenGettingFromCache() {
cloneSecondaryResourcesWhenGettingFromCache,
ConfigurationService::cloneSecondaryResourcesWhenGettingFromCache);
}

@Override
public Set<Class<? extends HasMetadata>>
withPreviousAnnotationForDependentResourcesBlocklist() {
return overriddenValueOrDefault(
previousAnnotationUsageBlocklist,
ConfigurationService::withPreviousAnnotationForDependentResourcesBlocklist);
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public abstract class KubernetesDependentResource<R extends HasMetadata, P exten
private final boolean garbageCollected = this instanceof GarbageCollected;
private KubernetesDependentResourceConfig<R> kubernetesDependentResourceConfig;
private volatile Boolean useSSA;
private volatile Boolean usePreviousAnnotationForEventFiltering;

public KubernetesDependentResource(Class<R> resourceType) {
this(resourceType, null);
Expand Down Expand Up @@ -163,10 +164,19 @@ protected boolean useSSA(Context<P> context) {
}

private boolean usePreviousAnnotation(Context<P> context) {
return context
.getControllerConfiguration()
.getConfigurationService()
.previousAnnotationForDependentResourcesEventFiltering();
if (usePreviousAnnotationForEventFiltering == null) {
usePreviousAnnotationForEventFiltering =
context
.getControllerConfiguration()
.getConfigurationService()
.previousAnnotationForDependentResourcesEventFiltering()
&& !context
.getControllerConfiguration()
.getConfigurationService()
.withPreviousAnnotationForDependentResourcesBlocklist()
.contains(this.resourceType());
}
return usePreviousAnnotationForEventFiltering;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ private Optional<ManagedFieldsEntry> checkIfFieldManagerExists(R actual, String
}

/** Correct for known issue with SSA */
private void sanitizeState(R actual, R desired, Map<String, Object> actualMap) {
protected void sanitizeState(R actual, R desired, Map<String, Object> actualMap) {
if (actual instanceof StatefulSet actualStatefulSet
&& desired instanceof StatefulSet desiredStatefulSet) {
var actualSpec = actualStatefulSet.getSpec();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.javaoperatorsdk.operator.dependent.prevblocklist;

import java.util.Map;

import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.LabelSelectorBuilder;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder;
import io.fabric8.kubernetes.api.model.Quantity;
import io.fabric8.kubernetes.api.model.ResourceRequirementsBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
import io.fabric8.kubernetes.api.model.apps.DeploymentSpecBuilder;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.GenericKubernetesResourceMatcher;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.SSABasedGenericKubernetesResourceMatcher;

@KubernetesDependent
public class DeploymentDependent
extends CRUDKubernetesDependentResource<Deployment, PrevAnnotationBlockCustomResource> {

public static final String RESOURCE_NAME = "test1";

public DeploymentDependent() {
super(Deployment.class);
}

@Override
protected Deployment desired(
PrevAnnotationBlockCustomResource primary,
Context<PrevAnnotationBlockCustomResource> context) {

return new DeploymentBuilder()
.withMetadata(
new ObjectMetaBuilder()
.withName(primary.getMetadata().getName())
.withNamespace(primary.getMetadata().getNamespace())
.build())
.withSpec(
new DeploymentSpecBuilder()
.withReplicas(1)
.withSelector(
new LabelSelectorBuilder().withMatchLabels(Map.of("app", "nginx")).build())
.withTemplate(
new PodTemplateSpecBuilder()
.withMetadata(
new ObjectMetaBuilder().withLabels(Map.of("app", "nginx")).build())
.withSpec(
new PodSpecBuilder()
.withContainers(
new ContainerBuilder()
.withName("nginx")
.withImage("nginx:1.14.2")
.withResources(
new ResourceRequirementsBuilder()
.withLimits(Map.of("cpu", new Quantity("2000m")))
.build())
.build())
.build())
.build())
.build())
.build();
}

// for testing purposes replicating the matching logic but with the special matcher
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should use the configuration option from #2760 instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If that is merged, can change it (might be a new PR). thx!

@Override
public Result<Deployment> match(
Deployment actualResource,
Deployment desired,
PrevAnnotationBlockCustomResource primary,
Context<PrevAnnotationBlockCustomResource> context) {
final boolean matches;
addMetadata(true, actualResource, desired, primary, context);
if (useSSA(context)) {
matches = new SSAMatcherWithoutSanitization().matches(actualResource, desired, context);
} else {
matches =
GenericKubernetesResourceMatcher.match(desired, actualResource, false, false, context)
.matched();
}
return Result.computed(matches, desired);
}

// using this matcher, so we are able to reproduce issue with resource limits
static class SSAMatcherWithoutSanitization<R extends HasMetadata>
extends SSABasedGenericKubernetesResourceMatcher<R> {

@Override
protected void sanitizeState(R actual, R desired, Map<String, Object> actualMap) {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.javaoperatorsdk.operator.dependent.prevblocklist;

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("pabc")
public class PrevAnnotationBlockCustomResource extends CustomResource<PrevAnnotationBlockSpec, Void>
implements Namespaced {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.javaoperatorsdk.operator.dependent.prevblocklist;

import java.util.concurrent.atomic.AtomicInteger;

import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;

@Workflow(dependents = {@Dependent(type = DeploymentDependent.class)})
@ControllerConfiguration()
public class PrevAnnotationBlockReconciler
implements Reconciler<PrevAnnotationBlockCustomResource>, TestExecutionInfoProvider {

private final AtomicInteger numberOfExecutions = new AtomicInteger(0);

public PrevAnnotationBlockReconciler() {}

@Override
public UpdateControl<PrevAnnotationBlockCustomResource> reconcile(
PrevAnnotationBlockCustomResource resource,
Context<PrevAnnotationBlockCustomResource> context) {
numberOfExecutions.getAndIncrement();

return UpdateControl.noUpdate();
}

public int getNumberOfExecutions() {
return numberOfExecutions.get();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.javaoperatorsdk.operator.dependent.prevblocklist;

import java.time.Duration;

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

import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;

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

class PrevAnnotationBlockReconcilerIT {

public static final String TEST_1 = "test1";

@RegisterExtension
LocallyRunOperatorExtension extension =
LocallyRunOperatorExtension.builder()
// Removing resource from blocklist List would result in test failure
// .withConfigurationService(
// o -> o.previousAnnotationUsageBlocklist(Collections.emptyList()))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this be a new test then?

Copy link
Collaborator Author

@csviri csviri May 12, 2025

Choose a reason for hiding this comment

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

It could be an additional test, I did not add that since I did not find it important to show that something is not working.

.withReconciler(PrevAnnotationBlockReconciler.class)
.build();

@Test
void doNotUsePrevAnnotationForDeploymentDependent() {
extension.create(testResource(TEST_1));

var reconciler = extension.getReconcilerOfType(PrevAnnotationBlockReconciler.class);
await()
.pollDelay(Duration.ofMillis(200))
.untilAsserted(
() -> {
var deployment = extension.get(Deployment.class, TEST_1);
assertThat(deployment).isNotNull();
assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(0).isLessThan(10);
});
}

PrevAnnotationBlockCustomResource testResource(String name) {
var res = new PrevAnnotationBlockCustomResource();
res.setMetadata(new ObjectMetaBuilder().withName(name).build());
res.setSpec(new PrevAnnotationBlockSpec());
res.getSpec().setValue("value");
return res;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.dependent.prevblocklist;

public class PrevAnnotationBlockSpec {

private String value;

public String getValue() {
return value;
}

public PrevAnnotationBlockSpec setValue(String value) {
this.value = value;
return this;
}
}