-
Notifications
You must be signed in to change notification settings - Fork 5.2k
CAMEL-24638: camel-jdbc/camel-sql implement SecretRotationAware to evict stale pool connections on secret rotation #26845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3adfa98
c1aa008
7be4657
c30a53b
2e8599a
bc71bd1
bd4bc27
35c3d96
cc0bb0f
b796416
d711425
79ad28d
ae4aa49
2b58bfc
beb6afe
a54a9dc
673fa2b
7ed1cba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| /* | ||
| * Licensed to the Apache Software Foundation (ASF) under one or more | ||
| * contributor license agreements. See the NOTICE file distributed with | ||
| * this work for additional information regarding copyright ownership. | ||
| * The ASF licenses this file to You under the Apache License, Version 2.0 | ||
| * (the "License"); you may not use this file except in compliance with | ||
| * the License. You may obtain a copy of the License at | ||
| * | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| */ | ||
| package org.apache.camel.component.jdbc; | ||
|
|
||
| import java.io.PrintWriter; | ||
| import java.sql.Connection; | ||
| import java.sql.SQLException; | ||
| import java.sql.SQLFeatureNotSupportedException; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.logging.Logger; | ||
|
|
||
| import javax.sql.DataSource; | ||
|
|
||
| import org.apache.camel.impl.DefaultCamelContext; | ||
| import org.apache.camel.spi.SecretRotationAware; | ||
| import org.junit.jupiter.api.Test; | ||
|
|
||
| import static org.junit.jupiter.api.Assertions.assertInstanceOf; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
|
||
| /** | ||
| * Verifies that {@link JdbcComponent} implements {@link SecretRotationAware} and correctly evicts stale connections on | ||
| * rotation. | ||
| */ | ||
| class JdbcComponentSecretRotationAwareTest { | ||
|
|
||
| @Test | ||
| void implementsSecretRotationAware() { | ||
| assertInstanceOf(SecretRotationAware.class, new JdbcComponent()); | ||
| } | ||
|
|
||
| @Test | ||
| void onSecretRotation_withComponentOwnedDataSource_evictsConnections() throws Exception { | ||
| // Arrange: DataSource injected directly on the component | ||
| HikariLikeDataSource hikariLike = new HikariLikeDataSource(); | ||
|
|
||
| JdbcComponent component = new JdbcComponent(); | ||
| component.setDataSource(hikariLike); | ||
| DefaultCamelContext ctx = new DefaultCamelContext(); | ||
| component.setCamelContext(ctx); | ||
|
|
||
| // Act | ||
| component.onSecretRotation("vault-rotation"); | ||
|
|
||
| // Assert | ||
| assertTrue(hikariLike.mxBean.softEvictCalled.get(), | ||
| "Component-owned DataSource should have been soft-evicted"); | ||
| } | ||
|
|
||
| @Test | ||
| void onSecretRotation_withEndpointDataSource_evictsEndpointConnections() throws Exception { | ||
| // Arrange: DataSource only on the endpoint (not on the component — the typical jdbc:myDs case) | ||
| HikariLikeDataSource endpointDs = new HikariLikeDataSource(); | ||
|
|
||
| DefaultCamelContext ctx = new DefaultCamelContext(); | ||
| JdbcComponent component = new JdbcComponent(); | ||
| component.setCamelContext(ctx); | ||
| // component.dataSource stays null — matches the default jdbc:myDs usage | ||
|
|
||
| JdbcEndpoint endpoint = new JdbcEndpoint("jdbc:myDs", component, endpointDs); | ||
| ctx.addEndpoint("jdbc:myDs", endpoint); | ||
|
|
||
| // Act | ||
| component.onSecretRotation("vault-rotation"); | ||
|
|
||
| // Assert | ||
| assertTrue(endpointDs.mxBean.softEvictCalled.get(), | ||
| "Endpoint-owned DataSource should have been soft-evicted"); | ||
| } | ||
|
|
||
| @Test | ||
| void onSecretRotation_withoutDataSource_doesNotThrow() throws Exception { | ||
| // Arrange: no DataSource on component | ||
| JdbcComponent component = new JdbcComponent(); | ||
| DefaultCamelContext ctx = new DefaultCamelContext(); | ||
| component.setCamelContext(ctx); | ||
|
|
||
| // Act — must not throw even with no DataSource configured | ||
| component.onSecretRotation("vault-rotation"); | ||
| } | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // DataSource stubs — public so that reflection in DataSourceHelper | ||
| // can invoke methods without setAccessible(true) | ||
| // --------------------------------------------------------------------------- | ||
|
|
||
| /** Simulates a HikariPoolMXBean with a trackable {@code softEvictConnections()} call. */ | ||
| public static class MockPoolMXBean { | ||
| public final AtomicBoolean softEvictCalled = new AtomicBoolean(false); | ||
|
|
||
| public void softEvictConnections() { | ||
| softEvictCalled.set(true); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Simulates a {@code HikariDataSource} by exposing {@code getHikariPoolMXBean()}, which returns a | ||
| * {@link MockPoolMXBean}. This matches the real HikariCP API where {@code softEvictConnections()} lives on | ||
| * {@code HikariPoolMXBean}, not on {@code HikariDataSource} itself. | ||
| */ | ||
| public static class HikariLikeDataSource implements DataSource { | ||
| public final MockPoolMXBean mxBean = new MockPoolMXBean(); | ||
|
|
||
| public Object getHikariPoolMXBean() { | ||
| return mxBean; | ||
| } | ||
|
|
||
| @Override | ||
| public Connection getConnection() throws SQLException { | ||
| throw new UnsupportedOperationException(); | ||
| } | ||
|
|
||
| @Override | ||
| public Connection getConnection(String username, String password) throws SQLException { | ||
| throw new UnsupportedOperationException(); | ||
| } | ||
|
|
||
| @Override | ||
| public PrintWriter getLogWriter() { | ||
| return null; | ||
| } | ||
|
|
||
| @Override | ||
| public void setLogWriter(PrintWriter out) { | ||
| } | ||
|
|
||
| @Override | ||
| public void setLoginTimeout(int seconds) { | ||
| } | ||
|
|
||
| @Override | ||
| public int getLoginTimeout() { | ||
| return 0; | ||
| } | ||
|
|
||
| @Override | ||
| public Logger getParentLogger() throws SQLFeatureNotSupportedException { | ||
| throw new SQLFeatureNotSupportedException(); | ||
| } | ||
|
|
||
| @Override | ||
| public <T> T unwrap(Class<T> iface) throws SQLException { | ||
| throw new SQLException("Not a wrapper for " + iface); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean isWrapperFor(Class<?> iface) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,9 @@ | |
| import org.apache.camel.CamelContext; | ||
| import org.apache.camel.Endpoint; | ||
| import org.apache.camel.spi.Metadata; | ||
| import org.apache.camel.spi.SecretRotationAware; | ||
| import org.apache.camel.spi.annotations.Component; | ||
| import org.apache.camel.support.DataSourceHelper; | ||
| import org.apache.camel.support.HealthCheckComponent; | ||
| import org.apache.camel.support.PropertyBindingSupport; | ||
| import org.apache.camel.util.ObjectHelper; | ||
|
|
@@ -35,7 +37,7 @@ | |
| * queries. | ||
| */ | ||
| @Component("sql") | ||
| public class SqlComponent extends HealthCheckComponent { | ||
| public class SqlComponent extends HealthCheckComponent implements SecretRotationAware { | ||
|
|
||
| @Metadata(autowired = true) | ||
| private DataSource dataSource; | ||
|
|
@@ -151,6 +153,16 @@ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Obje | |
| return endpoint; | ||
| } | ||
|
|
||
| @Override | ||
| public void onSecretRotation(Object source) throws Exception { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell, nothing updates the pool's credentials before this eviction. Hikari re-reads
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in bd4bc27. Documented the limitation in the Javadoc of
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Docs updated in cc0bb0f — the credential refresh limitation wording now aligns with the IMPORTANT note across all four doc copies (jdbc/sql source + catalog). |
||
| DataSourceHelper.evictComponentDataSources( | ||
| this.dataSource, | ||
| getCamelContext().getEndpoints(), | ||
| this, | ||
| ep -> ep instanceof DefaultSqlEndpoint ? ((DefaultSqlEndpoint) ep).getDataSource() : null, | ||
| source); | ||
| } | ||
|
gnodet marked this conversation as resolved.
|
||
|
|
||
| /** | ||
| * Sets the DataSource to use to communicate with the database. | ||
| */ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.