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

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 641dae5e058d CAMEL-25552: camel-hashicorp-vault - the properties 
function no longer shares the secret engine between lookups (#27679)
641dae5e058d is described below

commit 641dae5e058d12666b38882a52f5b07d8f207b07
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 14:25:20 2026 +0200

    CAMEL-25552: camel-hashicorp-vault - the properties function no longer 
shares the secret engine between lookups (#27679)
    
    * CAMEL-25552: camel-hashicorp-vault - the properties function no longer 
shares the secret engine between lookups
    
    The engine of a hashicorp: lookup was kept in a field of the shared 
properties function, so concurrent lookups could read from the wrong engine and 
a lookup without engine used the engine of the lookup before. The engine is now 
local to each lookup and defaults to the 'secret' engine. A missing secret with 
a field now resolves to no value (or the default value) instead of failing with 
a NullPointerException. Fixed the docs example of a default value with a 
version, which was missing t [...]
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25552: camel-hashicorp-vault - update a test comment after rebase 
on CAMEL-25518
    
    A lookup without engine now reads from the default secret engine instead of 
a null engine.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25552: camel-hashicorp-vault - a response without data resolves to 
no value also without a field
    
    Address review: the lookup without a field read 
rawSecret.getData().get("data") unguarded, so a response without data failed 
with a NullPointerException. Both paths now read the secret data once, 
null-safe.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25552: Address review feedback
    
    Move the upgrade guide note from 4.23 to 4.24 (this is not in 4.23.0) and
    mention that a version without engine is now read as a version, say that a
    default value with a field needs no engine, and use # for the field in the
    javadoc.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../catalog/docs/hashicorp-vault-component.adoc    |  12 +-
 .../src/main/docs/hashicorp-vault-component.adoc   |  12 +-
 .../vault/HashicorpVaultPropertiesFunction.java    |  50 ++++--
 .../HashicorpVaultPropertiesFunctionTest.java      | 184 +++++++++++++++++++++
 .../vault/HashicorpVaultReloadTriggerTaskTest.java |   2 +-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  29 ++++
 .../modules/ROOT/pages/camel-4x-upgrade-guide.adoc |   1 +
 7 files changed, 265 insertions(+), 25 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
index d8b2e2ad3091..845a2c662568 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hashicorp-vault-component.adoc
@@ -363,6 +363,12 @@ YAML::
 
 Where route will be the name of the secret stored in the HashiCorp Vault 
instance, in the 'secret' engine.
 
+NOTE: The engine is the part before the first `:`; for a property with a 
field, only the part before the `#` counts.
+When the property has no engine, such as `{{hashicorp:route}}` or 
`{{hashicorp:database#username}}`, the secret is read
+from the 'secret' engine. Without a field, a default value needs the engine, 
as in `{{hashicorp:secret:route:default}}`:
+in `{{hashicorp:route:default}}` the engine would be `route`. With a field it 
does not, as in
+`{{hashicorp:database#username:admin}}`.
+
 You could specify a default value in case the secret is not present on 
HashiCorp Vault instance:
 
 [tabs]
@@ -540,7 +546,7 @@ Java::
 [source,java]
 ----
 from("direct:start")
-    .to("{{hashicorp:route:default@2}}");
+    .to("{{hashicorp:secret:route:default@2}}");
 ----
 
 XML::
@@ -550,7 +556,7 @@ XML::
 <camelContext>
     <route>
         <from uri="direct:start"/>
-        <to uri="{{hashicorp:route:default@2}}"/>
+        <to uri="{{hashicorp:secret:route:default@2}}"/>
     </route>
 </camelContext>
 ----
@@ -564,7 +570,7 @@ YAML::
       uri: direct:start
       steps:
         - to:
-            uri: "{{hashicorp:route:default@2}}"
+            uri: "{{hashicorp:secret:route:default@2}}"
 ----
 ====
 
diff --git 
a/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc 
b/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
index d8b2e2ad3091..845a2c662568 100644
--- 
a/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
+++ 
b/components/camel-hashicorp-vault/src/main/docs/hashicorp-vault-component.adoc
@@ -363,6 +363,12 @@ YAML::
 
 Where route will be the name of the secret stored in the HashiCorp Vault 
instance, in the 'secret' engine.
 
+NOTE: The engine is the part before the first `:`; for a property with a 
field, only the part before the `#` counts.
+When the property has no engine, such as `{{hashicorp:route}}` or 
`{{hashicorp:database#username}}`, the secret is read
+from the 'secret' engine. Without a field, a default value needs the engine, 
as in `{{hashicorp:secret:route:default}}`:
+in `{{hashicorp:route:default}}` the engine would be `route`. With a field it 
does not, as in
+`{{hashicorp:database#username:admin}}`.
+
 You could specify a default value in case the secret is not present on 
HashiCorp Vault instance:
 
 [tabs]
@@ -540,7 +546,7 @@ Java::
 [source,java]
 ----
 from("direct:start")
-    .to("{{hashicorp:route:default@2}}");
+    .to("{{hashicorp:secret:route:default@2}}");
 ----
 
 XML::
@@ -550,7 +556,7 @@ XML::
 <camelContext>
     <route>
         <from uri="direct:start"/>
-        <to uri="{{hashicorp:route:default@2}}"/>
+        <to uri="{{hashicorp:secret:route:default@2}}"/>
     </route>
 </camelContext>
 ----
@@ -564,7 +570,7 @@ YAML::
       uri: direct:start
       steps:
         - to:
-            uri: "{{hashicorp:route:default@2}}"
+            uri: "{{hashicorp:secret:route:default@2}}"
 ----
 ====
 
diff --git 
a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
 
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
index a1909d805768..04082f515402 100644
--- 
a/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
+++ 
b/components/camel-hashicorp-vault/src/main/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunction.java
@@ -62,13 +62,14 @@ import org.springframework.vault.support.VaultResponse;
  * This implementation is to return the secret value associated with a key. 
The properties related to this kind of
  * Properties Function are all prefixed with <tt>hashicorp:</tt>. For example 
asking for
  * <tt>hashicorp:engine:token</tt>, will return the secret value associated to 
the secret named token on Hashicorp Vault
- * instance on the engine 'engine'.
+ * instance on the engine 'engine'. When no engine is given, such as 
<tt>hashicorp:token</tt>, the 'secret' engine is
+ * used.
  *
- * Another way of retrieving a secret value is using the following notation 
<tt>hashicorp:engine:database/username</tt>:
+ * Another way of retrieving a secret value is using the following notation 
<tt>hashicorp:engine:database#username</tt>:
  * in this case the field username of the secret database will be returned. As 
a fallback, the user could provide a
  * default value, which will be returned in case the secret doesn't exist, the 
secret has been marked for deletion or,
  * for example, if a particular field of the secret doesn't exist. For using 
this feature, the user could use the
- * following notation <tt>hashicorp:engine:database/username:admin</tt>. The 
admin value will be returned as default
+ * following notation <tt>hashicorp:engine:database#username:admin</tt>. The 
admin value will be returned as default
  * value, if the conditions above were all met.
  */
 
@@ -87,12 +88,12 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
             = "CAMEL_HASHICORP_VAULT_CLOUD";
     private static final String CAMEL_HASHICORP_VAULT_NAMESPACE_ENV
             = "CAMEL_HASHICORP_VAULT_NAMESPACE";
+    private static final String DEFAULT_ENGINE = "secret";
     private CamelContext camelContext;
     private VaultTemplate client;
     private final Set<String> secrets = ConcurrentHashMap.newKeySet();
     private final Map<String, Integer> secretVersions = new 
ConcurrentHashMap<>();
 
-    private String engine;
     private String namespace;
     private boolean cloud;
 
@@ -159,6 +160,8 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
 
     @Override
     public String apply(String remainder) {
+        // the function is shared by all lookups (also concurrent ones), so 
the parsed parts must be local
+        String engine = DEFAULT_ENGINE;
         String key = remainder;
         String subkey = null;
         String returnValue = null;
@@ -166,8 +169,12 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
         String version = null;
         if (remainder.contains("#")) {
             String keyRemainder = StringHelper.before(remainder, "#");
-            engine = StringHelper.before(keyRemainder, ":");
-            key = StringHelper.after(keyRemainder, ":");
+            if (keyRemainder.contains(":")) {
+                engine = StringHelper.before(keyRemainder, ":");
+                key = StringHelper.after(keyRemainder, ":");
+            } else {
+                key = keyRemainder;
+            }
             subkey = StringHelper.after(remainder, "#");
             defaultValue = StringHelper.after(subkey, ":");
             if (ObjectHelper.isNotEmpty(defaultValue)) {
@@ -207,11 +214,14 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
                     key = StringHelper.before(key, "@");
                 }
             }
+        } else if (remainder.contains("@")) {
+            version = StringHelper.after(remainder, "@");
+            key = StringHelper.before(remainder, "@");
         }
 
         if (key != null) {
             try {
-                returnValue = getSecretFromSource(key, subkey, defaultValue, 
version);
+                returnValue = getSecretFromSource(engine, key, subkey, 
defaultValue, version);
             } catch (Exception e) {
                 throw new RuntimeCamelException(
                         "Error getting secret from vault using key: " + key + 
" due to: " + e.getMessage(), e);
@@ -221,7 +231,7 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
         return returnValue;
     }
 
-    private String getSecretFromSource(String key, String subkey, String 
defaultValue, String version) {
+    private String getSecretFromSource(String engine, String key, String 
subkey, String defaultValue, String version) {
         // capture name of secret in the form the refresh task reads the 
metadata of
         String secretName = trackedSecretName(engine, key);
         secrets.add(secretName);
@@ -240,18 +250,22 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
                 completePath = completePath + "?version=" + version;
             }
             VaultResponse rawSecret = client.read(completePath);
-            if (ObjectHelper.isNotEmpty(rawSecret)) {
-                returnValue = rawSecret.getData().get("data").toString();
+            // the secret may not exist, or the response may have no data: 
then there is no value (nor field)
+            Map<String, Object> responseData = rawSecret != null ? 
rawSecret.getData() : null;
+            Object secretData = responseData != null ? 
responseData.get("data") : null;
+            if (secretData != null) {
+                returnValue = secretData.toString();
                 if (ObjectHelper.isEmpty(version)) {
-                    captureVersion(secretName, rawSecret);
+                    captureVersion(secretName, responseData);
                 }
             }
             if (ObjectHelper.isNotEmpty(subkey)) {
-                Object field = ((Map) 
rawSecret.getData().get("data")).get(subkey);
-                if (ObjectHelper.isNotEmpty(field)) {
-                    returnValue = field.toString();
-                } else {
-                    returnValue = null;
+                returnValue = null;
+                if (secretData instanceof Map<?, ?> data) {
+                    Object field = data.get(subkey);
+                    if (ObjectHelper.isNotEmpty(field)) {
+                        returnValue = field.toString();
+                    }
                 }
             }
             if (ObjectHelper.isEmpty(returnValue)) {
@@ -276,8 +290,8 @@ public class HashicorpVaultPropertiesFunction extends 
ServiceSupport implements
         return secretEngine == null || "secret".equals(secretEngine) ? key : 
secretEngine + ":" + key;
     }
 
-    private void captureVersion(String secretName, VaultResponse rawSecret) {
-        if (rawSecret.getData().get("metadata") instanceof Map<?, ?> metadata 
&& metadata.get("version") != null) {
+    private void captureVersion(String secretName, Map<String, Object> 
responseData) {
+        if (responseData.get("metadata") instanceof Map<?, ?> metadata && 
metadata.get("version") != null) {
             try {
                 // keep the oldest version resolved: a value resolved earlier 
may still be in use, so the refresh
                 // task must compare with that one (at worst one extra reload)
diff --git 
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
 
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
new file mode 100644
index 000000000000..7e1efd682734
--- /dev/null
+++ 
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultPropertiesFunctionTest.java
@@ -0,0 +1,184 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.vault.authentication.TokenAuthentication;
+import org.springframework.vault.client.VaultEndpoint;
+import org.springframework.vault.core.VaultTemplate;
+import org.springframework.vault.support.VaultResponse;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Lookups of the properties function against a fake {@link VaultTemplate}, 
which answers the KV v2 data paths.
+ */
+class HashicorpVaultPropertiesFunctionTest {
+
+    @Test
+    void lookupWithoutEngineDoesNotUseTheEngineOfThePreviousLookup() {
+        FakeVaultTemplate vault = new FakeVaultTemplate(
+                Map.of(
+                        "kv/data/api", Map.of("password", "from-kv"),
+                        "secret/data/api", Map.of("password", "from-secret")));
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(vault);
+
+        assertEquals("from-kv", function.apply("kv:api#password"));
+        // no engine: the default 'secret' engine, not the 'kv' engine of the 
lookup before
+        assertEquals("from-secret", function.apply("api#password"));
+        assertEquals("{password=from-secret}", function.apply("api"));
+        assertEquals("from-kv", function.apply("kv:api#password"));
+
+        assertEquals(List.of("kv/data/api", "secret/data/api", 
"secret/data/api", "kv/data/api"), vault.reads);
+    }
+
+    @Test
+    void lookupWithoutEngineWithVersion() {
+        FakeVaultTemplate vault = new FakeVaultTemplate(
+                Map.of(
+                        "secret/data/api?version=2", Map.of("password", 
"v2")));
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(vault);
+
+        assertEquals("{password=v2}", function.apply("api@2"));
+        assertEquals("v2", function.apply("api#password@2"));
+        assertEquals("v2", function.apply("secret:api#password@2"));
+    }
+
+    @Test
+    void concurrentLookupsReadFromTheirOwnEngine() throws Exception {
+        FakeVaultTemplate vault = new FakeVaultTemplate(
+                Map.of(
+                        "engine1/data/db", Map.of("password", "engine1"),
+                        "engine2/data/db", Map.of("password", "engine2"),
+                        "secret/data/db", Map.of("password", "secret")));
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(vault);
+
+        List<String> mismatches = new CopyOnWriteArrayList<>();
+        List<Callable<Void>> tasks = new ArrayList<>();
+        for (String engine : List.of("engine1", "engine2", "secret", 
"engine1", "engine2", "secret")) {
+            String property = "secret".equals(engine) ? "db#password" : engine 
+ ":db#password";
+            tasks.add(() -> {
+                for (int i = 0; i < 500; i++) {
+                    String value = function.apply(property);
+                    if (!engine.equals(value)) {
+                        mismatches.add(property + " -> " + value);
+                    }
+                }
+                return null;
+            });
+        }
+
+        ExecutorService executor = Executors.newFixedThreadPool(tasks.size());
+        try {
+            for (Future<Void> future : executor.invokeAll(tasks, 30, 
TimeUnit.SECONDS)) {
+                future.get();
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        assertTrue(mismatches.isEmpty(), "lookups read from the wrong engine: 
" + mismatches);
+    }
+
+    @Test
+    void missingSecretWithFieldReturnsNull() {
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(new FakeVaultTemplate(Map.of()));
+
+        // same as a missing secret without a field, so optional placeholders 
work
+        assertNull(function.apply("secret:missing"));
+        assertNull(function.apply("secret:missing#password"));
+        assertNull(function.apply("secret:missing#password@2"));
+    }
+
+    @Test
+    void missingSecretWithFieldReturnsDefaultValue() {
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(new FakeVaultTemplate(Map.of()));
+
+        assertEquals("admin", function.apply("secret:missing#username:admin"));
+        assertEquals("admin", 
function.apply("secret:missing#username:admin@2"));
+    }
+
+    @Test
+    void missingFieldReturnsNullOrDefaultValue() {
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(
+                new FakeVaultTemplate(Map.of("secret/data/database", 
Map.of("password", "pw"))));
+
+        assertEquals("pw", function.apply("secret:database#password"));
+        assertNull(function.apply("secret:database#username"));
+        assertEquals("admin", 
function.apply("secret:database#username:admin"));
+    }
+
+    @Test
+    void responseWithoutDataReturnsNullOrDefaultValue() {
+        FakeVaultTemplate vault = new FakeVaultTemplate(Map.of());
+        // a response without any data, and a response whose data has no 
secret data
+        vault.responses.put("secret/data/nodata", new VaultResponse());
+        VaultResponse onlyMetadata = new VaultResponse();
+        onlyMetadata.setData(Map.of("metadata", Map.of("version", 1)));
+        vault.responses.put("secret/data/onlymetadata", onlyMetadata);
+        HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction(vault);
+
+        assertNull(function.apply("secret:nodata"));
+        assertNull(function.apply("secret:nodata#password"));
+        assertEquals("admin", function.apply("secret:nodata#password:admin"));
+        assertNull(function.apply("secret:onlymetadata"));
+        assertNull(function.apply("secret:onlymetadata#password"));
+        assertEquals("admin", 
function.apply("secret:onlymetadata#password:admin"));
+    }
+
+    private static final class FakeVaultTemplate extends VaultTemplate {
+
+        private final Map<String, Map<String, Object>> secrets;
+        private final List<String> reads = new CopyOnWriteArrayList<>();
+        // raw responses by path, for responses that are not a regular secret
+        private final Map<String, VaultResponse> responses = new 
ConcurrentHashMap<>();
+
+        FakeVaultTemplate(Map<String, Map<String, Object>> secrets) {
+            super(VaultEndpoint.create("localhost", 8200), new 
TokenAuthentication("token"));
+            this.secrets = secrets;
+        }
+
+        @Override
+        public VaultResponse read(String path) {
+            reads.add(path);
+            if (responses.containsKey(path)) {
+                return responses.get(path);
+            }
+            Map<String, Object> data = secrets.get(path);
+            if (data == null) {
+                // as Vault answers 404 for a secret that does not exist
+                return null;
+            }
+            VaultResponse response = new VaultResponse();
+            response.setData(Map.of("data", data));
+            return response;
+        }
+    }
+}
diff --git 
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
 
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
index d8385f86d624..3c362e8a08f1 100644
--- 
a/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
+++ 
b/components/camel-hashicorp-vault/src/test/java/org/apache/camel/component/hashicorp/vault/HashicorpVaultReloadTriggerTaskTest.java
@@ -201,7 +201,7 @@ class HashicorpVaultReloadTriggerTaskTest extends 
CamelTestSupport {
 
     @Test
     void secretWithoutEngineIsTrackedAsDefaultEngine() {
-        // a fresh function has no engine yet, so a plain hashicorp:api lookup 
runs with a null engine
+        // a plain hashicorp:api lookup has no engine, so it reads from the 
default secret engine
         HashicorpVaultPropertiesFunction function = new 
HashicorpVaultPropertiesFunction();
         function.setCamelContext(context);
         function.start();
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
new file mode 100644
index 000000000000..5a3c1009e3ab
--- /dev/null
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -0,0 +1,29 @@
+= Apache Camel 4.x Upgrade Guide
+
+This document is for helping you upgrade your Apache Camel application
+from Camel 4.x to 4.y. For example, if you are upgrading Camel 4.0 to 4.2, 
then you should follow the guides
+from both 4.0 to 4.1 and 4.1 to 4.2.
+
+[NOTE]
+====
+https://github.com/apache/camel-upgrade-recipes/[The Camel Upgrade Recipes 
project] provides automated assistance for some common migration tasks.
+Note that manual migration is still required.
+See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details.
+====
+
+== Upgrading Camel 4.23 to 4.24
+
+=== camel-hashicorp-vault - properties without engine use the `secret` engine
+
+A `hashicorp:` property without an engine, such as `{{hashicorp:mysecret}}` or 
`{{hashicorp:mysecret#field}}`, now
+reads the secret from the `secret` engine. Before, it read from the engine of 
the `hashicorp:` property resolved
+before it (or from a `null` engine), and `{{hashicorp:mysecret#field}}` always 
resolved to no value. Properties with
+an engine, such as `{{hashicorp:secret:mysecret#field}}`, are not affected.
+
+A `hashicorp:` property without an engine and with a version, such as 
`{{hashicorp:mysecret@2}}`, now reads version 2
+of the secret `mysecret` in the `secret` engine. Before, the `@2` was kept as 
part of the secret name.
+
+A `hashicorp:` property with a field of a secret that does not exist, such as 
`{{hashicorp:secret:missing#field}}`,
+now resolves to no value, as it already did without a field, so its default 
value or an optional placeholder
+(`{{?hashicorp:secret:missing#field}}`) applies. Before, without a default 
value, it failed with a
+`NullPointerException`.
diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
index 982e18bb2fba..91f8f833c7fe 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide.adoc
@@ -32,6 +32,7 @@ You can find the upgrade guide for each release in the 
following pages:
 - xref:camel-4x-upgrade-guide-4_21.adoc[Upgrade guide 4.20 -> 4.21]
 - xref:camel-4x-upgrade-guide-4_22.adoc[Upgrade guide 4.21 -> 4.22 LTS]
 - xref:camel-4x-upgrade-guide-4_23.adoc[Upgrade guide 4.22 -> 4.23]
+- xref:camel-4x-upgrade-guide-4_24.adoc[Upgrade guide 4.23 -> 4.24]
 
 [NOTE]
 ====

Reply via email to