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