Skip to content

[Question] desired method of CRUDKubernetesDependentResource called twice when dependent resource exists #2895

Closed

Description

@michaelkoepf

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

  1. 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
  1. 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
  1. 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

Activity

  1. metacosm commented on Aug 11, 2025

    @metacosm
    Collaborator

    Which version of the SDK are you using?

  2. michaelkoepf commented on Aug 11, 2025

    @michaelkoepf
    ContributorAuthor
  3. self-assigned this
    on Aug 11, 2025
  4. csviri commented on Aug 12, 2025

    @csviri
    Collaborator

    Hi @michaelkoepf ,

    see here: https://javaoperatorsdk.io/blog/2025/01/06/version-5-released/#resourcediscriminator-is-removed-and-related-changes

    @Override
    protected Optional<R> selectTargetSecondaryResource(
    Set<R> secondaryResources, P primary, Context<P> context) {
    ResourceID managedResourceID = targetSecondaryResourceID(primary, context);
    return secondaryResources.stream()
    .filter(
    r ->
    r.getMetadata().getName().equals(managedResourceID.getName())
    && Objects.equals(
    r.getMetadata().getNamespace(),
    managedResourceID.getNamespace().orElse(null)))
    .findFirst();
    }
    /**
    * Override this method in order to optimize and not compute the desired when selecting the target
    * secondary resource. Simply, a static ResourceID can be returned.
    *
    * @param primary resource
    * @param context of current reconciliation
    * @return id of the target managed resource
    */
    protected ResourceID targetSecondaryResourceID(P primary, Context<P> context) {
    return ResourceID.fromResource(desired(primary, context));
    }

    Basically by default calls desired to select the target resource. you can easily optimize this by overriding:

    protected ResourceID targetSecondaryResourceID(P primary, Context<P> context) {
    return ResourceID.fromResource(desired(primary, context));
    }

  5. michaelkoepf commented on Aug 18, 2025

    @michaelkoepf
    ContributorAuthor

    Hi @csviri,

    When I override targetSecondaryResourceID as in the example, the reconciliation of the workflow throws an exception when the secondary resource should be updated (i.e., when executing the kubectl apply in 2. of my original post). The updated source code can be found here.

    Exception with stack trace
    2025-08-18 15:00:35,429 WARN  [io.jav.ope.pro.eve.EventProcessor] (ReconcilerExecutor-webpagestandalonedependentsreconciler-194) Uncaught error during event processing ExecutionScope{ resource id: ResourceID{name='hellows', namespace='default'}, version: 729} - but another reconciliation will be attempted because a superseding event has been received or another retry attempt is pending.
    
    Exception in WebPageStandaloneDependentsReconciler.java:37
              35        throws Exception {
              36      LOG.info("Starting reconciliation at {}", System.currentTimeMillis());                                                                                                                                                                                                                                                                                                                                     
            → 37      workflow.reconcile(webPage, context);                                                                                                                                                                                                                                                                                                                                                                      
              38                                                                                                                                                                                                                                                                                                                                                                                                                 
              39      return UpdateControl.noUpdate();                                                                                                                                                                                                                                                                                                                                                                           
                                                                                                                                                                                                                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                                                                 
    Exception in WebPageStandaloneDependentsReconciler.java:17                                                                                                                                                                                                                                                                                                                                                                   
              15  // Based on https://github.com/operator-framework/java-operator-sdk/tree/main/sample-operators/webpage                                                                                                                                                                                                                                                                                                         
              16                                                                                                                                                                                                                                                                                                                                                                                                                 
            → 17  @ControllerConfiguration                                                                                                                                                                                                                                                                                                                                                                                       
              18  public class WebPageStandaloneDependentsReconciler implements Reconciler<WebPage> {                                                                                                                                                                                                                                                                                                                            
              19                                                                                                                                                                                                                                                                                                                                                                                                                 
                                                                                                                                                                                                                                                                                                                                                                                                                                 
    : io.javaoperatorsdk.operator.AggregatedOperatorException: Exception(s) during workflow execution. Details:                                                                                                                                                                                                                                                                                                                  
     - com.example.ConfigMapDependentResource -> io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: POST at: https://0.0.0.0:45121/api/v1/namespaces/default/configmaps. Message: configmaps "webpage-configmap" already exists. Received status: Status(apiVersion=v1, code=409, details=StatusDetails(causes=[], group=null, kind=configmaps, name=webpage-configmap, retryAfterSeconds=null, uid=null, additionalProperties={}), kind=Status, message=configmaps "webpage-configmap" already exists, metadata=ListMeta(_continue=null, remainingItemCount=null, resourceVersion=null, selfLink=null, additionalProperties={}), reason=AlreadyExists, status=Failure, additionalProperties={}).                                                                                                                                     
            at io.fabric8.kubernetes.client.KubernetesClientException.copyAsCause(KubernetesClientException.java:205)                                                                                                                                                                                                                                                                                                            
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.waitForResult(OperationSupport.java:507)                                                                                                                                                                                                                                                                                                               
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.handleResponse(OperationSupport.java:524)                                                                                                                                                                                                                                                                                                              
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.handleCreate(OperationSupport.java:340)                                                                                                                                                                                                                                                                                                                
            at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.handleCreate(BaseOperation.java:754)                                                                                                                                                                                                                                                                                                                      
            at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.handleCreate(BaseOperation.java:98)                                                                                                                                                                                                                                                                                                                       
            at io.fabric8.kubernetes.client.dsl.internal.CreateOnlyResourceOperation.create(CreateOnlyResourceOperation.java:42)                                                                                                                                                                                                                                                                                                 
            at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.create(BaseOperation.java:1155)                                                                                                                                                                                                                                                                                                                           
            at io.fabric8.kubernetes.client.dsl.internal.BaseOperation.create(BaseOperation.java:98)                                                                                                                                                                                                                                                                                                                             
            at io.fabric8.kubernetes.client.extension.ResourceAdapter.create(ResourceAdapter.java:114)                                                                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource.create(KubernetesDependentResource.java:81)                                                                                                                                                                                                                                                                               
            at io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource.create(CRUDKubernetesDependentResource.java:16)                                                                                                                                                                                                                                                                       
            at io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource.handleCreate(AbstractDependentResource.java:167)                                                                                                                                                                                                                                                                                       
            at io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource.reconcile(AbstractDependentResource.java:76)                                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.dependent.SingleDependentResourceReconciler.reconcile(SingleDependentResourceReconciler.java:19)                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.dependent.AbstractDependentResource.reconcile(AbstractDependentResource.java:66)                                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileExecutor$NodeReconcileExecutor.doRun(WorkflowReconcileExecutor.java:145)                                                                                                                                                                                                                                                               
            at io.javaoperatorsdk.operator.processing.dependent.workflow.NodeExecutor.run(NodeExecutor.java:20)                                                                                                                                                                                                                                                                                                                  
            at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)                                                                                                                                                                                                                                                                                                                                 
            at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)                                                                                                                                                                                                                                                                                                                                                
            at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.lang.Thread.run(Thread.java:1583)                                                                                                                                                                                                                                                                                                                                                                  
    Caused by: io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: POST at: https://0.0.0.0:45121/api/v1/namespaces/default/configmaps. Message: configmaps "webpage-configmap" already exists. Received status: Status(apiVersion=v1, code=409, details=StatusDetails(causes=[], group=null, kind=configmaps, name=webpage-configmap, retryAfterSeconds=null, uid=null, additionalProperties={}), kind=Status, message=configmaps "webpage-configmap" already exists, metadata=ListMeta(_continue=null, remainingItemCount=null, resourceVersion=null, selfLink=null, additionalProperties={}), reason=AlreadyExists, status=Failure, additionalProperties={}).                                                                                                                                                                       
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.requestFailure(OperationSupport.java:642)                                                                                                                                                                                                                                                                                                              
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.requestFailure(OperationSupport.java:622)                                                                                                                                                                                                                                                                                                              
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.assertResponseCode(OperationSupport.java:582)                                                                                                                                                                                                                                                                                                          
            at io.fabric8.kubernetes.client.dsl.internal.OperationSupport.lambda$handleResponse$0(OperationSupport.java:549)                                                                                                                                                                                                                                                                                                     
            at java.base/java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:646)                                                                                                                                                                                                                                                                                                                     
            at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2179)                                                                                                                                                                                                                                                                                                                            
            at io.fabric8.kubernetes.client.http.StandardHttpClient.lambda$completeOrCancel$10(StandardHttpClient.java:141)                                                                                                                                                                                                                                                                                                      
            at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863)                                                                                                                                                                                                                                                                                                                      
            at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841)                                                                                                                                                                                                                                                                                                              
            at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2179)                                                                                                                                                                                                                                                                                                                            
            at io.fabric8.kubernetes.client.http.ByteArrayBodyHandler.onBodyDone(ByteArrayBodyHandler.java:51)                                                                                                                                                                                                                                                                                                                   
            at java.base/java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:863)                                                                                                                                                                                                                                                                                                                      
            at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:841)                                                                                                                                                                                                                                                                                                              
            at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2179)                                                                                                                                                                                                                                                                                                                            
            at io.fabric8.kubernetes.client.vertx.VertxHttpRequest.lambda$consumeBytes$1(VertxHttpRequest.java:84)                                                                                                                                                                                                                                                                                                               
            at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:270)                                                                                                                                                                                                                                                                                                                                             
            at io.vertx.core.impl.ContextInternal.dispatch(ContextInternal.java:252)                                                                                                                                                                                                                                                                                                                                             
            at io.vertx.core.http.impl.HttpEventHandler.handleEnd(HttpEventHandler.java:76)                                                                                                                                                                                                                                                                                                                                      
            at io.vertx.core.http.impl.HttpClientResponseImpl.handleEnd(HttpClientResponseImpl.java:250)                                                                                                                                                                                                                                                                                                                         
            at io.vertx.core.http.impl.Http1xClientConnection$StreamImpl.lambda$new$0(Http1xClientConnection.java:424)                                                                                                                                                                                                                                                                                                           
            at io.vertx.core.streams.impl.InboundBuffer.handleEvent(InboundBuffer.java:279)                                                                                                                                                                                                                                                                                                                                      
            at io.vertx.core.streams.impl.InboundBuffer.write(InboundBuffer.java:157)                                                                                                                                                                                                                                                                                                                                            
            at io.vertx.core.http.impl.Http1xClientConnection$StreamImpl.handleEnd(Http1xClientConnection.java:734)                                                                                                                                                                                                                                                                                                              
            at io.vertx.core.impl.ContextImpl.lambda$execute$7(ContextImpl.java:329)                                                                                                                                                                                                                                                                                                                                             
            at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:173)                                                                                                                                                                                                                                                                                                                            
            at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:166)                                                                                                                                                                                                                                                                                                                        
            at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)                                                                                                                                                                                                                                                                                                                
            at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:569)                                                                                                                                                                                                                                                                                                                                                      
            at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:998)                                                                                                                                                                                                                                                                                                                      
            at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)                                                                                                                                                                                                                                                                                                                                         
            at io.netty.util.concurrent.FastThrlRunnable.run(FastThreadLocalRunnable.java:30)                                                                                                                                                                                                                                                                                                                                    
            ... 1 more                                                                                                                                                                                                                                                                                                                                                                                                           
    
            at io.javaoperatorsdk.operator.processing.dependent.workflow.BaseWorkflowResult.throwAggregateExceptionIfErrorsPresent(BaseWorkflowResult.java:95)
            at io.javaoperatorsdk.operator.processing.dependent.workflow.DefaultWorkflow.reconcile(DefaultWorkflow.java:103)
            at com.example.WebPageStandaloneDependentsReconciler.reconcile(WebPageStandaloneDependentsReconciler.java:37)                                                                                                                                                                                                                                                                                                        
            at com.example.WebPageStandaloneDependentsReconciler.reconcile(WebPageStandaloneDependentsReconciler.java:17)                                                                                                                                                                                                                                                                                                        
            at com.example.WebPageStandaloneDependentsReconciler_ClientProxy.reconcile(Unknown Source)                                                                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:161)                                                                                                                                                                                                                                                                                                                                  
            at io.javaoperatorsdk.operator.processing.Controller$1.execute(Controller.java:117)                                                                                                                                                                                                                                                                                                                                  
            at io.javaoperatorsdk.operator.api.monitoring.Metrics.timeControllerExecution(Metrics.java:159)                                                                                                                                                                                                                                                                                                                      
            at io.javaoperatorsdk.operator.processing.Controller.reconcile(Controller.java:116)                                                                                                                                                                                                                                                                                                                                  
            at io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.reconcileExecution(ReconciliationDispatcher.java:153)                                                                                                                                                                                                                                                                                       
            at io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleReconcile(ReconciliationDispatcher.java:130)                                                                                                                                                                                                                                                                                          
            at io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleDispatch(ReconciliationDispatcher.java:97)                                                                                                                                                                                                                                                                                            
            at io.javaoperatorsdk.operator.processing.event.ReconciliationDispatcher.handleExecution(ReconciliationDispatcher.java:68)                                                                                                                                                                                                                                                                                           
            at io.javaoperatorsdk.operator.processing.event.EventProcessor$ReconcilerExecutor.run(EventProcessor.java:475)                                                                                                                                                                                                                                                                                                       
            at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)                                                                                                                                                                                                                                                                                                                         
            at java.base/java.lang.Thread.run(Thread.java:1583)                                                                                                                                                                                                                                                                                                                                                                  
    

    Is there anything wrong when computing the desired state?

  6. csviri commented on Aug 18, 2025

    @csviri
    Collaborator

    The problem is that

     return new ResourceID(primary.getMetadata().getName() + NAME_SUFFIX,
                primary.getMetadata().getNamespace());

    generates different name than in desired:

     new ObjectMetaBuilder()
                    .withName("webpage-configmap")

    that should be the same name.

  7. michaelkoepf commented on Aug 20, 2025

    @michaelkoepf
    ContributorAuthor

    Well... 👀 Thanks! Works now as expected. Closing this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions