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

oscerd 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 0b46030091bb CAMEL-24636: camel-core - re-authenticate components on 
context reload after a secret rotation (#26166)
0b46030091bb is described below

commit 0b46030091bbfebd808564643155a40e5d95da22
Author: Andrea Cosentino <[email protected]>
AuthorDate: Tue Sep 8 10:11:47 2026 +0200

    CAMEL-24636: camel-core - re-authenticate components on context reload 
after a secret rotation (#26166)
    
    The vault components detect a rotated secret and trigger a context reload, 
but
    the reload stopped short of the components holding the authenticated 
connection:
    it restarted the properties sources and reloaded the routes, leaving 
components
    and registry beans untouched.
    
    Re-apply the camel. options whose value is a property placeholder, so the 
newly
    resolved secret reaches the component. MainPropertiesReload already did 
this, but
    was only invoked from the file-watch strategies, and the raw placeholder is 
not
    retained on the component, so a component could not have re-resolved it on 
its own.
    
    Add the SecretRotationAware SPI so a component or registry bean holding a 
live
    authenticated resource can rebuild it. The callback runs after the options 
are
    re-applied and before the routes restart, and a callback that throws is 
logged
    and skipped so one component cannot break the reload.
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 .../org/apache/camel/spi/SecretRotationAware.java  |  57 +++++
 .../impl/CamelContextSecretRotationAwareTest.java  | 250 +++++++++++++++++++++
 .../impl/ContextReloadComponentPropertiesTest.java | 123 ++++++++++
 .../main/MainContextReloadSecretRotationTest.java  | 113 ++++++++++
 .../support/DefaultContextReloadStrategy.java      |  77 +++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    |  19 ++
 .../modules/ROOT/pages/context-reload.adoc         |  52 ++++-
 7 files changed, 687 insertions(+), 4 deletions(-)

diff --git 
a/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java
new file mode 100644
index 000000000000..dd1e154094b6
--- /dev/null
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/SecretRotationAware.java
@@ -0,0 +1,57 @@
+/*
+ * 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.spi;
+
+/**
+ * SPI for components and beans that capture a secret when they are 
configured, and that therefore need to be told when
+ * that secret has been rotated so they can re-authenticate in place.
+ * <p/>
+ * When a vault component detects that a secret changed it triggers a {@link 
ContextReloadStrategy}, which reloads the
+ * property placeholders and then all routes. Routes and endpoints are rebuilt 
from scratch, but components and beans in
+ * the {@link Registry} are not, so anything holding a live authenticated 
resource - a pooled JMS connection factory, a
+ * JDBC connection pool, a shared HTTP client - keeps using the credentials it 
captured at startup.
+ * <p/>
+ * Implement this interface on a {@link org.apache.camel.Component}, or 
register a bean implementing it in the
+ * {@link Registry}, to be notified when this happens. The callback runs after 
the property placeholders have been
+ * reloaded and the component options re-applied, but before the routes are 
restarted, so that by the time the routes
+ * come back up the underlying resource is already authenticated with the new 
secret.
+ * <p/>
+ * Implementations should re-establish the authenticated resource rather than 
assume the process will be restarted, and
+ * should be quick: the callback runs inline on the reload, and every 
implementation is notified before the routes come
+ * back. A callback that throws is logged and ignored, so that one component 
cannot prevent the others from being
+ * refreshed, nor break the reload as a whole.
+ *
+ * @see   ContextReloadStrategy
+ * @since 4.23
+ */
+@FunctionalInterface
+public interface SecretRotationAware {
+
+    /**
+     * Callback invoked when the secrets this component or bean captured may 
have been rotated, and it should
+     * re-authenticate in place.
+     * <p/>
+     * The callback is advisory: it says that a reload was triggered, not 
which secrets changed. Implementations should
+     * re-read their configuration and refresh the authenticated resource if 
the credentials it holds are no longer the
+     * configured ones.
+     *
+     * @param  source    the source that triggered the reload, such as the 
vault task that detected the change
+     * @throws Exception is thrown if the resource could not be 
re-authenticated
+     */
+    void onSecretRotation(Object source) throws Exception;
+
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java
new file mode 100644
index 000000000000..732d70d5b1b2
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/CamelContextSecretRotationAwareTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Endpoint;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.spi.SecretRotationAware;
+import org.apache.camel.support.DefaultComponent;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that {@link SecretRotationAware} components and beans are notified 
when the context is reloaded, so they can
+ * re-authenticate with a rotated secret before the routes are restarted.
+ */
+public class CamelContextSecretRotationAwareTest extends ContextTestSupport {
+
+    private final MyRotationAware bean = new MyRotationAware();
+    private final FailingRotationAware failing = new FailingRotationAware();
+    private final MyRotationAwareComponent component = new 
MyRotationAwareComponent();
+    private final MyRotationAwareComponent sharedComponent = new 
MyRotationAwareComponent();
+    private final List<String> order = new ArrayList<>();
+
+    @Test
+    public void testRegistryBeanIsNotified() {
+        assertThat(bean.getCounter()).isZero();
+
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        assertThat(crs).isNotNull();
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        assertThat(bean.getCounter()).isOne();
+        
assertThat(bean.getLastSource()).isEqualTo("CamelContextSecretRotationAwareTest");
+    }
+
+    @Test
+    public void testComponentIsNotified() {
+        assertThat(component.getCounter()).isZero();
+
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        // a component is the primary adopter of the SPI, as it is what holds 
the authenticated connection
+        assertThat(component.getCounter()).isOne();
+        
assertThat(component.getLastSource()).isEqualTo("CamelContextSecretRotationAwareTest");
+    }
+
+    @Test
+    public void testComponentAlsoInRegistryIsNotifiedOnce() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("CamelContextSecretRotationAwareTest");
+
+        // the same instance is both an added component and a registry bean, 
as happens on Spring Boot,
+        // and must not be notified twice
+        assertThat(sharedComponent.getCounter()).isOne();
+    }
+
+    @Test
+    public void testNotifiedOnEveryReload() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("first");
+        crs.onReload("second");
+
+        assertThat(bean.getCounter()).isEqualTo(2);
+        assertThat(component.getCounter()).isEqualTo(2);
+        assertThat(bean.getLastSource()).isEqualTo("second");
+    }
+
+    @Test
+    public void testFailingListenerDoesNotBreakTheReload() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("boom");
+
+        // the failing listener was invoked, but the reload still succeeded 
and the others were notified
+        assertThat(failing.getCounter()).isOne();
+        assertThat(bean.getCounter()).isOne();
+        assertThat(component.getCounter()).isOne();
+        assertThat(crs.getLastError()).isNull();
+        assertThat(context.getRoutes()).hasSize(1);
+    }
+
+    @Test
+    public void testNotifiedBeforeRoutesAreReloaded() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("ordering");
+
+        // the secret must be refreshed before the routes come back up, 
otherwise the new consumers
+        // would be created with the credentials that were just rotated away
+        assertThat(order).containsExactly("secret-rotation", "route-reload");
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        PropertiesComponent pc = context.getPropertiesComponent();
+        MySource my = new MySource();
+        ServiceHelper.startService(my);
+        pc.addPropertiesSource(my);
+
+        context.getRegistry().bind("myRotationAware", bean);
+        context.getRegistry().bind("failingRotationAware", failing);
+
+        context.addComponent("myrotate", component);
+        // the shared component is reachable both as a component and as a 
registry bean
+        context.addComponent("mysharedrotate", sharedComponent);
+        context.getRegistry().bind("mySharedRotateComponent", sharedComponent);
+
+        ContextReloadStrategy crs = new DefaultContextReloadStrategy() {
+            @Override
+            protected void notifySecretRotation(Object source) {
+                order.add("secret-rotation");
+                super.notifySecretRotation(source);
+            }
+
+            @Override
+            protected void reloadRoutes(Object source) throws Exception {
+                order.add("route-reload");
+                super.reloadRoutes(source);
+            }
+        };
+        context.addService(crs);
+
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .setBody(constant("{{hello}}"))
+                        .to("mock:result");
+            }
+        };
+    }
+
+    private static class MyRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+        private volatile String lastSource;
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            lastSource = source != null ? source.toString() : null;
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+
+        String getLastSource() {
+            return lastSource;
+        }
+    }
+
+    private static class FailingRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            throw new IllegalStateException("Cannot re-authenticate");
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+    }
+
+    private static class MyRotationAwareComponent extends DefaultComponent 
implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+        private volatile String lastSource;
+
+        @Override
+        protected Endpoint createEndpoint(String uri, String remaining, 
Map<String, Object> parameters) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+            lastSource = source != null ? source.toString() : null;
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+
+        String getLastSource() {
+            return lastSource;
+        }
+    }
+
+    private static class MySource extends ServiceSupport implements 
PropertiesSource {
+
+        private int counter;
+
+        @Override
+        public String getName() {
+            return "my";
+        }
+
+        @Override
+        public String getProperty(String name) {
+            if ("hello".equals(name)) {
+                return "Hello " + counter;
+            }
+            return null;
+        }
+
+        @Override
+        protected void doStart() {
+            // the properties source will be restarted
+            counter++;
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java
new file mode 100644
index 000000000000..366a86e6de07
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/impl/ContextReloadComponentPropertiesTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.impl;
+
+import java.util.Properties;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.NonManagedService;
+import org.apache.camel.StaticService;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesReload;
+import org.apache.camel.support.DefaultContextReloadStrategy;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.OrderedLocationProperties;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests that a context reload re-applies the Camel options whose value is a 
property placeholder, which is what allows
+ * a rotated secret to reach a component option that was resolved once at 
bootstrap.
+ */
+public class ContextReloadComponentPropertiesTest extends ContextTestSupport {
+
+    private final MyPropertiesReload reload = new MyPropertiesReload();
+
+    @Test
+    public void testPlaceholderOptionsAreReApplied() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        assertThat(crs).isNotNull();
+        crs.onReload("ContextReloadComponentPropertiesTest");
+
+        assertThat(reload.getCounter()).isOne();
+        Properties reloaded = reload.getProperties();
+        assertThat(reloaded).isNotNull();
+
+        // only camel options whose value is a placeholder are re-applied, as 
they are the only ones
+        // whose resolved value can change while the raw configuration stays 
the same
+        assertThat(reloaded.stringPropertyNames())
+                .containsExactlyInAnyOrder("camel.component.dummy.secret", 
"camel.component.dummy.token");
+        
assertThat(reloaded.getProperty("camel.component.dummy.secret")).isEqualTo("{{hello}}");
+    }
+
+    @Test
+    public void testPropertiesAreOrderedLocationProperties() {
+        ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+        crs.onReload("ContextReloadComponentPropertiesTest");
+
+        // MainPropertiesReload only acts on OrderedLocationProperties, so the 
contract must be kept
+        
assertThat(reload.getProperties()).isInstanceOf(OrderedLocationProperties.class);
+    }
+
+    @Override
+    protected CamelContext createCamelContext() throws Exception {
+        CamelContext context = super.createCamelContext();
+
+        Properties prop = new Properties();
+        prop.setProperty("hello", "Hello World");
+        // placeholder based camel options: must be re-applied
+        prop.setProperty("camel.component.dummy.secret", "{{hello}}");
+        prop.setProperty("camel.component.dummy.token", 
"prefix-{{hello}}-suffix");
+        // plain camel option: cannot change, must be filtered out
+        prop.setProperty("camel.component.dummy.plain", "no-placeholder");
+        // placeholder, but not a camel option: not Camel configuration, must 
be filtered out
+        prop.setProperty("myapp.password", "{{hello}}");
+
+        PropertiesComponent pc = context.getPropertiesComponent();
+        pc.setInitialProperties(prop);
+
+        context.addService(reload);
+        context.addService(new DefaultContextReloadStrategy());
+
+        return context;
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start").to("mock:result");
+            }
+        };
+    }
+
+    private static class MyPropertiesReload extends ServiceSupport
+            implements PropertiesReload, StaticService, NonManagedService {
+
+        private int counter;
+        private Properties properties;
+
+        @Override
+        public void onReload(String name, Properties properties) {
+            this.counter++;
+            this.properties = properties;
+        }
+
+        int getCounter() {
+            return counter;
+        }
+
+        Properties getProperties() {
+            return properties;
+        }
+    }
+}
diff --git 
a/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java
 
b/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java
new file mode 100644
index 000000000000..a0f6c55a86aa
--- /dev/null
+++ 
b/core/camel-main/src/test/java/org/apache/camel/main/MainContextReloadSecretRotationTest.java
@@ -0,0 +1,113 @@
+/*
+ * 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.main;
+
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.main.support.MyDummyComponent;
+import org.apache.camel.spi.ContextReloadStrategy;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spi.SecretRotationAware;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End-to-end test of what a vault component triggers when it detects a 
rotated secret: the component option that was
+ * configured with a placeholder is re-resolved, and {@link 
SecretRotationAware} beans are told to re-authenticate.
+ */
+public class MainContextReloadSecretRotationTest {
+
+    @Test
+    public void testRotatedSecretReachesComponentOption() {
+        MyVaultFunction vault = new MyVaultFunction();
+
+        Properties properties = new Properties();
+        properties.setProperty("camel.main.context-reload-enabled", "true");
+        properties.setProperty("camel.component.dummy.component-value", 
"{{vault:password}}");
+
+        Main main = new Main();
+        MyRotationAware bean = new MyRotationAware();
+        try {
+            main.bind("dummy", new MyDummyComponent(false));
+            main.bind("myRotationAware", bean);
+            main.setOverrideProperties(properties);
+            main.setDefaultPropertyPlaceholderLocation("false");
+            main.addMainListener(new MainListenerSupport() {
+                @Override
+                public void beforeConfigure(BaseMainSupport main) {
+                    
main.getCamelContext().getPropertiesComponent().addPropertiesFunction(vault);
+                }
+            });
+            main.start();
+
+            CamelContext context = main.getCamelContext();
+            MyDummyComponent dummy = (MyDummyComponent) 
context.getComponent("dummy");
+            assertThat(dummy.getComponentValue()).isEqualTo("password-1");
+            assertThat(bean.getCounter()).isZero();
+
+            // the secret is rotated in the vault, and the vault component 
triggers a context reload
+            vault.rotate();
+            ContextReloadStrategy crs = 
context.hasService(ContextReloadStrategy.class);
+            assertThat(crs).isNotNull();
+            crs.onReload("MainContextReloadSecretRotationTest");
+
+            // the component option now holds the rotated secret, and the bean 
was told to re-authenticate
+            MyDummyComponent reloaded = (MyDummyComponent) 
context.getComponent("dummy");
+            assertThat(reloaded.getComponentValue()).isEqualTo("password-2");
+            assertThat(bean.getCounter()).isOne();
+            assertThat(crs.getLastError()).isNull();
+        } finally {
+            main.stop();
+        }
+    }
+
+    private static class MyVaultFunction implements PropertiesFunction {
+
+        private int counter = 1;
+
+        void rotate() {
+            counter++;
+        }
+
+        @Override
+        public String getName() {
+            return "vault";
+        }
+
+        @Override
+        public String apply(String remainder) {
+            return remainder + "-" + counter;
+        }
+    }
+
+    private static class MyRotationAware implements SecretRotationAware {
+
+        private final AtomicInteger counter = new AtomicInteger();
+
+        @Override
+        public void onSecretRotation(Object source) {
+            counter.incrementAndGet();
+        }
+
+        int getCounter() {
+            return counter.get();
+        }
+    }
+}
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
index f4590e8df6fb..58ee0f65bfe8 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultContextReloadStrategy.java
@@ -16,13 +16,20 @@
  */
 package org.apache.camel.support;
 
+import java.util.LinkedHashSet;
+import java.util.Properties;
+import java.util.Set;
+
 import org.apache.camel.CamelContext;
+import org.apache.camel.Component;
 import org.apache.camel.api.management.ManagedAttribute;
 import org.apache.camel.api.management.ManagedOperation;
 import org.apache.camel.api.management.ManagedResource;
 import org.apache.camel.spi.ContextReloadStrategy;
 import org.apache.camel.spi.PropertiesComponent;
+import org.apache.camel.spi.PropertiesReload;
 import org.apache.camel.spi.PropertiesSource;
+import org.apache.camel.spi.SecretRotationAware;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.support.service.ServiceSupport;
 import org.slf4j.Logger;
@@ -63,6 +70,11 @@ public class DefaultContextReloadStrategy extends 
ServiceSupport implements Cont
             lastError = null;
             EventHelper.notifyContextReloading(getCamelContext(), source);
             reloadProperties(source);
+            // the order matters: the components must hold the newly resolved 
secrets before they are asked
+            // to re-authenticate, and both must happen before the routes come 
back up, so that the new
+            // consumers and producers are created against an already 
re-authenticated resource
+            reloadComponentProperties(source);
+            notifySecretRotation(source);
             reloadRoutes(source);
             incSucceededCounter();
             EventHelper.notifyContextReloaded(getCamelContext(), source);
@@ -87,6 +99,71 @@ public class DefaultContextReloadStrategy extends 
ServiceSupport implements Cont
         }
     }
 
+    /**
+     * Re-applies the configuration properties whose value is a property 
placeholder, so that components are
+     * re-configured with what those placeholders resolve to now.
+     * <p/>
+     * A component option such as 
<tt>camel.component.kafka.saslJaasConfig</tt> has its placeholder resolved 
once, when
+     * the component is configured, and the resolved value is what is stored 
on the component. Reloading the routes
+     * rebuilds the endpoints from that same already-resolved value, so 
without this step a rotated secret would never
+     * reach the component. Only <tt>camel.</tt> options whose value is a 
placeholder are handed to the listener, as
+     * they are the only ones whose resolved value can change while the raw 
configuration stays the same.
+     */
+    protected void reloadComponentProperties(Object source) throws Exception {
+        PropertiesReload pr = 
getCamelContext().hasService(PropertiesReload.class);
+        if (pr == null) {
+            // component re-configuration is only supported when running with 
Camel Main
+            return;
+        }
+
+        PropertiesComponent pc = getCamelContext().getPropertiesComponent();
+        Properties prop = pc.loadProperties();
+        // filter on camel. rather than on the individual option prefixes: 
PropertiesReload is a generic SPI and
+        // each implementation decides which options it acts on. 
MainPropertiesReload, for example, re-applies only
+        // camel.component., camel.dataformat. and camel.language., and 
silently ignores everything else
+        // stringPropertyNames is a live view of the keys, so snapshot before 
removing
+        Set<String> keys = new LinkedHashSet<>(prop.stringPropertyNames());
+        for (String key : keys) {
+            Object value = prop.get(key);
+            boolean placeholder = key.startsWith("camel.")
+                    && value instanceof String str && 
str.contains(PropertiesComponent.PREFIX_TOKEN);
+            if (!placeholder) {
+                prop.remove(key);
+            }
+        }
+        if (!prop.isEmpty()) {
+            LOG.debug("Re-applying {} property placeholder based options to 
components", prop.size());
+            pr.onReload(source != null ? source.toString() : "ContextReload", 
prop);
+        }
+    }
+
+    /**
+     * Notifies every {@link SecretRotationAware} component and registry bean 
that the secrets they captured may have
+     * been rotated, so they can re-authenticate before the routes are 
restarted.
+     * <p/>
+     * A listener that throws is logged and skipped, so that one component 
cannot prevent the others from being
+     * refreshed, nor fail the reload as a whole.
+     */
+    protected void notifySecretRotation(Object source) {
+        Set<SecretRotationAware> targets = new LinkedHashSet<>();
+        for (String name : getCamelContext().getComponentNames()) {
+            Component component = getCamelContext().hasComponent(name);
+            if (component instanceof SecretRotationAware sra) {
+                targets.add(sra);
+            }
+        }
+        
targets.addAll(getCamelContext().getRegistry().findByType(SecretRotationAware.class));
+
+        for (SecretRotationAware target : targets) {
+            try {
+                target.onSecretRotation(source);
+            } catch (Exception e) {
+                LOG.warn("Error re-authenticating {} after secret rotation due 
to: {}. This exception is ignored.",
+                        target, e.getMessage(), e);
+            }
+        }
+    }
+
     @ManagedAttribute(description = "Number of reloads succeeded")
     public int getReloadCounter() {
         return succeeded;
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 09de0d03b17f..3f3927dac2d6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -13,6 +13,25 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] 
page for details.
 
 == Upgrading Camel 4.22 to 4.23
 
+=== Context reload now re-applies placeholder based component options
+
+When a context reload is triggered, for example by one of the vault components 
detecting that a secret was rotated,
+Camel now also re-applies the `camel.component.`, `camel.dataformat.` and 
`camel.language.` options whose configured
+value is a property placeholder, and notifies any bean implementing the new
+`org.apache.camel.spi.SecretRotationAware` SPI. Previously only the property 
placeholders and the routes were
+reloaded, so a rotated secret never reached a component option that had been 
resolved at bootstrap.
+
+Re-applying such an option removes the component and resolves it again, as it 
already does when the option changes
+in a watched properties file. The component is stopped either way, and what 
comes back depends on how it was
+registered: a component auto-detected from the classpath is replaced by a new 
instance, while a component registered
+as a bean is the same instance, re-configured and started again. A component 
that holds a connection will therefore
+close it on reload.
+
+Only options whose value is a placeholder are affected, and only when running 
with Camel Main, Camel Spring Boot or
+Camel Quarkus. If a component must not be stopped on reload, configure it 
programmatically rather than with a
+placeholder based property.
+
+
 === Apache Avro trusted packages
 
 Camel now uses Apache Avro 1.12.2. Avro validates classes resolved from schemas
diff --git a/docs/user-manual/modules/ROOT/pages/context-reload.adoc 
b/docs/user-manual/modules/ROOT/pages/context-reload.adoc
index 67475a748959..6a48f16f5793 100644
--- a/docs/user-manual/modules/ROOT/pages/context-reload.adoc
+++ b/docs/user-manual/modules/ROOT/pages/context-reload.adoc
@@ -6,12 +6,27 @@ upon an external triggered event.
 For example, if you are using 
xref:components::aws-secrets-manager-component.adoc[AWS Secrets], then
 enabling context-reload would then reload Camel routes upon a secret is 
updated in AWS.
 
-The context reload is limited to refresh the following on reload:
+The context reload refreshes the following on reload:
 
 - xref:using-propertyplaceholder.adoc[property placeholders]
+- component options whose configured value is a property placeholder (requires 
Camel Main, Camel Spring Boot or
+  Camel Quarkus)
+- all beans implementing `SecretRotationAware`, so they can re-authenticate in 
place (see below)
 - all existing xref:routes.adoc[routes] (no changes to structure of routes; 
see xref:route-reload.adoc[]])
 
-General services in xref:camelcontext.adoc[CamelContext] and java beans or 
Camel xref:processor.adoc[] is not updated.
+Other general services in xref:camelcontext.adoc[CamelContext] and java beans 
or Camel xref:processor.adoc[] are not
+updated.
+
+Re-applying the component options is what allows a rotated secret to reach a 
component, as an option such as the
+following has its placeholder resolved once, when the component is configured:
+
+[source,properties]
+----
+camel.component.kafka.saslJaasConfig = {{aws:broker-credentials}}
+----
+
+Only options whose value is a placeholder are re-applied, as they are the only 
ones whose resolved value can change
+while the configuration itself stays the same.
 
 == Using context reloading
 
@@ -47,9 +62,38 @@ if (reload != null) {
 }
 ----
 
-The method `onReload` will then reload all the 
xref:using-propertyplaceholder.adoc[property placeholders] and
-then afterward reload all existing xref:routes.adoc[routes].
+The method `onReload` will then reload all the 
xref:using-propertyplaceholder.adoc[property placeholders],
+re-apply the component options that were configured with a placeholder, notify 
all `SecretRotationAware` beans,
+and then afterward reload all existing xref:routes.adoc[routes].
+
+== Re-authenticating on rotated secrets
+
+Reloading the routes rebuilds the routes and their endpoints, but it does not 
rebuild the components themselves, nor
+any bean in the xref:registry.adoc[Registry]. A component that holds a live 
authenticated resource, such as a pooled
+JMS connection factory, a JDBC connection pool or a shared HTTP client, 
therefore keeps using the credentials it
+captured when it was created, even though the secret has been rotated.
+
+Such a component or bean can implement 
`org.apache.camel.spi.SecretRotationAware` to be told when this happens:
+
+[source,java]
+----
+public class MyComponent extends DefaultComponent implements 
SecretRotationAware {
+
+    @Override
+    public void onSecretRotation(Object source) throws Exception {
+        // re-read the configured credentials and re-authenticate the 
connection
+        // that was created with the secret that has just been rotated
+    }
+}
+----
+
+The callback is invoked after the property placeholders have been reloaded and 
the component options re-applied, but
+before the routes are restarted, so that by the time the routes come back up 
the underlying resource is already
+authenticated with the new secret.
 
+The callback is advisory: it says that a reload was triggered, not which 
secrets changed. Implementations should be
+quick, as the callback runs inline on the reload. A callback that throws is 
logged and ignored, so that one component
+cannot prevent the others from being refreshed, nor fail the reload as a whole.
 
 == See Also
 

Reply via email to