This is an automated email from the ASF dual-hosted git repository.
Croway pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git
The following commit(s) were added to refs/heads/main by this push:
new 1bebacb4941 CAMEL-24531: extract a shared early-resolution properties
parser (#1917)
1bebacb4941 is described below
commit 1bebacb49412c422785771fe577fbb8dfe037312
Author: Adriano Machado <[email protected]>
AuthorDate: Tue Sep 1 03:10:27 2026 -0400
CAMEL-24531: extract a shared early-resolution properties parser (#1917)
* CAMEL-24531: add guarded lifecycle skeleton for early-resolution parsers
* CAMEL-24531: resolve whole-value placeholders in the shared parser
* CAMEL-24531: aggregate early-resolution failures instead of aborting on
the first
* CAMEL-24531: address review feedback on the shared early-resolution parser
* CAMEL-24531: migrate aws-secrets-manager starter to the shared parser
* CAMEL-24531: migrate azure-key-vault starter to the shared parser
* CAMEL-24531: migrate cyberark-vault starter to the shared parser
* CAMEL-24531: migrate ibm-secrets-manager starter to the shared parser
* CAMEL-24531: migrate spring-cloud-config starter to the shared parser
* CAMEL-24531: migrate google-secret-manager starter and normalize its
IOException handling
* CAMEL-24531: migrate hashicorp-vault starter and normalize its
configuration exceptions
* CAMEL-24531: strengthen contract test coverage for the hashicorp vault
parser
---
...pringBootAwsSecretsManagerPropertiesParser.java | 126 +++------
...gBootAwsSecretsManagerPropertiesParserTest.java | 41 +++
.../SpringBootAzureKeyVaultPropertiesParser.java | 135 +++------
...pringBootAzureKeyVaultPropertiesParserTest.java | 40 +++
.../SpringBootCyberArkVaultPropertiesParser.java | 119 +++-----
...pringBootCyberArkVaultPropertiesParserTest.java | 40 +++
...ingBootGoogleSecretManagerPropertiesParser.java | 98 ++-----
...ootGoogleSecretManagerPropertiesParserTest.java | 64 +++++
.../SpringBootHashicorpVaultPropertiesParser.java | 112 +++-----
...ringBootHashicorpVaultPropertiesParserTest.java | 120 ++++++++
.../IBMSecretsManagerVaultPropertiesParser.java | 98 ++-----
...IBMSecretsManagerVaultPropertiesParserTest.java | 41 +++
.../SpringBootCloudConfigPropertiesParser.java | 81 ++----
.../SpringBootCloudConfigPropertiesParserTest.java | 46 +++
.../AbstractEarlyResolutionPropertiesParser.java | 163 +++++++++++
...bstractEarlyResolutionPropertiesParserTest.java | 312 +++++++++++++++++++++
16 files changed, 1102 insertions(+), 534 deletions(-)
diff --git
a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java
b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java
index 25b02114f79..d41cbb27714 100644
---
a/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java
+++
b/components-starter/camel-aws-secrets-manager-starter/src/main/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParser.java
@@ -18,16 +18,10 @@ package
org.apache.camel.component.aws.secretsmanager.springboot;
import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.aws.secretsmanager.SecretsManagerPropertiesFunction;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
@@ -35,87 +29,49 @@ import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import
software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;
-import java.util.Properties;
-
-public class SpringBootAwsSecretsManagerPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootAwsSecretsManagerPropertiesParser.class);
+public class SpringBootAwsSecretsManagerPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
- SecretsManagerClient client;
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.aws-secrets-manager.early-resolve-properties")))
{
- String accessKey =
environment.getProperty("camel.vault.aws.accessKey");
- String secretKey =
environment.getProperty("camel.vault.aws.secretKey");
- String region = environment.getProperty("camel.vault.aws.region");
- boolean useDefaultCredentialsProvider =
Boolean.parseBoolean(environment.getProperty("camel.vault.aws.defaultCredentialsProvider"));
- boolean useProfileCredentialsProvider =
Boolean.parseBoolean(environment.getProperty("camel.vault.aws.profileCredentialsProvider"));
- String profileName =
environment.getProperty("camel.vault.aws.profileName");
- if (ObjectHelper.isNotEmpty(accessKey) &&
ObjectHelper.isNotEmpty(secretKey) && ObjectHelper.isNotEmpty(region)) {
- SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
- AwsBasicCredentials cred =
AwsBasicCredentials.create(accessKey, secretKey);
- clientBuilder =
clientBuilder.credentialsProvider(StaticCredentialsProvider.create(cred));
- clientBuilder.region(Region.of(region));
- client = clientBuilder.build();
- } else if (useDefaultCredentialsProvider &&
ObjectHelper.isNotEmpty(region)) {
- SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
- clientBuilder.region(Region.of(region));
- client = clientBuilder.build();
- } else if (useProfileCredentialsProvider &&
ObjectHelper.isNotEmpty(profileName)) {
- SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
-
clientBuilder.credentialsProvider(ProfileCredentialsProvider.create(profileName));
- clientBuilder.region(Region.of(region));
- client = clientBuilder.build();
- } else {
- throw new RuntimeCamelException(
- "Using the AWS Secrets Manager Properties Function
requires setting AWS credentials as application properties or environment
variables");
- }
- SecretsManagerPropertiesFunction secretsManagerPropertiesFunction
= new SecretsManagerPropertiesFunction(client);
-
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.aws-secrets-manager.early-resolve-properties";
+ }
- if (stringValue != null &&
- stringValue.startsWith("{{aws:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
secretsManagerPropertiesFunction.apply(stringValue
- .replace("{{aws:", "")
- .replace("}}", ""));
- props.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-aws-secrets-manager-properties";
+ }
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-camel-aws-secrets-manager-properties",
props));
+ @Override
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
+ SecretsManagerClient client;
+ String accessKey =
environment.getProperty("camel.vault.aws.accessKey");
+ String secretKey =
environment.getProperty("camel.vault.aws.secretKey");
+ String region = environment.getProperty("camel.vault.aws.region");
+ boolean useDefaultCredentialsProvider
+ =
Boolean.parseBoolean(environment.getProperty("camel.vault.aws.defaultCredentialsProvider"));
+ boolean useProfileCredentialsProvider
+ =
Boolean.parseBoolean(environment.getProperty("camel.vault.aws.profileCredentialsProvider"));
+ String profileName =
environment.getProperty("camel.vault.aws.profileName");
+ if (ObjectHelper.isNotEmpty(accessKey) &&
ObjectHelper.isNotEmpty(secretKey)
+ && ObjectHelper.isNotEmpty(region)) {
+ SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
+ AwsBasicCredentials cred = AwsBasicCredentials.create(accessKey,
secretKey);
+ clientBuilder =
clientBuilder.credentialsProvider(StaticCredentialsProvider.create(cred));
+ clientBuilder.region(Region.of(region));
+ client = clientBuilder.build();
+ } else if (useDefaultCredentialsProvider &&
ObjectHelper.isNotEmpty(region)) {
+ SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
+ clientBuilder.region(Region.of(region));
+ client = clientBuilder.build();
+ } else if (useProfileCredentialsProvider &&
ObjectHelper.isNotEmpty(profileName)) {
+ SecretsManagerClientBuilder clientBuilder =
SecretsManagerClient.builder();
+
clientBuilder.credentialsProvider(ProfileCredentialsProvider.create(profileName));
+ clientBuilder.region(Region.of(region));
+ client = clientBuilder.build();
+ } else {
+ throw new RuntimeCamelException(
+ "Using the AWS Secrets Manager Properties Function
requires setting AWS credentials as application properties or environment
variables");
}
+ return new SecretsManagerPropertiesFunction(client);
}
}
diff --git
a/components-starter/camel-aws-secrets-manager-starter/src/test/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParserTest.java
b/components-starter/camel-aws-secrets-manager-starter/src/test/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParserTest.java
new file mode 100644
index 00000000000..25571b966f1
--- /dev/null
+++
b/components-starter/camel-aws-secrets-manager-starter/src/test/java/org/apache/camel/component/aws/secretsmanager/springboot/SpringBootAwsSecretsManagerPropertiesParserTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.aws.secretsmanager.springboot;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SpringBootAwsSecretsManagerPropertiesParserTest {
+
+ private final SpringBootAwsSecretsManagerPropertiesParser parser
+ = new SpringBootAwsSecretsManagerPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.aws-secrets-manager.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-aws-secrets-manager-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+}
diff --git
a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java
b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java
index f240c1cd013..6acf34c7691 100644
---
a/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java
+++
b/components-starter/camel-azure-key-vault-starter/src/main/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParser.java
@@ -24,106 +24,63 @@ import com.azure.security.keyvault.secrets.SecretClient;
import com.azure.security.keyvault.secrets.SecretClientBuilder;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.azure.key.vault.KeyVaultPropertiesFunction;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
-import java.util.Properties;
+public class SpringBootAzureKeyVaultPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
-public class SpringBootAzureKeyVaultPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootAzureKeyVaultPropertiesParser.class);
+ @Override
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.azure-key-vault.early-resolve-properties";
+ }
+
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-azure-key-vault-properties";
+ }
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
SecretClient client;
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.azure-key-vault.early-resolve-properties")))
{
- String vaultName =
environment.getProperty("camel.vault.azure.vaultName");
- String clientId =
environment.getProperty("camel.vault.azure.clientId");
- String clientSecret =
environment.getProperty("camel.vault.azure.clientSecret");
- String tenantId =
environment.getProperty("camel.vault.azure.tenantId");
- boolean azureIdentityEnabled =
Boolean.parseBoolean(System.getenv("CAMEL_VAULT_AZURE_IDENTITY_ENABLED"));
- if (ObjectHelper.isNotEmpty(vaultName) &&
ObjectHelper.isNotEmpty(clientId) && ObjectHelper.isNotEmpty(clientSecret)
- && ObjectHelper.isNotEmpty(tenantId) &&
!azureIdentityEnabled) {
- String keyVaultUri = "https://" + vaultName +
".vault.azure.net";
+ String vaultName =
environment.getProperty("camel.vault.azure.vaultName");
+ String clientId =
environment.getProperty("camel.vault.azure.clientId");
+ String clientSecret =
environment.getProperty("camel.vault.azure.clientSecret");
+ String tenantId =
environment.getProperty("camel.vault.azure.tenantId");
+ boolean azureIdentityEnabled =
Boolean.parseBoolean(System.getenv("CAMEL_VAULT_AZURE_IDENTITY_ENABLED"));
+ if (ObjectHelper.isNotEmpty(vaultName) &&
ObjectHelper.isNotEmpty(clientId)
+ && ObjectHelper.isNotEmpty(clientSecret)
+ && ObjectHelper.isNotEmpty(tenantId) && !azureIdentityEnabled)
{
+ String keyVaultUri = "https://" + vaultName + ".vault.azure.net";
- // Credential
- ClientSecretCredential credential = new
ClientSecretCredentialBuilder()
- .tenantId(tenantId)
- .clientId(clientId)
- .clientSecret(clientSecret)
- .build();
+ // Credential
+ ClientSecretCredential credential = new
ClientSecretCredentialBuilder()
+ .tenantId(tenantId)
+ .clientId(clientId)
+ .clientSecret(clientSecret)
+ .build();
- // Build Client
- client = new SecretClientBuilder()
- .vaultUrl(keyVaultUri)
- .credential(credential)
- .buildClient();
- } else if (ObjectHelper.isNotEmpty(vaultName) &&
azureIdentityEnabled) {
- String keyVaultUri = "https://" + vaultName +
".vault.azure.net";
+ // Build Client
+ client = new SecretClientBuilder()
+ .vaultUrl(keyVaultUri)
+ .credential(credential)
+ .buildClient();
+ } else if (ObjectHelper.isNotEmpty(vaultName) && azureIdentityEnabled)
{
+ String keyVaultUri = "https://" + vaultName + ".vault.azure.net";
- // Credential
- TokenCredential credential = new
DefaultAzureCredentialBuilder().build();
+ // Credential
+ TokenCredential credential = new
DefaultAzureCredentialBuilder().build();
- // Build Client
- client = new SecretClientBuilder()
- .vaultUrl(keyVaultUri)
- .credential(credential)
- .buildClient();
- } else {
- throw new RuntimeCamelException(
- "Using the Azure Key Vault Properties Function
requires setting Azure credentials as application properties or environment
variables or enable the Azure Identity Authentication mechanism");
- }
- KeyVaultPropertiesFunction keyVaultPropertiesFunction = new
KeyVaultPropertiesFunction(client);
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
- if (stringValue != null &&
- stringValue.startsWith("{{azure:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
keyVaultPropertiesFunction.apply(stringValue
- .replace("{{azure:", "")
- .replace("}}", ""));
- props.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-camel-azure-key-vault-properties", props));
+ // Build Client
+ client = new SecretClientBuilder()
+ .vaultUrl(keyVaultUri)
+ .credential(credential)
+ .buildClient();
+ } else {
+ throw new RuntimeCamelException(
+ "Using the Azure Key Vault Properties Function requires
setting Azure credentials as application properties or environment variables or
enable the Azure Identity Authentication mechanism");
}
+ return new KeyVaultPropertiesFunction(client);
}
}
diff --git
a/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParserTest.java
b/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParserTest.java
new file mode 100644
index 00000000000..44c4bd63995
--- /dev/null
+++
b/components-starter/camel-azure-key-vault-starter/src/test/java/org/apache/camel/component/azure/key/vault/springboot/SpringBootAzureKeyVaultPropertiesParserTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.azure.key.vault.springboot;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SpringBootAzureKeyVaultPropertiesParserTest {
+
+ private final SpringBootAzureKeyVaultPropertiesParser parser = new
SpringBootAzureKeyVaultPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.azure-key-vault.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-azure-key-vault-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+}
diff --git
a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java
b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java
index 4242da15ea2..4ac3a08b178 100644
---
a/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java
+++
b/components-starter/camel-cyberark-vault-starter/src/main/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParser.java
@@ -20,100 +20,53 @@ import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.cyberark.vault.CyberArkVaultPropertiesFunction;
import org.apache.camel.component.cyberark.vault.client.ConjurClient;
import org.apache.camel.component.cyberark.vault.client.ConjurClientFactory;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
-import java.util.Properties;
+public class SpringBootCyberArkVaultPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
-public class SpringBootCyberArkVaultPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootCyberArkVaultPropertiesParser.class);
+ @Override
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.cyberark-vault.early-resolve-properties";
+ }
+
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-cyberark-vault-properties";
+ }
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
ConjurClient client;
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.cyberark-vault.early-resolve-properties")))
{
- String url = environment.getProperty("camel.vault.cyberark.url");
- String account =
environment.getProperty("camel.vault.cyberark.account");
- String username =
environment.getProperty("camel.vault.cyberark.username");
- String password =
environment.getProperty("camel.vault.cyberark.password");
- String apiKey =
environment.getProperty("camel.vault.cyberark.apiKey");
- String authToken =
environment.getProperty("camel.vault.cyberark.authToken");
+ String url = environment.getProperty("camel.vault.cyberark.url");
+ String account =
environment.getProperty("camel.vault.cyberark.account");
+ String username =
environment.getProperty("camel.vault.cyberark.username");
+ String password =
environment.getProperty("camel.vault.cyberark.password");
+ String apiKey = environment.getProperty("camel.vault.cyberark.apiKey");
+ String authToken =
environment.getProperty("camel.vault.cyberark.authToken");
- if (ObjectHelper.isNotEmpty(url) &&
ObjectHelper.isNotEmpty(account)) {
- // Create Conjur client based on authentication method
- if (ObjectHelper.isNotEmpty(authToken)) {
- // Use pre-authenticated token
- client = ConjurClientFactory.createWithToken(url, account,
authToken);
- } else if (ObjectHelper.isNotEmpty(apiKey) &&
ObjectHelper.isNotEmpty(username)) {
- // Use API key authentication
- client = ConjurClientFactory.createWithApiKey(url,
account, username, apiKey);
- } else if (ObjectHelper.isNotEmpty(username) &&
ObjectHelper.isNotEmpty(password)) {
- // Use username/password authentication
- client = ConjurClientFactory.createWithCredentials(url,
account, username, password);
- } else {
- throw new RuntimeCamelException(
- "Using the CyberArk Conjur Vault Properties
Function requires authentication credentials (authToken, apiKey, or
username/password)");
- }
+ if (ObjectHelper.isNotEmpty(url) && ObjectHelper.isNotEmpty(account)) {
+ // Create Conjur client based on authentication method
+ if (ObjectHelper.isNotEmpty(authToken)) {
+ // Use pre-authenticated token
+ client = ConjurClientFactory.createWithToken(url, account,
authToken);
+ } else if (ObjectHelper.isNotEmpty(apiKey) &&
ObjectHelper.isNotEmpty(username)) {
+ // Use API key authentication
+ client = ConjurClientFactory.createWithApiKey(url, account,
username, apiKey);
+ } else if (ObjectHelper.isNotEmpty(username) &&
ObjectHelper.isNotEmpty(password)) {
+ // Use username/password authentication
+ client = ConjurClientFactory.createWithCredentials(url,
account, username, password);
} else {
throw new RuntimeCamelException(
- "Using the CyberArk Conjur Vault Properties Function
requires setting URL and account as application properties or environment
variables");
+ "Using the CyberArk Conjur Vault Properties Function
requires authentication credentials (authToken, apiKey, or username/password)");
}
-
- CyberArkVaultPropertiesFunction cyberArkVaultPropertiesFunction =
new CyberArkVaultPropertiesFunction(client);
-
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
-
- if (stringValue != null &&
- stringValue.startsWith("{{cyberark:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
cyberArkVaultPropertiesFunction.apply(stringValue
- .replace("{{cyberark:", "")
- .replace("}}", ""));
- props.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
-
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-camel-cyberark-vault-properties", props));
+ } else {
+ throw new RuntimeCamelException(
+ "Using the CyberArk Conjur Vault Properties Function
requires setting URL and account as application properties or environment
variables");
}
+
+ return new CyberArkVaultPropertiesFunction(client);
}
}
diff --git
a/components-starter/camel-cyberark-vault-starter/src/test/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParserTest.java
b/components-starter/camel-cyberark-vault-starter/src/test/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParserTest.java
new file mode 100644
index 00000000000..620a8671b1c
--- /dev/null
+++
b/components-starter/camel-cyberark-vault-starter/src/test/java/org/apache/camel/component/cyberark/vault/springboot/SpringBootCyberArkVaultPropertiesParserTest.java
@@ -0,0 +1,40 @@
+/*
+ * 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.cyberark.vault.springboot;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SpringBootCyberArkVaultPropertiesParserTest {
+
+ private final SpringBootCyberArkVaultPropertiesParser parser = new
SpringBootCyberArkVaultPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+ assertEquals("camel.component.cyberark-vault.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-cyberark-vault-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+}
diff --git
a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java
b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java
index 696c9c4d0de..45d56030228 100644
---
a/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java
+++
b/components-starter/camel-google-secret-manager-starter/src/main/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParser.java
@@ -20,86 +20,42 @@ import
com.google.cloud.secretmanager.v1.SecretManagerServiceClient;
import com.google.cloud.secretmanager.v1.SecretManagerServiceSettings;
import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.google.secret.manager.GoogleSecretManagerPropertiesFunction;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
import java.io.IOException;
-import java.util.Properties;
-public class SpringBootGoogleSecretManagerPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootGoogleSecretManagerPropertiesParser.class);
+public class SpringBootGoogleSecretManagerPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+ protected String getEarlyResolutionProperty() {
+ return
"camel.component.google-secret-manager.early-resolve-properties";
+ }
+
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-google-secret-manager-properties";
+ }
+
+ @Override
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
SecretManagerServiceClient client;
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- String projectId;
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.google-secret-manager.early-resolve-properties")))
{
- projectId = environment.getProperty("camel.vault.gcp.projectId");
- boolean useDefaultInstance =
Boolean.parseBoolean(environment.getProperty("camel.vault.gcp.useDefaultInstance"));
- if (useDefaultInstance && ObjectHelper.isNotEmpty(projectId)) {
- SecretManagerServiceSettings settings = null;
- try {
- settings =
SecretManagerServiceSettings.newBuilder().build();
- client = SecretManagerServiceClient.create(settings);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- } else {
- throw new RuntimeCamelException(
- "Using the GCP Secret Manager Properties Function in
Spring Boot early resolver mode requires setting GCP project Id as application
properties and use default instance option to true");
- }
- GoogleSecretManagerPropertiesFunction
secretsManagerPropertiesFunction = new
GoogleSecretManagerPropertiesFunction(client, projectId);
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
- if (stringValue != null &&
- stringValue.startsWith("{{gcp:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
secretsManagerPropertiesFunction.apply(stringValue
- .replace("{{gcp:", "")
- .replace("}}", ""));
- props.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
+ String projectId =
environment.getProperty("camel.vault.gcp.projectId");
+ boolean useDefaultInstance
+ =
Boolean.parseBoolean(environment.getProperty("camel.vault.gcp.useDefaultInstance"));
+ if (useDefaultInstance && ObjectHelper.isNotEmpty(projectId)) {
+ try {
+ SecretManagerServiceSettings settings =
SecretManagerServiceSettings.newBuilder().build();
+ client = SecretManagerServiceClient.create(settings);
+ } catch (IOException e) {
+ throw new RuntimeCamelException(e);
}
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-camel-google-secret-manager-properties",
props));
+ } else {
+ throw new RuntimeCamelException(
+ "Using the GCP Secret Manager Properties Function in
Spring Boot early resolver mode requires setting GCP project Id as application
properties and use default instance option to true");
}
+ return new GoogleSecretManagerPropertiesFunction(client, projectId);
}
}
diff --git
a/components-starter/camel-google-secret-manager-starter/src/test/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParserTest.java
b/components-starter/camel-google-secret-manager-starter/src/test/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParserTest.java
new file mode 100644
index 00000000000..a4cd10a5961
--- /dev/null
+++
b/components-starter/camel-google-secret-manager-starter/src/test/java/org/apache/camel/component/google/secret/manager/springboot/SpringBootGoogleSecretManagerPropertiesParserTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.google.secret.manager.springboot;
+
+import org.apache.camel.RuntimeCamelException;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.env.StandardEnvironment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SpringBootGoogleSecretManagerPropertiesParserTest {
+
+ private final SpringBootGoogleSecretManagerPropertiesParser parser
+ = new SpringBootGoogleSecretManagerPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.google-secret-manager.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-google-secret-manager-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+
+ /**
+ * The integration test in this module sets {@code camel.vault.gcp.*}
through {@code System.setProperty},
+ * and Surefire reuses the JVM across test classes. Dropping the system
property sources keeps this test
+ * independent of execution order.
+ */
+ private static StandardEnvironment isolatedEnvironment() {
+ StandardEnvironment environment = new StandardEnvironment();
+
environment.getPropertySources().remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
+
environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
+ return environment;
+ }
+
+ @Test
+ public void missingConfigurationFailsAsACamelException() {
+ assertThrows(RuntimeCamelException.class,
+ () -> parser.createPropertiesFunction(isolatedEnvironment()),
+ "configuration problems must surface as RuntimeCamelException
so operators and callers can "
+ + "treat them as one category");
+ }
+}
diff --git
a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java
b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java
index eb930f40d1f..1a90708674d 100644
---
a/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java
+++
b/components-starter/camel-hashicorp-vault-starter/src/main/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParser.java
@@ -18,93 +18,57 @@ package
org.apache.camel.component.hashicorp.vault.springboot;
import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.hashicorp.vault.HashicorpVaultPropertiesFunction;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
+import org.apache.camel.util.ObjectHelper;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
import org.springframework.vault.authentication.TokenAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.core.VaultTemplate;
-import java.util.Objects;
-import java.util.Properties;
-
-public class SpringBootHashicorpVaultPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootHashicorpVaultPropertiesParser.class);
+public class SpringBootHashicorpVaultPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.hashicorp-vault.early-resolve-properties")))
{
-
Objects.requireNonNull(environment.getProperty("camel.vault.hashicorp.token"),
"Hashicorp Vault token is required");
-
Objects.requireNonNull(environment.getProperty("camel.vault.hashicorp.host"),
"Hashicorp Vault host is required");
-
Objects.requireNonNull(environment.getProperty("camel.vault.hashicorp.port"),
"Hashicorp Vault port is required");
-
Objects.requireNonNull(environment.getProperty("camel.vault.hashicorp.scheme"),
"Hashicorp Vault scheme is required");
-
- String token =
environment.getProperty("camel.vault.hashicorp.token");
- String host =
environment.getProperty("camel.vault.hashicorp.host");
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.hashicorp-vault.early-resolve-properties";
+ }
- int port =
Integer.parseInt(environment.getProperty("camel.vault.hashicorp.port"));
- String scheme =
environment.getProperty("camel.vault.hashicorp.scheme");
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-hashicorp-vault-properties";
+ }
- VaultEndpoint vaultEndpoint = new VaultEndpoint();
- vaultEndpoint.setHost(host);
- vaultEndpoint.setPort(port);
- vaultEndpoint.setScheme(scheme);
+ @Override
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
+ String token = required(environment, "camel.vault.hashicorp.token",
"Hashicorp Vault token is required");
+ String host = required(environment, "camel.vault.hashicorp.host",
"Hashicorp Vault host is required");
+ String portValue = required(environment, "camel.vault.hashicorp.port",
"Hashicorp Vault port is required");
+ String scheme = required(environment, "camel.vault.hashicorp.scheme",
"Hashicorp Vault scheme is required");
- VaultTemplate client = new VaultTemplate(
- vaultEndpoint,
- new TokenAuthentication(token));
- HashicorpVaultPropertiesFunction hashicorpVaultPropertiesFunction
= new HashicorpVaultPropertiesFunction(client);
+ int port;
+ try {
+ port = Integer.parseInt(portValue);
+ } catch (NumberFormatException e) {
+ throw new RuntimeCamelException(
+ "camel.vault.hashicorp.port must be a number but was: " +
portValue, e);
+ }
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
+ VaultEndpoint vaultEndpoint = new VaultEndpoint();
+ vaultEndpoint.setHost(host);
+ vaultEndpoint.setPort(port);
+ vaultEndpoint.setScheme(scheme);
- if (stringValue != null &&
- stringValue.startsWith("{{hashicorp:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- props.put(key,
hashicorpVaultPropertiesFunction.apply(stringValue
- .replace("{{hashicorp:", "")
- .replace("}}", "")));
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
+ VaultTemplate client = new VaultTemplate(
+ vaultEndpoint,
+ new TokenAuthentication(token));
+ return new HashicorpVaultPropertiesFunction(client);
+ }
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-camel-hashicorp-vault-properties", props));
+ private static String required(ConfigurableEnvironment environment, String
key, String message) {
+ String value = environment.getProperty(key);
+ if (ObjectHelper.isEmpty(value)) {
+ throw new RuntimeCamelException(message + " (set " + key + ")");
}
+ return value;
}
}
diff --git
a/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserTest.java
b/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserTest.java
new file mode 100644
index 00000000000..c6c79905ad6
--- /dev/null
+++
b/components-starter/camel-hashicorp-vault-starter/src/test/java/org/apache/camel/component/hashicorp/vault/springboot/SpringBootHashicorpVaultPropertiesParserTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.hashicorp.vault.springboot;
+
+import org.apache.camel.RuntimeCamelException;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class SpringBootHashicorpVaultPropertiesParserTest {
+
+ private final SpringBootHashicorpVaultPropertiesParser parser = new
SpringBootHashicorpVaultPropertiesParser();
+
+ /**
+ * The integration tests in this module set {@code
camel.vault.hashicorp.*} through
+ * {@code System.setProperty}, and Surefire reuses the JVM across test
classes. Dropping the system
+ * property sources keeps these tests independent of execution order.
+ */
+ private static StandardEnvironment environmentWith(Map<String, ?>
properties) {
+ StandardEnvironment environment = new StandardEnvironment();
+
environment.getPropertySources().remove(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
+
environment.getPropertySources().remove(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME);
+ environment.getPropertySources()
+ .addFirst(new MapPropertySource("test-properties", new
LinkedHashMap<>(properties)));
+ return environment;
+ }
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.hashicorp-vault.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-hashicorp-vault-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+
+ @Test
+ public void missingTokenFailsAsACamelExceptionNamingTheProperty() {
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () ->
parser.createPropertiesFunction(environmentWith(Map.of())),
+ "a missing setting is an operator error, not a programming
defect, so it must not surface "
+ + "as NullPointerException");
+
+ assertTrue(thrown.getMessage().contains("token"),
+ "the failure must name the missing setting, was: " +
thrown.getMessage());
+ }
+
+ @Test
+ public void missingHostFailsAsACamelExceptionNamingTheProperty() {
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () -> parser.createPropertiesFunction(environmentWith(Map.of(
+ "camel.vault.hashicorp.token", "a-token"))),
+ "a missing setting is an operator error, not a programming
defect, so it must not surface "
+ + "as NullPointerException");
+
+ assertTrue(thrown.getMessage().contains("host"),
+ "the failure must name the missing setting, was: " +
thrown.getMessage());
+ assertTrue(thrown.getMessage().contains("camel.vault.hashicorp.host"),
+ "the failure must name the missing property, was: " +
thrown.getMessage());
+ }
+
+ @Test
+ public void missingSchemeFailsAsACamelExceptionNamingTheProperty() {
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () -> parser.createPropertiesFunction(environmentWith(Map.of(
+ "camel.vault.hashicorp.token", "a-token",
+ "camel.vault.hashicorp.host", "127.0.0.1",
+ "camel.vault.hashicorp.port", "8200"))),
+ "a missing setting is an operator error, not a programming
defect, so it must not surface "
+ + "as NullPointerException");
+
+ assertTrue(thrown.getMessage().contains("scheme"),
+ "the failure must name the missing setting, was: " +
thrown.getMessage());
+
assertTrue(thrown.getMessage().contains("camel.vault.hashicorp.scheme"),
+ "the failure must name the missing property, was: " +
thrown.getMessage());
+ }
+
+ @Test
+ public void nonNumericPortFailsAsACamelExceptionNamingTheProperty() {
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () -> parser.createPropertiesFunction(environmentWith(Map.of(
+ "camel.vault.hashicorp.token", "a-token",
+ "camel.vault.hashicorp.host", "127.0.0.1",
+ "camel.vault.hashicorp.port", "not-a-number",
+ "camel.vault.hashicorp.scheme", "http"))),
+ "a bare NumberFormatException never says which property was
malformed");
+
+ assertTrue(thrown.getMessage().contains("camel.vault.hashicorp.port"),
+ "the failure must name the malformed property, was: " +
thrown.getMessage());
+ assertInstanceOf(NumberFormatException.class, thrown.getCause(),
+ "the cause must be preserved so the original
NumberFormatException is not lost");
+ }
+}
diff --git
a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java
b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java
index 5bca3dc9ef8..1bfdbcc75d2 100644
---
a/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java
+++
b/components-starter/camel-ibm-secrets-manager-starter/src/main/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParser.java
@@ -20,84 +20,38 @@ import com.ibm.cloud.sdk.core.security.IamAuthenticator;
import com.ibm.cloud.secrets_manager_sdk.secrets_manager.v2.SecretsManager;
import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.ibm.secrets.manager.IBMSecretsManagerPropertiesFunction;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.apache.camel.util.ObjectHelper;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
-import java.util.Properties;
+public class IBMSecretsManagerVaultPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
-public class IBMSecretsManagerVaultPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(IBMSecretsManagerVaultPropertiesParser.class);
+ @Override
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.ibm-secrets-manager.early-resolve-properties";
+ }
+
+ @Override
+ protected String getOverridePropertySourceName() {
+ return "overridden-ibm-secrets-manager-properties";
+ }
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
SecretsManager client;
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
- String token;
- String serviceUrl;
- if
(Boolean.parseBoolean(environment.getProperty("camel.component.ibm-secrets-manager.early-resolve-properties")))
{
- token = environment.getProperty("camel.vault.ibm.token");
- serviceUrl = environment.getProperty("camel.vault.ibm.serviceUrl");
- if (ObjectHelper.isNotEmpty(token) &&
ObjectHelper.isNotEmpty(serviceUrl)) {
- IamAuthenticator iamAuthenticator = new
IamAuthenticator.Builder()
- .apikey(token)
- .build();
- client = new SecretsManager("Camel Secrets Manager Service for
Properties", iamAuthenticator);
- client.setServiceUrl(serviceUrl);
- } else {
- throw new RuntimeCamelException(
- "Using the IBM Secrets Manager Properties Function
requires setting IBM Credentials and service url as application properties or
environment variables");
- }
- IBMSecretsManagerPropertiesFunction
secretsManagerPropertiesFunction = new
IBMSecretsManagerPropertiesFunction(client);
- final Properties props = new Properties();
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue &&
- originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
- if (stringValue != null &&
- stringValue.startsWith("{{ibm:") &&
- stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
secretsManagerPropertiesFunction.apply(stringValue
- .replace("{{ibm:", "")
- .replace("}}", ""));
- props.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from the vault; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from the vault. Startup is aborted so "
- + "that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
- environment.getPropertySources().addFirst(new
PropertiesPropertySource("overridden-ibm-secrets-manager-properties", props));
+ String token = environment.getProperty("camel.vault.ibm.token");
+ String serviceUrl =
environment.getProperty("camel.vault.ibm.serviceUrl");
+ if (ObjectHelper.isNotEmpty(token) &&
ObjectHelper.isNotEmpty(serviceUrl)) {
+ IamAuthenticator iamAuthenticator = new IamAuthenticator.Builder()
+ .apikey(token)
+ .build();
+ client = new SecretsManager("Camel Secrets Manager Service for
Properties", iamAuthenticator);
+ client.setServiceUrl(serviceUrl);
+ } else {
+ throw new RuntimeCamelException(
+ "Using the IBM Secrets Manager Properties Function
requires setting IBM Credentials and service url as application properties or
environment variables");
}
+ return new IBMSecretsManagerPropertiesFunction(client);
}
-}
\ No newline at end of file
+}
diff --git
a/components-starter/camel-ibm-secrets-manager-starter/src/test/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParserTest.java
b/components-starter/camel-ibm-secrets-manager-starter/src/test/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParserTest.java
new file mode 100644
index 00000000000..be46bb86a75
--- /dev/null
+++
b/components-starter/camel-ibm-secrets-manager-starter/src/test/java/org/apache/camel/component/ibm/secrets/manager/springboot/IBMSecretsManagerVaultPropertiesParserTest.java
@@ -0,0 +1,41 @@
+/*
+ * 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.ibm.secrets.manager.springboot;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class IBMSecretsManagerVaultPropertiesParserTest {
+
+ private final IBMSecretsManagerVaultPropertiesParser parser = new
IBMSecretsManagerVaultPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.ibm-secrets-manager.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameKeepsItsHistoricalSpelling() {
+ assertEquals("overridden-ibm-secrets-manager-properties",
+ parser.getOverridePropertySourceName(),
+ "this name lacks the camel- segment the other starters use; it
is observable through "
+ + "/actuator/env so it must not be normalised");
+ }
+}
diff --git
a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java
b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java
index d2c951b9023..b157d4749ab 100644
---
a/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java
+++
b/components-starter/camel-spring-cloud-config-starter/src/main/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParser.java
@@ -16,72 +16,33 @@
*/
package org.apache.camel.component.spring.cloud.config.springboot;
-import org.apache.camel.RuntimeCamelException;
import
org.apache.camel.component.spring.cloud.config.SpringCloudConfigPropertiesFunction;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
-import org.springframework.boot.origin.OriginTrackedValue;
-import org.springframework.context.ApplicationListener;
+import org.apache.camel.spi.PropertiesFunction;
+import org.apache.camel.spring.boot.AbstractEarlyResolutionPropertiesParser;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.core.env.MapPropertySource;
-import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertySource;
-import java.util.Properties;
+public class SpringBootCloudConfigPropertiesParser extends
AbstractEarlyResolutionPropertiesParser {
-public class SpringBootCloudConfigPropertiesParser implements
ApplicationListener<ApplicationEnvironmentPreparedEvent> {
- private static final Logger LOG =
LoggerFactory.getLogger(SpringBootCloudConfigPropertiesParser.class);
+ @Override
+ protected String getEarlyResolutionProperty() {
+ return "camel.component.spring-cloud-config.early-resolve-properties";
+ }
@Override
- public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
- Properties properties = new Properties();
- ConfigurableEnvironment environment = event.getEnvironment();
- // an unresolved placeholder would otherwise stay in the property
value and become the effective
- // configuration value, so resolution failures abort startup unless
the operator opts back in
- final boolean ignoreResolutionFailures
- =
Boolean.parseBoolean(environment.getProperty("camel.vault.ignore-resolution-failures"));
+ protected String getOverridePropertySourceName() {
+ return "overridden-camel-spring-config-properties";
+ }
- if (Boolean.parseBoolean(
-
environment.getProperty("camel.component.spring-cloud-config.early-resolve-properties")))
{
- SpringCloudConfigPropertiesFunction
springCloudConfigPropertiesFunction = new SpringCloudConfigPropertiesFunction();
- springCloudConfigPropertiesFunction.setEnvironment(environment);
- for (PropertySource mutablePropertySources :
event.getEnvironment().getPropertySources()) {
- if (mutablePropertySources instanceof MapPropertySource
mapPropertySource) {
- mapPropertySource.getSource().forEach((key, value) -> {
- String stringValue = null;
- if ((value instanceof OriginTrackedValue
originTrackedValue
- && originTrackedValue.getValue() instanceof
String v)) {
- stringValue = v;
- } else if (value instanceof String v) {
- stringValue = v;
- }
- if (stringValue != null &&
stringValue.startsWith("{{spring-config:")
- && stringValue.endsWith("}}")) {
- LOG.debug("decrypting and overriding property {}",
key);
- try {
- String element =
springCloudConfigPropertiesFunction
-
.apply(stringValue.replace("{{spring-config:", "").replace("}}", ""));
- properties.put(key, element);
- } catch (Exception e) {
- if (ignoreResolutionFailures) {
- LOG.warn("Failed to resolve property {}
from Spring Cloud Config; the placeholder is left "
- + "unresolved because
camel.vault.ignore-resolution-failures is enabled", key, e);
- } else {
- throw new RuntimeCamelException(
- "Failed to resolve property " +
key + " from Spring Cloud Config. Startup is aborted "
- + "so that the
unresolved placeholder cannot become the effective "
- + "value; set
camel.vault.ignore-resolution-failures=true to "
- + "continue
anyway.",
- e);
- }
- }
- }
- });
- }
- }
- environment.getPropertySources()
- .addFirst(new
PropertiesPropertySource("overridden-camel-spring-config-properties",
properties));
- }
+ @Override
+ protected String getSourceDescription() {
+ return "Spring Cloud Config";
+ }
+
+ @Override
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
+ SpringCloudConfigPropertiesFunction springCloudConfigPropertiesFunction
+ = new SpringCloudConfigPropertiesFunction();
+ springCloudConfigPropertiesFunction.setEnvironment(environment);
+ return springCloudConfigPropertiesFunction;
}
}
diff --git
a/components-starter/camel-spring-cloud-config-starter/src/test/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParserTest.java
b/components-starter/camel-spring-cloud-config-starter/src/test/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParserTest.java
new file mode 100644
index 00000000000..f0fe8e48df3
--- /dev/null
+++
b/components-starter/camel-spring-cloud-config-starter/src/test/java/org/apache/camel/component/spring/cloud/config/springboot/SpringBootCloudConfigPropertiesParserTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.spring.cloud.config.springboot;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SpringBootCloudConfigPropertiesParserTest {
+
+ private final SpringBootCloudConfigPropertiesParser parser = new
SpringBootCloudConfigPropertiesParser();
+
+ @Test
+ public void guardPropertyIsUnchanged() {
+
assertEquals("camel.component.spring-cloud-config.early-resolve-properties",
+ parser.getEarlyResolutionProperty(),
+ "a wrong guard key silently disables early resolution, leaving
placeholders as literal values");
+ }
+
+ @Test
+ public void overridePropertySourceNameIsUnchanged() {
+ assertEquals("overridden-camel-spring-config-properties",
+ parser.getOverridePropertySourceName(),
+ "the property source name is observable through
/actuator/env");
+ }
+
+ @Test
+ public void diagnosticsNameSpringCloudConfigRatherThanAVault() {
+ assertEquals("Spring Cloud Config", parser.getSourceDescription(),
+ "this starter reads a config server, so calling it a vault
would misdirect the operator");
+ }
+}
diff --git
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParser.java
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParser.java
new file mode 100644
index 00000000000..3067617deb7
--- /dev/null
+++
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParser.java
@@ -0,0 +1,163 @@
+/*
+ * 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.spring.boot;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.PropertiesFunction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
+import org.springframework.boot.origin.OriginTrackedValue;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.PropertiesPropertySource;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * Base class for the early-resolution listeners used by the Camel vault and
secrets starters.
+ * <p/>
+ * These listeners run on {@link ApplicationEnvironmentPreparedEvent}, before
the application context exists, so
+ * they cannot use dependency injection or conditional auto-configuration.
This class owns the parts that are the
+ * same for every component: reading the guard property, walking the property
sources, matching placeholders,
+ * resolving them and registering the resolved values. Subclasses contribute
only the component-specific client
+ * construction and naming.
+ * <p/>
+ * A property value is an early-resolution candidate only when it starts with
<code>{{<functionName>:</code>
+ * and ends with <code>}}</code>; everything in between is passed to the
properties function verbatim. A value
+ * that concatenates two placeholders, such as {@code
{{aws:user}}:{{aws:pass}}}, satisfies both conditions and
+ * is therefore not handled correctly; this is a known limitation, not a
supported use case.
+ */
+public abstract class AbstractEarlyResolutionPropertiesParser
+ implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
+
+ /**
+ * Lets an operator tolerate resolution failures instead of aborting
startup.
+ */
+ public static final String IGNORE_RESOLUTION_FAILURES =
"camel.vault.ignore-resolution-failures";
+
+ private static final String SUFFIX = "}}";
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AbstractEarlyResolutionPropertiesParser.class);
+
+ /**
+ * The property that enables early resolution for this component, for
example
+ * {@code camel.component.aws-secrets-manager.early-resolve-properties}.
+ */
+ protected abstract String getEarlyResolutionProperty();
+
+ /**
+ * The name of the property source holding the resolved values, for example
+ * {@code overridden-camel-aws-secrets-manager-properties}. These names
are observable through
+ * {@code /actuator/env}, so they must not change.
+ */
+ protected abstract String getOverridePropertySourceName();
+
+ /**
+ * Builds the component client and its resolver. Called only after the
guard property is enabled, so that
+ * client construction and its configuration validation never run for
applications that did not opt in.
+ */
+ protected abstract PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment);
+
+ /**
+ * Wording used in diagnostics to name where a value was being resolved
from.
+ */
+ protected String getSourceDescription() {
+ return "the vault";
+ }
+
+ @Override
+ public final void onApplicationEvent(ApplicationEnvironmentPreparedEvent
event) {
+ ConfigurableEnvironment environment = event.getEnvironment();
+ if
(!Boolean.parseBoolean(environment.getProperty(getEarlyResolutionProperty()))) {
+ return;
+ }
+
+ // an unresolved placeholder would otherwise stay in the property
value and become the effective
+ // secret, so resolution failures abort startup unless the operator
opts back into the old behaviour
+ final boolean ignoreResolutionFailures
+ =
Boolean.parseBoolean(environment.getProperty(IGNORE_RESOLUTION_FAILURES));
+
+ PropertiesFunction propertiesFunction =
createPropertiesFunction(environment);
+ LOG.debug("Early resolving properties using the {} function",
propertiesFunction.getName());
+
+ final String prefix = "{{" + propertiesFunction.getName() + ":";
+ final Properties props = new Properties();
+ final Map<String, RuntimeCamelException> failures = new
LinkedHashMap<>();
+
+ for (PropertySource<?> propertySource :
environment.getPropertySources()) {
+ if (propertySource instanceof MapPropertySource mapPropertySource)
{
+ mapPropertySource.getSource().forEach((key, value) -> {
+ String stringValue = asString(value);
+ if (stringValue != null && stringValue.startsWith(prefix)
&& stringValue.endsWith(SUFFIX)) {
+ String remainder =
stringValue.substring(prefix.length(),
+ stringValue.length() - SUFFIX.length());
+ LOG.debug("Resolving and overriding property {}", key);
+ try {
+ String resolved =
propertiesFunction.apply(remainder);
+ if (resolved == null) {
+ throw new RuntimeCamelException(
+ "The " + propertiesFunction.getName()
+ + " properties function
returned no value for " + remainder);
+ }
+ props.put(key, resolved);
+ } catch (Exception e) {
+ if (ignoreResolutionFailures) {
+ LOG.warn("Failed to resolve property {} from
{}; the placeholder is left "
+ + "unresolved because {} is enabled",
+ key, getSourceDescription(),
IGNORE_RESOLUTION_FAILURES, e);
+ } else {
+ failures.put(key, new RuntimeCamelException(
+ "Failed to resolve property " + key +
" from " + getSourceDescription()
+ + ".",
+ e));
+ }
+ }
+ }
+ });
+ }
+ }
+
+ if (!failures.isEmpty()) {
+ RuntimeCamelException aggregated = new RuntimeCamelException(
+ "Failed to resolve " + failures.size() + " property
placeholder(s) from "
+ + getSourceDescription() + ": " + String.join(",
", failures.keySet())
+ + ". Startup is aborted so that the unresolved
placeholders cannot "
+ + "become the effective values; set " +
IGNORE_RESOLUTION_FAILURES
+ + "=true to continue anyway.");
+ failures.values().forEach(aggregated::addSuppressed);
+ throw aggregated;
+ }
+
+ environment.getPropertySources()
+ .addFirst(new
PropertiesPropertySource(getOverridePropertySourceName(), props));
+ }
+
+ private static String asString(Object value) {
+ if (value instanceof OriginTrackedValue originTrackedValue
+ && originTrackedValue.getValue() instanceof String v) {
+ return v;
+ }
+ if (value instanceof String v) {
+ return v;
+ }
+ return null;
+ }
+}
diff --git
a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParserTest.java
b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParserTest.java
new file mode 100644
index 00000000000..0ea03ef856c
--- /dev/null
+++
b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/AbstractEarlyResolutionPropertiesParserTest.java
@@ -0,0 +1,312 @@
+/*
+ * 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.spring.boot;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.spi.PropertiesFunction;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.bootstrap.DefaultBootstrapContext;
+import
org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
+import org.springframework.boot.origin.OriginTrackedValue;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.MapPropertySource;
+import org.springframework.core.env.StandardEnvironment;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class AbstractEarlyResolutionPropertiesParserTest {
+
+ private static final String GUARD =
"camel.component.test-vault.early-resolve-properties";
+ private static final String OVERRIDE_SOURCE =
"overridden-camel-test-vault-properties";
+
+ /**
+ * Records every key the parser asks the environment for, so that the
order in which the guard and the
+ * opt-out flag are read can be asserted.
+ */
+ private static final class RecordingEnvironment extends
StandardEnvironment {
+ private final List<String> queried = new ArrayList<>();
+
+ @Override
+ public String getProperty(String key) {
+ queried.add(key);
+ return super.getProperty(key);
+ }
+ }
+
+ /**
+ * Stands in for a vault client. Records what it was asked to resolve and
fails for a configured set of
+ * remainders, so no container or network is needed.
+ */
+ private static final class StubPropertiesFunction implements
PropertiesFunction {
+ private final Map<String, String> values;
+ private final Set<String> failing;
+ private final List<String> applied = new ArrayList<>();
+
+ StubPropertiesFunction(Map<String, String> values, Set<String>
failing) {
+ this.values = values;
+ this.failing = failing;
+ }
+
+ @Override
+ public String getName() {
+ return "test";
+ }
+
+ @Override
+ public String apply(String remainder) {
+ applied.add(remainder);
+ if (failing.contains(remainder)) {
+ throw new IllegalStateException("cannot resolve " + remainder);
+ }
+ return values.get(remainder);
+ }
+ }
+
+ private static final class TestParser extends
AbstractEarlyResolutionPropertiesParser {
+ private final StubPropertiesFunction function;
+ private int factoryCalls;
+
+ TestParser(StubPropertiesFunction function) {
+ this.function = function;
+ }
+
+ @Override
+ protected String getEarlyResolutionProperty() {
+ return GUARD;
+ }
+
+ @Override
+ protected String getOverridePropertySourceName() {
+ return OVERRIDE_SOURCE;
+ }
+
+ @Override
+ protected PropertiesFunction
createPropertiesFunction(ConfigurableEnvironment environment) {
+ factoryCalls++;
+ return function;
+ }
+ }
+
+ private static ApplicationEnvironmentPreparedEvent
eventFor(ConfigurableEnvironment environment) {
+ return new ApplicationEnvironmentPreparedEvent(
+ new DefaultBootstrapContext(), new SpringApplication(), new
String[0], environment);
+ }
+
+ /**
+ * Accepts {@code Map<String, ?>} so that both {@code Map.of("k", "v")}
(which infers
+ * {@code Map<String, String>}) and maps holding an OriginTrackedValue can
be passed.
+ */
+ private static RecordingEnvironment environmentWith(Map<String, ?>
properties) {
+ RecordingEnvironment environment = new RecordingEnvironment();
+ environment.getPropertySources()
+ .addFirst(new MapPropertySource("test-properties", new
LinkedHashMap<>(properties)));
+ return environment;
+ }
+
+ @Test
+ public void guardDisabledDoesNotBuildTheClient() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ "my.secret", "{{test:some/secret}}"));
+ TestParser parser = new TestParser(new
StubPropertiesFunction(Map.of(), Set.of()));
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals(0, parser.factoryCalls,
+ "building a vault client while early resolution is disabled
would abort startup for every "
+ + "application that never opted in");
+ assertNull(environment.getPropertySources().get(OVERRIDE_SOURCE),
+ "no override property source may be added when early
resolution is disabled");
+ }
+
+ @Test
+ public void guardEnabledBuildsTheClientAndAddsTheOverrideSource() {
+ RecordingEnvironment environment = environmentWith(Map.of(GUARD,
"true"));
+ TestParser parser = new TestParser(new
StubPropertiesFunction(Map.of(), Set.of()));
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals(1, parser.factoryCalls, "the client is built exactly once
per event");
+ assertNotNull(environment.getPropertySources().get(OVERRIDE_SOURCE),
+ "the override source must be registered so later property
resolution sees it");
+ }
+
+ @Test
+ public void resolvesWholeValuePlaceholders() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "my.secret", "{{test:database/password}}"));
+ TestParser parser = new TestParser(
+ new StubPropertiesFunction(Map.of("database/password",
"pazzword"), Set.of()));
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals("pazzword", environment.getProperty("my.secret"),
+ "the resolved value must take effect, because the placeholder
text is not a usable secret");
+ }
+
+ @Test
+ public void leavesNonMatchingValuesAlone() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "embedded.placeholder", "jdbc://{{test:host}}/db",
+ "foreign.prefix", "{{other:host}}",
+ "plain.value", "just-a-string"));
+ StubPropertiesFunction function = new
StubPropertiesFunction(Map.of("host", "resolved-host"), Set.of());
+ TestParser parser = new TestParser(function);
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals(List.of(), function.applied,
+ "early resolution deliberately handles only values that are
entirely one placeholder; "
+ + "embedded placeholders belong to Camel's normal
property parser");
+ assertEquals("jdbc://{{test:host}}/db",
environment.getProperty("embedded.placeholder"));
+ assertEquals("{{other:host}}",
environment.getProperty("foreign.prefix"));
+ assertEquals("just-a-string", environment.getProperty("plain.value"));
+ }
+
+ @Test
+ public void stripsOnlyTheLeadingPrefixAndTrailingDelimiters() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "my.secret", "{{test:a}}b}}"));
+ StubPropertiesFunction function = new
StubPropertiesFunction(Map.of("a}}b", "resolved"), Set.of());
+ TestParser parser = new TestParser(function);
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals(List.of("a}}b"), function.applied,
+ "a global replace of the delimiters would corrupt a remainder
that itself contains them");
+ assertEquals("resolved", environment.getProperty("my.secret"));
+ }
+
+ @Test
+ public void resolvesOriginTrackedValues() {
+ Map<String, Object> properties = new LinkedHashMap<>();
+ properties.put(GUARD, "true");
+ properties.put("my.secret",
OriginTrackedValue.of("{{test:database/password}}"));
+ RecordingEnvironment environment = environmentWith(properties);
+ TestParser parser = new TestParser(
+ new StubPropertiesFunction(Map.of("database/password",
"pazzword"), Set.of()));
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertEquals("pazzword", environment.getProperty("my.secret"),
+ "values loaded from application.properties arrive wrapped in
OriginTrackedValue");
+ }
+
+ @Test
+ public void guardDisabledDoesNotReadTheIgnoreFlag() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ "my.secret", "{{test:some/secret}}"));
+ TestParser parser = new TestParser(new
StubPropertiesFunction(Map.of(), Set.of()));
+
+ parser.onApplicationEvent(eventFor(environment));
+
+ assertTrue(environment.queried.contains(GUARD),
+ "the guard property must be queried so that the ordering below
is actually exercised");
+ assertFalse(environment.queried.contains(
+
AbstractEarlyResolutionPropertiesParser.IGNORE_RESOLUTION_FAILURES),
+ "the opt-out flag is only meaningful once early resolution is
enabled, so it must not be "
+ + "evaluated before the guard");
+ }
+
+ @Test
+ public void failClosedReportsEveryFailingPropertyTogether() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "first.secret", "{{test:missing/one}}",
+ "second.secret", "{{test:missing/two}}"));
+ StubPropertiesFunction function = new StubPropertiesFunction(
+ Map.of(), Set.of("missing/one", "missing/two"));
+ TestParser parser = new TestParser(function);
+
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () -> parser.onApplicationEvent(eventFor(environment)));
+
+ assertTrue(thrown.getMessage().contains("first.secret"),
+ "the failure must name every property that could not be
resolved, was: " + thrown.getMessage());
+ assertTrue(thrown.getMessage().contains("second.secret"),
+ "the failure must name every property that could not be
resolved, was: " + thrown.getMessage());
+ assertTrue(thrown.getMessage().contains(
+
AbstractEarlyResolutionPropertiesParser.IGNORE_RESOLUTION_FAILURES),
+ "the failure must point at the opt-out property, was: " +
thrown.getMessage());
+ assertEquals(2, thrown.getSuppressed().length,
+ "each individual failure is attached so its cause survives");
+ assertEquals(2, function.applied.size(),
+ "aborting on the first failure would hide the remaining broken
placeholders from the operator");
+ }
+
+ @Test
+ public void nullResolutionResultIsTreatedAsAFailure() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "my.secret", "{{test:missing/subkey}}"));
+ TestParser parser = new TestParser(new
StubPropertiesFunction(Map.of(), Set.of()));
+
+ RuntimeCamelException thrown =
assertThrows(RuntimeCamelException.class,
+ () -> parser.onApplicationEvent(eventFor(environment)));
+
+ assertTrue(thrown.getMessage().contains("my.secret"),
+ "a null result is a resolution failure like any other, so it
must name the property, was: "
+ + thrown.getMessage());
+ assertNull(environment.getPropertySources().get(OVERRIDE_SOURCE),
+ "a null resolution result must abort startup rather than
silently storing no value");
+ }
+
+ @Test
+ public void failClosedDoesNotRegisterAnOverrideSource() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+ "my.secret", "{{test:missing}}"));
+ TestParser parser = new TestParser(new
StubPropertiesFunction(Map.of(), Set.of("missing")));
+
+ assertThrows(RuntimeCamelException.class, () ->
parser.onApplicationEvent(eventFor(environment)));
+
+ assertNull(environment.getPropertySources().get(OVERRIDE_SOURCE),
+ "a partially populated override source must not be left behind
after an aborted startup");
+ }
+
+ @Test
+ public void ignoreResolutionFailuresKeepsStartupGoing() {
+ RecordingEnvironment environment = environmentWith(Map.of(
+ GUARD, "true",
+
AbstractEarlyResolutionPropertiesParser.IGNORE_RESOLUTION_FAILURES, "true",
+ "good.secret", "{{test:present}}",
+ "bad.secret", "{{test:missing}}"));
+ TestParser parser = new TestParser(
+ new StubPropertiesFunction(Map.of("present", "resolved"),
Set.of("missing")));
+
+ assertDoesNotThrow(() ->
parser.onApplicationEvent(eventFor(environment)));
+
+ assertEquals("resolved", environment.getProperty("good.secret"),
+ "a failure elsewhere must not discard the values that did
resolve");
+ assertEquals("{{test:missing}}", environment.getProperty("bad.secret"),
+ "in tolerant mode the unresolved placeholder is deliberately
left in place");
+ }
+}