You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by ac...@apache.org on 2026/09/08 08:11:57 UTC
(camel) branch main updated: CAMEL-24636: camel-core - re-authenticate components on context reload after a secret rotation (#26166)
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 <an...@gmail.com>
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) <no...@anthropic.com>
---
.../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