This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 28f3170dcc6a CAMEL-26887: camel-zookeeper-master - 
ZookeeperGroupSupport closes only what it created, and reads the 
zookeeper.password property (#29791)
28f3170dcc6a is described below

commit 28f3170dcc6a16ac2d1b89e242bc4589ba5b8b65
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Nov 13 20:23:36 2026 +0530

    CAMEL-26887: camel-zookeeper-master - ZookeeperGroupSupport closes only 
what it created, and reads the zookeeper.password property (#29791)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../catalog/docs/zookeeper-master-component.adoc   |   2 +
 .../src/main/docs/zookeeper-master-component.adoc  |   2 +
 .../zookeepermaster/ZookeeperGroupSupport.java     |  30 +++-
 .../ZookeeperGroupSupportLifecycleTest.java        | 184 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  11 ++
 5 files changed, 225 insertions(+), 4 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
index 5f67f1d03de9..857e0803fc14 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/zookeeper-master-component.adoc
@@ -135,6 +135,8 @@ However, you can also configure the url of the ZooKeeper 
ensemble using environm
 export ZOOKEEPER_URL = "myzookeeper:2181"
 ----
 
+When no `zooKeeperPassword` is configured, the password can also be set with 
the `zookeeper.password` system property.
+
 === Master RoutePolicy
 
 You can also use a `RoutePolicy` to control routes in master/slave mode.
diff --git 
a/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
 
b/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
index 5f67f1d03de9..857e0803fc14 100644
--- 
a/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
+++ 
b/components/camel-zookeeper-master/src/main/docs/zookeeper-master-component.adoc
@@ -135,6 +135,8 @@ However, you can also configure the url of the ZooKeeper 
ensemble using environm
 export ZOOKEEPER_URL = "myzookeeper:2181"
 ----
 
+When no `zooKeeperPassword` is configured, the password can also be set with 
the `zookeeper.password` system property.
+
 === Master RoutePolicy
 
 You can also use a `RoutePolicy` to control routes in master/slave mode.
diff --git 
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
 
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
index ec5fe0b5fbf0..4a0120331ddd 100644
--- 
a/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
+++ 
b/components/camel-zookeeper-master/src/main/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupport.java
@@ -59,6 +59,10 @@ public class ZookeeperGroupSupport<T extends NodeState> 
extends ServiceSupport
     // the component, or the "curator" bean of the registry, which others 
share), so the listener is removed from it
     // when this stops
     private volatile CuratorFramework listenedCurator;
+    // the client that call() created, and the group factory that doStart 
created: only these are closed when this stops
+    // (a client or a group factory of the registry, or set by the user, may 
be shared, and is kept)
+    private volatile CuratorFramework createdCurator;
+    private volatile ManagedGroupFactory createdManagedGroupFactory;
     @Metadata(defaultValue = "10000")
     private int maximumConnectionTimeout = 10 * 1000;
     @Metadata(defaultValue = "localhost:2181")
@@ -194,6 +198,7 @@ public class ZookeeperGroupSupport<T extends NodeState> 
extends ServiceSupport
         if (managedGroupFactory == null) {
             
setManagedGroupFactory(getManagedGroupFactoryStrategy().createGroupFactory(curator,
 getClass().getClassLoader(),
                     getCamelContext(), this));
+            createdManagedGroupFactory = managedGroupFactory;
         }
     }
 
@@ -215,7 +220,7 @@ public class ZookeeperGroupSupport<T extends NodeState> 
extends ServiceSupport
         }
         String password = getZooKeeperPassword();
         if (password == null) {
-            System.getProperty(ZOOKEEPER_PASSWORD);
+            password = System.getProperty(ZOOKEEPER_PASSWORD);
         }
         LOG.info("Creating new CuratorFramework with connection: {}", 
connectString);
         CuratorFrameworkFactory.Builder builder = 
CuratorFrameworkFactory.builder()
@@ -228,6 +233,7 @@ public class ZookeeperGroupSupport<T extends NodeState> 
extends ServiceSupport
         }
 
         curator = builder.build();
+        createdCurator = curator;
         LOG.debug("Starting CuratorFramework {}", curator);
         curator.start();
         return curator;
@@ -236,11 +242,27 @@ public class ZookeeperGroupSupport<T extends NodeState> 
extends ServiceSupport
     @Override
     protected void doStop() throws Exception {
         try {
-            if (managedGroupFactory != null) {
-                managedGroupFactory.close();
-                managedGroupFactory = null;
+            // close only the group factory this created (the one of 
DefaultGroupFactoryStrategy also closes the client
+            // that call() created): a factory of the registry may be shared 
by other consumers and route policies
+            ManagedGroupFactory created = createdManagedGroupFactory;
+            createdManagedGroupFactory = null;
+            if (created != null) {
+                if (created == managedGroupFactory) {
+                    managedGroupFactory = null;
+                }
+                created.close();
             }
         } finally {
+            // the client call() created is closed (again, if its group 
factory did): a start after this stop creates a
+            // new one
+            CuratorFramework client = createdCurator;
+            createdCurator = null;
+            if (client != null) {
+                if (client == curator) {
+                    curator = null;
+                }
+                client.close();
+            }
             // the client may be shared (the curator of the component, or the 
"curator" bean of the registry): a listener
             // left on it would keep this (and the consumer or route policy 
that created it) for as long as the client
             // lives, one more at each start
diff --git 
a/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
 
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
new file mode 100644
index 000000000000..3cd35799d98f
--- /dev/null
+++ 
b/components/camel-zookeeper-master/src/test/java/org/apache/camel/component/zookeepermaster/ZookeeperGroupSupportLifecycleTest.java
@@ -0,0 +1,184 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.zookeepermaster;
+
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.component.zookeepermaster.group.Group;
+import org.apache.camel.component.zookeepermaster.group.ManagedGroupFactory;
+import org.apache.camel.component.zookeepermaster.group.NodeState;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.apache.curator.framework.AuthInfo;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.imps.CuratorFrameworkState;
+import org.apache.curator.retry.RetryOneTime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+/**
+ * The client and the group factory of a ZookeeperGroupSupport across a stop 
and a start (no ZooKeeper: the client is
+ * created and started, and does not connect).
+ */
+class ZookeeperGroupSupportLifecycleTest extends CamelTestSupport {
+
+    ZookeeperGroupSupportLifecycleTest() {
+        testConfiguration().withUseRouteBuilder(false);
+    }
+
+    @Test
+    void testCreatedClientIsNotReusedAfterAStop() {
+        ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+        support.start();
+        CuratorFramework first = support.getCurator();
+        assertEquals(CuratorFrameworkState.STARTED, first.getState());
+
+        support.stop();
+        assertEquals(CuratorFrameworkState.STOPPED, first.getState());
+
+        // started again, it creates a new client, not the one its stop closed
+        support.start();
+        try {
+            CuratorFramework second = support.getCurator();
+            assertNotSame(first, second);
+            assertEquals(CuratorFrameworkState.STARTED, second.getState());
+        } finally {
+            support.stop();
+        }
+    }
+
+    @Test
+    void testClientSetByTheUserIsKeptAfterAStop() throws Exception {
+        CuratorFramework client = 
CuratorFrameworkFactory.builder().connectString("localhost:1")
+                .retryPolicy(new RetryOneTime(100)).build();
+        client.start();
+        try {
+            ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+            support.setCurator(client);
+            support.start();
+            assertSame(client, support.getCurator());
+            support.stop();
+
+            // the client of the user is not closed, and is used again by the 
next start
+            assertEquals(CuratorFrameworkState.STARTED, client.getState());
+            support.start();
+            assertSame(client, support.getCurator());
+            support.stop();
+            assertEquals(CuratorFrameworkState.STARTED, client.getState());
+        } finally {
+            client.close();
+        }
+    }
+
+    @Test
+    void testGroupFactoryOfTheRegistryIsNotClosedByAStop() {
+        FakeGroupFactory factory = new FakeGroupFactory();
+        context.getRegistry().bind("groupFactory", factory);
+
+        ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+        support.start();
+        assertSame(factory, support.getManagedGroupFactory());
+        support.stop();
+
+        // the factory of the registry may be shared by other consumers and 
route policies: it stays open
+        assertEquals(0, factory.closes.get());
+        support.start();
+        assertSame(factory, support.getManagedGroupFactory());
+        support.stop();
+        assertEquals(0, factory.closes.get());
+    }
+
+    @Test
+    void testPasswordFromTheSystemProperty() throws Exception {
+        String old = System.getProperty("zookeeper.password");
+        System.setProperty("zookeeper.password", "secret");
+        CuratorFramework curator = null;
+        try {
+            ZookeeperGroupSupport<CamelNodeState> support = newSupport();
+            curator = support.call();
+
+            // reads the private authInfos field of CuratorFrameworkImpl 
(curator-framework 5.9.x) by reflection: this
+            // fails loudly when a Curator upgrade renames or changes the field
+            Field field = curator.getClass().getDeclaredField("authInfos");
+            field.setAccessible(true);
+            @SuppressWarnings("unchecked")
+            List<AuthInfo> authInfos = (List<AuthInfo>) field.get(curator);
+            assertEquals(1, authInfos.size());
+            assertEquals("digest", authInfos.get(0).getScheme());
+            assertEquals("fabric:secret", new 
String(authInfos.get(0).getAuth(), StandardCharsets.UTF_8));
+        } finally {
+            if (curator != null) {
+                curator.close();
+            }
+            if (old == null) {
+                System.clearProperty("zookeeper.password");
+            } else {
+                System.setProperty("zookeeper.password", old);
+            }
+        }
+    }
+
+    private ZookeeperGroupSupport<CamelNodeState> newSupport() {
+        ZookeeperGroupSupport<CamelNodeState> support = new 
ZookeeperGroupSupport<>();
+        support.setCamelContext(context);
+        // no ZooKeeper there: the client starts and does not connect
+        support.setZooKeeperUrl("localhost:1");
+        support.setMaximumConnectionTimeout(100);
+        return support;
+    }
+
+    private static final class FakeGroupFactory implements ManagedGroupFactory 
{
+        final AtomicInteger closes = new AtomicInteger();
+
+        @Override
+        public <T extends NodeState> Group<T> createGroup(String path, 
Class<T> clazz) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T extends NodeState> Group<T> createGroup(String path, 
Class<T> clazz, ThreadFactory threadFactory) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T extends NodeState> Group<T> createMultiGroup(String path, 
Class<T> clazz) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public <T extends NodeState> Group<T> createMultiGroup(String path, 
Class<T> clazz, ThreadFactory threadFactory) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public CuratorFramework getCurator() {
+            return null;
+        }
+
+        @Override
+        public void close() {
+            closes.incrementAndGet();
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 1f74fee69eee..c50fc5208253 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -3994,6 +3994,17 @@ the consumer of such a route kept running next to the 
one of the master. This in
 consume while the node cannot reach ZooKeeper. Setting the route to 
`autoStartup=false` is still recommended, so the
 consumer does not run for a moment when the CamelContext starts.
 
+=== camel-zookeeper-master - the zookeeper.password system property, and a 
group factory of the application
+
+The `zookeeper.password` system property is now used when no 
`zooKeeperPassword` is configured: the ZooKeeper client
+that the component creates then sends the `digest` authorization 
`fabric:<password>`. Before, the property was read but
+not used, so a deployment that set it connected without that authorization.
+
+A `ManagedGroupFactory` bound in the registry, or set with 
`setManagedGroupFactory`, is no longer closed when a
+`zookeeper-master` consumer, a `MasterRoutePolicy` or the component stops. The 
application that created it owns its
+lifecycle, as for a `CuratorFramework` bound in the registry. A group factory 
and a client that Camel created are
+still closed on stop, and created again on the next start.
+
 === camel-milo - each client connection has thread pools of its own
 
 A `milo-client` or `milo-browse` connection now handles the outcome of its 
write, method call, read and browse

Reply via email to