Repository navigation
Closed
Description
Hi,
I have an operator that uses a standalone dependent resource. The dependent resource is of type CRUDKubernetesDependentResource<ConfigMap, T> and only overrides method protected ConfigMap desired(T primary, Context<T> context). When the dependent resource does not exist yet, desired is called once. However, when the dependent resource exists, desired is called twice in a row. Why is that? According to the flowchart in the docs, I would have expected only one call.
Minimal reproducible example based on the WebPage Example Operator
Full source code can be found here: https://github.com/michaelkoepf/dependent-resource-test. Most important classes can be found below.
@ControllerConfiguration
public class WebPageStandaloneDependentsReconciler implements Reconciler<WebPage> {
private static final Logger LOG = LoggerFactory.getLogger(WebPageStandaloneDependentsReconciler.class);
private final Workflow<WebPage> workflow;
public WebPageStandaloneDependentsReconciler() {
workflow = createDependentResourcesAndWorkflow();
}
@Override
public List<EventSource<?, WebPage>> prepareEventSources(EventSourceContext<WebPage> context) {
return EventSourceUtils.eventSourcesFromWorkflow(context, workflow);
}
@Override
public UpdateControl<WebPage> reconcile(WebPage webPage, Context<WebPage> context)
throws Exception {
LOG.info("Starting reconciliation at {}", System.currentTimeMillis());
workflow.reconcile(webPage, context);
return UpdateControl.noUpdate();
}
@SuppressWarnings({"unchecked", "rawtypes"})
private Workflow<WebPage> createDependentResourcesAndWorkflow() {
// create the dependent resources
var configMapDR = new ConfigMapDependentResource();
// configure them with our label selector
Arrays.asList(configMapDR)
.forEach(
dr ->
dr.configureWith(
new KubernetesDependentResourceConfigBuilder()
.withKubernetesDependentInformerConfig(
InformerConfiguration.builder(dr.resourceType())
.withLabelSelector("managed=true")
.build())
.build()));
return new WorkflowBuilder<WebPage>()
.addDependentResource(configMapDR)
.build();
}
}
public class ConfigMapDependentResource
extends CRUDKubernetesDependentResource<ConfigMap, WebPage> {
private static final Logger LOG = LoggerFactory.getLogger(ConfigMapDependentResource.class);
@Override
protected ConfigMap desired(WebPage webPage, Context<WebPage> context) {
LOG.info("Evaluating desired state at {}", System.currentTimeMillis());
Map<String, String> data = new HashMap<>();
data.put("index.html", webPage.getSpec().getHtml());
Map<String, String> labels = new HashMap<>();
labels.put("managed", "true");
return new ConfigMapBuilder()
.withMetadata(
new ObjectMetaBuilder()
.withName("webpage-configmap")
.withNamespace(webPage.getMetadata().getNamespace())
.withLabels(labels)
.build())
.withData(data)
.build();
}
}
Example
- Trigger reconciliation by
kubectl apply -f k8s/webpage-v1.yaml. Dependent resource does not exist yet. Log:
2025-08-11 13:32:02,237 INFO [com.exa.WebPageStandaloneDependentsReconciler] (ReconcilerExecutor-webpagestandalonedependentsreconciler-185) Starting reconciliation at 1754911922237
2025-08-11 13:32:02,240 INFO [com.exa.ConfigMapDependentResource] (pool-16-thread-1) Evaluating desired state at 1754911922240
- Dependent resource now exists. Trigger reconciliation again by
kubectl apply -f k8s/webpage-v2.yaml. Log:
2025-08-11 13:32:04,816 INFO [com.exa.WebPageStandaloneDependentsReconciler] (ReconcilerExecutor-webpagestandalonedependentsreconciler-188) Starting reconciliation at 1754911924816
2025-08-11 13:32:04,817 INFO [com.exa.ConfigMapDependentResource] (pool-16-thread-2) Evaluating desired state at 1754911924817
2025-08-11 13:32:04,818 INFO [com.exa.ConfigMapDependentResource] (pool-16-thread-2) Evaluating desired state at 1754911924818
- Trigger reconciliation again by
kubectl apply -f k8s/webpage-v3.yaml. Log:
2025-08-11 13:32:06,888 INFO [com.exa.WebPageStandaloneDependentsReconciler] (ReconcilerExecutor-webpagestandalonedependentsreconciler-191) Starting reconciliation at 1754911926888
2025-08-11 13:32:06,889 INFO [com.exa.ConfigMapDependentResource] (pool-16-thread-3) Evaluating desired state at 1754911926889
2025-08-11 13:32:06,889 INFO [com.exa.ConfigMapDependentResource] (pool-16-thread-3) Evaluating desired state at 1754911926889
Metadata
Metadata
Assignees
Labels
No labels
Activity
metacosm commentedon Aug 11, 2025
Which version of the SDK are you using?
michaelkoepf commentedon Aug 11, 2025
@metacosm
csviri commentedon Aug 12, 2025
Hi @michaelkoepf ,
see here: https://javaoperatorsdk.io/blog/2025/01/06/version-5-released/#resourcediscriminator-is-removed-and-related-changes
java-operator-sdk/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java
Line 139 in 03a0949
java-operator-sdk/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
Lines 266 to 290 in 03a0949
Basically by default calls desired to select the target resource. you can easily optimize this by overriding:
java-operator-sdk/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java
Lines 288 to 290 in 03a0949
michaelkoepf commentedon Aug 18, 2025
Hi @csviri,
When I override
targetSecondaryResourceIDas in the example, the reconciliation of the workflow throws an exception when the secondary resource should be updated (i.e., when executing thekubectl applyin 2. of my original post). The updated source code can be found here.Exception with stack trace
Is there anything wrong when computing the desired state?
csviri commentedon Aug 18, 2025
The problem is that
generates different name than in desired:
that should be the same name.
michaelkoepf commentedon Aug 20, 2025
Well... 👀 Thanks! Works now as expected. Closing this issue.